"""
左侧一级菜单组件
- 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)