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

388 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 -*-
"""
Screenshot management panel - manual screenshot, view history, open directory
"""
import os
import subprocess
import sys
from datetime import datetime
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
QListWidget, QListWidgetItem, QComboBox, QGroupBox, QMessageBox,
QFileDialog, QScrollArea, QFrame, QSizePolicy
)
from PyQt6.QtCore import Qt, pyqtSignal, QSize
from PyQt6.QtGui import QPixmap, QIcon
from config import get_config, SCREENSHOTS_DIR
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_SPACING, LIST_MIN_HEIGHT, ICON_SIZE_LARGE,
get_title_style
)
class ScreenshotWidget(QWidget):
"""Screenshot management panel"""
log_signal = pyqtSignal(str)
def __init__(self, parent=None):
super().__init__(parent)
self._worker = None
self._setup_ui()
self._refresh_screenshots()
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)
# ==================== Manual screenshot ====================
manual_group = QGroupBox("手动截图")
manual_layout = QVBoxLayout(manual_group)
manual_layout.setContentsMargins(GROUP_PADDING_SIDE, GROUP_PADDING_TOP, GROUP_PADDING_SIDE, GROUP_PADDING_BOTTOM)
manual_layout.setSpacing(GROUP_SPACING)
# Account row
account_layout = QHBoxLayout()
account_layout.setSpacing(FORM_H_SPACING)
account_label = QLabel("选择账号:")
account_label.setFixedWidth(FORM_LABEL_WIDTH)
account_layout.addWidget(account_label)
self.account_combo = QComboBox()
self.account_combo.setMinimumWidth(INPUT_MIN_WIDTH + 100)
self.account_combo.setMinimumHeight(INPUT_HEIGHT)
account_layout.addWidget(self.account_combo)
refresh_account_btn = QPushButton("🔄 刷新")
refresh_account_btn.setMinimumHeight(BUTTON_HEIGHT_NORMAL)
refresh_account_btn.clicked.connect(self._refresh_accounts)
account_layout.addWidget(refresh_account_btn)
account_layout.addStretch()
manual_layout.addLayout(account_layout)
# Browse type row
type_layout = QHBoxLayout()
type_layout.setSpacing(FORM_H_SPACING)
type_label = QLabel("浏览类型:")
type_label.setFixedWidth(FORM_LABEL_WIDTH)
type_layout.addWidget(type_label)
self.browse_type_combo = QComboBox()
self.browse_type_combo.addItems(["应读", "注册前未读"])
self.browse_type_combo.setMinimumWidth(INPUT_MIN_WIDTH)
self.browse_type_combo.setMinimumHeight(INPUT_HEIGHT)
type_layout.addWidget(self.browse_type_combo)
type_layout.addStretch()
manual_layout.addLayout(type_layout)
# Screenshot button
btn_layout = QHBoxLayout()
btn_layout.setSpacing(BUTTON_SPACING)
self.screenshot_btn = QPushButton("📷 执行截图")
self.screenshot_btn.setObjectName("primary")
self.screenshot_btn.setMinimumWidth(BUTTON_MIN_WIDTH)
self.screenshot_btn.setMinimumHeight(BUTTON_HEIGHT)
self.screenshot_btn.clicked.connect(self._take_screenshot)
btn_layout.addWidget(self.screenshot_btn)
btn_layout.addStretch()
manual_layout.addLayout(btn_layout)
layout.addWidget(manual_group)
# ==================== Screenshot history ====================
history_group = QGroupBox("截图历史")
history_group.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
history_layout = QVBoxLayout(history_group)
history_layout.setContentsMargins(GROUP_PADDING_SIDE, GROUP_PADDING_TOP, GROUP_PADDING_SIDE, GROUP_PADDING_BOTTOM)
history_layout.setSpacing(GROUP_SPACING)
# Toolbar
toolbar = QHBoxLayout()
toolbar.setSpacing(BUTTON_SPACING)
open_dir_btn = QPushButton("📂 打开目录")
open_dir_btn.setMinimumHeight(BUTTON_HEIGHT_NORMAL)
open_dir_btn.clicked.connect(self._open_screenshot_dir)
toolbar.addWidget(open_dir_btn)
toolbar.addStretch()
refresh_btn = QPushButton("🔄 刷新列表")
refresh_btn.setMinimumHeight(BUTTON_HEIGHT_NORMAL)
refresh_btn.clicked.connect(self._refresh_screenshots)
toolbar.addWidget(refresh_btn)
history_layout.addLayout(toolbar)
# Screenshot list - 图标模式,一行显示多个
self.screenshot_list = QListWidget()
self.screenshot_list.setViewMode(QListWidget.ViewMode.IconMode) # 图标模式
self.screenshot_list.setIconSize(QSize(150, 100)) # 缩略图大小
self.screenshot_list.setGridSize(QSize(170, 130)) # 网格大小(含文字)
self.screenshot_list.setResizeMode(QListWidget.ResizeMode.Adjust) # 自动调整
self.screenshot_list.setWrapping(True) # 自动换行
self.screenshot_list.setSpacing(8)
self.screenshot_list.setMinimumHeight(200)
self.screenshot_list.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
self.screenshot_list.itemDoubleClicked.connect(self._preview_screenshot)
history_layout.addWidget(self.screenshot_list, 1)
# Action buttons
action_layout = QHBoxLayout()
action_layout.setSpacing(BUTTON_SPACING)
self.delete_all_btn = QPushButton("🗑 删除全部")
self.delete_all_btn.setObjectName("danger")
self.delete_all_btn.setMinimumHeight(BUTTON_HEIGHT_NORMAL)
self.delete_all_btn.clicked.connect(self._delete_all)
action_layout.addWidget(self.delete_all_btn)
action_layout.addStretch()
self.preview_btn = QPushButton("👁 预览")
self.preview_btn.setMinimumHeight(BUTTON_HEIGHT_NORMAL)
self.preview_btn.clicked.connect(self._preview_selected)
action_layout.addWidget(self.preview_btn)
self.delete_btn = QPushButton("🗑 删除选中")
self.delete_btn.setMinimumHeight(BUTTON_HEIGHT_NORMAL)
self.delete_btn.clicked.connect(self._delete_selected)
action_layout.addWidget(self.delete_btn)
history_layout.addLayout(action_layout)
layout.addWidget(history_group, 1)
scroll.setWidget(content)
main_layout.addWidget(scroll)
# Load accounts
self._refresh_accounts()
def _refresh_accounts(self):
"""Refresh account list"""
self.account_combo.clear()
config = get_config()
for account in config.accounts:
if account.enabled:
display_name = account.remark if account.remark else account.username
self.account_combo.addItem(f"{display_name} ({account.username})", account)
def _refresh_screenshots(self):
"""Refresh screenshot list"""
self.screenshot_list.clear()
if not SCREENSHOTS_DIR.exists():
return
files = []
for f in SCREENSHOTS_DIR.iterdir():
if f.suffix.lower() in (".jpg", ".jpeg", ".png"):
files.append(f)
# Sort by modification time, newest first
files.sort(key=lambda x: x.stat().st_mtime, reverse=True)
for f in files[:50]: # Only show last 50
item = QListWidgetItem()
# 文件名太长时截断显示
name = f.stem # 不含扩展名
if len(name) > 15:
name = name[:12] + "..."
item.setText(name)
item.setData(Qt.ItemDataRole.UserRole, str(f))
item.setToolTip(f.name) # 完整文件名显示在提示中
# Try to load thumbnail
try:
pixmap = QPixmap(str(f))
if not pixmap.isNull():
# 缩放到合适大小显示缩略图
scaled = pixmap.scaled(
150, 100,
Qt.AspectRatioMode.KeepAspectRatio,
Qt.TransformationMode.SmoothTransformation
)
item.setIcon(QIcon(scaled))
except Exception as e:
print(f"加载缩略图失败: {f.name}, {e}")
self.screenshot_list.addItem(item)
def _take_screenshot(self):
"""Execute screenshot"""
if self.account_combo.currentIndex() < 0:
QMessageBox.information(self, "提示", "请先选择一个账号")
return
account = self.account_combo.currentData()
if not account:
return
browse_type = self.browse_type_combo.currentText()
password = decrypt_password(account.password) if is_encrypted(account.password) else account.password
self.screenshot_btn.setEnabled(False)
self.screenshot_btn.setText("截图中...")
self.log_signal.emit(f"开始截图: {account.username}")
from utils.worker import Worker
def do_screenshot(_signals=None, _should_stop=None):
from core.screenshot import take_screenshot
from config import get_config as _get_config
cfg = _get_config()
proxy_config = None
if cfg.proxy.enabled and cfg.proxy.server:
proxy_config = {"server": cfg.proxy.server}
result = take_screenshot(
account.username,
password,
browse_type,
remark=account.remark,
log_callback=lambda msg: _signals.log.emit(msg),
proxy_config=proxy_config
)
return result
def on_result(result):
self.screenshot_btn.setEnabled(True)
self.screenshot_btn.setText("📷 执行截图")
if result and result.success:
self.log_signal.emit(f"[OK] 截图成功: {result.filename}")
self._refresh_screenshots()
else:
error = result.error_message if result else "未知错误"
self.log_signal.emit(f"[FAIL] 截图失败: {error}")
def on_error(error):
self.screenshot_btn.setEnabled(True)
self.screenshot_btn.setText("📷 执行截图")
self.log_signal.emit(f"截图出错: {error}")
self._worker = Worker(do_screenshot)
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()
def _preview_screenshot(self, item):
"""Preview screenshot"""
filepath = item.data(Qt.ItemDataRole.UserRole)
if filepath and os.path.exists(filepath):
if sys.platform == "win32":
os.startfile(filepath)
elif sys.platform == "darwin":
subprocess.run(["open", filepath])
else:
subprocess.run(["xdg-open", filepath])
def _preview_selected(self):
"""Preview selected screenshot"""
current = self.screenshot_list.currentItem()
if current:
self._preview_screenshot(current)
else:
QMessageBox.information(self, "提示", "请先选择一个截图")
def _delete_selected(self):
"""Delete selected screenshot"""
current = self.screenshot_list.currentItem()
if not current:
QMessageBox.information(self, "提示", "请先选择一个截图")
return
filepath = current.data(Qt.ItemDataRole.UserRole)
filename = current.text()
reply = QMessageBox.question(
self, "确认删除",
f"确定要删除截图 {filename} 吗?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
if reply == QMessageBox.StandardButton.Yes:
try:
if os.path.exists(filepath):
os.remove(filepath)
self._refresh_screenshots()
self.log_signal.emit(f"已删除截图: {filename}")
except Exception as e:
QMessageBox.warning(self, "错误", f"删除失败: {e}")
def _delete_all(self):
"""Delete all screenshots"""
count = self.screenshot_list.count()
if count == 0:
QMessageBox.information(self, "提示", "没有截图可删除")
return
reply = QMessageBox.warning(
self, "⚠️ 确认删除全部",
f"确定要删除全部 {count} 张截图吗?\n\n此操作不可恢复!",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No
)
if reply == QMessageBox.StandardButton.Yes:
deleted = 0
failed = 0
for i in range(count):
item = self.screenshot_list.item(i)
filepath = item.data(Qt.ItemDataRole.UserRole)
try:
if os.path.exists(filepath):
os.remove(filepath)
deleted += 1
except Exception:
failed += 1
self._refresh_screenshots()
msg = f"已删除 {deleted} 张截图"
if failed > 0:
msg += f"{failed} 张删除失败"
self.log_signal.emit(msg)
def _open_screenshot_dir(self):
"""Open screenshot directory"""
SCREENSHOTS_DIR.mkdir(exist_ok=True)
if sys.platform == "win32":
os.startfile(str(SCREENSHOTS_DIR))
elif sys.platform == "darwin":
subprocess.run(["open", str(SCREENSHOTS_DIR)])
else:
subprocess.run(["xdg-open", str(SCREENSHOTS_DIR)])