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

356
ui/browse_widget.py Normal file
View File

@@ -0,0 +1,356 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Browse task panel - select accounts, browsing type, execute task with progress
"""
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QComboBox,
QPushButton, QProgressBar, QGroupBox, QCheckBox, QListWidget,
QListWidgetItem, QMessageBox, QScrollArea, QFrame, QSizePolicy
)
from PyQt6.QtCore import Qt, pyqtSignal, QSize
from config import get_config, AccountConfig
from utils.crypto import decrypt_password, is_encrypted
from .constants import (
PAGE_PADDING, SECTION_SPACING, GROUP_PADDING_TOP, GROUP_PADDING_SIDE,
GROUP_PADDING_BOTTOM, GROUP_SPACING, FORM_LABEL_WIDTH, FORM_H_SPACING,
INPUT_HEIGHT, INPUT_MIN_WIDTH, BUTTON_HEIGHT, BUTTON_HEIGHT_NORMAL,
BUTTON_MIN_WIDTH, BUTTON_MIN_WIDTH_NORMAL, BUTTON_SPACING,
LIST_MIN_HEIGHT, LIST_ITEM_HEIGHT, PROGRESS_HEIGHT,
get_title_style, get_help_text_style
)
class BrowseWidget(QWidget):
"""Browse task panel"""
log_signal = pyqtSignal(str)
def __init__(self, parent=None):
super().__init__(parent)
self._worker = None
self._is_running = False
self._setup_ui()
self._refresh_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(16, 12, 16, 12)
layout.setSpacing(10)
# ==================== Title + Control buttons ====================
header_layout = QHBoxLayout()
header_layout.setSpacing(SECTION_SPACING)
title = QLabel("🔄 浏览任务")
title.setStyleSheet(get_title_style())
header_layout.addWidget(title)
header_layout.addStretch()
self.stop_btn = QPushButton("⏹ 停止")
self.stop_btn.setMinimumWidth(BUTTON_MIN_WIDTH_NORMAL)
self.stop_btn.setMinimumHeight(BUTTON_HEIGHT)
self.stop_btn.setEnabled(False)
self.stop_btn.clicked.connect(self._stop_task)
header_layout.addWidget(self.stop_btn)
self.start_btn = QPushButton("▶ 开始任务")
self.start_btn.setObjectName("primary")
self.start_btn.setMinimumWidth(BUTTON_MIN_WIDTH)
self.start_btn.setMinimumHeight(BUTTON_HEIGHT)
self.start_btn.clicked.connect(self._start_task)
header_layout.addWidget(self.start_btn)
layout.addLayout(header_layout)
# ==================== Progress area (compact) ====================
progress_group = QGroupBox("任务进度")
progress_layout = QVBoxLayout(progress_group)
progress_layout.setContentsMargins(12, 8, 12, 8)
progress_layout.setSpacing(6)
# Current task + stats in one row
task_row = QHBoxLayout()
self.current_task_label = QLabel("当前任务: 无")
task_row.addWidget(self.current_task_label)
task_row.addStretch()
self.stats_label = QLabel("已处理: 0 条内容0 个附件")
self.stats_label.setStyleSheet(get_help_text_style())
task_row.addWidget(self.stats_label)
progress_layout.addLayout(task_row)
# Progress bars in one row
prog_row = QHBoxLayout()
prog_row.setSpacing(12)
prog_row.addWidget(QLabel("总体:"))
self.total_progress = QProgressBar()
self.total_progress.setValue(0)
self.total_progress.setFixedHeight(18)
prog_row.addWidget(self.total_progress, 1)
prog_row.addWidget(QLabel("当前:"))
self.account_progress = QProgressBar()
self.account_progress.setValue(0)
self.account_progress.setFixedHeight(18)
prog_row.addWidget(self.account_progress, 1)
progress_layout.addLayout(prog_row)
layout.addWidget(progress_group)
# ==================== Browse options (compact, one row) ====================
options_group = QGroupBox("浏览选项")
options_layout = QHBoxLayout(options_group)
options_layout.setContentsMargins(12, 8, 12, 8)
options_layout.setSpacing(16)
options_layout.addWidget(QLabel("浏览类型:"))
self.browse_type_combo = QComboBox()
self.browse_type_combo.addItems(["应读", "注册前未读"])
self.browse_type_combo.setMinimumWidth(140)
self.browse_type_combo.setFixedHeight(32)
self.browse_type_combo.setStyleSheet("QComboBox { padding-left: 10px; }")
options_layout.addWidget(self.browse_type_combo)
options_layout.addSpacing(20)
self.auto_screenshot_check = QCheckBox("浏览后自动截图")
self.auto_screenshot_check.setChecked(True)
options_layout.addWidget(self.auto_screenshot_check)
self.auto_upload_check = QCheckBox("截图后自动上传")
self.auto_upload_check.setChecked(False)
options_layout.addWidget(self.auto_upload_check)
options_layout.addStretch()
layout.addWidget(options_group)
# ==================== Account selection (compact) ====================
account_group = QGroupBox("选择账号")
account_group.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
account_layout = QVBoxLayout(account_group)
account_layout.setContentsMargins(12, 8, 12, 8)
account_layout.setSpacing(6)
# Toolbar - compact buttons with proper width
toolbar = QHBoxLayout()
toolbar.setSpacing(8)
self.select_all_btn = QPushButton("全选")
self.select_all_btn.setFixedSize(70, 28)
self.select_all_btn.setStyleSheet("QPushButton { padding: 2px 8px; }")
self.select_all_btn.clicked.connect(self._select_all)
toolbar.addWidget(self.select_all_btn)
self.deselect_all_btn = QPushButton("取消全选")
self.deselect_all_btn.setFixedSize(90, 28)
self.deselect_all_btn.setStyleSheet("QPushButton { padding: 2px 8px; }")
self.deselect_all_btn.clicked.connect(self._deselect_all)
toolbar.addWidget(self.deselect_all_btn)
toolbar.addStretch()
self.refresh_btn = QPushButton("刷新")
self.refresh_btn.setFixedSize(70, 28)
self.refresh_btn.setStyleSheet("QPushButton { padding: 2px 8px; }")
self.refresh_btn.clicked.connect(self._refresh_accounts)
toolbar.addWidget(self.refresh_btn)
account_layout.addLayout(toolbar)
# Account list - compact
self.account_list = QListWidget()
self.account_list.setMinimumHeight(120)
self.account_list.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
self.account_list.setSpacing(1)
account_layout.addWidget(self.account_list, 1)
layout.addWidget(account_group, 1)
scroll.setWidget(content)
main_layout.addWidget(scroll)
def _refresh_accounts(self):
"""Refresh account list"""
self.account_list.clear()
config = get_config()
for account in config.accounts:
if account.enabled:
item = QListWidgetItem()
item.setFlags(item.flags() | Qt.ItemFlag.ItemIsUserCheckable)
item.setCheckState(Qt.CheckState.Checked)
display_name = account.remark if account.remark else account.username
item.setText(f"{display_name} ({account.username})")
item.setData(Qt.ItemDataRole.UserRole, account)
item.setSizeHint(item.sizeHint().expandedTo(QSize(0, LIST_ITEM_HEIGHT)))
self.account_list.addItem(item)
def _select_all(self):
"""Select all"""
for i in range(self.account_list.count()):
self.account_list.item(i).setCheckState(Qt.CheckState.Checked)
def _deselect_all(self):
"""Deselect all"""
for i in range(self.account_list.count()):
self.account_list.item(i).setCheckState(Qt.CheckState.Unchecked)
def _get_selected_accounts(self) -> list:
"""Get selected accounts"""
accounts = []
for i in range(self.account_list.count()):
item = self.account_list.item(i)
if item.checkState() == Qt.CheckState.Checked:
account = item.data(Qt.ItemDataRole.UserRole)
accounts.append(account)
return accounts
def _start_task(self):
"""Start task"""
accounts = self._get_selected_accounts()
if not accounts:
QMessageBox.information(self, "提示", "请选择至少一个账号")
return
self._is_running = True
self.start_btn.setEnabled(False)
self.stop_btn.setEnabled(True)
self._stop_requested = False
browse_type = self.browse_type_combo.currentText()
auto_screenshot = self.auto_screenshot_check.isChecked()
auto_upload = self.auto_upload_check.isChecked()
self.total_progress.setMaximum(len(accounts))
self.total_progress.setValue(0)
self.log_signal.emit(f"开始任务: {len(accounts)} 个账号, 类型: {browse_type}")
from utils.worker import Worker
def run_tasks(_signals=None, _should_stop=None):
results = []
for i, account in enumerate(accounts):
if _should_stop and _should_stop():
break
_signals.log.emit(f"[{i+1}/{len(accounts)}] 处理账号: {account.username}")
_signals.progress.emit(i, f"正在处理: {account.username}")
password = decrypt_password(account.password) if is_encrypted(account.password) else account.password
from core.api_browser import APIBrowser
from config import get_config as _get_config
proxy_config = None
cfg = _get_config()
if cfg.proxy.enabled and cfg.proxy.server:
proxy_config = {"server": cfg.proxy.server}
browse_result = None
with APIBrowser(log_callback=lambda msg: _signals.log.emit(msg), proxy_config=proxy_config) as browser:
if browser.login(account.username, password):
browser.save_cookies_for_screenshot(account.username)
result = browser.browse_content(
browse_type,
should_stop_callback=_should_stop,
)
browse_result = {
"success": result.success,
"total_items": result.total_items,
"total_attachments": result.total_attachments,
}
screenshot_path = None
if browse_result and browse_result.get("success") and auto_screenshot:
from core.screenshot import take_screenshot
_signals.log.emit("正在截图...")
ss_result = take_screenshot(
account.username,
password,
browse_type,
remark=account.remark,
log_callback=lambda msg: _signals.log.emit(msg),
proxy_config=proxy_config
)
if ss_result.success:
screenshot_path = ss_result.filepath
if screenshot_path and auto_upload:
from core.kdocs_uploader import get_kdocs_uploader
from config import get_config as _get_config2
cfg2 = _get_config2()
if cfg2.kdocs.enabled:
_signals.log.emit("正在上传到金山文档...")
uploader = get_kdocs_uploader()
uploader._log_callback = lambda msg: _signals.log.emit(msg)
upload_result = uploader.upload_image(
screenshot_path,
cfg2.kdocs.unit,
account.remark or account.username
)
if upload_result.get("success"):
_signals.log.emit("[OK] 上传成功")
else:
_signals.log.emit(f"上传失败: {upload_result.get('error', '未知错误')}")
results.append({
"account": account.username,
"browse": browse_result,
"screenshot": screenshot_path,
})
_signals.progress.emit(i + 1, f"完成: {account.username}")
return results
def on_progress(current, message):
self.total_progress.setValue(current)
self.current_task_label.setText(f"当前任务: {message}")
def on_finished(success, message):
self._is_running = False
self.start_btn.setEnabled(True)
self.stop_btn.setEnabled(False)
self.current_task_label.setText("当前任务: 完成")
self.log_signal.emit(f"任务完成: {message}")
def on_result(results):
if results:
total_items = sum(r.get("browse", {}).get("total_items", 0) for r in results if r.get("browse"))
total_attachments = sum(r.get("browse", {}).get("total_attachments", 0) for r in results if r.get("browse"))
self.stats_label.setText(f"已处理: {total_items} 条内容,{total_attachments} 个附件")
self._worker = Worker(run_tasks)
self._worker.signals.log.connect(self.log_signal.emit)
self._worker.signals.progress.connect(on_progress)
self._worker.signals.finished.connect(on_finished)
self._worker.signals.result.connect(on_result)
self._worker.start()
def _stop_task(self):
"""Stop task"""
if self._worker:
self._worker.stop()
self.log_signal.emit("正在停止任务...")
self.stop_btn.setEnabled(False)