Files
zsglpt-pc/ui/browse_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

357 lines
14 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 -*-
"""
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)