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

120
ui/log_widget.py Normal file
View File

@@ -0,0 +1,120 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Log display panel - collapsible log output area
"""
from datetime import datetime
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QPlainTextEdit,
QPushButton, QFrame, QLabel
)
from PyQt6.QtCore import Qt, pyqtSlot
from PyQt6.QtGui import QTextCursor
from .constants import LOG_MIN_HEIGHT, BUTTON_HEIGHT_SMALL, BUTTON_SPACING
class LogWidget(QWidget):
"""Log display panel"""
def __init__(self, parent=None):
super().__init__(parent)
self.setObjectName("log_widget")
self._collapsed = False
self._max_lines = 500
self._setup_ui()
def _setup_ui(self):
"""Setup UI"""
layout = QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
# Header bar
header = QFrame()
header.setFixedHeight(36)
header.setStyleSheet("background-color: rgba(0,0,0,0.03); border-bottom: 1px solid rgba(0,0,0,0.08);")
header_layout = QHBoxLayout(header)
header_layout.setContentsMargins(16, 0, 16, 0)
header_layout.setSpacing(BUTTON_SPACING)
# Title
title = QLabel("📜 日志输出")
title.setStyleSheet("font-weight: bold; font-size: 13px;")
header_layout.addWidget(title)
header_layout.addStretch()
# Clear button
self._clear_btn = QPushButton("清空")
self._clear_btn.setFixedWidth(60)
self._clear_btn.setFixedHeight(BUTTON_HEIGHT_SMALL)
self._clear_btn.clicked.connect(self.clear)
header_layout.addWidget(self._clear_btn)
# Toggle button
self._toggle_btn = QPushButton("")
self._toggle_btn.setFixedWidth(36)
self._toggle_btn.setFixedHeight(BUTTON_HEIGHT_SMALL)
self._toggle_btn.clicked.connect(self._toggle_collapse)
header_layout.addWidget(self._toggle_btn)
layout.addWidget(header)
# Log text area
self._text_edit = QPlainTextEdit()
self._text_edit.setReadOnly(True)
self._text_edit.setMinimumHeight(LOG_MIN_HEIGHT)
self._text_edit.setMaximumHeight(250)
self._text_edit.setStyleSheet("""
QPlainTextEdit {
font-family: "Consolas", "Monaco", "Courier New", monospace;
font-size: 12px;
line-height: 1.5;
padding: 8px 12px;
}
""")
layout.addWidget(self._text_edit)
def _toggle_collapse(self):
"""Toggle collapse state"""
self._collapsed = not self._collapsed
self._text_edit.setVisible(not self._collapsed)
self._toggle_btn.setText("" if self._collapsed else "")
@pyqtSlot(str)
def append_log(self, message: str):
"""Append log message"""
timestamp = datetime.now().strftime("%H:%M:%S")
formatted = f"[{timestamp}] {message}"
self._text_edit.appendPlainText(formatted)
# Limit lines
doc = self._text_edit.document()
if doc.blockCount() > self._max_lines:
cursor = QTextCursor(doc)
cursor.movePosition(QTextCursor.MoveOperation.Start)
cursor.select(QTextCursor.SelectionType.BlockUnderCursor)
cursor.movePosition(
QTextCursor.MoveOperation.Down,
QTextCursor.MoveMode.KeepAnchor,
doc.blockCount() - self._max_lines
)
cursor.removeSelectedText()
# Scroll to bottom
self._text_edit.moveCursor(QTextCursor.MoveOperation.End)
def log(self, message: str):
"""Log alias"""
self.append_log(message)
def clear(self):
"""Clear log"""
self._text_edit.clear()
def get_text(self) -> str:
"""Get all log text"""
return self._text_edit.toPlainText()