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

394 lines
16 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, save_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._load_task_config()
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; }")
self.browse_type_combo.currentTextChanged.connect(self._save_task_config)
options_layout.addWidget(self.browse_type_combo)
options_layout.addSpacing(20)
self.auto_screenshot_check = QCheckBox("浏览后自动截图")
self.auto_screenshot_check.setChecked(True)
self.auto_screenshot_check.stateChanged.connect(self._save_task_config)
options_layout.addWidget(self.auto_screenshot_check)
self.auto_upload_check = QCheckBox("截图后自动上传")
self.auto_upload_check.setChecked(False)
self.auto_upload_check.stateChanged.connect(self._save_task_config)
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)}个账号")
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
display_name = account.remark or account.username
_signals.progress.emit(i, f"正在处理: {display_name}")
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=None, 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,
}
if result.success:
_signals.log.emit(f"{display_name} 浏览完成 ({result.total_items}条)")
else:
_signals.log.emit(f"{display_name} 登录失败")
screenshot_path = None
if browse_result and browse_result.get("success") and auto_screenshot:
from core.screenshot import take_screenshot
# 静默截图
ss_result = take_screenshot(
account.username,
password,
browse_type,
remark=account.remark,
log_callback=None,
proxy_config=proxy_config
)
if ss_result.success:
screenshot_path = ss_result.filepath
_signals.log.emit(f"📸 {display_name} 截图完成")
else:
_signals.log.emit(f"⚠️ {display_name} 截图失败")
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:
uploader = get_kdocs_uploader()
uploader._log_callback = None # 静默模式
upload_result = uploader.upload_image(
screenshot_path,
cfg2.kdocs.unit,
account.remark or account.username
)
if upload_result.get("success"):
_signals.log.emit(f"📤 {display_name} 已上传文档")
else:
_signals.log.emit(f"⚠️ {display_name} 上传失败")
results.append({
"account": account.username,
"browse": browse_result,
"screenshot": screenshot_path,
})
_signals.progress.emit(i + 1, f"完成: {display_name}")
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("🎉 全部任务完成")
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)
def _load_task_config(self):
"""加载任务配置"""
config = get_config()
task = config.task
# 设置浏览类型
index = self.browse_type_combo.findText(task.browse_type)
if index >= 0:
self.browse_type_combo.setCurrentIndex(index)
# 设置选项(先断开信号避免循环保存)
self.auto_screenshot_check.blockSignals(True)
self.auto_upload_check.blockSignals(True)
self.auto_screenshot_check.setChecked(task.auto_screenshot)
self.auto_upload_check.setChecked(task.auto_upload)
self.auto_screenshot_check.blockSignals(False)
self.auto_upload_check.blockSignals(False)
def _save_task_config(self):
"""保存任务配置"""
config = get_config()
config.task.browse_type = self.browse_type_combo.currentText()
config.task.auto_screenshot = self.auto_screenshot_check.isChecked()
config.task.auto_upload = self.auto_upload_check.isChecked()
save_config(config)