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

265
ui/settings_widget.py Normal file
View File

@@ -0,0 +1,265 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Settings panel - wkhtmltoimage path, screenshot quality, theme
"""
import os
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QPushButton,
QGroupBox, QFormLayout, QSpinBox, QComboBox, QMessageBox,
QFileDialog, QScrollArea, QFrame
)
from PyQt6.QtCore import Qt, pyqtSignal
from config import get_config, save_config
from .constants import (
PAGE_PADDING, SECTION_SPACING, GROUP_PADDING_TOP, GROUP_PADDING_SIDE,
GROUP_PADDING_BOTTOM, FORM_ROW_SPACING, FORM_H_SPACING,
INPUT_HEIGHT, INPUT_MIN_WIDTH, BUTTON_HEIGHT, BUTTON_HEIGHT_NORMAL,
BUTTON_MIN_WIDTH, BUTTON_MIN_WIDTH_NORMAL, BUTTON_SPACING,
get_title_style
)
class SettingsWidget(QWidget):
"""Settings panel"""
log_signal = pyqtSignal(str)
theme_changed = pyqtSignal(str)
def __init__(self, parent=None):
super().__init__(parent)
self._setup_ui()
self._load_settings()
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(PAGE_PADDING, PAGE_PADDING, PAGE_PADDING, PAGE_PADDING)
layout.setSpacing(SECTION_SPACING)
# Title
title = QLabel("⚙️ 系统设置")
title.setStyleSheet(get_title_style())
layout.addWidget(title)
# ==================== Screenshot settings ====================
screenshot_group = QGroupBox("截图设置")
screenshot_layout = QFormLayout(screenshot_group)
screenshot_layout.setContentsMargins(GROUP_PADDING_SIDE, GROUP_PADDING_TOP, GROUP_PADDING_SIDE, GROUP_PADDING_BOTTOM)
screenshot_layout.setSpacing(FORM_ROW_SPACING)
screenshot_layout.setLabelAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
# wkhtmltoimage path
wkhtml_layout = QHBoxLayout()
wkhtml_layout.setSpacing(FORM_H_SPACING)
self.wkhtml_edit = QLineEdit()
self.wkhtml_edit.setPlaceholderText("留空则自动检测")
self.wkhtml_edit.setMinimumHeight(INPUT_HEIGHT)
wkhtml_layout.addWidget(self.wkhtml_edit)
browse_btn = QPushButton("浏览...")
browse_btn.setMinimumHeight(BUTTON_HEIGHT_NORMAL)
browse_btn.clicked.connect(self._browse_wkhtml)
wkhtml_layout.addWidget(browse_btn)
detect_btn = QPushButton("检测")
detect_btn.setMinimumHeight(BUTTON_HEIGHT_NORMAL)
detect_btn.clicked.connect(self._detect_wkhtml)
wkhtml_layout.addWidget(detect_btn)
screenshot_layout.addRow("wkhtmltoimage:", wkhtml_layout)
# Screenshot width
self.width_spin = QSpinBox()
self.width_spin.setRange(800, 3840)
self.width_spin.setSingleStep(100)
self.width_spin.setMinimumHeight(INPUT_HEIGHT)
self.width_spin.setMinimumWidth(130)
screenshot_layout.addRow("截图宽度:", self.width_spin)
# Screenshot height
self.height_spin = QSpinBox()
self.height_spin.setRange(600, 2160)
self.height_spin.setSingleStep(100)
self.height_spin.setMinimumHeight(INPUT_HEIGHT)
self.height_spin.setMinimumWidth(130)
screenshot_layout.addRow("截图高度:", self.height_spin)
# Quality
self.quality_spin = QSpinBox()
self.quality_spin.setRange(50, 100)
self.quality_spin.setMinimumHeight(INPUT_HEIGHT)
self.quality_spin.setMinimumWidth(130)
self.quality_spin.setSuffix(" %")
screenshot_layout.addRow("JPEG质量:", self.quality_spin)
# JS delay
self.js_delay_spin = QSpinBox()
self.js_delay_spin.setRange(1000, 10000)
self.js_delay_spin.setSingleStep(500)
self.js_delay_spin.setSuffix(" ms")
self.js_delay_spin.setMinimumHeight(INPUT_HEIGHT)
self.js_delay_spin.setMinimumWidth(130)
screenshot_layout.addRow("JS等待时间:", self.js_delay_spin)
layout.addWidget(screenshot_group)
# ==================== Theme settings ====================
theme_group = QGroupBox("主题设置")
theme_layout = QFormLayout(theme_group)
theme_layout.setContentsMargins(GROUP_PADDING_SIDE, GROUP_PADDING_TOP, GROUP_PADDING_SIDE, GROUP_PADDING_BOTTOM)
theme_layout.setSpacing(FORM_ROW_SPACING)
theme_layout.setLabelAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
self.theme_combo = QComboBox()
self.theme_combo.addItem("浅色主题", "light")
self.theme_combo.addItem("深色主题", "dark")
self.theme_combo.setMinimumHeight(INPUT_HEIGHT)
self.theme_combo.setMinimumWidth(INPUT_MIN_WIDTH)
self.theme_combo.currentIndexChanged.connect(self._on_theme_change)
theme_layout.addRow("界面主题:", self.theme_combo)
layout.addWidget(theme_group)
# ==================== Platform API settings ====================
api_group = QGroupBox("平台设置")
api_layout = QFormLayout(api_group)
api_layout.setContentsMargins(GROUP_PADDING_SIDE, GROUP_PADDING_TOP, GROUP_PADDING_SIDE, GROUP_PADDING_BOTTOM)
api_layout.setSpacing(FORM_ROW_SPACING)
api_layout.setLabelAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
self.base_url_edit = QLineEdit()
self.base_url_edit.setMinimumHeight(INPUT_HEIGHT)
api_layout.addRow("平台地址:", self.base_url_edit)
self.login_url_edit = QLineEdit()
self.login_url_edit.setMinimumHeight(INPUT_HEIGHT)
api_layout.addRow("登录地址:", self.login_url_edit)
layout.addWidget(api_group)
# ==================== Action buttons ====================
btn_layout = QHBoxLayout()
btn_layout.setSpacing(BUTTON_SPACING)
btn_layout.addStretch()
reset_btn = QPushButton("恢复默认")
reset_btn.setMinimumWidth(BUTTON_MIN_WIDTH_NORMAL)
reset_btn.setMinimumHeight(BUTTON_HEIGHT)
reset_btn.clicked.connect(self._reset_defaults)
btn_layout.addWidget(reset_btn)
save_btn = QPushButton("💾 保存设置")
save_btn.setObjectName("primary")
save_btn.setMinimumWidth(BUTTON_MIN_WIDTH)
save_btn.setMinimumHeight(BUTTON_HEIGHT)
save_btn.clicked.connect(self._save_settings)
btn_layout.addWidget(save_btn)
layout.addLayout(btn_layout)
layout.addStretch()
scroll.setWidget(content)
main_layout.addWidget(scroll)
def _load_settings(self):
"""Load settings from config"""
config = get_config()
# Screenshot settings
self.wkhtml_edit.setText(config.screenshot.wkhtmltoimage_path)
self.width_spin.setValue(config.screenshot.width)
self.height_spin.setValue(config.screenshot.height)
self.quality_spin.setValue(config.screenshot.quality)
self.js_delay_spin.setValue(config.screenshot.js_delay_ms)
# Theme
idx = self.theme_combo.findData(config.theme)
if idx >= 0:
self.theme_combo.setCurrentIndex(idx)
# API settings
self.base_url_edit.setText(config.zsgl.base_url)
self.login_url_edit.setText(config.zsgl.login_url)
def _save_settings(self):
"""Save settings to config"""
config = get_config()
# Screenshot settings
config.screenshot.wkhtmltoimage_path = self.wkhtml_edit.text().strip()
config.screenshot.width = self.width_spin.value()
config.screenshot.height = self.height_spin.value()
config.screenshot.quality = self.quality_spin.value()
config.screenshot.js_delay_ms = self.js_delay_spin.value()
# Theme
config.theme = self.theme_combo.currentData()
# API settings
config.zsgl.base_url = self.base_url_edit.text().strip()
config.zsgl.login_url = self.login_url_edit.text().strip()
save_config(config)
self.log_signal.emit("设置已保存")
QMessageBox.information(self, "提示", "设置已保存")
def _browse_wkhtml(self):
"""Browse for wkhtmltoimage executable"""
filepath, _ = QFileDialog.getOpenFileName(
self, "选择 wkhtmltoimage",
"",
"可执行文件 (*.exe);;所有文件 (*)"
)
if filepath:
self.wkhtml_edit.setText(filepath)
def _detect_wkhtml(self):
"""Detect wkhtmltoimage path"""
from core.screenshot import _resolve_wkhtmltoimage_path
path = _resolve_wkhtmltoimage_path()
if path:
self.wkhtml_edit.setText(path)
self.log_signal.emit(f"检测到 wkhtmltoimage: {path}")
QMessageBox.information(self, "检测成功", f"找到 wkhtmltoimage:\n{path}")
else:
self.log_signal.emit("未检测到 wkhtmltoimage")
QMessageBox.warning(self, "检测失败", "未找到 wkhtmltoimage请手动指定路径或安装该工具。")
def _on_theme_change(self, index):
"""Handle theme change"""
theme = self.theme_combo.currentData()
self.theme_changed.emit(theme)
def _reset_defaults(self):
"""Reset to default settings"""
reply = QMessageBox.question(
self, "确认",
"确定要恢复默认设置吗?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
if reply == QMessageBox.StandardButton.Yes:
self.wkhtml_edit.clear()
self.width_spin.setValue(1920)
self.height_spin.setValue(1080)
self.quality_spin.setValue(95)
self.js_delay_spin.setValue(3000)
self.theme_combo.setCurrentIndex(0) # 默认浅色主题
self.base_url_edit.setText("https://postoa.aidunsoft.com")
self.login_url_edit.setText("https://postoa.aidunsoft.com/admin/login.aspx")
self.log_signal.emit("已恢复默认设置")