Files
zsglpt-pc/ui/account_widget.py
237899745 9743186a9e feat: 添加依赖自动检测与安装、选项记忆、KDocs登录优化
- 新增依赖检测模块:启动时自动检测wkhtmltoimage和Playwright Chromium
- 新增依赖安装对话框:缺失时提示用户一键下载安装
- 修复选项记忆功能:浏览类型、自动截图、自动上传选项现在会保存
- 优化KDocs登录检测:未登录时自动切换到金山文档页面并显示二维码
- 简化日志输出:移除debug信息,保留用户友好的状态提示
- 新增账号变化信号:账号管理页面的修改会自动同步到浏览任务页面

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-19 01:28:06 +08:00

422 lines
17 KiB
Python
Raw Permalink 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)
accounts_changed = pyqtSignal() # 账号列表变化时发出通知
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} 添加成功")
self.accounts_changed.emit() # 通知其他模块账号列表已更新
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} 已更新")
self.accounts_changed.emit() # 通知其他模块账号列表已更新
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} 已删除")
self.accounts_changed.emit() # 通知其他模块账号列表已更新
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)
self.accounts_changed.emit() # 通知其他模块账号启用状态已更新
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]