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

350 lines
13 KiB
Python
Raw Permalink 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 -*-
"""
KDocs panel - configure doc URL, columns, QR login, manual upload
"""
import base64
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit,
QPushButton, QGroupBox, QSpinBox, QCheckBox,
QMessageBox, QScrollArea, QFrame
)
from PyQt6.QtCore import Qt, pyqtSignal, QTimer
from PyQt6.QtGui import QPixmap
from config import get_config, save_config
from .constants import (
PAGE_PADDING, SECTION_SPACING, GROUP_PADDING_TOP, GROUP_PADDING_SIDE,
GROUP_PADDING_BOTTOM, GROUP_SPACING, FORM_ROW_SPACING, FORM_LABEL_WIDTH,
FORM_H_SPACING, INPUT_HEIGHT, INPUT_MIN_WIDTH, INPUT_WIDTH_SHORT,
BUTTON_HEIGHT, BUTTON_HEIGHT_NORMAL, BUTTON_MIN_WIDTH, BUTTON_SPACING,
QR_CODE_SIZE, get_title_style, get_status_style
)
class KDocsWidget(QWidget):
"""KDocs panel"""
log_signal = pyqtSignal(str)
def __init__(self, parent=None):
super().__init__(parent)
self._worker = None
self._login_check_timer = None
self._setup_ui()
self._load_config()
def _setup_ui(self):
main_layout = QVBoxLayout(self)
main_layout.setContentsMargins(16, 16, 16, 16)
main_layout.setSpacing(16)
# ==================== Title + Save button ====================
header_layout = QHBoxLayout()
title = QLabel("📤 金山文档上传")
title.setStyleSheet(get_title_style())
header_layout.addWidget(title)
header_layout.addStretch()
self.enable_check = QCheckBox("启用上传")
self.enable_check.stateChanged.connect(self._on_enable_changed)
header_layout.addWidget(self.enable_check)
self.save_btn = QPushButton("保存配置")
self.save_btn.setObjectName("primary")
self.save_btn.setFixedHeight(36)
self.save_btn.setMinimumWidth(100)
self.save_btn.clicked.connect(self._save_config)
header_layout.addWidget(self.save_btn)
main_layout.addLayout(header_layout)
# ==================== Document config ====================
config_group = QGroupBox("文档配置")
config_layout = QVBoxLayout(config_group)
config_layout.setContentsMargins(16, 24, 16, 16)
config_layout.setSpacing(12)
# Row 1: Doc URL
row1 = QHBoxLayout()
row1.setSpacing(10)
lbl1 = QLabel("文档链接:")
lbl1.setFixedWidth(70)
row1.addWidget(lbl1)
self.doc_url_edit = QLineEdit()
self.doc_url_edit.setPlaceholderText("https://kdocs.cn/l/xxxxxx")
self.doc_url_edit.setMinimumHeight(36)
row1.addWidget(self.doc_url_edit, 1)
config_layout.addLayout(row1)
# Row 2: Sheet name + Unit
row2 = QHBoxLayout()
row2.setSpacing(10)
lbl2 = QLabel("工作表:")
lbl2.setFixedWidth(70)
row2.addWidget(lbl2)
self.sheet_name_edit = QLineEdit()
self.sheet_name_edit.setPlaceholderText("Sheet1")
self.sheet_name_edit.setMinimumHeight(36)
self.sheet_name_edit.setFixedWidth(150)
row2.addWidget(self.sheet_name_edit)
row2.addSpacing(30)
lbl2b = QLabel("所属县区:")
lbl2b.setFixedWidth(70)
row2.addWidget(lbl2b)
self.unit_edit = QLineEdit()
self.unit_edit.setPlaceholderText("XX县")
self.unit_edit.setMinimumHeight(36)
row2.addWidget(self.unit_edit, 1)
config_layout.addLayout(row2)
# Row 3: Columns
row3 = QHBoxLayout()
row3.setSpacing(10)
lbl3 = QLabel("县区列:")
lbl3.setFixedWidth(70)
row3.addWidget(lbl3)
self.unit_col_edit = QLineEdit()
self.unit_col_edit.setFixedWidth(80)
self.unit_col_edit.setMinimumHeight(36)
self.unit_col_edit.setPlaceholderText("A")
row3.addWidget(self.unit_col_edit)
row3.addSpacing(20)
lbl3b = QLabel("姓名列:")
lbl3b.setFixedWidth(60)
row3.addWidget(lbl3b)
self.name_col_edit = QLineEdit()
self.name_col_edit.setFixedWidth(80)
self.name_col_edit.setMinimumHeight(36)
self.name_col_edit.setPlaceholderText("C")
row3.addWidget(self.name_col_edit)
row3.addSpacing(20)
lbl3c = QLabel("图片列:")
lbl3c.setFixedWidth(60)
row3.addWidget(lbl3c)
self.image_col_edit = QLineEdit()
self.image_col_edit.setFixedWidth(80)
self.image_col_edit.setMinimumHeight(36)
self.image_col_edit.setPlaceholderText("D")
row3.addWidget(self.image_col_edit)
row3.addStretch()
config_layout.addLayout(row3)
# Row 4: Row range - SpinBox加宽确保"不限制"显示完整
row4 = QHBoxLayout()
row4.setSpacing(10)
lbl4 = QLabel("起始行:")
lbl4.setFixedWidth(70)
row4.addWidget(lbl4)
self.row_start_spin = QSpinBox()
self.row_start_spin.setRange(0, 10000)
self.row_start_spin.setSpecialValueText("不限制")
self.row_start_spin.setFixedWidth(120)
self.row_start_spin.setMinimumHeight(36)
row4.addWidget(self.row_start_spin)
row4.addSpacing(20)
lbl4b = QLabel("结束行:")
lbl4b.setFixedWidth(60)
row4.addWidget(lbl4b)
self.row_end_spin = QSpinBox()
self.row_end_spin.setRange(0, 10000)
self.row_end_spin.setSpecialValueText("不限制")
self.row_end_spin.setFixedWidth(120)
self.row_end_spin.setMinimumHeight(36)
row4.addWidget(self.row_end_spin)
row4.addStretch()
config_layout.addLayout(row4)
main_layout.addWidget(config_group)
# ==================== Login status ====================
login_group = QGroupBox("登录状态")
login_layout = QHBoxLayout(login_group)
login_layout.setContentsMargins(16, 24, 16, 16)
login_layout.setSpacing(20)
# QR code
self.qr_label = QLabel()
self.qr_label.setFixedSize(150, 150)
self.qr_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.qr_label.setStyleSheet("border: 1px solid #d0d0d0; border-radius: 6px; background: #fafafa;")
self.qr_label.setText("点击获取二维码")
login_layout.addWidget(self.qr_label)
# Status and buttons
status_btn_layout = QVBoxLayout()
status_btn_layout.setSpacing(10)
self.status_label = QLabel("未登录")
self.status_label.setStyleSheet(get_status_style(False))
status_btn_layout.addWidget(self.status_label)
status_btn_layout.addStretch()
self.get_qr_btn = QPushButton("获取二维码")
self.get_qr_btn.setObjectName("primary")
self.get_qr_btn.setFixedHeight(36)
self.get_qr_btn.setMinimumWidth(120)
self.get_qr_btn.clicked.connect(self._get_qr_code)
status_btn_layout.addWidget(self.get_qr_btn)
self.clear_login_btn = QPushButton("清除登录")
self.clear_login_btn.setFixedHeight(36)
self.clear_login_btn.setMinimumWidth(120)
self.clear_login_btn.clicked.connect(self._clear_login)
status_btn_layout.addWidget(self.clear_login_btn)
status_btn_layout.addStretch()
login_layout.addLayout(status_btn_layout)
login_layout.addStretch()
main_layout.addWidget(login_group)
main_layout.addStretch()
def _load_config(self):
"""Load config"""
config = get_config()
kdocs = config.kdocs
self.enable_check.setChecked(kdocs.enabled)
self.doc_url_edit.setText(kdocs.doc_url)
self.sheet_name_edit.setText(kdocs.sheet_name)
self.unit_col_edit.setText(kdocs.unit_column)
self.name_col_edit.setText(kdocs.name_column)
self.image_col_edit.setText(kdocs.image_column)
self.row_start_spin.setValue(kdocs.row_start)
self.row_end_spin.setValue(kdocs.row_end)
self.unit_edit.setText(kdocs.unit)
self._on_enable_changed(Qt.CheckState.Checked.value if kdocs.enabled else Qt.CheckState.Unchecked.value)
def _save_config(self):
"""Save config"""
config = get_config()
config.kdocs.enabled = self.enable_check.isChecked()
config.kdocs.doc_url = self.doc_url_edit.text().strip()
config.kdocs.sheet_name = self.sheet_name_edit.text().strip()
config.kdocs.unit_column = self.unit_col_edit.text().strip().upper() or "A"
config.kdocs.name_column = self.name_col_edit.text().strip().upper() or "C"
config.kdocs.image_column = self.image_col_edit.text().strip().upper() or "D"
config.kdocs.row_start = self.row_start_spin.value()
config.kdocs.row_end = self.row_end_spin.value()
config.kdocs.unit = self.unit_edit.text().strip()
save_config(config)
self.log_signal.emit("金山文档配置已保存")
QMessageBox.information(self, "提示", "配置已保存")
def _on_enable_changed(self, state):
"""Enable state changed"""
pass # Can disable/enable other controls if needed
def _get_qr_code(self):
"""Get login QR code and poll for login status"""
self.get_qr_btn.setEnabled(False)
self.qr_label.setText("获取中...")
self.log_signal.emit("🔄 正在获取登录二维码...")
# 停止之前的检查
if self._login_check_timer:
self._login_check_timer.stop()
self._login_check_timer = None
from utils.worker import Worker
def get_qr_and_wait_login(_signals=None, _should_stop=None):
"""获取二维码并轮询等待登录"""
from core.kdocs_uploader import get_kdocs_uploader
import time
uploader = get_kdocs_uploader()
uploader._log_callback = None # 静默模式
# 1. 获取二维码
result = uploader.request_qr(force=True)
if not result.get("success"):
return result
if result.get("logged_in"):
return result
# 2. 发送二维码图片(通过信号)
qr_image = result.get("qr_image", "")
if qr_image:
_signals.screenshot_ready.emit(qr_image) # 复用这个信号传递二维码
# 3. 轮询检查登录状态
max_wait = 120
check_interval = 3
waited = 0
while waited < max_wait:
if _should_stop and _should_stop():
return {"success": False, "error": "用户取消"}
time.sleep(check_interval)
waited += check_interval
check_result = uploader.check_login_status()
if check_result.get("logged_in"):
return {"success": True, "logged_in": True}
return {"success": False, "error": "登录超时,请重新获取二维码"}
def on_qr_ready(qr_base64):
"""二维码准备好了,显示出来"""
if qr_base64:
qr_bytes = base64.b64decode(qr_base64)
pixmap = QPixmap()
pixmap.loadFromData(qr_bytes)
scaled = pixmap.scaled(140, 140, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation)
self.qr_label.setPixmap(scaled)
self.log_signal.emit("📱 请使用微信扫描二维码登录")
def on_result(result):
self.get_qr_btn.setEnabled(True)
if result and result.get("success"):
if result.get("logged_in"):
self.status_label.setText("✅ 已登录")
self.status_label.setStyleSheet(get_status_style(True))
self.qr_label.setText("登录成功!")
self.log_signal.emit("✅ 金山文档登录成功")
else:
error = result.get("error", "未知错误") if result else "未知错误"
self.qr_label.setText(f"失败")
self.status_label.setText("❌ 未登录")
self.status_label.setStyleSheet(get_status_style(False))
self.log_signal.emit(f"❌ 登录失败: {error}")
def on_error(error):
self.get_qr_btn.setEnabled(True)
self.qr_label.setText("获取失败")
self.log_signal.emit(f"❌ 二维码获取失败: {error}")
self._worker = Worker(get_qr_and_wait_login)
self._worker.signals.log.connect(self.log_signal.emit)
self._worker.signals.screenshot_ready.connect(on_qr_ready) # 用于接收二维码
self._worker.signals.result.connect(on_result)
self._worker.signals.error.connect(on_error)
self._worker.start()
def _clear_login(self):
"""Clear login status"""
from core.kdocs_uploader import get_kdocs_uploader
uploader = get_kdocs_uploader()
uploader.clear_login()
self.status_label.setText("❌ 未登录")
self.status_label.setStyleSheet(get_status_style(False))
self.qr_label.setText("点击获取二维码")
self.log_signal.emit("🔓 登录状态已清除")
if self._login_check_timer:
self._login_check_timer.stop()
self._login_check_timer = None