初始化
This commit is contained in:
31
ui/__init__.py
Normal file
31
ui/__init__.py
Normal file
@@ -0,0 +1,31 @@
|
||||
"""
|
||||
UI 模块
|
||||
- 主窗口
|
||||
- 侧边栏
|
||||
- 工作区
|
||||
- 动画效果
|
||||
"""
|
||||
from .main_window import MainWindow
|
||||
from .sidebar import PrimarySidebar
|
||||
from .tool_list import SecondarySidebar
|
||||
from .workspace import BaseWorkspace, WelcomeWorkspace, UploadArea
|
||||
from .settings import SettingsPage
|
||||
from .log_viewer import LogViewer
|
||||
from .image_preview import ImagePreviewWidget, DualPreviewWidget
|
||||
from .animations import AnimationMixin, animate_widget_show, animate_widget_hide
|
||||
|
||||
__all__ = [
|
||||
'MainWindow',
|
||||
'PrimarySidebar',
|
||||
'SecondarySidebar',
|
||||
'BaseWorkspace',
|
||||
'WelcomeWorkspace',
|
||||
'UploadArea',
|
||||
'SettingsPage',
|
||||
'LogViewer',
|
||||
'ImagePreviewWidget',
|
||||
'DualPreviewWidget',
|
||||
'AnimationMixin',
|
||||
'animate_widget_show',
|
||||
'animate_widget_hide'
|
||||
]
|
||||
183
ui/animations.py
Normal file
183
ui/animations.py
Normal file
@@ -0,0 +1,183 @@
|
||||
"""
|
||||
UI动画效果模块
|
||||
- 渐入渐出
|
||||
- 滑动效果
|
||||
- 缩放效果
|
||||
"""
|
||||
from PySide6.QtWidgets import QWidget, QGraphicsOpacityEffect
|
||||
from PySide6.QtCore import (
|
||||
QPropertyAnimation, QEasingCurve, QParallelAnimationGroup,
|
||||
QSequentialAnimationGroup, Property, QPoint, QSize
|
||||
)
|
||||
from PySide6.QtGui import QColor
|
||||
|
||||
from core.config import config
|
||||
|
||||
|
||||
class AnimationMixin:
|
||||
"""动画混入类,为QWidget添加动画功能"""
|
||||
|
||||
def setup_animation(self):
|
||||
"""初始化动画效果"""
|
||||
if not hasattr(self, '_opacity_effect'):
|
||||
self._opacity_effect = QGraphicsOpacityEffect(self)
|
||||
self._opacity_effect.setOpacity(1.0)
|
||||
self.setGraphicsEffect(self._opacity_effect)
|
||||
|
||||
def fade_in(self, duration: int = None, callback=None):
|
||||
"""渐入动画"""
|
||||
if not config.get("animation_enabled", True):
|
||||
if callback:
|
||||
callback()
|
||||
return
|
||||
|
||||
self.setup_animation()
|
||||
duration = duration or config.get("animation_duration", 300)
|
||||
|
||||
self._opacity_effect.setOpacity(0)
|
||||
self.show()
|
||||
|
||||
self._fade_anim = QPropertyAnimation(self._opacity_effect, b"opacity")
|
||||
self._fade_anim.setDuration(duration)
|
||||
self._fade_anim.setStartValue(0.0)
|
||||
self._fade_anim.setEndValue(1.0)
|
||||
self._fade_anim.setEasingCurve(QEasingCurve.Type.OutCubic)
|
||||
|
||||
if callback:
|
||||
self._fade_anim.finished.connect(callback)
|
||||
|
||||
self._fade_anim.start()
|
||||
|
||||
def fade_out(self, duration: int = None, callback=None):
|
||||
"""渐出动画"""
|
||||
if not config.get("animation_enabled", True):
|
||||
self.hide()
|
||||
if callback:
|
||||
callback()
|
||||
return
|
||||
|
||||
self.setup_animation()
|
||||
duration = duration or config.get("animation_duration", 300)
|
||||
|
||||
self._fade_anim = QPropertyAnimation(self._opacity_effect, b"opacity")
|
||||
self._fade_anim.setDuration(duration)
|
||||
self._fade_anim.setStartValue(1.0)
|
||||
self._fade_anim.setEndValue(0.0)
|
||||
self._fade_anim.setEasingCurve(QEasingCurve.Type.InCubic)
|
||||
|
||||
def on_finished():
|
||||
self.hide()
|
||||
self._opacity_effect.setOpacity(1.0)
|
||||
if callback:
|
||||
callback()
|
||||
|
||||
self._fade_anim.finished.connect(on_finished)
|
||||
self._fade_anim.start()
|
||||
|
||||
|
||||
def create_fade_animation(widget: QWidget, fade_in: bool = True, duration: int = None) -> QPropertyAnimation:
|
||||
"""创建渐变动画"""
|
||||
if not config.get("animation_enabled", True):
|
||||
return None
|
||||
|
||||
duration = duration or config.get("animation_duration", 300)
|
||||
|
||||
# 确保有透明度效果
|
||||
effect = widget.graphicsEffect()
|
||||
if not isinstance(effect, QGraphicsOpacityEffect):
|
||||
effect = QGraphicsOpacityEffect(widget)
|
||||
widget.setGraphicsEffect(effect)
|
||||
|
||||
anim = QPropertyAnimation(effect, b"opacity")
|
||||
anim.setDuration(duration)
|
||||
|
||||
if fade_in:
|
||||
anim.setStartValue(0.0)
|
||||
anim.setEndValue(1.0)
|
||||
anim.setEasingCurve(QEasingCurve.Type.OutCubic)
|
||||
else:
|
||||
anim.setStartValue(1.0)
|
||||
anim.setEndValue(0.0)
|
||||
anim.setEasingCurve(QEasingCurve.Type.InCubic)
|
||||
|
||||
return anim
|
||||
|
||||
|
||||
def create_slide_animation(widget: QWidget, start_pos: QPoint, end_pos: QPoint,
|
||||
duration: int = None) -> QPropertyAnimation:
|
||||
"""创建滑动动画"""
|
||||
if not config.get("animation_enabled", True):
|
||||
widget.move(end_pos)
|
||||
return None
|
||||
|
||||
duration = duration or config.get("animation_duration", 300)
|
||||
|
||||
anim = QPropertyAnimation(widget, b"pos")
|
||||
anim.setDuration(duration)
|
||||
anim.setStartValue(start_pos)
|
||||
anim.setEndValue(end_pos)
|
||||
anim.setEasingCurve(QEasingCurve.Type.OutCubic)
|
||||
|
||||
return anim
|
||||
|
||||
|
||||
def animate_widget_show(widget: QWidget, direction: str = "fade"):
|
||||
"""显示控件动画"""
|
||||
if not config.get("animation_enabled", True):
|
||||
widget.show()
|
||||
return
|
||||
|
||||
duration = config.get("animation_duration", 300)
|
||||
|
||||
if direction == "fade":
|
||||
effect = QGraphicsOpacityEffect(widget)
|
||||
effect.setOpacity(0)
|
||||
widget.setGraphicsEffect(effect)
|
||||
widget.show()
|
||||
|
||||
anim = QPropertyAnimation(effect, b"opacity")
|
||||
anim.setDuration(duration)
|
||||
anim.setStartValue(0.0)
|
||||
anim.setEndValue(1.0)
|
||||
anim.setEasingCurve(QEasingCurve.Type.OutCubic)
|
||||
anim.start()
|
||||
|
||||
# 保持引用
|
||||
widget._show_anim = anim
|
||||
else:
|
||||
widget.show()
|
||||
|
||||
|
||||
def animate_widget_hide(widget: QWidget, callback=None):
|
||||
"""隐藏控件动画"""
|
||||
if not config.get("animation_enabled", True):
|
||||
widget.hide()
|
||||
if callback:
|
||||
callback()
|
||||
return
|
||||
|
||||
duration = config.get("animation_duration", 300)
|
||||
|
||||
effect = widget.graphicsEffect()
|
||||
if not isinstance(effect, QGraphicsOpacityEffect):
|
||||
effect = QGraphicsOpacityEffect(widget)
|
||||
widget.setGraphicsEffect(effect)
|
||||
|
||||
anim = QPropertyAnimation(effect, b"opacity")
|
||||
anim.setDuration(duration)
|
||||
anim.setStartValue(1.0)
|
||||
anim.setEndValue(0.0)
|
||||
anim.setEasingCurve(QEasingCurve.Type.InCubic)
|
||||
|
||||
def on_finished():
|
||||
widget.hide()
|
||||
effect.setOpacity(1.0)
|
||||
if callback:
|
||||
callback()
|
||||
|
||||
anim.finished.connect(on_finished)
|
||||
anim.start()
|
||||
|
||||
# 保持引用
|
||||
widget._hide_anim = anim
|
||||
|
||||
379
ui/image_preview.py
Normal file
379
ui/image_preview.py
Normal file
@@ -0,0 +1,379 @@
|
||||
"""
|
||||
图片预览组件
|
||||
- 原图/处理后对比预览
|
||||
- 缩放/平移
|
||||
- 信息显示
|
||||
"""
|
||||
import os
|
||||
from pathlib import Path
|
||||
from PIL import Image
|
||||
import io
|
||||
|
||||
from PySide6.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
|
||||
QFrame, QScrollArea, QSplitter, QFileDialog, QMessageBox
|
||||
)
|
||||
from PySide6.QtCore import Qt, Signal, QSize
|
||||
from PySide6.QtGui import QPixmap, QImage, QFont
|
||||
|
||||
from core.config import config
|
||||
|
||||
|
||||
class ImagePreviewWidget(QFrame):
|
||||
"""单个图片预览组件"""
|
||||
|
||||
def __init__(self, title: str = "预览", parent=None):
|
||||
super().__init__(parent)
|
||||
self.title = title
|
||||
self._pixmap = None
|
||||
self._image_info = {}
|
||||
self.setup_ui()
|
||||
|
||||
def setup_ui(self):
|
||||
self.setStyleSheet("""
|
||||
ImagePreviewWidget {
|
||||
background: rgba(15, 23, 42, 0.5);
|
||||
border: 1px solid #334155;
|
||||
border-radius: 12px;
|
||||
}
|
||||
""")
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(12, 12, 12, 12)
|
||||
layout.setSpacing(8)
|
||||
|
||||
# 标题栏
|
||||
header = QHBoxLayout()
|
||||
|
||||
self.title_label = QLabel(self.title)
|
||||
self.title_label.setStyleSheet("""
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
color: white;
|
||||
font-size: 11px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 4px;
|
||||
""")
|
||||
header.addWidget(self.title_label)
|
||||
header.addStretch()
|
||||
|
||||
# 尺寸信息
|
||||
self.size_label = QLabel("")
|
||||
self.size_label.setStyleSheet("color: #64748b; font-size: 10px;")
|
||||
header.addWidget(self.size_label)
|
||||
|
||||
layout.addLayout(header)
|
||||
|
||||
# 图片显示区域
|
||||
self.image_container = QFrame()
|
||||
self.image_container.setStyleSheet("""
|
||||
background: #0f172a;
|
||||
border-radius: 8px;
|
||||
""")
|
||||
self.image_container.setMinimumHeight(250)
|
||||
|
||||
container_layout = QVBoxLayout(self.image_container)
|
||||
container_layout.setContentsMargins(8, 8, 8, 8)
|
||||
container_layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
# 占位图标
|
||||
self.placeholder_icon = QLabel("🖼️")
|
||||
self.placeholder_icon.setFont(QFont("Segoe UI Emoji", 40))
|
||||
self.placeholder_icon.setStyleSheet("color: #334155;")
|
||||
self.placeholder_icon.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
container_layout.addWidget(self.placeholder_icon)
|
||||
|
||||
# 占位文字
|
||||
self.placeholder_text = QLabel("暂无图片")
|
||||
self.placeholder_text.setStyleSheet("color: #64748b; font-size: 12px;")
|
||||
self.placeholder_text.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
container_layout.addWidget(self.placeholder_text)
|
||||
|
||||
# 图片标签
|
||||
self.image_label = QLabel()
|
||||
self.image_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.image_label.setVisible(False)
|
||||
container_layout.addWidget(self.image_label)
|
||||
|
||||
layout.addWidget(self.image_container, 1)
|
||||
|
||||
# 文件信息
|
||||
self.info_label = QLabel("")
|
||||
self.info_label.setStyleSheet("color: #94a3b8; font-size: 11px;")
|
||||
self.info_label.setWordWrap(True)
|
||||
layout.addWidget(self.info_label)
|
||||
|
||||
def set_image(self, pixmap: QPixmap = None, file_path: str = None,
|
||||
pil_image: Image.Image = None, image_bytes: bytes = None):
|
||||
"""设置预览图片"""
|
||||
if pixmap:
|
||||
self._pixmap = pixmap
|
||||
elif file_path and os.path.exists(file_path):
|
||||
self._pixmap = QPixmap(file_path)
|
||||
self._image_info = self._get_file_info(file_path)
|
||||
elif pil_image:
|
||||
self._pixmap = self._pil_to_pixmap(pil_image)
|
||||
elif image_bytes:
|
||||
self._pixmap = QPixmap()
|
||||
self._pixmap.loadFromData(image_bytes)
|
||||
else:
|
||||
self._pixmap = None
|
||||
|
||||
self._update_display()
|
||||
|
||||
def set_image_from_bytes(self, data: bytes, info: dict = None):
|
||||
"""从字节数据设置图片"""
|
||||
self._pixmap = QPixmap()
|
||||
self._pixmap.loadFromData(data)
|
||||
if info:
|
||||
self._image_info = info
|
||||
self._update_display()
|
||||
|
||||
def _pil_to_pixmap(self, pil_image: Image.Image) -> QPixmap:
|
||||
"""PIL Image转QPixmap"""
|
||||
if pil_image.mode == "RGB":
|
||||
data = pil_image.tobytes("raw", "RGB")
|
||||
qimage = QImage(data, pil_image.width, pil_image.height,
|
||||
pil_image.width * 3, QImage.Format.Format_RGB888)
|
||||
elif pil_image.mode == "RGBA":
|
||||
data = pil_image.tobytes("raw", "RGBA")
|
||||
qimage = QImage(data, pil_image.width, pil_image.height,
|
||||
pil_image.width * 4, QImage.Format.Format_RGBA8888)
|
||||
else:
|
||||
pil_image = pil_image.convert("RGB")
|
||||
data = pil_image.tobytes("raw", "RGB")
|
||||
qimage = QImage(data, pil_image.width, pil_image.height,
|
||||
pil_image.width * 3, QImage.Format.Format_RGB888)
|
||||
return QPixmap.fromImage(qimage)
|
||||
|
||||
def _get_file_info(self, file_path: str) -> dict:
|
||||
"""获取文件信息"""
|
||||
try:
|
||||
size = os.path.getsize(file_path)
|
||||
with Image.open(file_path) as img:
|
||||
return {
|
||||
"path": file_path,
|
||||
"name": Path(file_path).name,
|
||||
"width": img.width,
|
||||
"height": img.height,
|
||||
"size": size,
|
||||
"format": img.format
|
||||
}
|
||||
except:
|
||||
return {}
|
||||
|
||||
def _update_display(self):
|
||||
"""更新显示"""
|
||||
if self._pixmap and not self._pixmap.isNull():
|
||||
# 隐藏占位符
|
||||
self.placeholder_icon.setVisible(False)
|
||||
self.placeholder_text.setVisible(False)
|
||||
self.image_label.setVisible(True)
|
||||
|
||||
# 缩放图片适应容器
|
||||
container_size = self.image_container.size()
|
||||
max_width = container_size.width() - 20
|
||||
max_height = container_size.height() - 20
|
||||
|
||||
scaled = self._pixmap.scaled(
|
||||
QSize(max_width, max_height),
|
||||
Qt.AspectRatioMode.KeepAspectRatio,
|
||||
Qt.TransformationMode.SmoothTransformation
|
||||
)
|
||||
self.image_label.setPixmap(scaled)
|
||||
|
||||
# 更新尺寸信息
|
||||
self.size_label.setText(f"{self._pixmap.width()}×{self._pixmap.height()}")
|
||||
|
||||
# 更新文件信息
|
||||
if self._image_info:
|
||||
size_str = self._format_size(self._image_info.get("size", 0))
|
||||
self.info_label.setText(
|
||||
f"📁 {self._image_info.get('name', '')} | 💾 {size_str}"
|
||||
)
|
||||
else:
|
||||
# 显示占位符
|
||||
self.placeholder_icon.setVisible(True)
|
||||
self.placeholder_text.setVisible(True)
|
||||
self.image_label.setVisible(False)
|
||||
self.size_label.setText("")
|
||||
self.info_label.setText("")
|
||||
|
||||
def set_info(self, info: dict):
|
||||
"""设置图片信息"""
|
||||
self._image_info = info
|
||||
if info:
|
||||
size_str = self._format_size(info.get("size", 0))
|
||||
self.info_label.setText(
|
||||
f"📁 {info.get('name', '')} | 💾 {size_str}"
|
||||
)
|
||||
|
||||
def clear(self):
|
||||
"""清空预览"""
|
||||
self._pixmap = None
|
||||
self._image_info = {}
|
||||
self._update_display()
|
||||
|
||||
def resizeEvent(self, event):
|
||||
"""窗口大小变化时重新缩放图片"""
|
||||
super().resizeEvent(event)
|
||||
if self._pixmap and not self._pixmap.isNull():
|
||||
self._update_display()
|
||||
|
||||
@staticmethod
|
||||
def _format_size(size: int) -> str:
|
||||
"""格式化文件大小"""
|
||||
for unit in ['B', 'KB', 'MB', 'GB']:
|
||||
if size < 1024:
|
||||
return f"{size:.1f} {unit}"
|
||||
size /= 1024
|
||||
return f"{size:.1f} TB"
|
||||
|
||||
|
||||
class DualPreviewWidget(QWidget):
|
||||
"""双栏预览组件 - 原图/处理后对比"""
|
||||
|
||||
save_requested = Signal(object) # 发出保存请求信号,携带处理后的数据
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self._processed_data = None # 保存处理后的数据
|
||||
self._output_filename = ""
|
||||
self.setup_ui()
|
||||
|
||||
def setup_ui(self):
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setSpacing(16)
|
||||
|
||||
# 预览区 - 左右分栏
|
||||
preview_layout = QHBoxLayout()
|
||||
preview_layout.setSpacing(16)
|
||||
|
||||
# 原图预览
|
||||
self.original_preview = ImagePreviewWidget("📷 原图")
|
||||
preview_layout.addWidget(self.original_preview, 1)
|
||||
|
||||
# 处理后预览
|
||||
self.result_preview = ImagePreviewWidget("✨ 处理结果")
|
||||
preview_layout.addWidget(self.result_preview, 1)
|
||||
|
||||
layout.addLayout(preview_layout, 1)
|
||||
|
||||
# 对比信息
|
||||
self.compare_label = QLabel("")
|
||||
self.compare_label.setStyleSheet("""
|
||||
color: #22c55e;
|
||||
font-size: 13px;
|
||||
padding: 8px;
|
||||
background: rgba(34, 197, 94, 0.1);
|
||||
border-radius: 8px;
|
||||
""")
|
||||
self.compare_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.compare_label.setVisible(False)
|
||||
layout.addWidget(self.compare_label)
|
||||
|
||||
# 操作按钮
|
||||
btn_layout = QHBoxLayout()
|
||||
btn_layout.addStretch()
|
||||
|
||||
self.save_btn = QPushButton("💾 保存结果")
|
||||
self.save_btn.setObjectName("primary_btn")
|
||||
self.save_btn.setMinimumSize(150, 40)
|
||||
self.save_btn.clicked.connect(self._on_save_clicked)
|
||||
self.save_btn.setEnabled(False)
|
||||
btn_layout.addWidget(self.save_btn)
|
||||
|
||||
layout.addLayout(btn_layout)
|
||||
|
||||
def set_original(self, file_path: str):
|
||||
"""设置原图"""
|
||||
self.original_preview.set_image(file_path=file_path)
|
||||
|
||||
def set_result(self, data: bytes, info: dict = None, filename: str = "",
|
||||
show_size_compare: bool = True):
|
||||
"""设置处理结果"""
|
||||
self._processed_data = data
|
||||
self._output_filename = filename
|
||||
self.result_preview.set_image_from_bytes(data, info)
|
||||
self.save_btn.setEnabled(True)
|
||||
|
||||
# 显示对比信息(仅在压缩场景下显示大小对比)
|
||||
if info and show_size_compare:
|
||||
original_size = self.original_preview._image_info.get("size", 0)
|
||||
result_size = info.get("size", len(data))
|
||||
if original_size > 0:
|
||||
saved = original_size - result_size
|
||||
percent = (saved / original_size) * 100
|
||||
if saved > 0:
|
||||
self.compare_label.setText(
|
||||
f"✅ 处理完成! 节省 {self._format_size(saved)} ({percent:.1f}%)"
|
||||
)
|
||||
self.compare_label.setStyleSheet("""
|
||||
color: #22c55e;
|
||||
font-size: 13px;
|
||||
padding: 8px;
|
||||
background: rgba(34, 197, 94, 0.1);
|
||||
border-radius: 8px;
|
||||
""")
|
||||
self.compare_label.setVisible(True)
|
||||
else:
|
||||
# 文件变大或相同,不显示对比(可能是添加水印等操作)
|
||||
self.compare_label.setVisible(False)
|
||||
elif not show_size_compare:
|
||||
# 不需要显示大小对比(如水印、格式转换)
|
||||
self.compare_label.setText("✅ 处理完成!")
|
||||
self.compare_label.setStyleSheet("""
|
||||
color: #22c55e;
|
||||
font-size: 13px;
|
||||
padding: 8px;
|
||||
background: rgba(34, 197, 94, 0.1);
|
||||
border-radius: 8px;
|
||||
""")
|
||||
self.compare_label.setVisible(True)
|
||||
|
||||
def _on_save_clicked(self):
|
||||
"""保存按钮点击"""
|
||||
if not self._processed_data:
|
||||
return
|
||||
|
||||
# 检查是否有默认保存路径
|
||||
default_dir = config.get_output_directory()
|
||||
auto_save = config.get("auto_save_to_default", False)
|
||||
|
||||
if default_dir and auto_save:
|
||||
# 自动保存
|
||||
save_path = os.path.join(default_dir, self._output_filename)
|
||||
else:
|
||||
# 询问保存位置
|
||||
save_path, _ = QFileDialog.getSaveFileName(
|
||||
self, "保存文件",
|
||||
os.path.join(default_dir, self._output_filename) if default_dir else self._output_filename,
|
||||
"图片文件 (*.jpg *.png *.webp)"
|
||||
)
|
||||
|
||||
if save_path:
|
||||
try:
|
||||
with open(save_path, 'wb') as f:
|
||||
f.write(self._processed_data)
|
||||
QMessageBox.information(self, "成功", f"文件已保存到:\n{save_path}")
|
||||
self.save_requested.emit(save_path)
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "错误", f"保存失败:\n{e}")
|
||||
|
||||
def clear(self):
|
||||
"""清空预览"""
|
||||
self.original_preview.clear()
|
||||
self.result_preview.clear()
|
||||
self._processed_data = None
|
||||
self._output_filename = ""
|
||||
self.save_btn.setEnabled(False)
|
||||
self.compare_label.setVisible(False)
|
||||
|
||||
@staticmethod
|
||||
def _format_size(size: int) -> str:
|
||||
for unit in ['B', 'KB', 'MB', 'GB']:
|
||||
if size < 1024:
|
||||
return f"{size:.1f} {unit}"
|
||||
size /= 1024
|
||||
return f"{size:.1f} TB"
|
||||
|
||||
242
ui/log_viewer.py
Normal file
242
ui/log_viewer.py
Normal file
@@ -0,0 +1,242 @@
|
||||
"""
|
||||
日志查看器界面
|
||||
- 日志文件列表
|
||||
- 日志内容显示
|
||||
- 搜索过滤
|
||||
- 导出功能
|
||||
"""
|
||||
from PySide6.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit,
|
||||
QPushButton, QListWidget, QListWidgetItem, QTextEdit,
|
||||
QSplitter, QFrame, QFileDialog, QMessageBox
|
||||
)
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtGui import QFont
|
||||
from pathlib import Path
|
||||
import logging
|
||||
|
||||
from core.logger import get_all_log_files, read_log_file
|
||||
|
||||
|
||||
class LogViewer(QWidget):
|
||||
"""日志查看器 - 可嵌入设置页面"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.current_log_content = ""
|
||||
self.setup_ui()
|
||||
self.load_log_files()
|
||||
|
||||
def setup_ui(self):
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(16, 16, 16, 16)
|
||||
layout.setSpacing(12)
|
||||
|
||||
# 主内容区 - 分割器
|
||||
splitter = QSplitter(Qt.Orientation.Horizontal)
|
||||
splitter.setStyleSheet("QSplitter::handle { background: #334155; }")
|
||||
|
||||
# 左侧 - 日志文件列表
|
||||
left_panel = QFrame()
|
||||
left_panel.setStyleSheet("background: rgba(30, 41, 59, 0.5); border-radius: 12px;")
|
||||
left_layout = QVBoxLayout(left_panel)
|
||||
left_layout.setContentsMargins(12, 12, 12, 12)
|
||||
left_layout.setSpacing(8)
|
||||
|
||||
list_title = QLabel("日志文件")
|
||||
list_title.setStyleSheet("color: #94a3b8; font-size: 12px; font-weight: 600;")
|
||||
left_layout.addWidget(list_title)
|
||||
|
||||
self.file_list = QListWidget()
|
||||
self.file_list.setMinimumWidth(180)
|
||||
self.file_list.currentItemChanged.connect(self.on_file_selected)
|
||||
left_layout.addWidget(self.file_list)
|
||||
|
||||
# 刷新按钮
|
||||
refresh_btn = QPushButton("🔄 刷新")
|
||||
refresh_btn.setObjectName("secondary_btn")
|
||||
refresh_btn.clicked.connect(self.load_log_files)
|
||||
left_layout.addWidget(refresh_btn)
|
||||
|
||||
splitter.addWidget(left_panel)
|
||||
|
||||
# 右侧 - 日志内容
|
||||
right_panel = QFrame()
|
||||
right_panel.setStyleSheet("background: rgba(30, 41, 59, 0.5); border-radius: 12px;")
|
||||
right_layout = QVBoxLayout(right_panel)
|
||||
right_layout.setContentsMargins(12, 12, 12, 12)
|
||||
right_layout.setSpacing(8)
|
||||
|
||||
# 搜索和操作栏
|
||||
toolbar = QHBoxLayout()
|
||||
|
||||
self.search_input = QLineEdit()
|
||||
self.search_input.setPlaceholderText("🔍 搜索日志内容...")
|
||||
self.search_input.textChanged.connect(self.filter_log)
|
||||
toolbar.addWidget(self.search_input, 1)
|
||||
|
||||
# 级别过滤
|
||||
self.level_buttons = {}
|
||||
for level, color in [("ERROR", "#ef4444"), ("WARNING", "#f59e0b"), ("INFO", "#22c55e"), ("DEBUG", "#64748b")]:
|
||||
btn = QPushButton(level)
|
||||
btn.setCheckable(True)
|
||||
btn.setChecked(True)
|
||||
btn.setStyleSheet(f"""
|
||||
QPushButton {{
|
||||
background: rgba({self.hex_to_rgb(color)}, 0.2);
|
||||
border: 1px solid {color};
|
||||
color: {color};
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
}}
|
||||
QPushButton:checked {{
|
||||
background: {color};
|
||||
color: white;
|
||||
}}
|
||||
""")
|
||||
btn.clicked.connect(self.filter_log)
|
||||
toolbar.addWidget(btn)
|
||||
self.level_buttons[level] = btn
|
||||
|
||||
right_layout.addLayout(toolbar)
|
||||
|
||||
# 日志内容显示
|
||||
self.log_text = QTextEdit()
|
||||
self.log_text.setReadOnly(True)
|
||||
self.log_text.setFont(QFont("Consolas", 11))
|
||||
self.log_text.setStyleSheet("""
|
||||
QTextEdit {
|
||||
background: #0f172a;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
""")
|
||||
right_layout.addWidget(self.log_text, 1)
|
||||
|
||||
# 底部操作
|
||||
bottom_bar = QHBoxLayout()
|
||||
|
||||
self.status_label = QLabel("选择左侧日志文件查看")
|
||||
self.status_label.setStyleSheet("color: #64748b; font-size: 12px;")
|
||||
bottom_bar.addWidget(self.status_label)
|
||||
|
||||
bottom_bar.addStretch()
|
||||
|
||||
export_btn = QPushButton("📤 导出日志")
|
||||
export_btn.setObjectName("primary_btn")
|
||||
export_btn.clicked.connect(self.export_log)
|
||||
bottom_bar.addWidget(export_btn)
|
||||
|
||||
right_layout.addLayout(bottom_bar)
|
||||
|
||||
splitter.addWidget(right_panel)
|
||||
splitter.setSizes([200, 600])
|
||||
|
||||
layout.addWidget(splitter, 1)
|
||||
|
||||
def load_log_files(self):
|
||||
"""加载日志文件列表"""
|
||||
self.file_list.clear()
|
||||
log_files = get_all_log_files()
|
||||
|
||||
for log_file in log_files:
|
||||
item = QListWidgetItem(log_file.name)
|
||||
item.setData(Qt.ItemDataRole.UserRole, log_file)
|
||||
self.file_list.addItem(item)
|
||||
|
||||
# 自动选择第一个(最新)
|
||||
if self.file_list.count() > 0:
|
||||
self.file_list.setCurrentRow(0)
|
||||
|
||||
logging.info(f"加载了 {len(log_files)} 个日志文件")
|
||||
|
||||
def on_file_selected(self, current: QListWidgetItem, previous: QListWidgetItem):
|
||||
"""日志文件选择变化"""
|
||||
if current:
|
||||
file_path = current.data(Qt.ItemDataRole.UserRole)
|
||||
self.current_log_content = read_log_file(file_path)
|
||||
self.filter_log()
|
||||
self.status_label.setText(f"文件: {file_path.name}")
|
||||
|
||||
def filter_log(self):
|
||||
"""过滤日志内容"""
|
||||
if not self.current_log_content:
|
||||
return
|
||||
|
||||
search_text = self.search_input.text().lower()
|
||||
active_levels = [level for level, btn in self.level_buttons.items() if btn.isChecked()]
|
||||
|
||||
lines = self.current_log_content.split('\n')
|
||||
filtered_lines = []
|
||||
|
||||
for line in lines:
|
||||
# 检查级别过滤
|
||||
level_match = any(f"[{level}]" in line for level in active_levels)
|
||||
if not level_match and line.strip():
|
||||
# 如果不是日志行(没有级别标记),检查是否是错误堆栈的一部分
|
||||
if filtered_lines and not line.startswith('['):
|
||||
level_match = True
|
||||
else:
|
||||
continue
|
||||
|
||||
# 检查搜索过滤
|
||||
if search_text and search_text not in line.lower():
|
||||
continue
|
||||
|
||||
filtered_lines.append(line)
|
||||
|
||||
# 语法高亮
|
||||
highlighted = self.highlight_log('\n'.join(filtered_lines))
|
||||
self.log_text.setHtml(highlighted)
|
||||
|
||||
def highlight_log(self, text: str) -> str:
|
||||
"""日志语法高亮"""
|
||||
import html
|
||||
text = html.escape(text)
|
||||
|
||||
# 替换颜色
|
||||
text = text.replace('[ERROR]', '<span style="color: #ef4444; font-weight: bold;">[ERROR]</span>')
|
||||
text = text.replace('[WARNING]', '<span style="color: #f59e0b; font-weight: bold;">[WARNING]</span>')
|
||||
text = text.replace('[INFO]', '<span style="color: #22c55e;">[INFO]</span>')
|
||||
text = text.replace('[DEBUG]', '<span style="color: #64748b;">[DEBUG]</span>')
|
||||
|
||||
# 时间戳颜色
|
||||
import re
|
||||
text = re.sub(
|
||||
r'\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\]',
|
||||
r'<span style="color: #94a3b8;">[\1]</span>',
|
||||
text
|
||||
)
|
||||
|
||||
return f'<pre style="margin: 0; font-family: Consolas, monospace;">{text}</pre>'
|
||||
|
||||
def export_log(self):
|
||||
"""导出日志文件"""
|
||||
if not self.current_log_content:
|
||||
QMessageBox.warning(self, "提示", "请先选择一个日志文件")
|
||||
return
|
||||
|
||||
file_path, _ = QFileDialog.getSaveFileName(
|
||||
self, "导出日志", "log_export.txt", "文本文件 (*.txt)"
|
||||
)
|
||||
|
||||
if file_path:
|
||||
try:
|
||||
with open(file_path, 'w', encoding='utf-8') as f:
|
||||
f.write(self.log_text.toPlainText())
|
||||
QMessageBox.information(self, "成功", f"日志已导出到:\n{file_path}")
|
||||
logging.info(f"日志已导出到: {file_path}")
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "错误", f"导出失败: {e}")
|
||||
logging.error(f"导出日志失败: {e}")
|
||||
|
||||
@staticmethod
|
||||
def hex_to_rgb(hex_color: str) -> str:
|
||||
"""十六进制颜色转RGB"""
|
||||
hex_color = hex_color.lstrip('#')
|
||||
r, g, b = tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
|
||||
return f"{r}, {g}, {b}"
|
||||
|
||||
189
ui/main_window.py
Normal file
189
ui/main_window.py
Normal file
@@ -0,0 +1,189 @@
|
||||
"""
|
||||
主窗口 - 三栏布局
|
||||
- 左侧: 一级菜单(80px)
|
||||
- 中间: 二级菜单(256px)
|
||||
- 右侧: 工作区(弹性宽度)
|
||||
"""
|
||||
from PySide6.QtWidgets import (
|
||||
QMainWindow, QWidget, QHBoxLayout, QStackedWidget, QGraphicsOpacityEffect
|
||||
)
|
||||
from PySide6.QtCore import Qt, QPropertyAnimation, QEasingCurve
|
||||
from PySide6.QtGui import QFont
|
||||
import logging
|
||||
|
||||
from ui.sidebar import PrimarySidebar
|
||||
from ui.tool_list import SecondarySidebar, TOOLS_DATA
|
||||
from ui.workspace import WelcomeWorkspace
|
||||
from ui.settings import SettingsPage
|
||||
from ui.animations import animate_widget_show
|
||||
from ui.profile_card import ProfileCard
|
||||
from core.config import config
|
||||
|
||||
|
||||
class MainWindow(QMainWindow):
|
||||
"""主窗口"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setWindowTitle("奶酪云工具箱 | Cheese Cloud Tools")
|
||||
self.setMinimumSize(1200, 700)
|
||||
self.resize(1400, 800)
|
||||
|
||||
# 工具页面映射
|
||||
self.tool_pages = {}
|
||||
|
||||
self.setup_ui()
|
||||
self.setup_connections()
|
||||
|
||||
logging.info("主窗口初始化完成")
|
||||
|
||||
def setup_ui(self):
|
||||
"""设置UI"""
|
||||
# 中央部件
|
||||
central_widget = QWidget()
|
||||
self.setCentralWidget(central_widget)
|
||||
|
||||
# 主布局
|
||||
main_layout = QHBoxLayout(central_widget)
|
||||
main_layout.setContentsMargins(0, 0, 0, 0)
|
||||
main_layout.setSpacing(0)
|
||||
|
||||
# 1. 左侧一级菜单
|
||||
self.primary_sidebar = PrimarySidebar()
|
||||
main_layout.addWidget(self.primary_sidebar)
|
||||
|
||||
# 2. 中间二级菜单
|
||||
self.secondary_sidebar = SecondarySidebar()
|
||||
main_layout.addWidget(self.secondary_sidebar)
|
||||
|
||||
# 3. 右侧工作区(使用堆叠窗口切换不同工具页面)
|
||||
self.workspace_stack = QStackedWidget()
|
||||
self.workspace_stack.setObjectName("workspace")
|
||||
main_layout.addWidget(self.workspace_stack, 1)
|
||||
|
||||
# 添加欢迎页
|
||||
self.welcome_page = WelcomeWorkspace()
|
||||
self.workspace_stack.addWidget(self.welcome_page)
|
||||
|
||||
# 添加设置页面
|
||||
self.settings_page = SettingsPage()
|
||||
self.workspace_stack.addWidget(self.settings_page)
|
||||
|
||||
# 延迟导入并添加工具页面
|
||||
self.init_tool_pages()
|
||||
|
||||
def init_tool_pages(self):
|
||||
"""初始化所有工具页面"""
|
||||
# 图片工具
|
||||
from tools.image.compress import ImageCompressPage
|
||||
from tools.image.convert import ImageConvertPage
|
||||
from tools.image.watermark import ImageWatermarkPage
|
||||
|
||||
self.tool_pages["img-compress"] = ImageCompressPage()
|
||||
self.tool_pages["img-convert"] = ImageConvertPage()
|
||||
self.tool_pages["img-watermark"] = ImageWatermarkPage()
|
||||
|
||||
# PDF工具
|
||||
from tools.pdf.split import PDFSplitPage
|
||||
from tools.pdf.merge import PDFMergePage
|
||||
from tools.pdf.to_word import PDFToWordPage
|
||||
|
||||
self.tool_pages["pdf-split"] = PDFSplitPage()
|
||||
self.tool_pages["pdf-merge"] = PDFMergePage()
|
||||
self.tool_pages["pdf-word"] = PDFToWordPage()
|
||||
|
||||
# Excel工具
|
||||
from tools.excel.preview import ExcelPreviewPage
|
||||
from tools.excel.chart import ExcelChartPage
|
||||
|
||||
self.tool_pages["xls-view"] = ExcelPreviewPage()
|
||||
self.tool_pages["xls-chart"] = ExcelChartPage()
|
||||
|
||||
# 添加到堆叠窗口
|
||||
for page in self.tool_pages.values():
|
||||
self.workspace_stack.addWidget(page)
|
||||
|
||||
def setup_connections(self):
|
||||
"""设置信号连接"""
|
||||
# 一级菜单切换
|
||||
self.primary_sidebar.category_changed.connect(self.on_category_changed)
|
||||
self.primary_sidebar.settings_clicked.connect(self.show_settings)
|
||||
self.primary_sidebar.logo_clicked.connect(self.show_profile_card)
|
||||
|
||||
# 二级菜单工具选择
|
||||
self.secondary_sidebar.tool_selected.connect(self.on_tool_selected)
|
||||
|
||||
# 欢迎页快捷入口
|
||||
self.welcome_page.tool_clicked.connect(self.on_shortcut_clicked)
|
||||
|
||||
def on_category_changed(self, category: str):
|
||||
"""分类切换"""
|
||||
logging.debug(f"切换分类: {category}")
|
||||
self.secondary_sidebar.load_tools(category)
|
||||
# 显示欢迎页
|
||||
self._switch_page(self.welcome_page)
|
||||
|
||||
def on_tool_selected(self, tool_data: dict):
|
||||
"""工具选择"""
|
||||
tool_id = tool_data.get("id")
|
||||
logging.info(f"选择工具: {tool_data.get('name')} ({tool_id})")
|
||||
|
||||
if tool_id in self.tool_pages:
|
||||
page = self.tool_pages[tool_id]
|
||||
# 设置面包屑
|
||||
category_title = TOOLS_DATA.get(self.secondary_sidebar.current_category, {}).get("title", "")
|
||||
page.set_breadcrumb(category_title, tool_data.get("name", ""))
|
||||
page.set_title(tool_data.get("name", ""))
|
||||
|
||||
self._switch_page(page)
|
||||
else:
|
||||
logging.warning(f"未找到工具页面: {tool_id}")
|
||||
|
||||
def on_shortcut_clicked(self, tool_id: str, category: str):
|
||||
"""快捷入口点击"""
|
||||
logging.debug(f"快捷入口: {tool_id}, {category}")
|
||||
self.primary_sidebar.set_category(category)
|
||||
self.secondary_sidebar.load_tools(category)
|
||||
self.secondary_sidebar.select_tool(tool_id)
|
||||
|
||||
def show_settings(self):
|
||||
"""显示设置页面"""
|
||||
logging.debug("打开设置页面")
|
||||
self._switch_page(self.settings_page)
|
||||
|
||||
def show_profile_card(self):
|
||||
"""显示个人信息卡片"""
|
||||
logging.debug("打开个人信息卡片")
|
||||
dialog = ProfileCard(self)
|
||||
dialog.exec()
|
||||
|
||||
def _switch_page(self, page: QWidget):
|
||||
"""切换页面(带动画)"""
|
||||
if not config.get("animation_enabled", True):
|
||||
self.workspace_stack.setCurrentWidget(page)
|
||||
return
|
||||
|
||||
# 获取当前页面
|
||||
current = self.workspace_stack.currentWidget()
|
||||
if current == page:
|
||||
return
|
||||
|
||||
# 设置新页面透明度效果
|
||||
effect = QGraphicsOpacityEffect(page)
|
||||
effect.setOpacity(0)
|
||||
page.setGraphicsEffect(effect)
|
||||
|
||||
# 切换页面
|
||||
self.workspace_stack.setCurrentWidget(page)
|
||||
|
||||
# 渐入动画
|
||||
duration = config.get("animation_duration", 300)
|
||||
anim = QPropertyAnimation(effect, b"opacity")
|
||||
anim.setDuration(duration)
|
||||
anim.setStartValue(0.0)
|
||||
anim.setEndValue(1.0)
|
||||
anim.setEasingCurve(QEasingCurve.Type.OutCubic)
|
||||
|
||||
# 保持引用防止被垃圾回收
|
||||
page._fade_anim = anim
|
||||
anim.start()
|
||||
539
ui/profile_card.py
Normal file
539
ui/profile_card.py
Normal file
@@ -0,0 +1,539 @@
|
||||
"""
|
||||
个人信息卡片模态框
|
||||
- 头像、微信二维码、邮箱
|
||||
- 高级UI设计,带装饰元素和动画
|
||||
"""
|
||||
import os
|
||||
from pathlib import Path
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
|
||||
QWidget, QGraphicsOpacityEffect, QApplication
|
||||
)
|
||||
from PySide6.QtCore import (
|
||||
Qt, QPropertyAnimation, QEasingCurve, QTimer,
|
||||
QPoint, QRect, QParallelAnimationGroup, Signal
|
||||
)
|
||||
from PySide6.QtGui import (
|
||||
QPixmap, QPainter, QColor, QLinearGradient, QRadialGradient,
|
||||
QFont, QPen, QBrush, QPainterPath, QCursor
|
||||
)
|
||||
import math
|
||||
|
||||
|
||||
def get_resource_path(relative_path: str) -> str:
|
||||
"""获取资源文件的绝对路径"""
|
||||
base_path = Path(__file__).parent.parent
|
||||
return str(base_path / relative_path)
|
||||
|
||||
|
||||
# 资源路径
|
||||
AVATAR_PATH = get_resource_path("image/头像.jpg")
|
||||
QRCODE_PATH = get_resource_path("image/二维码.jpg")
|
||||
|
||||
|
||||
class FloatingOrb(QWidget):
|
||||
"""浮动装饰球"""
|
||||
|
||||
def __init__(self, size: int, color: QColor, parent=None):
|
||||
super().__init__(parent)
|
||||
self.orb_size = size
|
||||
self.orb_color = color
|
||||
self.setFixedSize(size, size)
|
||||
self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents)
|
||||
|
||||
# 设置透明效果
|
||||
self.opacity_effect = QGraphicsOpacityEffect(self)
|
||||
self.opacity_effect.setOpacity(0.6)
|
||||
self.setGraphicsEffect(self.opacity_effect)
|
||||
|
||||
def paintEvent(self, event):
|
||||
painter = QPainter(self)
|
||||
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||
|
||||
# 径向渐变
|
||||
gradient = QRadialGradient(
|
||||
self.orb_size / 2, self.orb_size / 2, self.orb_size / 2
|
||||
)
|
||||
gradient.setColorAt(0, QColor(self.orb_color.red(), self.orb_color.green(), self.orb_color.blue(), 120))
|
||||
gradient.setColorAt(0.5, QColor(self.orb_color.red(), self.orb_color.green(), self.orb_color.blue(), 60))
|
||||
gradient.setColorAt(1, QColor(self.orb_color.red(), self.orb_color.green(), self.orb_color.blue(), 0))
|
||||
|
||||
painter.setBrush(gradient)
|
||||
painter.setPen(Qt.PenStyle.NoPen)
|
||||
painter.drawEllipse(0, 0, self.orb_size, self.orb_size)
|
||||
|
||||
|
||||
class GlowButton(QPushButton):
|
||||
"""发光按钮"""
|
||||
|
||||
def __init__(self, text: str, primary: bool = True, parent=None):
|
||||
super().__init__(text, parent)
|
||||
self.primary = primary
|
||||
self._hovered = False
|
||||
self.setFixedHeight(44)
|
||||
self.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self.setFont(QFont("Microsoft YaHei UI", 10, QFont.Weight.Medium))
|
||||
|
||||
self.setStyleSheet("""
|
||||
QPushButton {
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
padding: 10px 24px;
|
||||
color: white;
|
||||
}
|
||||
""")
|
||||
|
||||
def enterEvent(self, event):
|
||||
self._hovered = True
|
||||
self.update()
|
||||
|
||||
def leaveEvent(self, event):
|
||||
self._hovered = False
|
||||
self.update()
|
||||
|
||||
def paintEvent(self, event):
|
||||
painter = QPainter(self)
|
||||
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||
|
||||
rect = self.rect()
|
||||
|
||||
if self.primary:
|
||||
# 主按钮 - 奶酪色渐变
|
||||
gradient = QLinearGradient(0, 0, rect.width(), 0)
|
||||
if self._hovered:
|
||||
gradient.setColorAt(0, QColor("#fcd34d"))
|
||||
gradient.setColorAt(1, QColor("#f59e0b"))
|
||||
else:
|
||||
gradient.setColorAt(0, QColor("#fbbf24"))
|
||||
gradient.setColorAt(1, QColor("#d97706"))
|
||||
|
||||
painter.setBrush(gradient)
|
||||
painter.setPen(Qt.PenStyle.NoPen)
|
||||
painter.drawRoundedRect(rect, 10, 10)
|
||||
|
||||
# 发光效果
|
||||
if self._hovered:
|
||||
glow = QRadialGradient(rect.center().x(), rect.center().y(), rect.width() / 2)
|
||||
glow.setColorAt(0, QColor(251, 191, 36, 60))
|
||||
glow.setColorAt(1, QColor(251, 191, 36, 0))
|
||||
painter.setBrush(glow)
|
||||
painter.drawRoundedRect(rect.adjusted(-5, -5, 5, 5), 15, 15)
|
||||
else:
|
||||
# 次按钮 - 透明边框
|
||||
if self._hovered:
|
||||
painter.setBrush(QColor(71, 85, 105, 80))
|
||||
else:
|
||||
painter.setBrush(QColor(71, 85, 105, 40))
|
||||
|
||||
painter.setPen(QPen(QColor("#475569"), 1))
|
||||
painter.drawRoundedRect(rect.adjusted(1, 1, -1, -1), 10, 10)
|
||||
|
||||
# 绘制文字
|
||||
painter.setPen(QColor("white") if self.primary else QColor("#94a3b8"))
|
||||
painter.setFont(self.font())
|
||||
painter.drawText(rect, Qt.AlignmentFlag.AlignCenter, self.text())
|
||||
|
||||
|
||||
class ProfileCard(QDialog):
|
||||
"""个人信息卡片模态框"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setWindowFlags(
|
||||
Qt.WindowType.FramelessWindowHint |
|
||||
Qt.WindowType.Dialog
|
||||
)
|
||||
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
||||
self.setModal(True)
|
||||
self.setFixedSize(480, 780)
|
||||
|
||||
self._drag_pos = None
|
||||
self.setup_ui()
|
||||
self.setup_animations()
|
||||
self.add_decorations()
|
||||
|
||||
def setup_ui(self):
|
||||
"""设置UI"""
|
||||
# 主容器
|
||||
self.container = QWidget(self)
|
||||
self.container.setGeometry(0, 0, 480, 780)
|
||||
|
||||
main_layout = QVBoxLayout(self.container)
|
||||
main_layout.setContentsMargins(0, 0, 0, 0)
|
||||
main_layout.setSpacing(0)
|
||||
|
||||
# 内容卡片
|
||||
self.card = QWidget()
|
||||
self.card.setObjectName("profile_card")
|
||||
card_layout = QVBoxLayout(self.card)
|
||||
card_layout.setContentsMargins(32, 24, 32, 24)
|
||||
card_layout.setSpacing(12)
|
||||
|
||||
# 关闭按钮
|
||||
close_btn = QPushButton("×")
|
||||
close_btn.setFixedSize(36, 36)
|
||||
close_btn.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
close_btn.setStyleSheet("""
|
||||
QPushButton {
|
||||
background: rgba(71, 85, 105, 0.4);
|
||||
border: none;
|
||||
border-radius: 18px;
|
||||
color: #94a3b8;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background: rgba(239, 68, 68, 0.6);
|
||||
color: white;
|
||||
}
|
||||
""")
|
||||
close_btn.clicked.connect(self.close_with_animation)
|
||||
|
||||
close_layout = QHBoxLayout()
|
||||
close_layout.addStretch()
|
||||
close_layout.addWidget(close_btn)
|
||||
card_layout.addLayout(close_layout)
|
||||
|
||||
# 头像区域
|
||||
avatar_container = QWidget()
|
||||
avatar_container.setFixedHeight(100)
|
||||
avatar_layout = QHBoxLayout(avatar_container)
|
||||
avatar_layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
self.avatar_label = QLabel()
|
||||
self.avatar_label.setFixedSize(100, 100)
|
||||
self.avatar_label.setStyleSheet("""
|
||||
QLabel {
|
||||
border-radius: 50px;
|
||||
border: 3px solid #fbbf24;
|
||||
}
|
||||
""")
|
||||
self._load_avatar()
|
||||
avatar_layout.addWidget(self.avatar_label)
|
||||
card_layout.addWidget(avatar_container)
|
||||
|
||||
# 名称
|
||||
name_label = QLabel("奶酪云工具箱")
|
||||
name_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
name_label.setStyleSheet("""
|
||||
QLabel {
|
||||
color: #f8fafc;
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
font-family: "Microsoft YaHei UI";
|
||||
}
|
||||
""")
|
||||
card_layout.addWidget(name_label)
|
||||
|
||||
# 副标题
|
||||
subtitle_label = QLabel("高效办公 · 精致生活")
|
||||
subtitle_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
subtitle_label.setStyleSheet("""
|
||||
QLabel {
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
font-family: "Microsoft YaHei UI";
|
||||
}
|
||||
""")
|
||||
card_layout.addWidget(subtitle_label)
|
||||
|
||||
card_layout.addSpacing(8)
|
||||
|
||||
# 分割线
|
||||
divider = QWidget()
|
||||
divider.setFixedHeight(1)
|
||||
divider.setStyleSheet("background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 transparent, stop:0.5 #334155, stop:1 transparent);")
|
||||
card_layout.addWidget(divider)
|
||||
|
||||
card_layout.addSpacing(8)
|
||||
|
||||
# 二维码区域
|
||||
qr_container = QWidget()
|
||||
qr_layout = QVBoxLayout(qr_container)
|
||||
qr_layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
qr_layout.setSpacing(8)
|
||||
|
||||
qr_title = QLabel("📱 微信扫码添加好友")
|
||||
qr_title.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
qr_title.setStyleSheet("""
|
||||
QLabel {
|
||||
color: #94a3b8;
|
||||
font-size: 12px;
|
||||
font-family: "Microsoft YaHei UI";
|
||||
}
|
||||
""")
|
||||
qr_layout.addWidget(qr_title)
|
||||
|
||||
self.qr_label = QLabel()
|
||||
self.qr_label.setFixedSize(220, 220)
|
||||
self.qr_label.setStyleSheet("""
|
||||
QLabel {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
}
|
||||
""")
|
||||
self._load_qrcode()
|
||||
qr_layout.addWidget(self.qr_label, 0, Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
card_layout.addWidget(qr_container)
|
||||
|
||||
card_layout.addSpacing(10)
|
||||
|
||||
# 邮箱区域
|
||||
email_container = QWidget()
|
||||
email_container.setStyleSheet("""
|
||||
QWidget {
|
||||
background: rgba(30, 41, 59, 0.6);
|
||||
border-radius: 12px;
|
||||
}
|
||||
""")
|
||||
email_layout = QVBoxLayout(email_container)
|
||||
email_layout.setContentsMargins(20, 16, 20, 16)
|
||||
email_layout.setSpacing(8)
|
||||
|
||||
email_title = QLabel("📧 合作联系邮箱")
|
||||
email_title.setStyleSheet("""
|
||||
QLabel {
|
||||
color: #94a3b8;
|
||||
font-size: 12px;
|
||||
background: transparent;
|
||||
}
|
||||
""")
|
||||
email_layout.addWidget(email_title)
|
||||
|
||||
email_row = QHBoxLayout()
|
||||
|
||||
self.email_label = QLabel("workerqi@163.com")
|
||||
self.email_label.setStyleSheet("""
|
||||
QLabel {
|
||||
color: #fbbf24;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
background: transparent;
|
||||
}
|
||||
""")
|
||||
email_row.addWidget(self.email_label)
|
||||
|
||||
email_row.addStretch()
|
||||
|
||||
copy_btn = GlowButton("复制", False)
|
||||
copy_btn.setFixedSize(70, 34)
|
||||
copy_btn.clicked.connect(self.copy_email)
|
||||
email_row.addWidget(copy_btn)
|
||||
|
||||
send_btn = GlowButton("发送邮件", True)
|
||||
send_btn.setFixedSize(90, 34)
|
||||
send_btn.clicked.connect(self.send_email)
|
||||
email_row.addWidget(send_btn)
|
||||
|
||||
email_layout.addLayout(email_row)
|
||||
card_layout.addWidget(email_container)
|
||||
|
||||
card_layout.addStretch()
|
||||
|
||||
# 底部提示
|
||||
tip_label = QLabel("点击空白处关闭")
|
||||
tip_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
tip_label.setStyleSheet("""
|
||||
QLabel {
|
||||
color: #475569;
|
||||
font-size: 11px;
|
||||
}
|
||||
""")
|
||||
card_layout.addWidget(tip_label)
|
||||
|
||||
main_layout.addWidget(self.card)
|
||||
|
||||
def _load_avatar(self):
|
||||
"""加载头像"""
|
||||
if os.path.exists(AVATAR_PATH):
|
||||
pixmap = QPixmap(AVATAR_PATH)
|
||||
if not pixmap.isNull():
|
||||
# 创建圆形头像
|
||||
scaled = pixmap.scaled(94, 94, Qt.AspectRatioMode.KeepAspectRatioByExpanding,
|
||||
Qt.TransformationMode.SmoothTransformation)
|
||||
|
||||
# 裁剪为圆形
|
||||
rounded = QPixmap(94, 94)
|
||||
rounded.fill(Qt.GlobalColor.transparent)
|
||||
|
||||
painter = QPainter(rounded)
|
||||
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||
|
||||
path = QPainterPath()
|
||||
path.addEllipse(0, 0, 94, 94)
|
||||
painter.setClipPath(path)
|
||||
|
||||
# 居中裁剪
|
||||
x = (scaled.width() - 94) // 2
|
||||
y = (scaled.height() - 94) // 2
|
||||
painter.drawPixmap(0, 0, scaled, x, y, 94, 94)
|
||||
painter.end()
|
||||
|
||||
self.avatar_label.setPixmap(rounded)
|
||||
return
|
||||
|
||||
# 默认头像
|
||||
self.avatar_label.setText("🧀")
|
||||
self.avatar_label.setStyleSheet(self.avatar_label.styleSheet() + """
|
||||
font-size: 48px;
|
||||
background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #fbbf24, stop:1 #d97706);
|
||||
""")
|
||||
|
||||
def _load_qrcode(self):
|
||||
"""加载二维码"""
|
||||
if os.path.exists(QRCODE_PATH):
|
||||
pixmap = QPixmap(QRCODE_PATH)
|
||||
if not pixmap.isNull():
|
||||
# 缩放到完整显示,保持宽高比
|
||||
scaled = pixmap.scaled(210, 210, Qt.AspectRatioMode.KeepAspectRatio,
|
||||
Qt.TransformationMode.SmoothTransformation)
|
||||
self.qr_label.setPixmap(scaled)
|
||||
self.qr_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
return
|
||||
|
||||
self.qr_label.setText("二维码")
|
||||
self.qr_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
def add_decorations(self):
|
||||
"""添加装饰元素"""
|
||||
# 添加浮动装饰球
|
||||
orb1 = FloatingOrb(120, QColor("#fbbf24"), self.container)
|
||||
orb1.move(-30, -30)
|
||||
|
||||
orb2 = FloatingOrb(80, QColor("#3b82f6"), self.container)
|
||||
orb2.move(420, 50)
|
||||
|
||||
orb3 = FloatingOrb(60, QColor("#8b5cf6"), self.container)
|
||||
orb3.move(400, 500)
|
||||
|
||||
orb4 = FloatingOrb(100, QColor("#ec4899"), self.container)
|
||||
orb4.move(-20, 480)
|
||||
|
||||
# 确保装饰在底层
|
||||
orb1.lower()
|
||||
orb2.lower()
|
||||
orb3.lower()
|
||||
orb4.lower()
|
||||
|
||||
def setup_animations(self):
|
||||
"""设置动画"""
|
||||
# 卡片淡入效果
|
||||
self.card_opacity = QGraphicsOpacityEffect(self.card)
|
||||
self.card_opacity.setOpacity(0)
|
||||
self.card.setGraphicsEffect(self.card_opacity)
|
||||
|
||||
self.fade_anim = QPropertyAnimation(self.card_opacity, b"opacity")
|
||||
self.fade_anim.setDuration(300)
|
||||
self.fade_anim.setEasingCurve(QEasingCurve.Type.OutCubic)
|
||||
|
||||
def showEvent(self, event):
|
||||
super().showEvent(event)
|
||||
# 居中显示
|
||||
if self.parent():
|
||||
parent_rect = self.parent().rect()
|
||||
x = (parent_rect.width() - self.width()) // 2
|
||||
y = (parent_rect.height() - self.height()) // 2
|
||||
self.move(self.parent().mapToGlobal(QPoint(x, y)))
|
||||
|
||||
# 播放淡入动画
|
||||
self.fade_anim.setStartValue(0)
|
||||
self.fade_anim.setEndValue(1)
|
||||
self.fade_anim.start()
|
||||
|
||||
def close_with_animation(self):
|
||||
"""带动画关闭"""
|
||||
self.fade_anim.setStartValue(1)
|
||||
self.fade_anim.setEndValue(0)
|
||||
self.fade_anim.finished.connect(self.accept)
|
||||
self.fade_anim.start()
|
||||
|
||||
def copy_email(self):
|
||||
"""复制邮箱"""
|
||||
clipboard = QApplication.clipboard()
|
||||
clipboard.setText("workerqi@163.com")
|
||||
|
||||
# 临时显示"已复制"
|
||||
original_text = self.email_label.text()
|
||||
self.email_label.setText("✓ 已复制到剪贴板")
|
||||
self.email_label.setStyleSheet("""
|
||||
QLabel {
|
||||
color: #22c55e;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
background: transparent;
|
||||
}
|
||||
""")
|
||||
QTimer.singleShot(1500, lambda: self._reset_email_label(original_text))
|
||||
|
||||
def _reset_email_label(self, text):
|
||||
"""重置邮箱标签"""
|
||||
self.email_label.setText(text)
|
||||
self.email_label.setStyleSheet("""
|
||||
QLabel {
|
||||
color: #fbbf24;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
background: transparent;
|
||||
}
|
||||
""")
|
||||
|
||||
def send_email(self):
|
||||
"""发送邮件"""
|
||||
import webbrowser
|
||||
webbrowser.open("mailto:workerqi@163.com?subject=奶酪云工具箱-合作咨询")
|
||||
|
||||
def paintEvent(self, event):
|
||||
"""绘制背景"""
|
||||
painter = QPainter(self)
|
||||
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||
|
||||
# 半透明遮罩背景
|
||||
painter.fillRect(self.rect(), QColor(0, 0, 0, 120))
|
||||
|
||||
# 卡片背景
|
||||
card_rect = self.card.geometry().adjusted(20, 20, -20, -20)
|
||||
|
||||
# 卡片阴影
|
||||
shadow_color = QColor(0, 0, 0, 80)
|
||||
for i in range(10):
|
||||
painter.setPen(Qt.PenStyle.NoPen)
|
||||
painter.setBrush(QColor(0, 0, 0, int(80 - i * 8)))
|
||||
painter.drawRoundedRect(card_rect.adjusted(-i*2, -i*2, i*2, i*2), 24 + i, 24 + i)
|
||||
|
||||
# 卡片主体 - 玻璃质感
|
||||
gradient = QLinearGradient(card_rect.topLeft(), card_rect.bottomRight())
|
||||
gradient.setColorAt(0, QColor(30, 41, 59, 240))
|
||||
gradient.setColorAt(0.5, QColor(15, 23, 42, 250))
|
||||
gradient.setColorAt(1, QColor(30, 41, 59, 240))
|
||||
|
||||
painter.setBrush(gradient)
|
||||
painter.setPen(QPen(QColor(71, 85, 105, 100), 1))
|
||||
painter.drawRoundedRect(card_rect, 24, 24)
|
||||
|
||||
# 顶部高光
|
||||
highlight_rect = QRect(card_rect.x() + 40, card_rect.y(), card_rect.width() - 80, 2)
|
||||
highlight_gradient = QLinearGradient(highlight_rect.topLeft(), highlight_rect.topRight())
|
||||
highlight_gradient.setColorAt(0, QColor(251, 191, 36, 0))
|
||||
highlight_gradient.setColorAt(0.5, QColor(251, 191, 36, 150))
|
||||
highlight_gradient.setColorAt(1, QColor(251, 191, 36, 0))
|
||||
painter.setBrush(highlight_gradient)
|
||||
painter.setPen(Qt.PenStyle.NoPen)
|
||||
painter.drawRoundedRect(highlight_rect, 1, 1)
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
"""点击空白处关闭"""
|
||||
card_rect = self.card.geometry().adjusted(20, 20, -20, -20)
|
||||
if not card_rect.contains(event.pos()):
|
||||
self.close_with_animation()
|
||||
else:
|
||||
self._drag_pos = event.globalPosition().toPoint() - self.frameGeometry().topLeft()
|
||||
|
||||
def mouseMoveEvent(self, event):
|
||||
"""拖动窗口"""
|
||||
if self._drag_pos:
|
||||
self.move(event.globalPosition().toPoint() - self._drag_pos)
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
self._drag_pos = None
|
||||
|
||||
234
ui/settings.py
Normal file
234
ui/settings.py
Normal file
@@ -0,0 +1,234 @@
|
||||
"""
|
||||
设置页面
|
||||
- 全局配置选项
|
||||
- 文件保存位置设置
|
||||
- 日志查看器
|
||||
"""
|
||||
from PySide6.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
|
||||
QFrame, QLineEdit, QFileDialog, QCheckBox, QSlider,
|
||||
QTabWidget, QSpinBox, QMessageBox, QScrollArea, QGroupBox
|
||||
)
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtGui import QFont
|
||||
import logging
|
||||
|
||||
from core.config import config
|
||||
from ui.log_viewer import LogViewer
|
||||
|
||||
|
||||
class SettingsPage(QWidget):
|
||||
"""设置页面"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setup_ui()
|
||||
self.load_settings()
|
||||
|
||||
def setup_ui(self):
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(24, 24, 24, 24)
|
||||
layout.setSpacing(16)
|
||||
|
||||
# 标题
|
||||
title = QLabel("⚙️ 设置")
|
||||
title.setFont(QFont("Microsoft YaHei", 20, QFont.Weight.Bold))
|
||||
title.setStyleSheet("color: white;")
|
||||
layout.addWidget(title)
|
||||
|
||||
# 标签页
|
||||
self.tab_widget = QTabWidget()
|
||||
self.tab_widget.setStyleSheet("""
|
||||
QTabWidget::pane {
|
||||
border: none;
|
||||
background: transparent;
|
||||
}
|
||||
QTabBar::tab {
|
||||
background: #1e293b;
|
||||
color: #94a3b8;
|
||||
padding: 12px 24px;
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
font-size: 13px;
|
||||
}
|
||||
QTabBar::tab:hover {
|
||||
color: #e2e8f0;
|
||||
}
|
||||
QTabBar::tab:selected {
|
||||
color: #fbbf24;
|
||||
border-bottom: 2px solid #fbbf24;
|
||||
}
|
||||
""")
|
||||
|
||||
# 通用设置标签页
|
||||
self.tab_widget.addTab(self.create_general_tab(), "📁 通用设置")
|
||||
|
||||
# 界面设置标签页
|
||||
self.tab_widget.addTab(self.create_ui_tab(), "🎨 界面设置")
|
||||
|
||||
# 日志查看标签页
|
||||
self.log_viewer = LogViewer()
|
||||
self.tab_widget.addTab(self.log_viewer, "📋 日志查看")
|
||||
|
||||
layout.addWidget(self.tab_widget, 1)
|
||||
|
||||
# 底部按钮
|
||||
btn_layout = QHBoxLayout()
|
||||
btn_layout.addStretch()
|
||||
|
||||
reset_btn = QPushButton("🔄 恢复默认")
|
||||
reset_btn.setObjectName("secondary_btn")
|
||||
reset_btn.clicked.connect(self.reset_settings)
|
||||
btn_layout.addWidget(reset_btn)
|
||||
|
||||
save_btn = QPushButton("💾 保存设置")
|
||||
save_btn.setObjectName("primary_btn")
|
||||
save_btn.clicked.connect(self.save_settings)
|
||||
btn_layout.addWidget(save_btn)
|
||||
|
||||
layout.addLayout(btn_layout)
|
||||
|
||||
def create_general_tab(self) -> QWidget:
|
||||
"""创建通用设置标签页"""
|
||||
widget = QWidget()
|
||||
layout = QVBoxLayout(widget)
|
||||
layout.setContentsMargins(16, 16, 16, 16)
|
||||
layout.setSpacing(20)
|
||||
|
||||
# 输出目录设置
|
||||
output_group = QGroupBox("📂 默认保存位置")
|
||||
output_layout = QVBoxLayout(output_group)
|
||||
output_layout.setSpacing(12)
|
||||
|
||||
# 路径选择
|
||||
path_layout = QHBoxLayout()
|
||||
|
||||
self.output_path_edit = QLineEdit()
|
||||
self.output_path_edit.setPlaceholderText("未设置 - 每次操作时询问保存位置")
|
||||
self.output_path_edit.setReadOnly(True)
|
||||
self.output_path_edit.setStyleSheet("""
|
||||
QLineEdit {
|
||||
background: #0f172a;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
color: #e2e8f0;
|
||||
font-size: 13px;
|
||||
}
|
||||
""")
|
||||
path_layout.addWidget(self.output_path_edit, 1)
|
||||
|
||||
browse_btn = QPushButton("📁 浏览")
|
||||
browse_btn.setObjectName("secondary_btn")
|
||||
browse_btn.clicked.connect(self.browse_output_dir)
|
||||
path_layout.addWidget(browse_btn)
|
||||
|
||||
clear_btn = QPushButton("✖ 清除")
|
||||
clear_btn.setObjectName("secondary_btn")
|
||||
clear_btn.clicked.connect(self.clear_output_dir)
|
||||
path_layout.addWidget(clear_btn)
|
||||
|
||||
output_layout.addLayout(path_layout)
|
||||
|
||||
# 自动保存选项
|
||||
self.auto_save_check = QCheckBox("自动保存到默认目录(不再询问)")
|
||||
self.auto_save_check.setStyleSheet("color: #cbd5e1; font-size: 13px;")
|
||||
output_layout.addWidget(self.auto_save_check)
|
||||
|
||||
hint = QLabel("提示: 设置默认保存位置后,处理完成的文件将自动保存到该目录")
|
||||
hint.setStyleSheet("color: #64748b; font-size: 11px;")
|
||||
hint.setWordWrap(True)
|
||||
output_layout.addWidget(hint)
|
||||
|
||||
layout.addWidget(output_group)
|
||||
|
||||
layout.addStretch()
|
||||
return widget
|
||||
|
||||
def create_ui_tab(self) -> QWidget:
|
||||
"""创建界面设置标签页"""
|
||||
widget = QWidget()
|
||||
layout = QVBoxLayout(widget)
|
||||
layout.setContentsMargins(16, 16, 16, 16)
|
||||
layout.setSpacing(20)
|
||||
|
||||
# 动画设置
|
||||
anim_group = QGroupBox("✨ 动画效果")
|
||||
anim_layout = QVBoxLayout(anim_group)
|
||||
anim_layout.setSpacing(12)
|
||||
|
||||
self.animation_check = QCheckBox("启用界面动画(渐入渐出效果)")
|
||||
self.animation_check.setStyleSheet("color: #cbd5e1; font-size: 13px;")
|
||||
self.animation_check.setChecked(True)
|
||||
self.animation_check.stateChanged.connect(self.on_animation_toggle)
|
||||
anim_layout.addWidget(self.animation_check)
|
||||
|
||||
# 动画时长
|
||||
duration_row = QHBoxLayout()
|
||||
duration_row.addWidget(QLabel("动画时长:"))
|
||||
duration_row.addStretch()
|
||||
|
||||
self.duration_spin = QSpinBox()
|
||||
self.duration_spin.setRange(100, 1000)
|
||||
self.duration_spin.setValue(300)
|
||||
self.duration_spin.setSuffix(" ms")
|
||||
self.duration_spin.setFixedWidth(100)
|
||||
duration_row.addWidget(self.duration_spin)
|
||||
|
||||
anim_layout.addLayout(duration_row)
|
||||
|
||||
layout.addWidget(anim_group)
|
||||
|
||||
layout.addStretch()
|
||||
return widget
|
||||
|
||||
def browse_output_dir(self):
|
||||
"""浏览输出目录"""
|
||||
current = self.output_path_edit.text() or ""
|
||||
path = QFileDialog.getExistingDirectory(self, "选择默认保存目录", current)
|
||||
if path:
|
||||
self.output_path_edit.setText(path)
|
||||
|
||||
def clear_output_dir(self):
|
||||
"""清除输出目录"""
|
||||
self.output_path_edit.setText("")
|
||||
self.auto_save_check.setChecked(False)
|
||||
|
||||
def on_animation_toggle(self, state):
|
||||
"""动画开关切换"""
|
||||
self.duration_spin.setEnabled(state == Qt.CheckState.Checked.value)
|
||||
|
||||
def load_settings(self):
|
||||
"""加载设置"""
|
||||
self.output_path_edit.setText(config.get("output_directory", ""))
|
||||
self.auto_save_check.setChecked(config.get("auto_save_to_default", False))
|
||||
|
||||
self.animation_check.setChecked(config.get("animation_enabled", True))
|
||||
self.duration_spin.setValue(config.get("animation_duration", 300))
|
||||
self.duration_spin.setEnabled(config.get("animation_enabled", True))
|
||||
|
||||
def save_settings(self):
|
||||
"""保存设置"""
|
||||
config.set("output_directory", self.output_path_edit.text())
|
||||
config.set("auto_save_to_default", self.auto_save_check.isChecked())
|
||||
|
||||
config.set("animation_enabled", self.animation_check.isChecked())
|
||||
config.set("animation_duration", self.duration_spin.value())
|
||||
|
||||
QMessageBox.information(self, "成功", "设置已保存!")
|
||||
logging.info("用户设置已保存")
|
||||
|
||||
def reset_settings(self):
|
||||
"""恢复默认设置"""
|
||||
reply = QMessageBox.question(
|
||||
self, "确认", "确定要恢复默认设置吗?",
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
|
||||
)
|
||||
|
||||
if reply == QMessageBox.StandardButton.Yes:
|
||||
for key, value in config.DEFAULT_CONFIG.items():
|
||||
config.set(key, value)
|
||||
self.load_settings()
|
||||
QMessageBox.information(self, "成功", "已恢复默认设置!")
|
||||
logging.info("用户设置已恢复默认")
|
||||
|
||||
471
ui/sidebar.py
Normal file
471
ui/sidebar.py
Normal file
@@ -0,0 +1,471 @@
|
||||
"""
|
||||
左侧一级菜单组件
|
||||
- Logo (使用图片,点击弹出个人信息卡片)
|
||||
- 图片工具、PDF工具箱、Excel表格 三个分类
|
||||
- 设置按钮
|
||||
- 选中状态有左侧高亮条
|
||||
- 水波纹点击效果
|
||||
"""
|
||||
import os
|
||||
from pathlib import Path
|
||||
from PySide6.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QButtonGroup,
|
||||
QSpacerItem, QSizePolicy, QLabel, QFrame, QGraphicsOpacityEffect
|
||||
)
|
||||
from PySide6.QtCore import (
|
||||
Signal, Qt, QSize, QPropertyAnimation, QEasingCurve,
|
||||
QPoint, QTimer, QParallelAnimationGroup, QSequentialAnimationGroup,
|
||||
Property
|
||||
)
|
||||
from PySide6.QtGui import (
|
||||
QFont, QPainter, QColor, QLinearGradient, QPen, QBrush, QPixmap,
|
||||
QRadialGradient, QPainterPath
|
||||
)
|
||||
from PySide6.QtSvg import QSvgRenderer
|
||||
from PySide6.QtCore import QByteArray
|
||||
|
||||
|
||||
# 获取资源目录
|
||||
def get_resource_path(relative_path: str) -> str:
|
||||
"""获取资源文件的绝对路径"""
|
||||
base_path = Path(__file__).parent.parent
|
||||
return str(base_path / relative_path)
|
||||
|
||||
|
||||
# Logo图片路径
|
||||
LOGO_IMAGE_PATH = get_resource_path("image/生成奶酪商城官方店介绍.png")
|
||||
|
||||
|
||||
# SVG 图标定义 (Phosphor Icons 风格)
|
||||
ICONS = {
|
||||
"cheese": """<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="currentColor" d="M184 32a8 8 0 0 0-8 8v16h-16a8 8 0 0 0 0 16h16v16a8 8 0 0 0 16 0V72h16a8 8 0 0 0 0-16h-16V40a8 8 0 0 0-8-8m48 88h-64a8 8 0 0 0-8 8v64a8 8 0 0 0 8 8h64a8 8 0 0 0 8-8v-64a8 8 0 0 0-8-8m-8 64h-48v-48h48ZM80 40a8 8 0 0 0-8 8v16H56a8 8 0 0 0 0 16h16v16a8 8 0 0 0 16 0V80h16a8 8 0 0 0 0-16H88V48a8 8 0 0 0-8-8m48 88H64a8 8 0 0 0-8 8v64a8 8 0 0 0 8 8h64a8 8 0 0 0 8-8v-64a8 8 0 0 0-8-8m-8 64H72v-48h48Z"/></svg>""",
|
||||
"image": """<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="currentColor" d="M216 40H40a16 16 0 0 0-16 16v144a16 16 0 0 0 16 16h176a16 16 0 0 0 16-16V56a16 16 0 0 0-16-16m0 16v102.75l-26.07-26.06a16 16 0 0 0-22.63 0l-20 20l-44-44a16 16 0 0 0-22.62 0L40 149.37V56ZM40 172l52-52l80 80H40Zm176 28h-21.37l-36-36l20-20L216 181.38V200m-72-100a12 12 0 1 1 12 12a12 12 0 0 1-12-12"/></svg>""",
|
||||
"file-pdf": """<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="currentColor" d="M224 152a8 8 0 0 1-8 8h-24v16h16a8 8 0 0 1 0 16h-16v16a8 8 0 0 1-16 0v-56a8 8 0 0 1 8-8h32a8 8 0 0 1 8 8M92 172a28 28 0 0 1-28 28h-8v8a8 8 0 0 1-16 0v-56a8 8 0 0 1 8-8h16a28 28 0 0 1 28 28m-16 0a12 12 0 0 0-12-12h-8v24h8a12 12 0 0 0 12-12m88 0c0 21.78-14.36 36-36 36h-16a8 8 0 0 1-8-8v-56a8 8 0 0 1 8-8h16c21.64 0 36 14.22 36 36m-16 0c0-13.56-8-20-20-20h-8v40h8c12 0 20-6.44 20-20M40 112V40a16 16 0 0 1 16-16h96a8 8 0 0 1 5.66 2.34l56 56A8 8 0 0 1 216 88v24a8 8 0 0 1-16 0V96h-48a8 8 0 0 1-8-8V40H56v72a8 8 0 0 1-16 0m120-32h28.69L160 51.31Z"/></svg>""",
|
||||
"file-xls": """<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="currentColor" d="M156 208a8 8 0 0 1-8 8h-24a8 8 0 0 1-8-8v-56a8 8 0 0 1 16 0v48h16a8 8 0 0 1 8 8m-62.37-55.58a8 8 0 0 0-11.26 1.21L68 172.32l-14.37-18.69a8 8 0 0 0-12.47 10.05L58.59 184l-17.43 20.32a8 8 0 0 0 12.47 10.05L68 195.68l14.37 18.69A8 8 0 0 0 88 216a8 8 0 0 0 5.05-1.79a8 8 0 0 0 1.21-11.26L77.41 184l16.85-19.63a8 8 0 0 0-1.63-11.95m72 5.58c-14.06 0-24.63 8.88-24.63 20.67S151.57 200 166.63 200a22.76 22.76 0 0 0 7.47-1.27a8 8 0 0 0-5.2-15.13a6.89 6.89 0 0 1-2.27.4c-4.39 0-8.63-2.72-8.63-4.67s4.24-4.66 8.63-4.66c3.36 0 5.15 1 5.79 1.55a8 8 0 0 0 10.63-12a22.6 22.6 0 0 0-16.42-5.89M40 112V40a16 16 0 0 1 16-16h96a8 8 0 0 1 5.66 2.34l56 56A8 8 0 0 1 216 88v24a8 8 0 0 1-16 0V96h-48a8 8 0 0 1-8-8V40H56v72a8 8 0 0 1-16 0m120-32h28.69L160 51.31Z"/></svg>""",
|
||||
"gear": """<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="currentColor" d="M128 80a48 48 0 1 0 48 48a48.05 48.05 0 0 0-48-48m0 80a32 32 0 1 1 32-32a32 32 0 0 1-32 32m88-29.84q.06-2.16 0-4.32l14.92-18.64a8 8 0 0 0 1.48-7.06a107.6 107.6 0 0 0-10.88-26.25a8 8 0 0 0-6-3.93l-23.72-2.64q-1.48-1.56-3-3L186 40.54a8 8 0 0 0-3.94-6a107.29 107.29 0 0 0-26.25-10.86a8 8 0 0 0-7.06 1.48L130.16 40h-4.32L107.2 25.11a8 8 0 0 0-7.06-1.48a107.6 107.6 0 0 0-26.25 10.88a8 8 0 0 0-3.93 6l-2.64 23.76q-1.56 1.49-3 3L40.54 70a8 8 0 0 0-6 3.94a107.71 107.71 0 0 0-10.87 26.25a8 8 0 0 0 1.49 7.06L40 125.84v4.32L25.11 148.8a8 8 0 0 0-1.48 7.06a107.6 107.6 0 0 0 10.88 26.25a8 8 0 0 0 6 3.93l23.72 2.64q1.49 1.56 3 3L70 215.46a8 8 0 0 0 3.94 6a107.71 107.71 0 0 0 26.25 10.87a8 8 0 0 0 7.06-1.49L125.84 216q2.16.06 4.32 0l18.64 14.92a8 8 0 0 0 7.06 1.48a107.21 107.21 0 0 0 26.25-10.88a8 8 0 0 0 3.93-6l2.64-23.72q1.56-1.48 3-3l23.78-2.8a8 8 0 0 0 6-3.94a107.71 107.71 0 0 0 10.87-26.25a8 8 0 0 0-1.49-7.06Zm-16.1-6.5a73.93 73.93 0 0 1 0 8.68a8 8 0 0 0 1.74 5.48l14.19 17.73a91.57 91.57 0 0 1-6.23 15l-22.6 2.56a8 8 0 0 0-5.1 2.64a74.11 74.11 0 0 1-6.14 6.14a8 8 0 0 0-2.64 5.1l-2.51 22.58a91.32 91.32 0 0 1-15 6.23l-17.74-14.19a8 8 0 0 0-5-1.75h-.48a73.93 73.93 0 0 1-8.68 0a8 8 0 0 0-5.48 1.74l-17.78 14.2a91.57 91.57 0 0 1-15-6.23L82.89 168a8 8 0 0 0-2.64-5.1a74.11 74.11 0 0 1-6.14-6.14a8 8 0 0 0-5.1-2.64l-22.58-2.52a91.32 91.32 0 0 1-6.23-15l14.19-17.74a8 8 0 0 0 1.74-5.48a73.93 73.93 0 0 1 0-8.68a8 8 0 0 0-1.74-5.48L40.2 81.76a91.57 91.57 0 0 1 6.23-15L69 64.2a8 8 0 0 0 5.1-2.64a74.11 74.11 0 0 1 6.14-6.14A8 8 0 0 0 82.89 50l2.51-22.57a91.32 91.32 0 0 1 15-6.23l17.74 14.19a8 8 0 0 0 5.48 1.74a73.93 73.93 0 0 1 8.68 0a8 8 0 0 0 5.48-1.74l17.77-14.19a91.57 91.57 0 0 1 15 6.23L168 50a8 8 0 0 0 2.64 5.1a74.11 74.11 0 0 1 6.14 6.14a8 8 0 0 0 5.1 2.64l22.58 2.51a91.32 91.32 0 0 1 6.23 15l-14.19 17.74a8 8 0 0 0-1.6 5.53Z"/></svg>""",
|
||||
}
|
||||
|
||||
|
||||
class IconWidget(QLabel):
|
||||
"""SVG图标组件"""
|
||||
|
||||
def __init__(self, icon_name: str, size: int = 24, color: str = "#94a3b8", parent=None):
|
||||
super().__init__(parent)
|
||||
self.icon_name = icon_name
|
||||
self.icon_size = size
|
||||
self.icon_color = color
|
||||
self.setFixedSize(size, size)
|
||||
self.update_icon()
|
||||
|
||||
def set_color(self, color: str):
|
||||
self.icon_color = color
|
||||
self.update_icon()
|
||||
|
||||
def update_icon(self):
|
||||
svg_data = ICONS.get(self.icon_name, "")
|
||||
if svg_data:
|
||||
# 替换颜色
|
||||
svg_data = svg_data.replace('currentColor', self.icon_color)
|
||||
renderer = QSvgRenderer(QByteArray(svg_data.encode()))
|
||||
pixmap = QPixmap(self.icon_size, self.icon_size)
|
||||
pixmap.fill(Qt.GlobalColor.transparent)
|
||||
painter = QPainter(pixmap)
|
||||
renderer.render(painter)
|
||||
painter.end()
|
||||
self.setPixmap(pixmap)
|
||||
|
||||
|
||||
class RippleEffect:
|
||||
"""水波纹效果数据"""
|
||||
def __init__(self, center: QPoint, max_radius: float):
|
||||
self.center = center
|
||||
self.max_radius = max_radius
|
||||
self.current_radius = 0.0
|
||||
self.opacity = 0.4
|
||||
|
||||
|
||||
class CategoryButton(QWidget):
|
||||
"""分类按钮 - 带左侧高亮条和水波纹效果"""
|
||||
|
||||
clicked = Signal()
|
||||
|
||||
def __init__(self, icon_name: str, tooltip: str, parent=None):
|
||||
super().__init__(parent)
|
||||
self.icon_name = icon_name
|
||||
self._checked = False
|
||||
self._hovered = False
|
||||
self._pressed = False
|
||||
self.setFixedSize(80, 56) # 放大按钮
|
||||
self.setToolTip(tooltip)
|
||||
self.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
|
||||
# 水波纹效果
|
||||
self._ripples = []
|
||||
self._ripple_timer = QTimer(self)
|
||||
self._ripple_timer.timeout.connect(self._update_ripples)
|
||||
|
||||
# 图标 - 放大
|
||||
self.icon_widget = IconWidget(icon_name, 28, "#94a3b8", self)
|
||||
self.icon_widget.move(26, 14)
|
||||
|
||||
def setChecked(self, checked: bool):
|
||||
self._checked = checked
|
||||
self.update_style()
|
||||
self.update()
|
||||
|
||||
def isChecked(self) -> bool:
|
||||
return self._checked
|
||||
|
||||
def update_style(self):
|
||||
if self._checked:
|
||||
self.icon_widget.set_color("#fbbf24")
|
||||
elif self._hovered:
|
||||
self.icon_widget.set_color("#fbbf24")
|
||||
else:
|
||||
self.icon_widget.set_color("#94a3b8")
|
||||
|
||||
def enterEvent(self, event):
|
||||
self._hovered = True
|
||||
self.update_style()
|
||||
self.update()
|
||||
|
||||
def leaveEvent(self, event):
|
||||
self._hovered = False
|
||||
self.update_style()
|
||||
self.update()
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
if event.button() == Qt.MouseButton.LeftButton:
|
||||
self._pressed = True
|
||||
# 创建水波纹
|
||||
self._create_ripple(event.pos())
|
||||
self.update()
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
if event.button() == Qt.MouseButton.LeftButton:
|
||||
self._pressed = False
|
||||
if self.rect().contains(event.pos()):
|
||||
self.clicked.emit()
|
||||
self.update()
|
||||
|
||||
def _create_ripple(self, pos: QPoint):
|
||||
"""创建水波纹效果"""
|
||||
# 计算最大半径
|
||||
distances = [
|
||||
(pos - QPoint(0, 0)).manhattanLength(),
|
||||
(pos - QPoint(self.width(), 0)).manhattanLength(),
|
||||
(pos - QPoint(0, self.height())).manhattanLength(),
|
||||
(pos - QPoint(self.width(), self.height())).manhattanLength()
|
||||
]
|
||||
max_radius = max(distances) * 0.8
|
||||
|
||||
ripple = RippleEffect(pos, max_radius)
|
||||
self._ripples.append(ripple)
|
||||
|
||||
if not self._ripple_timer.isActive():
|
||||
self._ripple_timer.start(16) # ~60fps
|
||||
|
||||
def _update_ripples(self):
|
||||
"""更新水波纹动画"""
|
||||
to_remove = []
|
||||
for ripple in self._ripples:
|
||||
ripple.current_radius += ripple.max_radius * 0.08
|
||||
ripple.opacity -= 0.025
|
||||
|
||||
if ripple.opacity <= 0:
|
||||
to_remove.append(ripple)
|
||||
|
||||
for ripple in to_remove:
|
||||
self._ripples.remove(ripple)
|
||||
|
||||
if not self._ripples:
|
||||
self._ripple_timer.stop()
|
||||
|
||||
self.update()
|
||||
|
||||
def paintEvent(self, event):
|
||||
painter = QPainter(self)
|
||||
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||
|
||||
# 绘制按钮背景
|
||||
btn_rect = self.rect().adjusted(12, 4, -12, -4)
|
||||
|
||||
if self._checked:
|
||||
# 选中背景
|
||||
painter.setBrush(QColor(251, 191, 36, 30)) # cheese-400/20
|
||||
painter.setPen(Qt.PenStyle.NoPen)
|
||||
painter.drawRoundedRect(btn_rect, 14, 14)
|
||||
|
||||
# 左侧高亮条
|
||||
indicator_height = int(self.height() * 0.6)
|
||||
indicator_y = (self.height() - indicator_height) // 2
|
||||
|
||||
# 发光效果
|
||||
glow_color = QColor(251, 191, 36, 80)
|
||||
painter.setBrush(glow_color)
|
||||
painter.drawRoundedRect(0, indicator_y - 2, 6, indicator_height + 4, 3, 3)
|
||||
|
||||
# 实体条
|
||||
painter.setBrush(QColor("#fbbf24"))
|
||||
painter.drawRoundedRect(0, indicator_y, 4, indicator_height, 2, 2)
|
||||
|
||||
elif self._hovered:
|
||||
painter.setBrush(QColor(51, 65, 85, 128)) # darkbg-700/50
|
||||
painter.setPen(Qt.PenStyle.NoPen)
|
||||
painter.drawRoundedRect(btn_rect, 14, 14)
|
||||
|
||||
# 绘制水波纹
|
||||
if self._ripples:
|
||||
painter.save()
|
||||
# 创建裁剪区域
|
||||
path = QPainterPath()
|
||||
path.addRoundedRect(btn_rect.x(), btn_rect.y(), btn_rect.width(), btn_rect.height(), 14, 14)
|
||||
painter.setClipPath(path)
|
||||
|
||||
for ripple in self._ripples:
|
||||
color = QColor(251, 191, 36, int(ripple.opacity * 255))
|
||||
gradient = QRadialGradient(ripple.center.x(), ripple.center.y(), ripple.current_radius)
|
||||
gradient.setColorAt(0, QColor(251, 191, 36, 0))
|
||||
gradient.setColorAt(0.5, color)
|
||||
gradient.setColorAt(1, QColor(251, 191, 36, 0))
|
||||
|
||||
painter.setBrush(gradient)
|
||||
painter.setPen(Qt.PenStyle.NoPen)
|
||||
painter.drawEllipse(
|
||||
ripple.center,
|
||||
int(ripple.current_radius),
|
||||
int(ripple.current_radius)
|
||||
)
|
||||
painter.restore()
|
||||
|
||||
|
||||
class LogoButton(QWidget):
|
||||
"""Logo按钮 - 使用图片,无背景,带水波纹效果"""
|
||||
|
||||
clicked = Signal()
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setFixedSize(60, 60) # 放大
|
||||
self.setToolTip("点击查看个人信息")
|
||||
self.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self._hovered = False
|
||||
self._pressed = False
|
||||
self._logo_pixmap = None
|
||||
self._scale = 1.0
|
||||
|
||||
# 水波纹效果
|
||||
self._ripples = []
|
||||
self._ripple_timer = QTimer(self)
|
||||
self._ripple_timer.timeout.connect(self._update_ripples)
|
||||
|
||||
# 悬停缩放动画
|
||||
self._scale_anim = QPropertyAnimation(self, b"scale_factor")
|
||||
self._scale_anim.setDuration(150)
|
||||
self._scale_anim.setEasingCurve(QEasingCurve.Type.OutCubic)
|
||||
|
||||
self._load_logo()
|
||||
|
||||
def get_scale_factor(self):
|
||||
return self._scale
|
||||
|
||||
def set_scale_factor(self, value):
|
||||
self._scale = value
|
||||
self.update()
|
||||
|
||||
# 使用 PySide6 Property 以支持动画
|
||||
scale_factor = Property(float, get_scale_factor, set_scale_factor)
|
||||
|
||||
def _load_logo(self):
|
||||
"""加载Logo图片"""
|
||||
if os.path.exists(LOGO_IMAGE_PATH):
|
||||
self._logo_pixmap = QPixmap(LOGO_IMAGE_PATH)
|
||||
if not self._logo_pixmap.isNull():
|
||||
# 缩放到合适大小
|
||||
self._logo_pixmap = self._logo_pixmap.scaled(
|
||||
56, 56,
|
||||
Qt.AspectRatioMode.KeepAspectRatio,
|
||||
Qt.TransformationMode.SmoothTransformation
|
||||
)
|
||||
|
||||
def enterEvent(self, event):
|
||||
self._hovered = True
|
||||
self._scale_anim.stop()
|
||||
self._scale_anim.setStartValue(self._scale)
|
||||
self._scale_anim.setEndValue(1.1)
|
||||
self._scale_anim.start()
|
||||
self.update()
|
||||
|
||||
def leaveEvent(self, event):
|
||||
self._hovered = False
|
||||
self._scale_anim.stop()
|
||||
self._scale_anim.setStartValue(self._scale)
|
||||
self._scale_anim.setEndValue(1.0)
|
||||
self._scale_anim.start()
|
||||
self.update()
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
if event.button() == Qt.MouseButton.LeftButton:
|
||||
self._pressed = True
|
||||
self._create_ripple(event.pos())
|
||||
self.update()
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
if event.button() == Qt.MouseButton.LeftButton:
|
||||
self._pressed = False
|
||||
if self.rect().contains(event.pos()):
|
||||
self.clicked.emit()
|
||||
self.update()
|
||||
|
||||
def _create_ripple(self, pos: QPoint):
|
||||
"""创建水波纹效果"""
|
||||
max_radius = max(self.width(), self.height())
|
||||
ripple = RippleEffect(pos, max_radius)
|
||||
self._ripples.append(ripple)
|
||||
|
||||
if not self._ripple_timer.isActive():
|
||||
self._ripple_timer.start(16)
|
||||
|
||||
def _update_ripples(self):
|
||||
"""更新水波纹动画"""
|
||||
to_remove = []
|
||||
for ripple in self._ripples:
|
||||
ripple.current_radius += ripple.max_radius * 0.1
|
||||
ripple.opacity -= 0.03
|
||||
|
||||
if ripple.opacity <= 0:
|
||||
to_remove.append(ripple)
|
||||
|
||||
for ripple in to_remove:
|
||||
self._ripples.remove(ripple)
|
||||
|
||||
if not self._ripples:
|
||||
self._ripple_timer.stop()
|
||||
|
||||
self.update()
|
||||
|
||||
def paintEvent(self, event):
|
||||
painter = QPainter(self)
|
||||
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||
painter.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform)
|
||||
|
||||
center = self.rect().center()
|
||||
|
||||
# 绘制Logo图片(无背景)
|
||||
if self._logo_pixmap and not self._logo_pixmap.isNull():
|
||||
# 应用缩放
|
||||
scaled_size = int(self._logo_pixmap.width() * self._scale)
|
||||
scaled_pixmap = self._logo_pixmap.scaled(
|
||||
scaled_size, scaled_size,
|
||||
Qt.AspectRatioMode.KeepAspectRatio,
|
||||
Qt.TransformationMode.SmoothTransformation
|
||||
)
|
||||
|
||||
x = center.x() - scaled_pixmap.width() // 2
|
||||
y = center.y() - scaled_pixmap.height() // 2
|
||||
|
||||
# 悬停时添加发光效果
|
||||
if self._hovered:
|
||||
glow_rect = self.rect().adjusted(-4, -4, 4, 4)
|
||||
glow = QRadialGradient(center.x(), center.y(), 40)
|
||||
glow.setColorAt(0, QColor(251, 191, 36, 60))
|
||||
glow.setColorAt(0.7, QColor(251, 191, 36, 30))
|
||||
glow.setColorAt(1, QColor(251, 191, 36, 0))
|
||||
painter.setBrush(glow)
|
||||
painter.setPen(Qt.PenStyle.NoPen)
|
||||
painter.drawEllipse(center, 36, 36)
|
||||
|
||||
painter.drawPixmap(x, y, scaled_pixmap)
|
||||
|
||||
# 绘制水波纹
|
||||
if self._ripples:
|
||||
painter.save()
|
||||
# 圆形裁剪
|
||||
path = QPainterPath()
|
||||
path.addEllipse(center.x() - 28, center.y() - 28, 56, 56)
|
||||
painter.setClipPath(path)
|
||||
|
||||
for ripple in self._ripples:
|
||||
color = QColor(251, 191, 36, int(ripple.opacity * 200))
|
||||
gradient = QRadialGradient(ripple.center.x(), ripple.center.y(), ripple.current_radius)
|
||||
gradient.setColorAt(0, QColor(251, 191, 36, 0))
|
||||
gradient.setColorAt(0.6, color)
|
||||
gradient.setColorAt(1, QColor(251, 191, 36, 0))
|
||||
|
||||
painter.setBrush(gradient)
|
||||
painter.setPen(Qt.PenStyle.NoPen)
|
||||
painter.drawEllipse(
|
||||
ripple.center,
|
||||
int(ripple.current_radius),
|
||||
int(ripple.current_radius)
|
||||
)
|
||||
painter.restore()
|
||||
else:
|
||||
# 如果图片加载失败,使用emoji作为备选
|
||||
painter.setPen(QColor("#fbbf24"))
|
||||
font = QFont("Segoe UI Emoji", 28)
|
||||
painter.setFont(font)
|
||||
painter.drawText(self.rect(), Qt.AlignmentFlag.AlignCenter, "🧀")
|
||||
|
||||
|
||||
class PrimarySidebar(QWidget):
|
||||
"""左侧一级菜单"""
|
||||
|
||||
category_changed = Signal(str)
|
||||
settings_clicked = Signal()
|
||||
logo_clicked = Signal() # 新增:Logo点击信号
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setObjectName("primary_sidebar")
|
||||
self.setFixedWidth(80)
|
||||
self.setup_ui()
|
||||
|
||||
def setup_ui(self):
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(0, 20, 0, 24)
|
||||
layout.setSpacing(8)
|
||||
layout.setAlignment(Qt.AlignmentFlag.AlignHCenter)
|
||||
|
||||
# Logo - 点击弹出个人信息卡片
|
||||
self.logo_btn = LogoButton()
|
||||
self.logo_btn.clicked.connect(self.logo_clicked.emit)
|
||||
layout.addWidget(self.logo_btn, 0, Qt.AlignmentFlag.AlignHCenter)
|
||||
|
||||
layout.addSpacing(28)
|
||||
|
||||
# 分类按钮
|
||||
self.image_btn = CategoryButton("image", "图片工具")
|
||||
self.image_btn.setChecked(True)
|
||||
self.image_btn.clicked.connect(lambda: self._on_category_click("image"))
|
||||
layout.addWidget(self.image_btn)
|
||||
|
||||
self.pdf_btn = CategoryButton("file-pdf", "PDF 工具箱")
|
||||
self.pdf_btn.clicked.connect(lambda: self._on_category_click("pdf"))
|
||||
layout.addWidget(self.pdf_btn)
|
||||
|
||||
self.excel_btn = CategoryButton("file-xls", "Excel 表格")
|
||||
self.excel_btn.clicked.connect(lambda: self._on_category_click("excel"))
|
||||
layout.addWidget(self.excel_btn)
|
||||
|
||||
self.buttons = {
|
||||
"image": self.image_btn,
|
||||
"pdf": self.pdf_btn,
|
||||
"excel": self.excel_btn
|
||||
}
|
||||
|
||||
layout.addStretch()
|
||||
|
||||
# 设置按钮
|
||||
self.settings_btn = CategoryButton("gear", "设置 / 日志查看")
|
||||
self.settings_btn.clicked.connect(self.settings_clicked.emit)
|
||||
layout.addWidget(self.settings_btn)
|
||||
|
||||
def _on_category_click(self, category: str):
|
||||
for cat, btn in self.buttons.items():
|
||||
btn.setChecked(cat == category)
|
||||
self.category_changed.emit(category)
|
||||
|
||||
def set_category(self, category: str):
|
||||
for cat, btn in self.buttons.items():
|
||||
btn.setChecked(cat == category)
|
||||
425
ui/tool_list.py
Normal file
425
ui/tool_list.py
Normal file
@@ -0,0 +1,425 @@
|
||||
"""
|
||||
中间二级菜单组件
|
||||
- 分类标题
|
||||
- 搜索框(带图标)
|
||||
- 工具列表(图标+名称+描述)
|
||||
- 用户信息
|
||||
"""
|
||||
from PySide6.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit,
|
||||
QPushButton, QButtonGroup, QScrollArea, QFrame, QGraphicsDropShadowEffect
|
||||
)
|
||||
from PySide6.QtCore import Signal, Qt
|
||||
from PySide6.QtGui import QFont, QColor, QPainter, QLinearGradient
|
||||
from PySide6.QtSvg import QSvgRenderer
|
||||
from PySide6.QtCore import QByteArray
|
||||
|
||||
|
||||
# 工具图标 SVG
|
||||
TOOL_ICONS = {
|
||||
"ph-arrows-in-line-horizontal": """<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="currentColor" d="M136 40v176a8 8 0 0 1-16 0V40a8 8 0 0 1 16 0M96 120H35.31l18.35-18.34a8 8 0 0 0-11.32-11.32l-32 32a8 8 0 0 0 0 11.32l32 32a8 8 0 0 0 11.32-11.32L35.31 136H96a8 8 0 0 0 0-16m149.66 2.34l-32-32a8 8 0 0 0-11.32 11.32L220.69 120H160a8 8 0 0 0 0 16h60.69l-18.35 18.34a8 8 0 0 0 11.32 11.32l32-32a8 8 0 0 0 0-11.32"/></svg>""",
|
||||
"ph-arrows-left-right": """<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="currentColor" d="m213.66 181.66l-32 32a8 8 0 0 1-11.32-11.32L188.69 184H48a8 8 0 0 1 0-16h140.69l-18.35-18.34a8 8 0 0 1 11.32-11.32l32 32a8 8 0 0 1 0 11.32m-139.32-96a8 8 0 0 0 11.32-11.32L67.31 56H208a8 8 0 0 0 0-16H67.31l18.35-18.34a8 8 0 0 0-11.32-11.32l-32 32a8 8 0 0 0 0 11.32Z"/></svg>""",
|
||||
"ph-stamp": """<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="currentColor" d="M224 224a8 8 0 0 1-8 8H40a8 8 0 0 1 0-16h176a8 8 0 0 1 8 8m0-80v40a16 16 0 0 1-16 16H48a16 16 0 0 1-16-16v-40a16 16 0 0 1 16-16h60.43l-15.52-46.55A40 40 0 0 1 128 32a40 40 0 0 1 35.09 49.45L147.57 128H208a16 16 0 0 1 16 16m-16 0H48v40h160Z"/></svg>""",
|
||||
"ph-scissors": """<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="currentColor" d="M157.73 113.13A8 8 0 0 1 159.82 102L227.48 55.7a8 8 0 0 1 9 13.21l-67.67 46.3a7.92 7.92 0 0 1-4.51 1.4a8 8 0 0 1-6.57-3.48m80.87 85.09a8 8 0 0 1-11.12 2.08L136 137.7l-31.11 21.29a36 36 0 1 1-8.87-12.94l27.46-18.79l-27.46-18.79a36 36 0 1 1 8.87-12.94l123.6 84.59a8 8 0 0 1 2.11 11.1M80 180a20 20 0 1 0-20 20a20 20 0 0 0 20-20m0-104a20 20 0 1 0-20 20a20 20 0 0 0 20-20"/></svg>""",
|
||||
"ph-files": """<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="currentColor" d="M213.66 66.34l-40-40A8 8 0 0 0 168 24H88a16 16 0 0 0-16 16v16H56a16 16 0 0 0-16 16v144a16 16 0 0 0 16 16h112a16 16 0 0 0 16-16v-16h16a16 16 0 0 0 16-16V72a8 8 0 0 0-2.34-5.66M168 216H56V72h76.69L168 107.31Zm32-32h-16v-80a8 8 0 0 0-2.34-5.66l-40-40A8 8 0 0 0 136 56H88V40h76.69L200 75.31Zm-56-32a8 8 0 0 1-8 8H88a8 8 0 0 1 0-16h48a8 8 0 0 1 8 8m0 32a8 8 0 0 1-8 8H88a8 8 0 0 1 0-16h48a8 8 0 0 1 8 8"/></svg>""",
|
||||
"ph-microsoft-word-logo": """<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="currentColor" d="M200 24H72a16 16 0 0 0-16 16v24H40a16 16 0 0 0-16 16v96a16 16 0 0 0 16 16h16v24a16 16 0 0 0 16 16h128a16 16 0 0 0 16-16V40a16 16 0 0 0-16-16m-40 176H72v-24h88a16 16 0 0 0 16-16v-23h24v63Zm0-79h-32V80h32Zm0-57h-32V40h32ZM40 80h88v96H40Zm160 136h-24v-24a16 16 0 0 0-16-16h-32v-55h72Zm0-111h-32V80a16 16 0 0 0-16-16h-12V40h60Zm-104.5 47.8l-9.36 34.14a6 6 0 0 1-11.56.12L68 162.18l-6.58 24.88a6 6 0 1 1-11.58-3.12l12-45a6 6 0 0 1 11.48-.16l7.18 27.14l9.86-36a6 6 0 0 1 11.56 0l9.86 36l7.18-27.14a6 6 0 0 1 11.48.16l12 45a6 6 0 1 1-11.58 3.12L124 162.18l-6.54 24.88a6 6 0 0 1-11.56-.12l-9.36-34.14Z"/></svg>""",
|
||||
"ph-eye": """<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="currentColor" d="M247.31 124.76c-.35-.79-8.82-19.58-27.65-38.41C194.57 61.26 162.88 48 128 48S61.43 61.26 36.34 86.35C17.51 105.18 9 124 8.69 124.76a8 8 0 0 0 0 6.5c.35.79 8.82 19.57 27.65 38.4C61.43 194.74 93.12 208 128 208s66.57-13.26 91.66-38.34c18.83-18.83 27.3-37.61 27.65-38.4a8 8 0 0 0 0-6.5M128 192c-30.78 0-57.67-11.19-79.93-33.25A133.47 133.47 0 0 1 25 128a133.33 133.33 0 0 1 23.07-30.75C70.33 75.19 97.22 64 128 64s57.67 11.19 79.93 33.25A133.46 133.46 0 0 1 231.05 128c-7.21 13.46-38.62 64-103.05 64m0-112a48 48 0 1 0 48 48a48.05 48.05 0 0 0-48-48m0 80a32 32 0 1 1 32-32a32 32 0 0 1-32 32"/></svg>""",
|
||||
"ph-chart-bar": """<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="currentColor" d="M224 200h-8V40a8 8 0 0 0-8-8h-56a8 8 0 0 0-8 8v40H96a8 8 0 0 0-8 8v40H48a8 8 0 0 0-8 8v64h-8a8 8 0 0 0 0 16h192a8 8 0 0 0 0-16M160 48h40v152h-40Zm-56 48h40v104h-40Zm-48 48h32v56H56Z"/></svg>""",
|
||||
"ph-magnifying-glass": """<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="currentColor" d="m229.66 218.34l-50.07-50.06a88.11 88.11 0 1 0-11.31 11.31l50.06 50.07a8 8 0 0 0 11.32-11.32M40 112a72 72 0 1 1 72 72a72.08 72.08 0 0 1-72-72"/></svg>""",
|
||||
}
|
||||
|
||||
|
||||
# 工具数据定义
|
||||
TOOLS_DATA = {
|
||||
"image": {
|
||||
"title": "图片工具",
|
||||
"items": [
|
||||
{"id": "img-compress", "name": "图片压缩", "icon": "ph-arrows-in-line-horizontal", "desc": "智能无损压缩"},
|
||||
{"id": "img-convert", "name": "格式转换", "icon": "ph-arrows-left-right", "desc": "JPG/PNG/WEBP"},
|
||||
{"id": "img-watermark", "name": "图片加水印", "icon": "ph-stamp", "desc": "批量添加水印"},
|
||||
]
|
||||
},
|
||||
"pdf": {
|
||||
"title": "PDF 工具箱",
|
||||
"items": [
|
||||
{"id": "pdf-split", "name": "PDF 拆分", "icon": "ph-scissors", "desc": "提取指定页面"},
|
||||
{"id": "pdf-merge", "name": "PDF 合并", "icon": "ph-files", "desc": "多文件合并"},
|
||||
{"id": "pdf-word", "name": "PDF 转 Word", "icon": "ph-microsoft-word-logo", "desc": "保持排版转换"},
|
||||
]
|
||||
},
|
||||
"excel": {
|
||||
"title": "Excel 表格",
|
||||
"items": [
|
||||
{"id": "xls-view", "name": "Excel 预览", "icon": "ph-eye", "desc": "在线查看表格"},
|
||||
{"id": "xls-chart", "name": "图表生成", "icon": "ph-chart-bar", "desc": "数据可视化"},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def render_svg_icon(svg_data: str, size: int = 18, color: str = "#94a3b8") -> 'QPixmap':
|
||||
"""渲染SVG图标"""
|
||||
from PySide6.QtGui import QPixmap
|
||||
svg_data = svg_data.replace('currentColor', color)
|
||||
renderer = QSvgRenderer(QByteArray(svg_data.encode()))
|
||||
pixmap = QPixmap(size, size)
|
||||
pixmap.fill(Qt.GlobalColor.transparent)
|
||||
painter = QPainter(pixmap)
|
||||
renderer.render(painter)
|
||||
painter.end()
|
||||
return pixmap
|
||||
|
||||
|
||||
class ToolButton(QFrame):
|
||||
"""工具按钮 - 还原HTML设计样式"""
|
||||
|
||||
clicked = Signal()
|
||||
|
||||
def __init__(self, tool_data: dict, parent=None):
|
||||
super().__init__(parent)
|
||||
self.tool_data = tool_data
|
||||
self._checked = False
|
||||
self._hovered = False
|
||||
self.setFixedHeight(56)
|
||||
self.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self.setup_ui()
|
||||
|
||||
def setup_ui(self):
|
||||
layout = QHBoxLayout(self)
|
||||
layout.setContentsMargins(12, 8, 12, 8)
|
||||
layout.setSpacing(12)
|
||||
|
||||
# 图标容器
|
||||
self.icon_frame = QFrame()
|
||||
self.icon_frame.setFixedSize(32, 32)
|
||||
self.icon_frame.setStyleSheet("""
|
||||
background: #0f172a;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 6px;
|
||||
""")
|
||||
icon_layout = QVBoxLayout(self.icon_frame)
|
||||
icon_layout.setContentsMargins(0, 0, 0, 0)
|
||||
icon_layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
# 图标
|
||||
self.icon_label = QLabel()
|
||||
self.icon_label.setFixedSize(18, 18)
|
||||
self.icon_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
svg_data = TOOL_ICONS.get(self.tool_data.get('icon', ''), '')
|
||||
if svg_data:
|
||||
self.icon_label.setPixmap(render_svg_icon(svg_data, 18, "#94a3b8"))
|
||||
icon_layout.addWidget(self.icon_label)
|
||||
|
||||
layout.addWidget(self.icon_frame)
|
||||
|
||||
# 文字区域
|
||||
text_layout = QVBoxLayout()
|
||||
text_layout.setSpacing(2)
|
||||
|
||||
self.name_label = QLabel(self.tool_data.get('name', ''))
|
||||
self.name_label.setStyleSheet("color: #e2e8f0; font-size: 13px; font-weight: 500;")
|
||||
text_layout.addWidget(self.name_label)
|
||||
|
||||
self.desc_label = QLabel(self.tool_data.get('desc', ''))
|
||||
self.desc_label.setStyleSheet("color: #64748b; font-size: 10px;")
|
||||
text_layout.addWidget(self.desc_label)
|
||||
|
||||
layout.addLayout(text_layout, 1)
|
||||
|
||||
self.update_style()
|
||||
|
||||
def setChecked(self, checked: bool):
|
||||
self._checked = checked
|
||||
self.update_style()
|
||||
|
||||
def isChecked(self) -> bool:
|
||||
return self._checked
|
||||
|
||||
def update_style(self):
|
||||
if self._checked:
|
||||
self.setStyleSheet("""
|
||||
ToolButton {
|
||||
background: #334155;
|
||||
border-radius: 8px;
|
||||
border-left: 2px solid #fbbf24;
|
||||
}
|
||||
""")
|
||||
self.icon_frame.setStyleSheet("""
|
||||
background: #0f172a;
|
||||
border: 1px solid rgba(251, 191, 36, 0.3);
|
||||
border-radius: 6px;
|
||||
""")
|
||||
svg_data = TOOL_ICONS.get(self.tool_data.get('icon', ''), '')
|
||||
if svg_data:
|
||||
self.icon_label.setPixmap(render_svg_icon(svg_data, 18, "#fbbf24"))
|
||||
self.name_label.setStyleSheet("color: white; font-size: 13px; font-weight: 500;")
|
||||
elif self._hovered:
|
||||
self.setStyleSheet("""
|
||||
ToolButton {
|
||||
background: rgba(51, 65, 85, 0.5);
|
||||
border-radius: 8px;
|
||||
}
|
||||
""")
|
||||
self.icon_frame.setStyleSheet("""
|
||||
background: #0f172a;
|
||||
border: 1px solid rgba(251, 191, 36, 0.3);
|
||||
border-radius: 6px;
|
||||
""")
|
||||
svg_data = TOOL_ICONS.get(self.tool_data.get('icon', ''), '')
|
||||
if svg_data:
|
||||
self.icon_label.setPixmap(render_svg_icon(svg_data, 18, "#fbbf24"))
|
||||
self.name_label.setStyleSheet("color: white; font-size: 13px; font-weight: 500;")
|
||||
else:
|
||||
self.setStyleSheet("""
|
||||
ToolButton {
|
||||
background: transparent;
|
||||
border-radius: 8px;
|
||||
}
|
||||
""")
|
||||
self.icon_frame.setStyleSheet("""
|
||||
background: #0f172a;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 6px;
|
||||
""")
|
||||
svg_data = TOOL_ICONS.get(self.tool_data.get('icon', ''), '')
|
||||
if svg_data:
|
||||
self.icon_label.setPixmap(render_svg_icon(svg_data, 18, "#94a3b8"))
|
||||
self.name_label.setStyleSheet("color: #e2e8f0; font-size: 13px; font-weight: 500;")
|
||||
|
||||
def enterEvent(self, event):
|
||||
self._hovered = True
|
||||
self.update_style()
|
||||
|
||||
def leaveEvent(self, event):
|
||||
self._hovered = False
|
||||
self.update_style()
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
if event.button() == Qt.MouseButton.LeftButton:
|
||||
self.clicked.emit()
|
||||
|
||||
|
||||
class SearchInput(QWidget):
|
||||
"""搜索框 - 带前置图标"""
|
||||
|
||||
textChanged = Signal(str)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setup_ui()
|
||||
|
||||
def setup_ui(self):
|
||||
layout = QHBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setSpacing(0)
|
||||
|
||||
# 容器
|
||||
container = QFrame()
|
||||
container.setStyleSheet("""
|
||||
QFrame {
|
||||
background: #0f172a;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 8px;
|
||||
}
|
||||
QFrame:focus-within {
|
||||
border-color: rgba(251, 191, 36, 0.5);
|
||||
}
|
||||
""")
|
||||
container_layout = QHBoxLayout(container)
|
||||
container_layout.setContentsMargins(12, 8, 12, 8)
|
||||
container_layout.setSpacing(8)
|
||||
|
||||
# 搜索图标
|
||||
icon_label = QLabel()
|
||||
icon_label.setFixedSize(16, 16)
|
||||
svg_data = TOOL_ICONS.get("ph-magnifying-glass", "")
|
||||
if svg_data:
|
||||
icon_label.setPixmap(render_svg_icon(svg_data, 16, "#64748b"))
|
||||
container_layout.addWidget(icon_label)
|
||||
|
||||
# 输入框
|
||||
self.input = QLineEdit()
|
||||
self.input.setPlaceholderText("搜索功能...")
|
||||
self.input.setStyleSheet("""
|
||||
QLineEdit {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #e2e8f0;
|
||||
font-size: 13px;
|
||||
}
|
||||
QLineEdit::placeholder {
|
||||
color: #475569;
|
||||
}
|
||||
""")
|
||||
self.input.textChanged.connect(self.textChanged.emit)
|
||||
container_layout.addWidget(self.input, 1)
|
||||
|
||||
layout.addWidget(container)
|
||||
|
||||
def text(self) -> str:
|
||||
return self.input.text()
|
||||
|
||||
|
||||
class UserInfoWidget(QFrame):
|
||||
"""用户信息区 - 渐变头像"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setFixedHeight(70)
|
||||
self.setStyleSheet("""
|
||||
UserInfoWidget {
|
||||
background: rgba(30, 41, 59, 0.3);
|
||||
border-top: 1px solid rgba(51, 65, 85, 0.5);
|
||||
}
|
||||
""")
|
||||
self.setup_ui()
|
||||
|
||||
def setup_ui(self):
|
||||
layout = QHBoxLayout(self)
|
||||
layout.setContentsMargins(16, 12, 16, 12)
|
||||
layout.setSpacing(12)
|
||||
|
||||
# 头像 - 渐变背景
|
||||
avatar = QFrame()
|
||||
avatar.setFixedSize(36, 36)
|
||||
avatar.setStyleSheet("""
|
||||
background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #8b5cf6, stop:1 #6366f1);
|
||||
border-radius: 18px;
|
||||
""")
|
||||
avatar_layout = QVBoxLayout(avatar)
|
||||
avatar_layout.setContentsMargins(0, 0, 0, 0)
|
||||
avatar_layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
avatar_text = QLabel("SV")
|
||||
avatar_text.setStyleSheet("color: white; font-size: 12px; font-weight: bold; background: transparent;")
|
||||
avatar_text.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
avatar_layout.addWidget(avatar_text)
|
||||
|
||||
layout.addWidget(avatar)
|
||||
|
||||
# 信息
|
||||
info_layout = QVBoxLayout()
|
||||
info_layout.setSpacing(2)
|
||||
|
||||
name_label = QLabel("超级会员")
|
||||
name_label.setStyleSheet("color: white; font-size: 12px; font-weight: 500;")
|
||||
info_layout.addWidget(name_label)
|
||||
|
||||
expire_label = QLabel("有效期至 2026-10")
|
||||
expire_label.setStyleSheet("color: #64748b; font-size: 10px;")
|
||||
info_layout.addWidget(expire_label)
|
||||
|
||||
layout.addLayout(info_layout, 1)
|
||||
|
||||
|
||||
class SecondarySidebar(QWidget):
|
||||
"""中间二级菜单"""
|
||||
|
||||
tool_selected = Signal(dict)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setObjectName("secondary_sidebar")
|
||||
self.setFixedWidth(256)
|
||||
self.current_category = "image"
|
||||
self.tool_buttons = []
|
||||
self._stretch_item = None # 保存stretch引用
|
||||
self.setup_ui()
|
||||
self.load_tools("image")
|
||||
|
||||
def setup_ui(self):
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setSpacing(0)
|
||||
|
||||
# 标题区域
|
||||
title_widget = QWidget()
|
||||
title_widget.setFixedHeight(80)
|
||||
title_widget.setStyleSheet("border-bottom: 1px solid rgba(51, 65, 85, 0.5);")
|
||||
title_layout = QVBoxLayout(title_widget)
|
||||
title_layout.setContentsMargins(24, 0, 24, 0)
|
||||
title_layout.setAlignment(Qt.AlignmentFlag.AlignVCenter)
|
||||
|
||||
self.category_title = QLabel("图片工具")
|
||||
self.category_title.setStyleSheet("color: white; font-size: 18px; font-weight: 600; letter-spacing: 1px;")
|
||||
title_layout.addWidget(self.category_title)
|
||||
|
||||
layout.addWidget(title_widget)
|
||||
|
||||
# 搜索区域
|
||||
search_widget = QWidget()
|
||||
search_layout = QVBoxLayout(search_widget)
|
||||
search_layout.setContentsMargins(16, 16, 16, 8)
|
||||
|
||||
self.search_input = SearchInput()
|
||||
self.search_input.textChanged.connect(self.filter_tools)
|
||||
search_layout.addWidget(self.search_input)
|
||||
|
||||
layout.addWidget(search_widget)
|
||||
|
||||
# 工具列表区域
|
||||
scroll_area = QScrollArea()
|
||||
scroll_area.setWidgetResizable(True)
|
||||
scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
||||
scroll_area.setFrameShape(QFrame.Shape.NoFrame)
|
||||
scroll_area.setStyleSheet("background: transparent;")
|
||||
|
||||
self.tools_container = QWidget()
|
||||
self.tools_layout = QVBoxLayout(self.tools_container)
|
||||
self.tools_layout.setContentsMargins(12, 4, 12, 16)
|
||||
self.tools_layout.setSpacing(4)
|
||||
self.tools_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
|
||||
|
||||
scroll_area.setWidget(self.tools_container)
|
||||
layout.addWidget(scroll_area, 1)
|
||||
|
||||
# 用户信息
|
||||
layout.addWidget(UserInfoWidget())
|
||||
|
||||
def load_tools(self, category: str):
|
||||
self.current_category = category
|
||||
data = TOOLS_DATA.get(category, {})
|
||||
|
||||
self.category_title.setText(data.get("title", ""))
|
||||
|
||||
# 清空所有内容(包括stretch)
|
||||
for btn in self.tool_buttons:
|
||||
self.tools_layout.removeWidget(btn)
|
||||
btn.deleteLater()
|
||||
self.tool_buttons.clear()
|
||||
|
||||
# 移除旧的stretch
|
||||
while self.tools_layout.count() > 0:
|
||||
item = self.tools_layout.takeAt(0)
|
||||
if item.widget():
|
||||
item.widget().deleteLater()
|
||||
|
||||
# 添加工具按钮
|
||||
for tool in data.get("items", []):
|
||||
btn = ToolButton(tool)
|
||||
btn.clicked.connect(lambda t=tool: self.on_tool_clicked(t))
|
||||
self.tools_layout.addWidget(btn)
|
||||
self.tool_buttons.append(btn)
|
||||
|
||||
# 添加新的stretch
|
||||
self.tools_layout.addStretch()
|
||||
|
||||
def on_tool_clicked(self, tool_data: dict):
|
||||
# 更新选中状态
|
||||
for btn in self.tool_buttons:
|
||||
btn.setChecked(btn.tool_data.get('id') == tool_data.get('id'))
|
||||
self.tool_selected.emit(tool_data)
|
||||
|
||||
def filter_tools(self, text: str):
|
||||
text = text.lower()
|
||||
for btn in self.tool_buttons:
|
||||
name = btn.tool_data.get("name", "").lower()
|
||||
desc = btn.tool_data.get("desc", "").lower()
|
||||
btn.setVisible(text in name or text in desc or not text)
|
||||
|
||||
def select_tool(self, tool_id: str):
|
||||
for btn in self.tool_buttons:
|
||||
if btn.tool_data.get("id") == tool_id:
|
||||
btn.setChecked(True)
|
||||
self.tool_selected.emit(btn.tool_data)
|
||||
break
|
||||
495
ui/workspace.py
Normal file
495
ui/workspace.py
Normal file
@@ -0,0 +1,495 @@
|
||||
"""
|
||||
右侧工作区基类
|
||||
- 装饰性背景光晕
|
||||
- 毛玻璃效果头部
|
||||
- 面包屑导航 + 版本标签
|
||||
- 上传区域(虚线边框+图标)
|
||||
"""
|
||||
from PySide6.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
|
||||
QFrame, QScrollArea, QFileDialog, QGraphicsDropShadowEffect,
|
||||
QGraphicsBlurEffect
|
||||
)
|
||||
from PySide6.QtCore import Signal, Qt, QSize
|
||||
from PySide6.QtGui import (
|
||||
QFont, QDragEnterEvent, QDropEvent, QPainter,
|
||||
QColor, QRadialGradient, QLinearGradient, QPen, QBrush
|
||||
)
|
||||
|
||||
|
||||
class BackgroundDecoration(QWidget):
|
||||
"""装饰性背景 - 渐变光晕"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents)
|
||||
|
||||
def paintEvent(self, event):
|
||||
painter = QPainter(self)
|
||||
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||
|
||||
# 右上角奶酪色光晕
|
||||
gradient1 = QRadialGradient(
|
||||
self.width() + 50, -50, # 圆心
|
||||
500 # 半径
|
||||
)
|
||||
gradient1.setColorAt(0, QColor(251, 191, 36, 25)) # cheese-500/10
|
||||
gradient1.setColorAt(1, QColor(251, 191, 36, 0))
|
||||
painter.setBrush(gradient1)
|
||||
painter.setPen(Qt.PenStyle.NoPen)
|
||||
painter.drawEllipse(
|
||||
int(self.width() * 0.5), int(-self.height() * 0.2),
|
||||
int(self.width() * 0.7), int(self.height() * 0.7)
|
||||
)
|
||||
|
||||
# 左侧紫色光晕
|
||||
gradient2 = QRadialGradient(
|
||||
self.width() * 0.1, self.height() * 0.3,
|
||||
350
|
||||
)
|
||||
gradient2.setColorAt(0, QColor(139, 92, 246, 12)) # purple-500/5
|
||||
gradient2.setColorAt(1, QColor(139, 92, 246, 0))
|
||||
painter.setBrush(gradient2)
|
||||
painter.drawEllipse(
|
||||
int(-self.width() * 0.1), int(self.height() * 0.1),
|
||||
int(self.width() * 0.5), int(self.height() * 0.5)
|
||||
)
|
||||
|
||||
|
||||
class UploadArea(QFrame):
|
||||
"""上传区域 - 还原HTML设计样式"""
|
||||
|
||||
files_dropped = Signal(list)
|
||||
|
||||
def __init__(self, accept_types: str = "所有文件", parent=None):
|
||||
super().__init__(parent)
|
||||
self.accept_types = accept_types
|
||||
self.setAcceptDrops(True)
|
||||
self.setMinimumHeight(208) # h-52 = 13rem = 208px
|
||||
self.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self._hovered = False
|
||||
self.setup_ui()
|
||||
|
||||
def setup_ui(self):
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
layout.setSpacing(12)
|
||||
|
||||
# 图标容器 (圆形背景)
|
||||
self.icon_container = QFrame()
|
||||
self.icon_container.setFixedSize(56, 56)
|
||||
self.icon_container.setStyleSheet("""
|
||||
background: #334155;
|
||||
border-radius: 28px;
|
||||
""")
|
||||
icon_layout = QVBoxLayout(self.icon_container)
|
||||
icon_layout.setContentsMargins(0, 0, 0, 0)
|
||||
icon_layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
# 云上传图标
|
||||
icon_label = QLabel("☁️")
|
||||
icon_label.setFont(QFont("Segoe UI Emoji", 24))
|
||||
icon_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
icon_label.setStyleSheet("background: transparent;")
|
||||
icon_layout.addWidget(icon_label)
|
||||
|
||||
layout.addWidget(self.icon_container, 0, Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
# 主文字
|
||||
self.title_label = QLabel("点击或拖拽文件到此处")
|
||||
self.title_label.setStyleSheet("color: white; font-size: 16px; font-weight: 500; background: transparent;")
|
||||
self.title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
layout.addWidget(self.title_label)
|
||||
|
||||
# 副文字
|
||||
self.desc_label = QLabel(f"支持批量上传 ({self.accept_types})")
|
||||
self.desc_label.setStyleSheet("color: #64748b; font-size: 13px; background: transparent;")
|
||||
self.desc_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
layout.addWidget(self.desc_label)
|
||||
|
||||
self.update_style()
|
||||
|
||||
def update_style(self):
|
||||
if self._hovered:
|
||||
self.setStyleSheet("""
|
||||
UploadArea {
|
||||
background: rgba(30, 41, 59, 0.5);
|
||||
border: 2px dashed rgba(251, 191, 36, 0.5);
|
||||
border-radius: 16px;
|
||||
}
|
||||
""")
|
||||
else:
|
||||
self.setStyleSheet("""
|
||||
UploadArea {
|
||||
background: rgba(30, 41, 59, 0.3);
|
||||
border: 2px dashed #334155;
|
||||
border-radius: 16px;
|
||||
}
|
||||
""")
|
||||
|
||||
def enterEvent(self, event):
|
||||
self._hovered = True
|
||||
self.update_style()
|
||||
|
||||
def leaveEvent(self, event):
|
||||
self._hovered = False
|
||||
self.update_style()
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
if event.button() == Qt.MouseButton.LeftButton:
|
||||
self.open_file_dialog()
|
||||
|
||||
def open_file_dialog(self):
|
||||
files, _ = QFileDialog.getOpenFileNames(
|
||||
self, "选择文件", "", f"{self.accept_types}"
|
||||
)
|
||||
if files:
|
||||
self.files_dropped.emit(files)
|
||||
|
||||
def dragEnterEvent(self, event: QDragEnterEvent):
|
||||
if event.mimeData().hasUrls():
|
||||
event.acceptProposedAction()
|
||||
self._hovered = True
|
||||
self.update_style()
|
||||
|
||||
def dragLeaveEvent(self, event):
|
||||
self._hovered = False
|
||||
self.update_style()
|
||||
|
||||
def dropEvent(self, event: QDropEvent):
|
||||
self._hovered = False
|
||||
self.update_style()
|
||||
files = []
|
||||
for url in event.mimeData().urls():
|
||||
if url.isLocalFile():
|
||||
files.append(url.toLocalFile())
|
||||
if files:
|
||||
self.files_dropped.emit(files)
|
||||
|
||||
|
||||
class GlassHeader(QFrame):
|
||||
"""毛玻璃效果头部"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setFixedHeight(80)
|
||||
self.setStyleSheet("""
|
||||
GlassHeader {
|
||||
background: rgba(30, 41, 59, 0.7);
|
||||
border-bottom: 1px solid rgba(51, 65, 85, 0.3);
|
||||
}
|
||||
""")
|
||||
|
||||
|
||||
class BaseWorkspace(QWidget):
|
||||
"""工作区基类"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setObjectName("workspace")
|
||||
self.setup_base_ui()
|
||||
|
||||
def setup_base_ui(self):
|
||||
# 主布局
|
||||
self.main_layout = QVBoxLayout(self)
|
||||
self.main_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.main_layout.setSpacing(0)
|
||||
|
||||
# 背景装饰层
|
||||
self.bg_decoration = BackgroundDecoration(self)
|
||||
|
||||
# 头部区域
|
||||
self.header = GlassHeader()
|
||||
header_layout = QVBoxLayout(self.header)
|
||||
header_layout.setContentsMargins(32, 16, 32, 16)
|
||||
header_layout.setSpacing(4)
|
||||
|
||||
# 面包屑
|
||||
breadcrumb_layout = QHBoxLayout()
|
||||
breadcrumb_layout.setSpacing(8)
|
||||
|
||||
self.breadcrumb_category = QLabel("首页")
|
||||
self.breadcrumb_category.setStyleSheet("color: #64748b; font-size: 12px;")
|
||||
breadcrumb_layout.addWidget(self.breadcrumb_category)
|
||||
|
||||
arrow = QLabel("›")
|
||||
arrow.setStyleSheet("color: #64748b; font-size: 12px;")
|
||||
breadcrumb_layout.addWidget(arrow)
|
||||
|
||||
self.breadcrumb_tool = QLabel("控制台")
|
||||
self.breadcrumb_tool.setStyleSheet("color: #fbbf24; font-size: 12px;")
|
||||
breadcrumb_layout.addWidget(self.breadcrumb_tool)
|
||||
|
||||
breadcrumb_layout.addStretch()
|
||||
header_layout.addLayout(breadcrumb_layout)
|
||||
|
||||
# 标题行
|
||||
title_layout = QHBoxLayout()
|
||||
|
||||
# 标题 + 版本标签
|
||||
title_container = QHBoxLayout()
|
||||
title_container.setSpacing(8)
|
||||
|
||||
self.title_label = QLabel("欢迎回来")
|
||||
self.title_label.setStyleSheet("color: white; font-size: 22px; font-weight: 700;")
|
||||
title_container.addWidget(self.title_label)
|
||||
|
||||
self.version_label = QLabel("v2.1")
|
||||
self.version_label.setStyleSheet("""
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
background: #1e293b;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 4px;
|
||||
padding: 2px 8px;
|
||||
""")
|
||||
self.version_label.setVisible(False)
|
||||
title_container.addWidget(self.version_label)
|
||||
title_container.addStretch()
|
||||
|
||||
title_layout.addLayout(title_container, 1)
|
||||
|
||||
# 按钮区域
|
||||
btn_layout = QHBoxLayout()
|
||||
btn_layout.setSpacing(12)
|
||||
|
||||
self.history_btn = QPushButton("🕐 历史")
|
||||
self.history_btn.setStyleSheet("""
|
||||
QPushButton {
|
||||
background: transparent;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 8px;
|
||||
color: #cbd5e1;
|
||||
padding: 8px 16px;
|
||||
font-size: 13px;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background: #334155;
|
||||
}
|
||||
""")
|
||||
btn_layout.addWidget(self.history_btn)
|
||||
|
||||
self.export_btn = QPushButton("📤 导出")
|
||||
self.export_btn.setStyleSheet("""
|
||||
QPushButton {
|
||||
background: #f59e0b;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
padding: 8px 16px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background: #fbbf24;
|
||||
}
|
||||
""")
|
||||
btn_layout.addWidget(self.export_btn)
|
||||
|
||||
title_layout.addLayout(btn_layout)
|
||||
header_layout.addLayout(title_layout)
|
||||
|
||||
self.main_layout.addWidget(self.header)
|
||||
|
||||
# 内容区域
|
||||
scroll_area = QScrollArea()
|
||||
scroll_area.setWidgetResizable(True)
|
||||
scroll_area.setFrameShape(QFrame.Shape.NoFrame)
|
||||
scroll_area.setStyleSheet("background: transparent;")
|
||||
|
||||
self.content_widget = QWidget()
|
||||
self.content_widget.setStyleSheet("background: transparent;")
|
||||
self.content_layout = QVBoxLayout(self.content_widget)
|
||||
self.content_layout.setContentsMargins(32, 24, 32, 24)
|
||||
self.content_layout.setSpacing(24)
|
||||
|
||||
scroll_area.setWidget(self.content_widget)
|
||||
self.main_layout.addWidget(scroll_area, 1)
|
||||
|
||||
def resizeEvent(self, event):
|
||||
super().resizeEvent(event)
|
||||
# 调整背景装饰大小
|
||||
self.bg_decoration.setGeometry(0, 0, self.width(), self.height())
|
||||
self.bg_decoration.lower()
|
||||
|
||||
def set_breadcrumb(self, category: str, tool: str):
|
||||
self.breadcrumb_category.setText(category)
|
||||
self.breadcrumb_tool.setText(tool)
|
||||
|
||||
def set_title(self, title: str):
|
||||
self.title_label.setText(title)
|
||||
self.version_label.setVisible(bool(title and title != "欢迎回来"))
|
||||
|
||||
def clear_content(self):
|
||||
while self.content_layout.count():
|
||||
item = self.content_layout.takeAt(0)
|
||||
if item.widget():
|
||||
item.widget().deleteLater()
|
||||
|
||||
|
||||
class ShortcutCard(QFrame):
|
||||
"""快捷入口卡片"""
|
||||
|
||||
clicked = Signal()
|
||||
|
||||
def __init__(self, data: dict, parent=None):
|
||||
super().__init__(parent)
|
||||
self.data = data
|
||||
self.setFixedSize(200, 140)
|
||||
self.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self._hovered = False
|
||||
self.setup_ui()
|
||||
|
||||
def setup_ui(self):
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(16, 16, 16, 16)
|
||||
layout.setSpacing(8)
|
||||
|
||||
# 图标
|
||||
color = self.data.get("color", "#64748b")
|
||||
self.icon_frame = QFrame()
|
||||
self.icon_frame.setFixedSize(40, 40)
|
||||
r, g, b = self.hex_to_rgb(color)
|
||||
self.icon_frame.setStyleSheet(f"""
|
||||
background: rgba({r}, {g}, {b}, 0.1);
|
||||
border-radius: 8px;
|
||||
""")
|
||||
icon_layout = QVBoxLayout(self.icon_frame)
|
||||
icon_layout.setContentsMargins(0, 0, 0, 0)
|
||||
icon_layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
icon = QLabel(self.data.get("icon", ""))
|
||||
icon.setFont(QFont("Segoe UI Emoji", 16))
|
||||
icon.setStyleSheet(f"color: {color}; background: transparent;")
|
||||
icon_layout.addWidget(icon)
|
||||
|
||||
layout.addWidget(self.icon_frame)
|
||||
|
||||
# 名称
|
||||
name = QLabel(self.data.get("name", ""))
|
||||
name.setStyleSheet("color: white; font-size: 14px; font-weight: 500;")
|
||||
layout.addWidget(name)
|
||||
|
||||
# 描述
|
||||
desc = QLabel(self.data.get("desc", ""))
|
||||
desc.setStyleSheet("color: #64748b; font-size: 11px;")
|
||||
layout.addWidget(desc)
|
||||
|
||||
layout.addStretch()
|
||||
self.update_style()
|
||||
|
||||
def update_style(self):
|
||||
if self._hovered:
|
||||
self.setStyleSheet("""
|
||||
ShortcutCard {
|
||||
background: rgba(30, 41, 59, 0.8);
|
||||
border: 1px solid rgba(251, 191, 36, 0.5);
|
||||
border-radius: 16px;
|
||||
}
|
||||
""")
|
||||
else:
|
||||
self.setStyleSheet("""
|
||||
ShortcutCard {
|
||||
background: rgba(30, 41, 59, 0.5);
|
||||
border: 1px solid #334155;
|
||||
border-radius: 16px;
|
||||
}
|
||||
""")
|
||||
|
||||
def enterEvent(self, event):
|
||||
self._hovered = True
|
||||
self.update_style()
|
||||
|
||||
def leaveEvent(self, event):
|
||||
self._hovered = False
|
||||
self.update_style()
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
if event.button() == Qt.MouseButton.LeftButton:
|
||||
self.clicked.emit()
|
||||
|
||||
@staticmethod
|
||||
def hex_to_rgb(hex_color: str) -> tuple:
|
||||
hex_color = hex_color.lstrip('#')
|
||||
return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
|
||||
|
||||
|
||||
class WelcomeWorkspace(BaseWorkspace):
|
||||
"""欢迎页面工作区"""
|
||||
|
||||
tool_clicked = Signal(str, str)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setup_welcome_ui()
|
||||
|
||||
def setup_welcome_ui(self):
|
||||
self.set_breadcrumb("首页", "控制台")
|
||||
self.set_title("欢迎回来")
|
||||
self.version_label.setVisible(False)
|
||||
|
||||
self.history_btn.hide()
|
||||
self.export_btn.hide()
|
||||
|
||||
# 中心内容
|
||||
center_widget = QWidget()
|
||||
center_layout = QVBoxLayout(center_widget)
|
||||
center_layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
center_layout.setSpacing(24)
|
||||
|
||||
# Logo区域
|
||||
logo_container = QFrame()
|
||||
logo_container.setFixedSize(128, 128)
|
||||
logo_container.setStyleSheet("""
|
||||
background: #1e293b;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 64px;
|
||||
""")
|
||||
logo_layout = QVBoxLayout(logo_container)
|
||||
logo_layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
logo = QLabel("🧀")
|
||||
logo.setFont(QFont("Segoe UI Emoji", 48))
|
||||
logo.setStyleSheet("color: #fbbf24; background: transparent;")
|
||||
logo_layout.addWidget(logo)
|
||||
|
||||
center_layout.addWidget(logo_container, 0, Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
# 标题
|
||||
title = QLabel("开始你的创作")
|
||||
title.setFont(QFont("Microsoft YaHei", 28, QFont.Weight.Bold))
|
||||
title.setStyleSheet("color: white;")
|
||||
title.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
center_layout.addWidget(title)
|
||||
|
||||
# 描述
|
||||
desc = QLabel("从左侧选择一个工具。无论是压缩图片、拆分PDF还是处理数据,我们都能搞定。")
|
||||
desc.setStyleSheet("color: #94a3b8; font-size: 14px;")
|
||||
desc.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
desc.setWordWrap(True)
|
||||
desc.setMaximumWidth(500)
|
||||
center_layout.addWidget(desc)
|
||||
|
||||
center_layout.addSpacing(20)
|
||||
|
||||
# 快捷入口
|
||||
shortcuts_layout = QHBoxLayout()
|
||||
shortcuts_layout.setSpacing(16)
|
||||
shortcuts_layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
shortcuts = [
|
||||
{"id": "img-compress", "category": "image", "name": "图片压缩", "desc": "智能无损压缩", "icon": "🖼", "color": "#3b82f6"},
|
||||
{"id": "pdf-split", "category": "pdf", "name": "PDF 拆分", "desc": "提取特定页面", "icon": "✂️", "color": "#ef4444"},
|
||||
{"id": "img-convert", "category": "image", "name": "格式转换", "desc": "格式互转工具", "icon": "🔄", "color": "#22c55e"},
|
||||
]
|
||||
|
||||
for item in shortcuts:
|
||||
card = ShortcutCard(item)
|
||||
card.clicked.connect(lambda i=item: self.tool_clicked.emit(i["id"], i["category"]))
|
||||
shortcuts_layout.addWidget(card)
|
||||
|
||||
center_layout.addLayout(shortcuts_layout)
|
||||
|
||||
self.content_layout.addStretch()
|
||||
self.content_layout.addWidget(center_widget)
|
||||
self.content_layout.addStretch()
|
||||
Reference in New Issue
Block a user