#!/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() self._check_wechat_process() # 检测微信进程 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.quick_login_btn = QPushButton("⚡ 微信快捷登录") self.quick_login_btn.setFixedHeight(36) self.quick_login_btn.setMinimumWidth(130) self.quick_login_btn.setStyleSheet(""" QPushButton { background-color: #07C160; color: white; border: none; border-radius: 4px; font-weight: bold; } QPushButton:hover { background-color: #06AD56; } QPushButton:disabled { background-color: #ccc; } """) self.quick_login_btn.clicked.connect(self._quick_login) self.quick_login_btn.setVisible(False) # 默认隐藏 status_btn_layout.addWidget(self.quick_login_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): """获取二维码并轮询等待登录(在同一个线程中完成所有Playwright操作)""" from core.kdocs_uploader import get_kdocs_uploader import time uploader = get_kdocs_uploader() uploader._log_callback = lambda msg: _signals.log.emit(msg) if _signals else 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 # 最多等待120秒 check_interval = 3 # 每3秒检查一次 waited = 0 check_count = 0 while waited < max_wait: if _should_stop and _should_stop(): return {"success": False, "error": "用户取消"} time.sleep(check_interval) waited += check_interval check_count += 1 # 检查登录状态(在同一个线程中,不会有线程问题) if _signals: _signals.log.emit(f"[KDocs] 检查登录状态... ({check_count})") check_result = uploader.check_login_status() if check_result.get("logged_in"): return {"success": True, "logged_in": True} # 每30秒提醒一下用户 if waited % 30 == 0 and _signals: remaining = max_wait - waited _signals.log.emit(f"[KDocs] 等待扫码登录...(剩余{remaining}秒)") return {"success": False, "error": "登录超时(120秒),请重新获取二维码"} 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("请使用微信扫描二维码登录(120秒内有效)") 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: # 不应该走到这里,因为上面会返回logged_in=True pass else: error = result.get("error", "未知错误") if result else "未知错误" self.qr_label.setText(f"失败: {error}") 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(f"错误: {error}") 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 def _check_wechat_process(self) -> bool: """检测微信进程是否运行 Returns: bool: 微信是否在运行 """ import subprocess try: # Windows下检测微信进程(可能是Weixin.exe或WeChat.exe) result = subprocess.run( ['tasklist'], capture_output=True, text=True, timeout=5 ) output_lower = result.stdout.lower() # 检测多种可能的微信进程名 is_running = 'weixin.exe' in output_lower or 'wechat.exe' in output_lower if is_running: self.log_signal.emit("检测到微信进程,可使用快捷登录") # 始终显示快捷登录按钮,让用户自己决定 self.quick_login_btn.setVisible(True) return is_running except Exception: # 检测失败也显示按钮 self.quick_login_btn.setVisible(True) return False def _quick_login(self): """微信快捷登录""" # 先检测微信是否运行 is_wechat_running = self._check_wechat_process() if not is_wechat_running: self.log_signal.emit("⚠️ 未检测到微信进程,快捷登录可能失败。建议先打开微信客户端。") self.quick_login_btn.setEnabled(False) self.quick_login_btn.setText("登录中...") self.log_signal.emit("正在执行微信快捷登录...") from utils.worker import Worker def do_quick_login(_signals=None, _should_stop=None): from core.kdocs_uploader import get_kdocs_uploader import time uploader = get_kdocs_uploader() uploader._log_callback = lambda msg: _signals.log.emit(msg) if _signals else None # 强制重新登录,使用快捷登录 from config import KDOCS_LOGIN_STATE_FILE try: if KDOCS_LOGIN_STATE_FILE.exists(): KDOCS_LOGIN_STATE_FILE.unlink() except Exception: pass uploader._cleanup_browser() # 启动浏览器并执行快捷登录 if not uploader._ensure_playwright(use_storage_state=False): return {"success": False, "error": uploader._last_error or "浏览器不可用"} from config import get_config config = get_config() doc_url = config.kdocs.doc_url.strip() if not doc_url: return {"success": False, "error": "未配置金山文档链接"} if not uploader._open_document(doc_url): return {"success": False, "error": uploader._last_error or "打开文档失败"} # 执行登录流程,优先使用微信快捷登录 agree_names = ["同意", "同意并继续", "我同意", "确定", "确认"] for loop_idx in range(15): current_url = uploader._page.url uploader.log(f"[KDocs] 循环{loop_idx+1}: URL={current_url[:60]}...") # 【优先】检查是否有"登录并加入编辑"按钮(可能在任何页面弹出) try: join_btn = uploader._page.get_by_role("button", name="登录并加入编辑") btn_count = join_btn.count() if btn_count > 0: uploader.log(f"[KDocs] 找到'登录并加入编辑'按钮,直接点击") join_btn.first.click(timeout=3000) time.sleep(2) continue except Exception as e: uploader.log(f"[KDocs] 点击'登录并加入编辑'失败: {e}") # 检查是否已到达文档页面(且没有登录相关元素) if "kdocs.cn/l/" in current_url and "account.wps.cn" not in current_url: uploader.log(f"[KDocs] URL是文档页面,检查是否真的登录成功...") time.sleep(1) # 等待页面稳定 # 检查页面上是否还有登录相关元素 has_login_elements = False # 检查是否有"立即登录"按钮 try: login_btn = uploader._page.get_by_role("button", name="立即登录") if login_btn.count() > 0 and login_btn.first.is_visible(timeout=500): uploader.log("[KDocs] 页面上有'立即登录'按钮,未真正登录") has_login_elements = True except Exception: pass # 检查是否有二维码(说明还在登录页) if not has_login_elements: try: qr_text = uploader._page.get_by_text("微信扫码登录", exact=False) if qr_text.count() > 0 and qr_text.first.is_visible(timeout=500): uploader.log("[KDocs] 页面上有'微信扫码登录',未真正登录") has_login_elements = True except Exception: pass # 检查是否有"微信快捷登录"按钮 if not has_login_elements: try: quick_btn = uploader._page.get_by_text("微信快捷登录", exact=False) if quick_btn.count() > 0 and quick_btn.first.is_visible(timeout=500): uploader.log("[KDocs] 页面上有'微信快捷登录'按钮,未真正登录") has_login_elements = True except Exception: pass if has_login_elements: uploader.log("[KDocs] 检测到登录元素,继续处理登录流程...") # 不return,继续下面的登录处理 else: uploader.log("[KDocs] 确认登录成功!") uploader._logged_in = True uploader._save_login_state() return {"success": True, "logged_in": True} clicked = False # 点击同意按钮 for name in agree_names: try: btn = uploader._page.get_by_role("button", name=name) if btn.count() > 0 and btn.first.is_visible(timeout=300): uploader.log(f"[KDocs] 点击同意: {name}") btn.first.click() time.sleep(1) clicked = True break except Exception: pass if clicked: continue # 检查页面上是否有"微信快捷登录"按钮(可能URL是kdocs但内容是登录页) has_quick_login_btn = False try: quick_btn = uploader._page.get_by_text("微信快捷登录", exact=False) if quick_btn.count() > 0 and quick_btn.first.is_visible(timeout=300): has_quick_login_btn = True except Exception: pass # 在登录页面,先勾选协议复选框,再点击微信快捷登录 # 条件:URL是account.wps.cn 或者 页面上有微信快捷登录按钮 if "account.wps.cn" in current_url or has_quick_login_btn: uploader.log("[KDocs] 检测到登录页面,尝试勾选协议...") # 1. 先勾选"我已阅读并同意"复选框 try: # 尝试多种方式找到协议复选框 checkbox_selectors = [ "input[type='checkbox']", "span.checkbox", "[class*='checkbox']", "[class*='agree']", ] checkbox_clicked = False for selector in checkbox_selectors: try: checkboxes = uploader._page.locator(selector) if checkboxes.count() > 0: for i in range(min(checkboxes.count(), 3)): cb = checkboxes.nth(i) if cb.is_visible(timeout=200): # 检查是否已勾选 is_checked = cb.is_checked() if hasattr(cb, 'is_checked') else False if not is_checked: uploader.log("[KDocs] 勾选协议复选框") cb.click() time.sleep(0.5) checkbox_clicked = True break if checkbox_clicked: break except Exception: pass # 也尝试点击包含"我已阅读"的文本区域 if not checkbox_clicked: try: agree_text = uploader._page.get_by_text("我已阅读并同意", exact=False) if agree_text.count() > 0 and agree_text.first.is_visible(timeout=300): uploader.log("[KDocs] 点击协议文本区域") agree_text.first.click() time.sleep(0.5) except Exception: pass except Exception as e: uploader.log(f"[KDocs] 勾选协议失败: {e}") # 2. 点击微信快捷登录按钮 try: quick = uploader._page.get_by_text("微信快捷登录", exact=False) if quick.count() > 0 and quick.first.is_visible(timeout=500): uploader.log("[KDocs] 点击微信快捷登录") quick.first.click() time.sleep(4) # 等待快捷登录完成 clicked = True continue except Exception: pass # 点击立即登录 try: btn = uploader._page.get_by_role("button", name="立即登录") if btn.count() > 0 and btn.first.is_visible(timeout=500): uploader.log("[KDocs] 点击立即登录") btn.first.click() time.sleep(2) clicked = True continue except Exception: pass if not clicked: break # 最终检查 if "kdocs.cn/l/" in uploader._page.url: uploader._logged_in = True uploader._save_login_state() return {"success": True, "logged_in": True} else: return {"success": False, "error": "快捷登录失败,请尝试扫码登录"} def on_result(result): self.quick_login_btn.setEnabled(True) self.quick_login_btn.setText("⚡ 微信快捷登录") if result and result.get("success") and 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.log_signal.emit(f"快捷登录失败: {error}") def on_error(error): self.quick_login_btn.setEnabled(True) self.quick_login_btn.setText("⚡ 微信快捷登录") self.log_signal.emit(f"快捷登录出错: {error}") self._worker = Worker(do_quick_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()