""" 左侧一级菜单组件 - Logo (使用图片,点击弹出个人信息卡片) - 图片工具、PDF工具箱、Excel表格 三个分类 - 设置按钮 - 选中状态有左侧高亮条 - 水波纹点击效果 """ import os import sys 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: """获取资源文件的绝对路径""" if getattr(sys, 'frozen', False): # 打包后的路径 base_path = Path(sys._MEIPASS) else: # 开发时的路径 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": """""", "image": """""", "file-pdf": """""", "file-xls": """""", "gear": """""", } 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)