feat: 知识管理平台精简版 - PyQt6桌面应用

主要功能:
- 账号管理:添加/编辑/删除账号,测试登录
- 浏览任务:批量浏览应读/选读内容并标记已读
- 截图管理:wkhtmltoimage截图,查看历史
- 金山文档:扫码登录/微信快捷登录,自动上传截图

技术栈:
- PyQt6 GUI框架
- Playwright 浏览器自动化
- SQLite 本地数据存储
- wkhtmltoimage 网页截图

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-18 22:16:36 +08:00
commit 83fef6dff2
24 changed files with 6133 additions and 0 deletions

416
ui/account_widget.py Normal file
View File

@@ -0,0 +1,416 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Account management panel - add/edit/delete accounts, test login
"""
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QTableWidget, QTableWidgetItem,
QPushButton, QLineEdit, QLabel, QDialog, QFormLayout, QMessageBox,
QHeaderView, QCheckBox, QScrollArea, QFrame, QSizePolicy, QSpacerItem
)
from PyQt6.QtCore import Qt, pyqtSignal
from config import get_config, save_config, AccountConfig
from utils.crypto import encrypt_password, decrypt_password, is_encrypted
from .constants import (
PAGE_PADDING, SECTION_SPACING, GROUP_PADDING_TOP, GROUP_PADDING_SIDE,
GROUP_PADDING_BOTTOM, GROUP_SPACING, FORM_ROW_SPACING, INPUT_HEIGHT,
BUTTON_HEIGHT, BUTTON_HEIGHT_NORMAL, BUTTON_HEIGHT_SMALL, BUTTON_MIN_WIDTH,
BUTTON_MIN_WIDTH_NORMAL, BUTTON_WIDTH_SMALL, BUTTON_SPACING,
TABLE_ROW_HEIGHT, get_title_style, get_help_text_style
)
class AccountEditDialog(QDialog):
"""Account edit dialog"""
def __init__(self, account: AccountConfig = None, parent=None):
super().__init__(parent)
self.account = account
self.setWindowTitle("编辑账号" if account else "添加账号")
self.setMinimumWidth(480)
self._setup_ui()
self.adjustSize()
def _setup_ui(self):
layout = QVBoxLayout(self)
layout.setContentsMargins(PAGE_PADDING, PAGE_PADDING, PAGE_PADDING, PAGE_PADDING)
layout.setSpacing(SECTION_SPACING)
# Form area
form_layout = QFormLayout()
form_layout.setSpacing(FORM_ROW_SPACING)
form_layout.setLabelAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
form_layout.setFormAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
# Username
self.username_edit = QLineEdit()
self.username_edit.setPlaceholderText("请输入用户名")
self.username_edit.setMinimumHeight(INPUT_HEIGHT)
if self.account:
self.username_edit.setText(self.account.username)
form_layout.addRow("用户名:", self.username_edit)
# Password
self.password_edit = QLineEdit()
self.password_edit.setEchoMode(QLineEdit.EchoMode.Password)
self.password_edit.setPlaceholderText("请输入密码")
self.password_edit.setMinimumHeight(INPUT_HEIGHT)
if self.account:
pwd = decrypt_password(self.account.password) if is_encrypted(self.account.password) else self.account.password
self.password_edit.setText(pwd)
form_layout.addRow("密码:", self.password_edit)
# Remark
self.remark_edit = QLineEdit()
self.remark_edit.setPlaceholderText("如:张三(用于截图文件名)")
self.remark_edit.setMinimumHeight(INPUT_HEIGHT)
if self.account:
self.remark_edit.setText(self.account.remark)
form_layout.addRow("备注:", self.remark_edit)
# Enabled checkbox
self.enabled_check = QCheckBox("启用此账号")
self.enabled_check.setChecked(self.account.enabled if self.account else True)
form_layout.addRow("", self.enabled_check)
layout.addLayout(form_layout)
# Spacer
layout.addSpacerItem(QSpacerItem(20, 20, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding))
# Buttons
btn_layout = QHBoxLayout()
btn_layout.setSpacing(BUTTON_SPACING)
btn_layout.addStretch()
cancel_btn = QPushButton("取消")
cancel_btn.setMinimumWidth(BUTTON_MIN_WIDTH_NORMAL)
cancel_btn.setMinimumHeight(BUTTON_HEIGHT_NORMAL)
cancel_btn.clicked.connect(self.reject)
btn_layout.addWidget(cancel_btn)
save_btn = QPushButton("保存")
save_btn.setObjectName("primary")
save_btn.setMinimumWidth(BUTTON_MIN_WIDTH_NORMAL)
save_btn.setMinimumHeight(BUTTON_HEIGHT_NORMAL)
save_btn.clicked.connect(self._save)
btn_layout.addWidget(save_btn)
layout.addLayout(btn_layout)
def _save(self):
username = self.username_edit.text().strip()
password = self.password_edit.text().strip()
if not username:
QMessageBox.warning(self, "提示", "请输入用户名")
return
if not password:
QMessageBox.warning(self, "提示", "请输入密码")
return
encrypted_pwd = encrypt_password(password)
if self.account:
self.account.username = username
self.account.password = encrypted_pwd
self.account.remark = self.remark_edit.text().strip()
self.account.enabled = self.enabled_check.isChecked()
else:
self.account = AccountConfig(
username=username,
password=encrypted_pwd,
remark=self.remark_edit.text().strip(),
enabled=self.enabled_check.isChecked()
)
self.accept()
def get_account(self) -> AccountConfig:
return self.account
class AccountWidget(QWidget):
"""Account management panel"""
log_signal = pyqtSignal(str)
def __init__(self, parent=None):
super().__init__(parent)
self._worker = None # 保存Worker引用防止被垃圾回收
self._setup_ui()
self._load_accounts()
def _setup_ui(self):
main_layout = QVBoxLayout(self)
main_layout.setContentsMargins(0, 0, 0, 0)
main_layout.setSpacing(0)
# Scroll area
scroll = QScrollArea()
scroll.setWidgetResizable(True)
scroll.setFrameShape(QFrame.Shape.NoFrame)
scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
content = QWidget()
layout = QVBoxLayout(content)
layout.setContentsMargins(PAGE_PADDING, PAGE_PADDING, PAGE_PADDING, PAGE_PADDING)
layout.setSpacing(SECTION_SPACING)
# Title
title = QLabel("📝 账号管理")
title.setStyleSheet(get_title_style())
layout.addWidget(title)
# Toolbar
toolbar = QHBoxLayout()
toolbar.setSpacing(BUTTON_SPACING)
add_btn = QPushButton(" 添加账号")
add_btn.setObjectName("primary")
add_btn.setMinimumHeight(BUTTON_HEIGHT_NORMAL)
add_btn.setMinimumWidth(BUTTON_MIN_WIDTH)
add_btn.clicked.connect(self._add_account)
toolbar.addWidget(add_btn)
toolbar.addStretch()
test_btn = QPushButton("🔑 测试登录")
test_btn.setMinimumHeight(BUTTON_HEIGHT_NORMAL)
test_btn.setMinimumWidth(BUTTON_MIN_WIDTH_NORMAL)
test_btn.clicked.connect(self._test_login)
toolbar.addWidget(test_btn)
layout.addLayout(toolbar)
# Account table
self.table = QTableWidget()
self.table.setColumnCount(5)
self.table.setHorizontalHeaderLabels(["启用", "用户名", "备注", "状态", "操作"])
# 列宽设置:启用-固定,用户名-拉伸,备注-拉伸,状态-固定,操作-固定
self.table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Fixed)
self.table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
self.table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch)
self.table.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeMode.Fixed)
self.table.horizontalHeader().setSectionResizeMode(4, QHeaderView.ResizeMode.Fixed) # 固定宽度不被压缩
self.table.setColumnWidth(0, 60)
self.table.setColumnWidth(3, 80)
self.table.setColumnWidth(4, 220) # 操作列固定220px
self.table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)
self.table.setSelectionMode(QTableWidget.SelectionMode.SingleSelection)
self.table.verticalHeader().setDefaultSectionSize(TABLE_ROW_HEIGHT)
self.table.verticalHeader().setVisible(False)
self.table.setAlternatingRowColors(True)
self.table.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
self.table.setMinimumHeight(250)
layout.addWidget(self.table, 1)
# Help text
help_text = QLabel("💡 提示: 密码将加密存储。备注用于截图文件命名,建议填写姓名。")
help_text.setStyleSheet(get_help_text_style())
help_text.setWordWrap(True)
layout.addWidget(help_text)
scroll.setWidget(content)
main_layout.addWidget(scroll)
def _load_accounts(self):
"""Load account list"""
config = get_config()
self.table.setRowCount(0)
for i, account in enumerate(config.accounts):
self.table.insertRow(i)
self.table.setRowHeight(i, TABLE_ROW_HEIGHT)
# Enabled checkbox
enabled_widget = QWidget()
enabled_layout = QHBoxLayout(enabled_widget)
enabled_layout.setContentsMargins(0, 0, 0, 0)
enabled_layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
enabled_check = QCheckBox()
enabled_check.setChecked(account.enabled)
enabled_check.stateChanged.connect(lambda state, row=i: self._toggle_enabled(row, state))
enabled_layout.addWidget(enabled_check)
self.table.setCellWidget(i, 0, enabled_widget)
# Username
username_item = QTableWidgetItem(account.username)
username_item.setTextAlignment(Qt.AlignmentFlag.AlignVCenter | Qt.AlignmentFlag.AlignLeft)
username_item.setFlags(username_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
self.table.setItem(i, 1, username_item)
# Remark
remark_item = QTableWidgetItem(account.remark)
remark_item.setTextAlignment(Qt.AlignmentFlag.AlignVCenter | Qt.AlignmentFlag.AlignLeft)
remark_item.setFlags(remark_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
self.table.setItem(i, 2, remark_item)
# Status
status_item = QTableWidgetItem("")
status_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
status_item.setFlags(status_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
self.table.setItem(i, 3, status_item)
# Action buttons - 用图标按钮彻底解决显示问题
btn_widget = QWidget()
btn_layout = QHBoxLayout(btn_widget)
btn_layout.setContentsMargins(0, 0, 0, 0)
btn_layout.setSpacing(4)
btn_layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
# 编辑按钮
edit_btn = QPushButton("编辑")
edit_btn.setCursor(Qt.CursorShape.PointingHandCursor)
edit_btn.setStyleSheet("""
QPushButton {
min-width: 50px;
padding: 5px 12px;
font-size: 13px;
border: 1px solid #d9d9d9;
border-radius: 4px;
background: #fff;
color: #333;
}
QPushButton:hover {
border-color: #1890ff;
color: #1890ff;
background: #e6f7ff;
}
""")
edit_btn.clicked.connect(lambda _, row=i: self._edit_account(row))
btn_layout.addWidget(edit_btn)
# 删除按钮
del_btn = QPushButton("删除")
del_btn.setCursor(Qt.CursorShape.PointingHandCursor)
del_btn.setStyleSheet("""
QPushButton {
min-width: 50px;
padding: 5px 12px;
font-size: 13px;
border: none;
border-radius: 4px;
background: #ff4d4f;
color: #fff;
}
QPushButton:hover {
background: #ff7875;
}
""")
del_btn.clicked.connect(lambda _, row=i: self._delete_account(row))
btn_layout.addWidget(del_btn)
self.table.setCellWidget(i, 4, btn_widget)
def _add_account(self):
"""Add account"""
dialog = AccountEditDialog(parent=self)
if dialog.exec() == QDialog.DialogCode.Accepted:
account = dialog.get_account()
config = get_config()
config.accounts.append(account)
save_config(config)
self._load_accounts()
self.log_signal.emit(f"账号 {account.username} 添加成功")
def _edit_account(self, row: int):
"""Edit account"""
config = get_config()
if row < len(config.accounts):
account = config.accounts[row]
dialog = AccountEditDialog(account=account, parent=self)
if dialog.exec() == QDialog.DialogCode.Accepted:
save_config(config)
self._load_accounts()
self.log_signal.emit(f"账号 {account.username} 已更新")
def _delete_account(self, row: int):
"""Delete account"""
config = get_config()
if row < len(config.accounts):
account = config.accounts[row]
reply = QMessageBox.question(
self, "确认删除",
f"确定要删除账号 {account.username} 吗?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
if reply == QMessageBox.StandardButton.Yes:
config.accounts.pop(row)
save_config(config)
self._load_accounts()
self.log_signal.emit(f"账号 {account.username} 已删除")
def _toggle_enabled(self, row: int, state: int):
"""Toggle account enabled state"""
config = get_config()
if row < len(config.accounts):
config.accounts[row].enabled = state == Qt.CheckState.Checked.value
save_config(config)
def _test_login(self):
"""Test login for selected account"""
selected = self.table.selectedItems()
if not selected:
QMessageBox.information(self, "提示", "请先选择一个账号")
return
row = selected[0].row()
config = get_config()
if row >= len(config.accounts):
return
account = config.accounts[row]
password = decrypt_password(account.password) if is_encrypted(account.password) else account.password
self.log_signal.emit(f"正在测试登录 {account.username}...")
status_item = self.table.item(row, 3)
status_item.setText("登录中...")
from utils.worker import Worker
def test_login(_signals=None, _should_stop=None):
from core.api_browser import APIBrowser
with APIBrowser(log_callback=lambda msg: _signals.log.emit(msg) if _signals else None) as browser:
if browser.login(account.username, password):
real_name = browser.get_real_name()
return {"success": True, "real_name": real_name}
else:
return {"success": False}
def on_result(result):
if result and result.get("success"):
status_item.setText("✅ 成功")
real_name = result.get("real_name", "")
msg = f"账号 {account.username} 登录成功"
if real_name:
msg += f",姓名: {real_name}"
# 如果备注为空,自动填入真实姓名
if not account.remark:
account.remark = real_name
save_config(config)
self._load_accounts() # 刷新表格
msg += "(已自动填入备注)"
self.log_signal.emit(msg)
else:
status_item.setText("❌ 失败")
self.log_signal.emit(f"账号 {account.username} 登录失败")
def on_error(error):
status_item.setText("❌ 错误")
self.log_signal.emit(f"测试登录出错: {error}")
# 保存为实例变量,防止被垃圾回收导致崩溃
self._worker = Worker(test_login)
self._worker.signals.log.connect(self.log_signal.emit)
self._worker.signals.result.connect(on_result)
self._worker.signals.error.connect(on_error)
self._worker.start()
def get_enabled_accounts(self) -> list:
"""Get all enabled accounts"""
config = get_config()
return [a for a in config.accounts if a.enabled]