"""
中间二级菜单组件
- 分类标题
- 搜索框(带图标)
- 工具列表(图标+名称+描述)
- 用户信息
"""
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": """""",
"ph-arrows-left-right": """""",
"ph-stamp": """""",
"ph-scissors": """""",
"ph-files": """""",
"ph-microsoft-word-logo": """""",
"ph-eye": """""",
"ph-chart-bar": """""",
"ph-magnifying-glass": """""",
}
# 工具数据定义
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