Files
zsglpt-pc/ui/account_widget.py
237899745 83fef6dff2 feat: 知识管理平台精简版 - PyQt6桌面应用
主要功能:
- 账号管理:添加/编辑/删除账号,测试登录
- 浏览任务:批量浏览应读/选读内容并标记已读
- 截图管理:wkhtmltoimage截图,查看历史
- 金山文档:扫码登录/微信快捷登录,自动上传截图

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-18 22:16:36 +08:00

417 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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]