#!/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]