feat: 实现CutThenThink P0阶段核心功能

项目初始化
- 创建完整项目结构(src/, data/, docs/, examples/, tests/)
- 配置requirements.txt依赖
- 创建.gitignore

P0基础框架
- 数据库模型:Record模型,6种分类类型
- 配置管理:YAML配置,支持AI/OCR/云存储/UI配置
- OCR模块:PaddleOCR本地识别,支持云端扩展
- AI模块:支持OpenAI/Claude/通义/Ollama,6种分类
- 存储模块:完整CRUD,搜索,统计,导入导出
- 主窗口框架:侧边栏导航,米白配色方案
- 图片处理:截图/剪贴板/文件选择/图片预览
- 处理流程整合:OCR→AI→存储串联,Markdown展示,剪贴板复制
- 分类浏览:卡片网格展示,分类筛选,搜索,详情查看

技术栈
- PyQt6 + SQLAlchemy + PaddleOCR + OpenAI/Claude SDK
- 共47个Python文件,4000+行代码

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
congsh
2026-02-11 18:21:31 +08:00
commit c4a77f8aa4
79 changed files with 19412 additions and 0 deletions

585
src/gui/main_window.py Normal file
View File

@@ -0,0 +1,585 @@
"""
主窗口模块
实现应用程序的主窗口,包括侧边栏导航和主内容区域
集成图片处理功能
"""
from PyQt6.QtWidgets import (
QMainWindow,
QWidget,
QHBoxLayout,
QVBoxLayout,
QPushButton,
QStackedWidget,
QLabel,
QFrame,
QScrollArea,
QApplication,
QFileDialog,
QMessageBox
)
from PyQt6.QtCore import Qt, QSize, pyqtSignal
from PyQt6.QtGui import QIcon, QShortcut, QKeySequence
from src.gui.styles import ThemeStyles
from src.gui.widgets import (
ScreenshotWidget,
ClipboardMonitor,
ImagePicker,
ImagePreviewWidget,
QuickScreenshotHelper,
ClipboardImagePicker
)
from src.gui.widgets.message_handler import show_info, show_error
class MainWindow(QMainWindow):
"""主窗口类"""
# 信号:图片加载完成
image_loaded = pyqtSignal(str)
def __init__(self):
"""初始化主窗口"""
super().__init__()
self.setWindowTitle("CutThenThink - 智能截图管理")
self.setMinimumSize(1000, 700)
self.resize(1200, 800)
# 图片处理组件
self.screenshot_widget = None
self.clipboard_monitor = None
self.current_image_path = None
# 初始化 UI
self._init_ui()
self._apply_styles()
self._init_shortcuts()
# 初始化图片处理组件
self._init_image_components()
def _init_ui(self):
"""初始化用户界面"""
# 创建中央部件
central_widget = QWidget()
self.setCentralWidget(central_widget)
# 主布局
main_layout = QHBoxLayout(central_widget)
main_layout.setContentsMargins(0, 0, 0, 0)
main_layout.setSpacing(0)
# 创建侧边栏
self._create_sidebar(main_layout)
# 创建主内容区域
self._create_content_area(main_layout)
def _create_sidebar(self, parent_layout):
"""
创建侧边栏
Args:
parent_layout: 父布局
"""
# 侧边栏容器
sidebar = QWidget()
sidebar.setObjectName("sidebar")
sidebar.setFixedWidth(240)
# 侧边栏布局
sidebar_layout = QVBoxLayout(sidebar)
sidebar_layout.setContentsMargins(8, 16, 8, 16)
sidebar_layout.setSpacing(4)
# 应用标题
app_title = QLabel("CutThenThink")
app_title.setStyleSheet("""
QLabel {
color: #8B6914;
font-size: 20px;
font-weight: 700;
padding: 8px;
}
""")
sidebar_layout.addWidget(app_title)
# 添加分隔线
separator1 = self._create_separator()
sidebar_layout.addWidget(separator1)
# 导航按钮组
self.nav_buttons = {}
nav_items = [
("screenshot", "📷 截图处理", "screenshot"),
("browse", "📁 分类浏览", "browse"),
("upload", "☁️ 批量上传", "upload"),
("settings", "⚙️ 设置", "settings"),
]
for nav_id, text, _icon_name in nav_items:
button = NavigationButton(text)
button.clicked.connect(lambda checked, nid=nav_id: self._on_nav_clicked(nid))
sidebar_layout.addWidget(button)
self.nav_buttons[nav_id] = button
# 添加弹性空间
sidebar_layout.addStretch()
# 底部信息
version_label = QLabel("v0.1.0")
version_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
version_label.setStyleSheet("""
QLabel {
color: #999999;
font-size: 11px;
padding: 8px;
}
""")
sidebar_layout.addWidget(version_label)
# 添加到主布局
parent_layout.addWidget(sidebar)
# 设置默认选中的导航项
self.nav_buttons["screenshot"].setChecked(True)
self.current_nav = "screenshot"
def _create_content_area(self, parent_layout):
"""
创建主内容区域
Args:
parent_layout: 父布局
"""
# 内容区域容器
content_area = QWidget()
content_area.setObjectName("contentArea")
# 内容区域布局
content_layout = QVBoxLayout(content_area)
content_layout.setContentsMargins(24, 16, 24, 16)
content_layout.setSpacing(16)
# 创建堆栈部件用于切换页面
self.content_stack = QStackedWidget()
self.content_stack.setObjectName("contentStack")
# 创建各个页面
self._create_pages()
content_layout.addWidget(self.content_stack)
# 添加到主布局
parent_layout.addWidget(content_area)
def _create_pages(self):
"""创建各个页面"""
# 截图处理页面
screenshot_page = self._create_screenshot_page()
self.content_stack.addWidget(screenshot_page)
# 分类浏览页面
browse_page = self._create_browse_page()
self.content_stack.addWidget(browse_page)
# 批量上传页面
upload_page = self._create_upload_page()
self.content_stack.addWidget(upload_page)
# 设置页面
settings_page = self._create_settings_page()
self.content_stack.addWidget(settings_page)
def _create_screenshot_page(self) -> QWidget:
"""创建截图处理页面"""
page = QWidget()
layout = QVBoxLayout(page)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(16)
# 页面标题
title = QLabel("📷 截图处理")
title.setObjectName("pageTitle")
layout.addWidget(title)
# 快捷操作按钮区域
actions_card = self._create_card("")
actions_layout = QVBoxLayout(actions_card)
actions_layout.setSpacing(12)
actions_title = QLabel("快捷操作")
actions_title.setObjectName("sectionTitle")
actions_layout.addWidget(actions_title)
# 新建截图按钮
self.new_screenshot_btn = QPushButton("📷 新建截图")
self.new_screenshot_btn.setObjectName("primaryButton")
self.new_screenshot_btn.setMinimumHeight(44)
self.new_screenshot_btn.setToolTip("快捷键: Ctrl+Shift+A")
self.new_screenshot_btn.clicked.connect(self._on_new_screenshot)
actions_layout.addWidget(self.new_screenshot_btn)
# 导入图片按钮
self.import_image_btn = QPushButton("📂 导入图片")
self.import_image_btn.setMinimumHeight(44)
self.import_image_btn.clicked.connect(self._on_import_image)
actions_layout.addWidget(self.import_image_btn)
# 粘贴剪贴板图片按钮
self.paste_btn = QPushButton("📋 粘贴剪贴板图片")
self.paste_btn.setMinimumHeight(44)
self.paste_btn.setToolTip("快捷键: Ctrl+Shift+V")
self.paste_btn.clicked.connect(self._on_paste_clipboard)
actions_layout.addWidget(self.paste_btn)
layout.addWidget(actions_card)
# 图片预览区域
preview_card = self._create_card("")
preview_layout = QVBoxLayout(preview_card)
preview_layout.setContentsMargins(16, 16, 16, 16)
preview_layout.setSpacing(12)
preview_title = QLabel("图片预览")
preview_title.setObjectName("sectionTitle")
preview_layout.addWidget(preview_title)
# 创建图片预览组件
self.image_preview = ImagePreviewWidget()
preview_layout.addWidget(self.image_preview)
layout.addWidget(preview_card, 1)
return page
def _create_browse_page(self) -> QWidget:
"""创建分类浏览页面"""
page = QWidget()
layout = QVBoxLayout(page)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(16)
# 页面标题
title = QLabel("📁 分类浏览")
title.setObjectName("pageTitle")
layout.addWidget(title)
# 内容卡片
content_card = self._create_card("""
<h3>浏览截图</h3>
<p>这里将显示您的所有截图和分类。</p>
<p>支持的浏览方式:</p>
<ul>
<li>🏷️ 按标签浏览</li>
<li>📅 按日期浏览</li>
<li>🔍 搜索和筛选</li>
</ul>
""")
layout.addWidget(content_card)
# 添加弹性空间
layout.addStretch()
return page
def _create_upload_page(self) -> QWidget:
"""创建批量上传页面"""
page = QWidget()
layout = QVBoxLayout(page)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(16)
# 页面标题
title = QLabel("☁️ 批量上传")
title.setObjectName("pageTitle")
layout.addWidget(title)
# 内容卡片
content_card = self._create_card("""
<h3>云存储上传</h3>
<p>这里将管理您的云存储上传任务。</p>
<p>功能包括:</p>
<ul>
<li>📤 批量上传截图</li>
<li>🔄 同步状态监控</li>
<li>📊 上传历史记录</li>
</ul>
""")
layout.addWidget(content_card)
# 添加弹性空间
layout.addStretch()
return page
def _create_settings_page(self) -> QWidget:
"""创建设置页面"""
page = QWidget()
layout = QVBoxLayout(page)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(16)
# 页面标题
title = QLabel("⚙️ 设置")
title.setObjectName("pageTitle")
layout.addWidget(title)
# 使用滚动区域以支持大量设置项
scroll = QScrollArea()
scroll.setWidgetResizable(True)
scroll.setFrameShape(QFrame.Shape.NoFrame)
scroll_content = QWidget()
scroll_layout = QVBoxLayout(scroll_content)
scroll_layout.setSpacing(16)
# AI 配置卡片
ai_card = self._create_card("""
<h3>🤖 AI 配置</h3>
<p>配置您的 AI 服务提供商和 API 设置。</p>
<p>支持OpenAI、Anthropic、Azure</p>
""")
scroll_layout.addWidget(ai_card)
# OCR 配置卡片
ocr_card = self._create_card("""
<h3>🔍 OCR 配置</h3>
<p>选择本地或云端 OCR 服务。</p>
<p>本地PaddleOCR | 云端:自定义 API</p>
""")
scroll_layout.addWidget(ocr_card)
# 云存储配置卡片
cloud_card = self._create_card("""
<h3>☁️ 云存储配置</h3>
<p>配置云存储服务用于同步。</p>
<p>支持S3、OSS、COS、MinIO</p>
""")
scroll_layout.addWidget(cloud_card)
# 界面配置卡片
ui_card = self._create_card("""
<h3>🎨 界面配置</h3>
<p>自定义应用程序外观和行为。</p>
<p>主题、语言、快捷键等</p>
""")
scroll_layout.addWidget(ui_card)
# 添加弹性空间
scroll_layout.addStretch()
scroll.setWidget(scroll_content)
layout.addWidget(scroll)
return page
def _create_card(self, content_html: str) -> QWidget:
"""
创建卡片部件
Args:
content_html: 卡片内容的 HTML
Returns:
卡片部件
"""
card = QWidget()
card.setObjectName("card")
layout = QVBoxLayout(card)
layout.setContentsMargins(0, 0, 0, 0)
label = QLabel(content_html)
label.setWordWrap(True)
label.setTextFormat(Qt.TextFormat.RichText)
label.setStyleSheet("""
QLabel {
color: #2C2C2C;
font-size: 14px;
line-height: 1.6;
}
QLabel h2 {
color: #8B6914;
font-size: 20px;
font-weight: 600;
margin-bottom: 8px;
}
QLabel h3 {
color: #8B6914;
font-size: 16px;
font-weight: 600;
margin-bottom: 6px;
}
QLabel p {
margin: 4px 0;
}
QLabel ul {
margin: 8px 0;
padding-left: 20px;
}
QLabel li {
margin: 4px 0;
}
""")
layout.addWidget(label)
return card
def _create_separator(self) -> QFrame:
"""创建分隔线"""
separator = QFrame()
separator.setObjectName("navSeparator")
return separator
def _on_nav_clicked(self, nav_id: str):
"""
导航按钮点击处理
Args:
nav_id: 导航项 ID
"""
# 取消当前选中的按钮
if self.current_nav in self.nav_buttons:
self.nav_buttons[self.current_nav].setChecked(False)
# 选中新按钮
self.nav_buttons[nav_id].setChecked(True)
self.current_nav = nav_id
# 切换页面
nav_order = ["screenshot", "browse", "upload", "settings"]
if nav_id in nav_order:
index = nav_order.index(nav_id)
self.content_stack.setCurrentIndex(index)
def _apply_styles(self):
"""应用样式表"""
ThemeStyles.apply_style(self)
def _init_shortcuts(self):
"""初始化全局快捷键"""
# 截图快捷键 Ctrl+Shift+A
screenshot_shortcut = QShortcut(QKeySequence("Ctrl+Shift+A"), self)
screenshot_shortcut.activated.connect(self._on_new_screenshot)
# 粘贴剪贴板快捷键 Ctrl+Shift+V
paste_shortcut = QShortcut(QKeySequence("Ctrl+Shift+V"), self)
paste_shortcut.activated.connect(self._on_paste_clipboard)
# 导入图片快捷键 Ctrl+Shift+O
import_shortcut = QShortcut(QKeySequence("Ctrl+Shift+O"), self)
import_shortcut.activated.connect(self._on_import_image)
def _init_image_components(self):
"""初始化图片处理组件"""
# 创建截图组件
self.screenshot_widget = ScreenshotWidget(self)
self.screenshot_widget.screenshot_saved.connect(self._on_screenshot_saved)
# 注册到全局助手
QuickScreenshotHelper.set_screenshot_widget(self.screenshot_widget)
# 创建剪贴板监听器
self.clipboard_monitor = ClipboardMonitor(self)
self.clipboard_monitor.image_detected.connect(self._on_clipboard_image_detected)
# ========== 图片处理方法 ==========
def _on_new_screenshot(self):
"""新建截图"""
if self.screenshot_widget:
self.screenshot_widget.take_screenshot()
def _on_screenshot_saved(self, filepath: str):
"""
截图保存完成回调
Args:
filepath: 保存的文件路径
"""
self.current_image_path = filepath
# 加载到预览组件
self.image_preview.load_image(filepath)
show_info(self, "截图完成", f"截图已保存到:\n{filepath}")
self.image_loaded.emit(filepath)
def _on_import_image(self):
"""导入图片"""
filepath, _ = QFileDialog.getOpenFileName(
self,
"选择图片",
"",
"图片文件 (*.png *.jpg *.jpeg *.bmp *.gif *.webp);;所有文件 (*.*)"
)
if filepath:
self.current_image_path = filepath
self.image_preview.load_image(filepath)
show_info(self, "导入成功", f"图片已导入:\n{filepath}")
self.image_loaded.emit(filepath)
def _on_paste_clipboard(self):
"""粘贴剪贴板图片"""
clipboard = QApplication.clipboard()
pixmap = clipboard.pixmap()
if pixmap.isNull():
show_error(self, "错误", "剪贴板中没有图片")
return
# 保存剪贴板图片
from datetime import datetime
from pathlib import Path
import tempfile
temp_dir = Path(tempfile.gettempdir()) / "cutthenthink" / "clipboard"
temp_dir.mkdir(parents=True, exist_ok=True)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filepath = temp_dir / f"clipboard_{timestamp}.png"
if pixmap.save(str(filepath)):
self.current_image_path = filepath
self.image_preview.load_image(filepath)
show_info(self, "粘贴成功", f"剪贴板图片已保存:\n{filepath}")
self.image_loaded.emit(filepath)
else:
show_error(self, "错误", "保存剪贴板图片失败")
def _on_clipboard_image_detected(self, filepath: str):
"""
剪贴板图片检测回调
Args:
filepath: 保存的图片路径
"""
# 可选:自动加载剪贴板图片或显示通知
pass
class NavigationButton(QPushButton):
"""导航按钮类"""
def __init__(self, text: str, icon_path: str = None, parent=None):
"""
初始化导航按钮
Args:
text: 按钮文字
icon_path: 图标路径(可选)
parent: 父部件
"""
super().__init__(text, parent)
self.setObjectName("navButton")
self.setCheckable(True)
self.setMinimumHeight(44)
# 如果有图标,加载图标
if icon_path:
self.setIcon(QIcon(icon_path))
self.setIconSize(QSize(20, 20))