Files
nl-utils/v1/views/app_center.py

1120 lines
38 KiB
Python
Raw Normal View History

2025-06-07 15:54:17 +08:00
import math
2025-06-07 23:43:57 +08:00
import random
from PyQt5.QtCore import Qt, QPropertyAnimation, QPoint, QTimer, QParallelAnimationGroup, pyqtProperty, \
QEasingCurve, QSize, pyqtSignal, QPointF, QByteArray
from PyQt5.QtGui import QPixmap, QIcon, QColor, QPainter, QLinearGradient
from PyQt5.QtSvg import QSvgRenderer, QSvgWidget
2025-06-07 15:54:17 +08:00
from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
2025-06-07 23:43:57 +08:00
QPushButton, QFrame, QGridLayout,
QSizePolicy, QScrollArea, QSizeGrip, QLineEdit)
2025-06-07 15:54:17 +08:00
2025-06-07 23:43:57 +08:00
class AnimatedCard(QFrame):
"""动画卡片控件,与Home页面一致"""
2025-06-07 15:54:17 +08:00
2025-06-07 23:43:57 +08:00
def __init__(self, start_color, end_color, parent=None):
2025-06-07 15:54:17 +08:00
super().__init__(parent)
self.start_color = QColor(start_color)
self.end_color = QColor(end_color)
2025-06-07 23:43:57 +08:00
self._angle = 0
self.hover_state = False
self.init_animations()
self.setMinimumSize(300, 320)
self.setMaximumSize(300, 320)
self.setCursor(Qt.PointingHandCursor)
self.setStyleSheet("""
AnimatedCard {
border-radius: 16px;
border: 1px solid rgba(255, 255, 255, 0.1);
transition: all 0.3s ease;
}
""")
def init_animations(self):
# 渐变角度动画
self.angle_animation = QPropertyAnimation(self, b"angle")
self.angle_animation.setDuration(20000)
self.angle_animation.setStartValue(0)
self.angle_animation.setEndValue(360)
self.angle_animation.setLoopCount(-1)
self.angle_animation.start()
# 悬停动画组
self.hover_animation = QPropertyAnimation(self, b"geometry")
self.hover_animation.setDuration(300)
self.hover_animation.setEasingCurve(QEasingCurve.OutBack)
# 阴影动画
self.shadow_animation = QPropertyAnimation(self, b"styleSheet")
self.shadow_animation.setDuration(300)
# 并行动画组
self.animation_group = QParallelAnimationGroup(self)
self.animation_group.addAnimation(self.hover_animation)
self.animation_group.addAnimation(self.shadow_animation)
2025-06-07 15:54:17 +08:00
def paintEvent(self, event):
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
2025-06-07 23:43:57 +08:00
painter.setPen(Qt.NoPen)
# 创建主渐变背景
rect = self.rect()
2025-06-07 15:54:17 +08:00
2025-06-07 23:43:57 +08:00
# 添加动态渐变背景
if self.hover_state:
# 悬停状态使用更亮的渐变
gradient = QLinearGradient(rect.topLeft(), rect.bottomRight())
gradient.setColorAt(0, self.start_color.lighter(120))
gradient.setColorAt(1, self.end_color.lighter(120))
2025-06-07 15:54:17 +08:00
else:
2025-06-07 23:43:57 +08:00
# 普通状态
gradient = QLinearGradient(rect.topLeft(), rect.bottomRight())
gradient.setColorAt(0, self.start_color)
gradient.setColorAt(1, self.end_color)
2025-06-07 15:54:17 +08:00
2025-06-07 23:43:57 +08:00
# 添加动态旋转效果
cx = rect.center().x()
cy = rect.center().y()
radius = max(rect.width(), rect.height()) * 1.2
start_x = cx + radius * 0.5 * math.cos(math.radians(self._angle))
start_y = cy + radius * 0.5 * math.sin(math.radians(self._angle))
end_x = cx - radius * 0.5 * math.cos(math.radians(self._angle))
end_y = cy - radius * 0.5 * math.sin(math.radians(self._angle))
2025-06-07 15:54:17 +08:00
2025-06-07 23:43:57 +08:00
moving_gradient = QLinearGradient(start_x, start_y, end_x, end_y)
moving_gradient.setColorAt(0, QColor(255, 255, 255, 30))
moving_gradient.setColorAt(1, QColor(255, 255, 255, 10))
2025-06-07 15:54:17 +08:00
2025-06-07 23:43:57 +08:00
# 绘制背景
painter.setBrush(gradient)
painter.drawRoundedRect(rect, 16, 16)
2025-06-07 15:54:17 +08:00
2025-06-07 23:43:57 +08:00
# 绘制动态效果层
painter.setBrush(moving_gradient)
painter.drawRoundedRect(rect, 16, 16)
2025-06-07 15:54:17 +08:00
2025-06-07 23:43:57 +08:00
# 绘制高光边框
if self.hover_state:
border_gradient = QLinearGradient(rect.topLeft(), rect.topRight())
border_gradient.setColorAt(0, QColor(255, 255, 255, 100))
border_gradient.setColorAt(1, QColor(255, 255, 255, 30))
painter.setPen(QColor(255, 255, 255, 150))
painter.drawRoundedRect(rect.adjusted(1, 1, -1, -1), 16, 16)
2025-06-07 15:54:17 +08:00
2025-06-07 23:43:57 +08:00
def enterEvent(self, event):
self.hover_state = True
self.animate_hover(True)
self.update()
super().enterEvent(event)
def leaveEvent(self, event):
self.hover_state = False
self.animate_hover(False)
self.update()
super().leaveEvent(event)
def animate_hover(self, hover):
"""执行悬停动画"""
# 保存当前位置和大小
current_geometry = self.geometry()
# 目标位置和大小
if hover:
target_geometry = current_geometry.adjusted(-5, -5, 5, 5)
shadow_value = 20
else:
target_geometry = current_geometry.adjusted(5, 5, -5, -5)
shadow_value = 5
# 设置位置动画
self.hover_animation.setStartValue(current_geometry)
self.hover_animation.setEndValue(target_geometry)
# 设置阴影动画
self.shadow_animation.setStartValue(self.styleSheet())
self.shadow_animation.setEndValue(f"""
AnimatedCard {{
border-radius: 16px;
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 10px {shadow_value}px rgba(0, 0, 0, 0.3);
transition: all 0.3s ease;
}}
""")
# 开始动画
self.animation_group.start()
@pyqtProperty(int)
def angle(self):
return self._angle
@angle.setter
def angle(self, value):
self._angle = value
self.update()
class AppDetailWindow(QWidget):
"""应用详情窗口 - 修复透明问题"""
def __init__(self, app_data, parent=None):
super().__init__(parent)
self.app_data = app_data
self.setObjectName("appDetail")
self.setWindowFlags(Qt.FramelessWindowHint | Qt.Popup)
self.setAttribute(Qt.WA_TranslucentBackground)
2025-06-07 15:54:17 +08:00
self.init_ui()
2025-06-07 23:43:57 +08:00
self.setStyleSheet(self.get_styles())
2025-06-07 15:54:17 +08:00
2025-06-07 23:43:57 +08:00
def get_styles(self):
return """
/* ===== 应用详情样式 ===== */
#appDetail {
background-color: rgba(26, 32, 44, 0.98); /* 提高不透明度 */
border: 1px solid rgba(74, 85, 104, 0.7);
border-radius: 12px;
box-shadow: 0 15px 50px rgba(0, 0, 0, 0.5);
2025-06-07 15:54:17 +08:00
}
2025-06-07 23:43:57 +08:00
#titleBar {
background-color: rgba(45, 55, 72, 1); /* 完全不透明 */
border-top-left-radius: 12px;
border-top-right-radius: 12px;
padding: 15px 20px;
}
#detailTitle {
font-size: 24px;
2025-06-07 15:54:17 +08:00
font-weight: bold;
2025-06-07 23:43:57 +08:00
color: #ffffff;
2025-06-07 15:54:17 +08:00
}
2025-06-07 23:43:57 +08:00
#closeBtn {
background-color: transparent;
border: none;
padding: 5px;
border-radius: 4px;
2025-06-07 15:54:17 +08:00
}
2025-06-07 23:43:57 +08:00
#closeBtn:hover {
background-color: rgba(239, 68, 68, 0.2);
2025-06-07 15:54:17 +08:00
}
2025-06-07 23:43:57 +08:00
#detailIcon {
background-color: rgba(255, 255, 255, 0.1);
border-radius: 60px;
padding: 10px;
}
#detailAuthor,
#detailVersion,
#detailUpload {
font-size: 16px;
color: rgba(255, 255, 255, 0.9);
padding: 8px 15px;
border-radius: 8px;
background-color: rgba(255, 255, 255, 0.05);
margin: 5px 0;
}
#detailDesc {
font-size: 17px;
color: rgba(255, 255, 255, 0.85);
line-height: 1.6;
padding: 0 0 15px 0;
}
#detailSection {
font-size: 19px;
font-weight: 600;
color: #ffffff;
2025-06-07 15:54:17 +08:00
margin-top: 15px;
2025-06-07 23:43:57 +08:00
margin-bottom: 10px;
2025-06-07 15:54:17 +08:00
}
2025-06-07 23:43:57 +08:00
#featureItem {
font-size: 15px;
color: rgba(255, 255, 255, 0.8);
padding-left: 15px;
padding-bottom: 8px;
2025-06-07 15:54:17 +08:00
}
2025-06-07 23:43:57 +08:00
#detailStartBtn {
2025-06-07 15:54:17 +08:00
background-color: #4f46e5;
color: white;
2025-06-07 23:43:57 +08:00
border-radius: 8px;
padding: 12px 30px;
font-size: 16px;
2025-06-07 15:54:17 +08:00
font-weight: 600;
2025-06-07 23:43:57 +08:00
min-width: 140px;
transition: background-color 0.3s;
2025-06-07 15:54:17 +08:00
}
2025-06-07 23:43:57 +08:00
#detailStartBtn:hover {
2025-06-07 15:54:17 +08:00
background-color: #4338ca;
}
2025-06-07 23:43:57 +08:00
#detailCancelBtn {
background-color: rgba(255, 255, 255, 0.1);
color: rgba(255, 255, 255, 0.9);
border-radius: 8px;
padding: 12px 30px;
font-size: 16px;
min-width: 100px;
transition: background-color 0.3s;
}
#detailCancelBtn:hover {
background-color: rgba(255, 255, 255, 0.2);
}
/* 内容区域背景色 */
QWidget#content {
background-color: rgba(26, 32, 44, 1); /* 完全不透明 */
border-radius: 0 0 12px 12px;
padding: 20px;
}
"""
def init_ui(self):
layout = QVBoxLayout(self)
layout.setContentsMargins(1, 1, 1, 1) # 减小边距使阴影更明显
layout.setSpacing(0)
# 标题栏
title_bar = QWidget()
title_bar.setObjectName("titleBar")
title_bar_layout = QHBoxLayout(title_bar)
title_bar_layout.setContentsMargins(15, 5, 15, 5)
title = QLabel(self.app_data["title"])
title.setObjectName("detailTitle")
close_btn = QPushButton()
close_btn.setObjectName("closeBtn")
close_btn.setIcon(QIcon(":/icons/close.png"))
close_btn.setIconSize(QSize(16, 16))
close_btn.clicked.connect(self.close)
title_bar_layout.addWidget(title)
title_bar_layout.addStretch()
title_bar_layout.addWidget(close_btn)
# 内容区域
content = QWidget()
content.setObjectName("content")
content_layout = QVBoxLayout(content)
content_layout.setContentsMargins(20, 20, 20, 20)
# 应用信息
info_layout = QHBoxLayout()
info_layout.setSpacing(20)
2025-06-07 15:54:17 +08:00
2025-06-07 23:43:57 +08:00
# 左侧 - 图标和基本信息
left_panel = QWidget()
left_layout = QVBoxLayout(left_panel)
left_layout.setSpacing(15)
# 图标
icon_frame = QFrame()
icon_frame.setFixedSize(120, 120)
icon_frame.setObjectName("detailIcon")
icon_layout = QVBoxLayout(icon_frame)
icon = QLabel()
icon_pixmap = QPixmap(f":/icons/{self.app_data['icon']}.png").scaled(80, 80,
Qt.KeepAspectRatio,
Qt.SmoothTransformation)
# 转换图标为白色
painter = QPainter(icon_pixmap)
painter.setCompositionMode(QPainter.CompositionMode_SourceIn)
painter.fillRect(icon_pixmap.rect(), Qt.white)
painter.end()
icon.setPixmap(icon_pixmap)
icon.setAlignment(Qt.AlignCenter)
icon_layout.addWidget(icon)
# 作者信息
author_label = QLabel(f"开发者: {self.app_data['author']}")
author_label.setObjectName("detailAuthor")
# 版本信息
version_label = QLabel(f"版本: {self.app_data['version']}")
version_label.setObjectName("detailVersion")
# 上传时间
upload_label = QLabel(f"上传时间: {self.app_data['upload_time']}")
upload_label.setObjectName("detailUpload")
left_layout.addWidget(icon_frame, 0, Qt.AlignCenter)
left_layout.addWidget(author_label)
left_layout.addWidget(version_label)
left_layout.addWidget(upload_label)
left_layout.addStretch()
# 右侧 - 描述和功能
right_panel = QWidget()
right_layout = QVBoxLayout(right_panel)
right_layout.setSpacing(15)
# 应用描述
desc_label = QLabel(self.app_data["desc"])
desc_label.setObjectName("detailDesc")
desc_label.setWordWrap(True)
# 功能列表
features_label = QLabel("功能特点:")
features_label.setObjectName("detailSection")
features_list = QWidget()
features_layout = QVBoxLayout(features_list)
features_layout.setSpacing(8)
for feature in self.app_data["features"]:
feature_item = QLabel(f"• {feature}")
feature_item.setObjectName("featureItem")
feature_item.setWordWrap(True)
features_layout.addWidget(feature_item)
# 操作按钮
btn_layout = QHBoxLayout()
btn_layout.setSpacing(15)
btn_layout.setContentsMargins(0, 20, 0, 0) # 添加上边距
start_btn = QPushButton("立即使用")
start_btn.setObjectName("detailStartBtn")
start_btn.clicked.connect(lambda: self.parent().app_selected.emit(self.app_data["id"]))
cancel_btn = QPushButton("返回")
cancel_btn.setObjectName("detailCancelBtn")
cancel_btn.clicked.connect(self.close)
btn_layout.addStretch()
btn_layout.addWidget(start_btn)
btn_layout.addWidget(cancel_btn)
btn_layout.addStretch()
right_layout.addWidget(desc_label)
right_layout.addWidget(features_label)
right_layout.addWidget(features_list)
right_layout.addStretch()
right_layout.addLayout(btn_layout)
info_layout.addWidget(left_panel)
info_layout.addWidget(right_panel)
content_layout.addLayout(info_layout)
# 添加到主布局
layout.addWidget(title_bar)
layout.addWidget(content)
# 添加大小控制点
size_grip = QSizeGrip(self)
size_grip.setFixedSize(16, 16)
size_grip.setStyleSheet("background: transparent;")
layout.addWidget(size_grip, 0, Qt.AlignBottom | Qt.AlignRight)
# 设置窗口尺寸
self.resize(700, 550)
class AppCenter(QWidget):
"""应用中心页面,与Home页面卡片样式一致"""
app_selected = pyqtSignal(str)
def __init__(self):
super().__init__()
self.setObjectName("AppCenterPage")
self.app_detail_window = None
self.current_category = "全部"
self.init_ui()
self.setStyleSheet(self.get_styles())
# 启动加载动画
self.start_loading_animation()
# 初始化应用数据 - 模拟API请求延迟
QTimer.singleShot(800, self.load_data) # 0.8秒后加载数据
# 添加背景粒子效果
self.particles = []
self.init_particles()
self.particle_timer = QTimer(self)
self.particle_timer.timeout.connect(self.update_particles)
self.particle_timer.start(30)
def init_ui(self):
# 主布局
main_layout = QVBoxLayout()
main_layout.setContentsMargins(40, 30, 40, 30)
main_layout.setSpacing(20)
# 标题和搜索区域
header_layout = QHBoxLayout()
2025-06-07 15:54:17 +08:00
title = QLabel("应用中心")
title.setObjectName("pageTitle")
2025-06-07 23:43:57 +08:00
# 搜索框
search_container = QFrame()
search_container.setObjectName("searchContainer")
search_layout = QHBoxLayout(search_container)
search_layout.setContentsMargins(15, 5, 15, 5)
2025-06-07 15:54:17 +08:00
2025-06-07 23:43:57 +08:00
self.search_input = QLineEdit()
self.search_input.setPlaceholderText("搜索应用...")
self.search_input.setObjectName("searchInput")
self.search_input.textChanged.connect(self.filter_apps)
2025-06-07 15:54:17 +08:00
2025-06-07 23:43:57 +08:00
search_btn = QPushButton()
search_btn.setObjectName("searchBtn")
search_btn.setIcon(QIcon(":/icons/search.png"))
search_btn.setIconSize(QSize(20, 20))
2025-06-07 15:54:17 +08:00
2025-06-07 23:43:57 +08:00
search_layout.addWidget(self.search_input)
search_layout.addWidget(search_btn)
2025-06-07 15:54:17 +08:00
2025-06-07 23:43:57 +08:00
header_layout.addWidget(title)
header_layout.addStretch()
header_layout.addWidget(search_container)
main_layout.addLayout(header_layout)
# 应用类别过滤
self.category_layout = QHBoxLayout()
self.category_layout.setSpacing(15)
categories = ["全部", "文档处理", "图像处理", "媒体工具", "其他"]
for category in categories:
btn = QPushButton(category)
btn.setObjectName("categoryBtn")
if category == "全部":
btn.setProperty("active", True)
btn.clicked.connect(lambda _, c=category: self.filter_by_category(c))
self.category_layout.addWidget(btn)
self.category_layout.addStretch()
main_layout.addLayout(self.category_layout)
# 加载动画容器(初始时显示)
self.loading_container = QWidget()
self.loading_container.setVisible(True)
loading_layout = QVBoxLayout(self.loading_container)
loading_layout.setAlignment(Qt.AlignCenter)
# 旋转SVG动画
self.loading_label = QLabel()
self.loading_label.setObjectName("loadingLabel")
loading_layout.addWidget(self.loading_label, alignment=Qt.AlignCenter)
# 加载中文本
loading_text = QLabel("正在加载中...")
loading_text.setObjectName("loadingText")
loading_layout.addWidget(loading_text, alignment=Qt.AlignCenter)
# 卡片容器(初始时隐藏)
self.apps_container = QWidget()
self.apps_container.setVisible(False)
self.apps_layout = QGridLayout(self.apps_container)
self.apps_layout.setSpacing(30)
self.apps_layout.setAlignment(Qt.AlignTop | Qt.AlignHCenter)
# 滚动区域(包含加载容器和卡片容器)
scroll_area = QScrollArea()
scroll_area.setWidgetResizable(True)
# 创建包装容器
scroll_widget = QWidget()
scroll_layout = QVBoxLayout(scroll_widget)
scroll_layout.addWidget(self.loading_container)
scroll_layout.addWidget(self.apps_container)
scroll_layout.setContentsMargins(0, 0, 0, 0)
scroll_area.setWidget(scroll_widget)
scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
scroll_area.setStyleSheet("""
QScrollArea {
border: none;
background: transparent;
2025-06-07 15:54:17 +08:00
}
2025-06-07 23:43:57 +08:00
QScrollBar:vertical {
border: none;
background: rgba(45, 55, 72, 0.3);
width: 10px;
margin: 0px 0px 0px 0px;
}
QScrollBar::handle:vertical {
background: rgba(74, 85, 104, 0.7);
min-height: 20px;
border-radius: 4px;
}
QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {
height: 0px;
background: none;
}
""")
2025-06-07 15:54:17 +08:00
2025-06-07 23:43:57 +08:00
main_layout.addWidget(scroll_area)
self.setLayout(main_layout)
2025-06-07 15:54:17 +08:00
2025-06-07 23:43:57 +08:00
# 启动加载动画
self.start_loading_animation()
2025-06-07 15:54:17 +08:00
2025-06-07 23:43:57 +08:00
# 初始化应用数据 - 模拟API请求延迟
QTimer.singleShot(800, self.load_data) # 0.8秒后加载数据
2025-06-07 15:54:17 +08:00
2025-06-07 23:43:57 +08:00
# 添加背景粒子效果
self.particles = []
self.init_particles()
self.particle_timer = QTimer(self)
self.particle_timer.timeout.connect(self.update_particles)
self.particle_timer.start(30)
def start_loading_animation(self):
"""启动加载动画"""
# 直接使用QSvgWidget加载SVG
self.loading_svg = QSvgWidget()
self.loading_svg.setFixedSize(80, 80)
self.loading_svg.setStyleSheet("background: transparent;")
# 添加SVG内容
svg_data = QByteArray(b'''
<svg width="80" height="80" viewBox="0 0 38 38" stroke="#4F46E5" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fill-rule="evenodd">
<g transform="translate(1 1)" stroke-width="2">
<circle stroke-opacity=".3" cx="18" cy="18" r="18"/>
<path d="M36 18c0-9.94-8.06-18-18-18">
<animateTransform attributeName="transform" type="rotate" from="0 18 18" to="360 18 18"
dur="0.8s" repeatCount="indefinite"/>
</path>
</g>
</g>
</svg>
''')
self.loading_svg.load(svg_data)
# 添加到布局
self.loading_label.setLayout(QVBoxLayout())
self.loading_label.layout().setContentsMargins(0, 0, 0, 0)
self.loading_label.layout().addWidget(self.loading_svg, alignment=Qt.AlignCenter)
# 添加样式类
self.loading_label.setObjectName("loadingLabel")
def load_data(self):
"""模拟加载数据"""
self.apps = self.get_apps_data()
# 隐藏加载动画,显示应用列表
self.loading_container.setVisible(False)
self.apps_container.setVisible(True)
# 计算列数并显示应用卡片
cards_per_row = max(1, self.width() // 350) if self.width() > 0 else 3
self.display_apps(self.apps, cards_per_row)
# 重置动画状态
if hasattr(self, 'loading_svg'):
self.loading_svg.deleteLater()
def filter_apps(self):
"""根据搜索关键词过滤应用"""
keyword = self.search_input.text().strip().lower()
if not keyword and self.current_category == "全部":
self.display_apps(self.apps,
max(1, self.width() // 350) if self.width() > 0 else 3)
return
filtered_apps = []
for app in self.apps:
# 检查搜索条件
match_search = (not keyword or
keyword in app["title"].lower() or
keyword in app["desc"].lower() or
keyword in app["author"].lower())
2025-06-07 15:54:17 +08:00
2025-06-07 23:43:57 +08:00
# 检查类别条件
match_category = (self.current_category == "全部" or
app["category"] == self.current_category)
if match_search and match_category:
filtered_apps.append(app)
# 计算列数并显示过滤后的应用
cards_per_row = max(1, self.width() // 350) if self.width() > 0 else 3
self.display_apps(filtered_apps, cards_per_row)
def filter_by_category(self, category):
"""根据类别过滤应用"""
self.current_category = category
# 更新按钮状态
for i in range(self.category_layout.count()):
item = self.category_layout.itemAt(i)
if item and item.widget():
btn = item.widget()
btn.setProperty("active", btn.text() == category)
# 重新应用样式
btn.style().unpolish(btn)
btn.style().polish(btn)
self.filter_apps() # 应用新的过滤
def display_apps(self, apps, cards_per_row=3):
"""显示应用列表"""
# 清除当前内容
for i in reversed(range(self.apps_layout.count())):
widget = self.apps_layout.itemAt(i).widget()
if widget:
widget.setParent(None)
widget.deleteLater()
# 添加新卡片 - 使用栅格布局
row, col = 0, 0
for i, app in enumerate(apps):
card = self.create_app_card(app)
self.apps_layout.addWidget(card, row, col)
col += 1
if col >= cards_per_row:
col = 0
row += 1
# 如果没有应用显示提示信息
if not apps:
no_result = QLabel("没有找到匹配的应用")
no_result.setObjectName("noResult")
no_result.setAlignment(Qt.AlignCenter)
self.apps_layout.addWidget(no_result, 0, 0, 1, cards_per_row)
def show_app_detail(self, app_data):
"""显示应用详情弹窗"""
if self.app_detail_window:
self.app_detail_window.close()
self.app_detail_window.deleteLater()
self.app_detail_window = AppDetailWindow(app_data, self)
# 计算位置(居中显示)
center_point = self.mapToGlobal(self.rect().center())
self.app_detail_window.move(center_point - QPoint(self.app_detail_window.width() // 2,
self.app_detail_window.height() // 2))
self.app_detail_window.show()
def init_particles(self):
"""初始化粒子效果"""
for _ in range(50):
particle = {
"pos": QPointF(random.randint(0, self.width()), random.randint(0, self.height())),
"size": random.randint(2, 5),
"speed": random.uniform(0.5, 2.0),
"color": random.choice([
(66, 153, 225, 70), # 蓝色
(72, 187, 120, 60), # 绿色
(237, 137, 54, 60), # 橙色
(224, 42, 94, 50) # 粉色
])
}
self.particles.append(particle)
def update_particles(self):
"""更新粒子位置"""
for particle in self.particles:
# 添加随机偏移
particle["pos"].setX(particle["pos"].x() + random.uniform(-particle["speed"], particle["speed"]))
particle["pos"].setY(particle["pos"].y() + particle["speed"])
# 如果粒子移出底部,重置到顶部
if particle["pos"].y() > self.height():
particle["pos"] = QPointF(random.randint(0, self.width()), -10)
self.update()
def paintEvent(self, event):
"""绘制背景粒子效果"""
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
# 绘制背景
painter.fillRect(self.rect(), QColor(26, 32, 44))
# 绘制粒子
for particle in self.particles:
r, g, b, a = particle["color"]
painter.setBrush(QColor(r, g, b, a))
painter.setPen(Qt.NoPen)
painter.drawEllipse(particle["pos"], particle["size"], particle["size"])
def resizeEvent(self, event):
"""窗口大小改变时调整粒子"""
# 移除超出边界的粒子
self.particles = [
p for p in self.particles
if 0 <= p["pos"].x() <= self.width() and 0 <= p["pos"].y() <= self.height()
]
# 补充新粒子
while len(self.particles) < 50:
self.init_particles()
super().resizeEvent(event)
2025-06-07 15:54:17 +08:00
def create_app_card(self, app):
2025-06-07 23:43:57 +08:00
"""创建应用卡片,完全使用Home页面样式"""
card = AnimatedCard(app['color'][0], app['color'][1])
2025-06-07 15:54:17 +08:00
card.setObjectName("appCard")
2025-06-07 23:43:57 +08:00
# 使用垂直布局作为卡片的主布局
2025-06-07 15:54:17 +08:00
layout = QVBoxLayout(card)
2025-06-07 23:43:57 +08:00
layout.setContentsMargins(25, 25, 25, 25)
2025-06-07 15:54:17 +08:00
layout.setSpacing(15)
2025-06-07 23:43:57 +08:00
# 图标部分
2025-06-07 15:54:17 +08:00
icon_frame = QFrame()
2025-06-07 23:43:57 +08:00
icon_frame.setFixedSize(90, 90)
2025-06-07 15:54:17 +08:00
icon_frame.setObjectName("appCardIcon")
icon_layout = QVBoxLayout(icon_frame)
icon_layout.setContentsMargins(0, 0, 0, 0)
icon = QLabel()
2025-06-07 23:43:57 +08:00
icon_pixmap = QPixmap(f":/icons/{app['icon']}.png").scaled(56, 56,
Qt.KeepAspectRatio,
Qt.SmoothTransformation)
2025-06-07 15:54:17 +08:00
# 转换图标为白色
2025-06-07 23:43:57 +08:00
painter = QPainter(icon_pixmap)
painter.setCompositionMode(QPainter.CompositionMode_SourceIn)
painter.fillRect(icon_pixmap.rect(), Qt.white)
painter.end()
2025-06-07 15:54:17 +08:00
icon.setPixmap(icon_pixmap)
icon.setAlignment(Qt.AlignCenter)
icon_layout.addWidget(icon)
layout.addWidget(icon_frame, 0, Qt.AlignHCenter)
# 标题
title = QLabel(app["title"])
title.setObjectName("appCardTitle")
2025-06-07 23:43:57 +08:00
title.setWordWrap(True) # 确保长标题可以换行
2025-06-07 15:54:17 +08:00
layout.addWidget(title)
# 描述
desc = QLabel(app["desc"])
desc.setObjectName("appCardDesc")
desc.setWordWrap(True)
layout.addWidget(desc)
# 添加伸缩空间保持底部对齐
2025-06-07 23:43:57 +08:00
layout.addStretch()
# 按钮区域 - 使用水平布局并使按钮居中
btn_container = QWidget()
btn_layout = QHBoxLayout(btn_container)
btn_layout.setContentsMargins(0, 0, 0, 0)
btn_layout.setSpacing(10)
# 开始使用按钮
start_btn = QPushButton("开始使用")
start_btn.setObjectName("appCardBtn")
start_btn.setCursor(Qt.PointingHandCursor)
start_btn.setMinimumWidth(100) # 设置最小宽度防止重叠
start_btn.clicked.connect(lambda: self.app_selected.emit(app["id"]))
# 详情按钮
detail_btn = QPushButton("详情")
detail_btn.setObjectName("appCardBtn")
detail_btn.setCursor(Qt.PointingHandCursor)
detail_btn.setMinimumWidth(100) # 设置最小宽度防止重叠
detail_btn.clicked.connect(lambda: self.show_app_detail(app))
# 添加到按钮布局
btn_layout.addWidget(start_btn)
btn_layout.addWidget(detail_btn)
# 将按钮布局添加到容器
layout.addWidget(btn_container, 0, Qt.AlignHCenter)
return card
def get_apps_data(self):
"""获取应用数据"""
return [
{
"id": "word_to_pdf",
"title": "Word转PDF",
"desc": "批量将Word文档转换为PDF格式",
"color": ("#6366F1", "#4F46E5"),
"icon": "word_to_pdf",
2025-06-08 23:09:59 +08:00
"author": "年糕崽崽",
2025-06-07 23:43:57 +08:00
"upload_time": "2023-06-15",
"version": "1.2.0",
"category": "文档处理",
"features": [
"支持批量转换多个Word文件",
"保持原始文档格式",
"支持DOC和DOCX格式",
"转换速度快,效率高"
]
},
{
2025-06-08 23:09:59 +08:00
"id": "pdf_tools_page",
"title": "PDF合并/拆分工具",
2025-06-07 23:43:57 +08:00
"desc": "合并多个PDF文件为一个",
"color": ("#8B5CF6", "#7C3AED"),
"icon": "pdf_merge",
2025-06-08 23:09:59 +08:00
"author": "年糕崽崽",
2025-06-07 23:43:57 +08:00
"upload_time": "2023-09-05",
"version": "极速版1.1.0",
"category": "文档处理",
"features": [
"拖拽排序PDF文件",
"自定义合并顺序",
"保留原始文档质量",
"支持大文件处理"
]
},
2025-06-08 23:09:59 +08:00
# {
# "id": "ocr",
# "title": "OCR文字识别",
# "desc": "从图片中提取文字内容",
# "color": ("#4ADE80", "#22C55E"),
# "icon": "ocr",
# "author": "张伟",
# "upload_time": "2023-07-22",
# "version": "2.0.1",
# "category": "图像处理",
# "features": [
# "支持多种图片格式",
# "识别准确率高",
# "支持多语言识别",
# "导出为可编辑文本"
# ]
# },
# {
# "id": "image_convert",
# "title": "图片格式转换",
# "desc": "转换图像文件格式并优化",
# "color": ("#F97316", "#EA580C"),
# "icon": "image_convert",
# "author": "王芳",
# "upload_time": "2023-08-10",
# "version": "1.5.3",
# "category": "图像处理",
# "features": [
# "支持JPEG、PNG、WEBP等格式",
# "批量转换功能",
# "图片压缩优化",
# "保持图片质量"
# ]
# },
# {
# "id": "video_compress",
# "title": "视频压缩",
# "desc": "减小视频文件大小",
# "color": ("#3B82F6", "#2563EB"),
# "icon": "video_compress",
# "author": "赵雷",
# "upload_time": "2023-10-02",
# "version": "1.3.2",
# "category": "媒体工具",
# "features": [
# "支持多种视频格式",
# "自定义压缩参数",
# "保持视频质量",
# "批量处理功能"
# ]
# },
# {
# "id": "audio_extract",
# "title": "音频提取",
# "desc": "从视频中提取音频",
# "color": ("#06B6D4", "#0891B2"),
# "icon": "audio_extract",
# "author": "杨帆",
# "upload_time": "2023-10-15",
# "version": "1.0.2",
# "category": "媒体工具",
# "features": [
# "支持常见视频格式",
# "无损音频提取",
# "批量处理功能",
# "转换速度快"
# ]
# },
# {
# "id": "text_summary",
# "title": "文本摘要",
# "desc": "自动提取文章关键信息",
# "color": ("#F59E0B", "#D97706"),
# "icon": "text_summary",
# "author": "周华",
# "upload_time": "2023-10-28",
# "version": "2.1.0",
# "category": "文档处理",
# "features": [
# "多种摘要算法可选",
# "自定义摘要长度",
# "支持多语言处理",
# "处理结果可导出"
# ]
# },
# {
# "id": "color_picker",
# "title": "颜色提取器",
# "desc": "从图像中提取配色方案",
# "color": ("#EC4899", "#DB2777"),
# "icon": "color_picker",
# "author": "吴敏",
# "upload_time": "2023-11-05",
# "version": "1.0.0",
# "category": "图像处理",
# "features": [
# "自动分析主色调",
# "提取多种配色方案",
# "支持导出配色代码",
# "直观的颜色展示"
# ]
# }
2025-06-07 23:43:57 +08:00
]
2025-06-07 15:54:17 +08:00
2025-06-07 23:43:57 +08:00
def get_styles(self):
return """
/* ===== 应用中心页面样式 ===== */
#AppCenterPage {
background-color: #1a202c;
}
#pageTitle {
font-size: 28px;
font-weight: bold;
color: #ffffff;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
margin-bottom: 30px;
}
2025-06-07 15:54:17 +08:00
2025-06-07 23:43:57 +08:00
#searchContainer {
background-color: rgba(255, 255, 255, 0.15);
border-radius: 12px;
min-width: 300px;
}
#searchInput {
background: transparent;
border: none;
color: white;
font-size: 16px;
padding: 8px 0;
}
#searchInput::placeholder {
color: rgba(255, 255, 255, 0.5);
}
#searchBtn {
background: transparent;
border: none;
padding: 0;
}
#searchBtn:hover {
background-color: rgba(255, 255, 255, 0.1);
border-radius: 6px;
}
#categoryBtn {
background-color: rgba(74, 85, 104, 0.5);
color: rgba(255, 255, 255, 0.8);
border-radius: 8px;
padding: 8px 20px;
font-size: 15px;
transition: all 0.3s ease;
}
#categoryBtn[active="true"] {
background-color: #4f46e5;
color: white;
}
#categoryBtn:hover {
background-color: rgba(74, 85, 104, 0.7);
}
#noResult {
font-size: 20px;
color: rgba(255, 255, 255, 0.5);
padding: 50px;
}
/* ===== 加载动画样式 ===== */
#loadingText {
font-size: 24px;
color: rgba(255, 255, 255, 0.7);
margin-top: 20px;
text-align: center;
letter-spacing: 2px;
}
#loadingLabel {
margin: 0;
padding: 0;
}
/* ===== 应用卡片样式 ===== */
#appCard {
border: 1px solid rgba(255, 255, 255, 0.1);
}
#appCardTitle {
font-size: 22px;
font-weight: 700;
color: white;
text-align: center;
margin-top: 15px;
letter-spacing: 0.5px;
padding: 0 10px; /* 添加内边距防止文字溢出 */
}
#appCardDesc {
font-size: 15px;
color: rgba(255, 255, 255, 0.8);
text-align: center;
padding: 0 15px;
line-height: 1.5;
}
#appCardBtn {
background-color: rgba(255, 255, 255, 0.15); /* 半透明背景 */
color: white;
border-radius: 12px;
padding: 10px 15px; /* 减少内边距 */
font-weight: 500;
border: 1px solid rgba(255, 255, 255, 0.2);
min-width: 80px; /* 减小最小宽度 */
transition: all 0.3s ease;
font-size: 14px; /* 减小字体大小 */
}
#appCardBtn:hover {
background-color: rgba(255, 255, 255, 0.25);
transform: translateY(-2px);
}
#appCardIcon {
background-color: rgba(255, 255, 255, 0.15);
border-radius: 50%;
transition: all 0.3s ease;
}
#appCardIcon:hover {
transform: scale(1.05);
}
"""