主要功能: - 账号管理:添加/编辑/删除账号,测试登录 - 浏览任务:批量浏览应读/选读内容并标记已读 - 截图管理:wkhtmltoimage截图,查看历史 - 金山文档:扫码登录/微信快捷登录,自动上传截图 技术栈: - PyQt6 GUI框架 - Playwright 浏览器自动化 - SQLite 本地数据存储 - wkhtmltoimage 网页截图 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
417 lines
16 KiB
Python
417 lines
16 KiB
Python
#!/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]
|