1137 lines
39 KiB
Python
1137 lines
39 KiB
Python
import math
|
||
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
|
||
from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
|
||
QPushButton, QFrame, QGridLayout,
|
||
QSizePolicy, QScrollArea, QSizeGrip, QLineEdit)
|
||
|
||
|
||
class AnimatedCard(QFrame):
|
||
"""动画卡片控件,与Home页面一致"""
|
||
|
||
def __init__(self, start_color, end_color, parent=None):
|
||
super().__init__(parent)
|
||
self.start_color = QColor(start_color)
|
||
self.end_color = QColor(end_color)
|
||
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)
|
||
|
||
def paintEvent(self, event):
|
||
painter = QPainter(self)
|
||
painter.setRenderHint(QPainter.Antialiasing)
|
||
painter.setPen(Qt.NoPen)
|
||
|
||
# 创建主渐变背景
|
||
rect = self.rect()
|
||
|
||
# 添加动态渐变背景
|
||
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))
|
||
else:
|
||
# 普通状态
|
||
gradient = QLinearGradient(rect.topLeft(), rect.bottomRight())
|
||
gradient.setColorAt(0, self.start_color)
|
||
gradient.setColorAt(1, self.end_color)
|
||
|
||
# 添加动态旋转效果
|
||
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))
|
||
|
||
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))
|
||
|
||
# 绘制背景
|
||
painter.setBrush(gradient)
|
||
painter.drawRoundedRect(rect, 16, 16)
|
||
|
||
# 绘制动态效果层
|
||
painter.setBrush(moving_gradient)
|
||
painter.drawRoundedRect(rect, 16, 16)
|
||
|
||
# 绘制高光边框
|
||
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)
|
||
|
||
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)
|
||
self.init_ui()
|
||
self.setStyleSheet(self.get_styles())
|
||
|
||
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);
|
||
}
|
||
|
||
#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;
|
||
font-weight: bold;
|
||
color: #ffffff;
|
||
}
|
||
|
||
#closeBtn {
|
||
background-color: transparent;
|
||
border: none;
|
||
padding: 5px;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
#closeBtn:hover {
|
||
background-color: rgba(239, 68, 68, 0.2);
|
||
}
|
||
|
||
#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;
|
||
margin-top: 15px;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
#featureItem {
|
||
font-size: 15px;
|
||
color: rgba(255, 255, 255, 0.8);
|
||
padding-left: 15px;
|
||
padding-bottom: 8px;
|
||
}
|
||
|
||
#detailStartBtn {
|
||
background-color: #4f46e5;
|
||
color: white;
|
||
border-radius: 8px;
|
||
padding: 12px 30px;
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
min-width: 140px;
|
||
transition: background-color 0.3s;
|
||
}
|
||
|
||
#detailStartBtn:hover {
|
||
background-color: #4338ca;
|
||
}
|
||
|
||
#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)
|
||
|
||
# 左侧 - 图标和基本信息
|
||
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()
|
||
|
||
title = QLabel("应用中心")
|
||
title.setObjectName("pageTitle")
|
||
|
||
# 搜索框
|
||
search_container = QFrame()
|
||
search_container.setObjectName("searchContainer")
|
||
search_layout = QHBoxLayout(search_container)
|
||
search_layout.setContentsMargins(15, 5, 15, 5)
|
||
|
||
self.search_input = QLineEdit()
|
||
self.search_input.setPlaceholderText("搜索应用...")
|
||
self.search_input.setObjectName("searchInput")
|
||
self.search_input.textChanged.connect(self.filter_apps)
|
||
|
||
search_btn = QPushButton()
|
||
search_btn.setObjectName("searchBtn")
|
||
search_btn.setIcon(QIcon(":/icons/search.png"))
|
||
search_btn.setIconSize(QSize(20, 20))
|
||
|
||
search_layout.addWidget(self.search_input)
|
||
search_layout.addWidget(search_btn)
|
||
|
||
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;
|
||
}
|
||
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;
|
||
}
|
||
""")
|
||
|
||
main_layout.addWidget(scroll_area)
|
||
self.setLayout(main_layout)
|
||
|
||
# 启动加载动画
|
||
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 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())
|
||
|
||
# 检查类别条件
|
||
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)
|
||
|
||
def create_app_card(self, app):
|
||
"""创建应用卡片,完全使用Home页面样式"""
|
||
card = AnimatedCard(app['color'][0], app['color'][1])
|
||
card.setObjectName("appCard")
|
||
|
||
# 使用垂直布局作为卡片的主布局
|
||
layout = QVBoxLayout(card)
|
||
layout.setContentsMargins(25, 25, 25, 25)
|
||
layout.setSpacing(15)
|
||
|
||
# 图标部分
|
||
icon_frame = QFrame()
|
||
icon_frame.setFixedSize(90, 90)
|
||
icon_frame.setObjectName("appCardIcon")
|
||
|
||
icon_layout = QVBoxLayout(icon_frame)
|
||
icon_layout.setContentsMargins(0, 0, 0, 0)
|
||
|
||
icon = QLabel()
|
||
icon_pixmap = QPixmap(f":/icons/{app['icon']}.png").scaled(56, 56,
|
||
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)
|
||
|
||
layout.addWidget(icon_frame, 0, Qt.AlignHCenter)
|
||
|
||
# 标题
|
||
title = QLabel(app["title"])
|
||
title.setObjectName("appCardTitle")
|
||
title.setWordWrap(True) # 确保长标题可以换行
|
||
layout.addWidget(title)
|
||
|
||
# 描述
|
||
desc = QLabel(app["desc"])
|
||
desc.setObjectName("appCardDesc")
|
||
desc.setWordWrap(True)
|
||
layout.addWidget(desc)
|
||
|
||
# 添加伸缩空间保持底部对齐
|
||
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",
|
||
"author": "李明",
|
||
"upload_time": "2023-06-15",
|
||
"version": "1.2.0",
|
||
"category": "文档处理",
|
||
"features": [
|
||
"支持批量转换多个Word文件",
|
||
"保持原始文档格式",
|
||
"支持DOC和DOCX格式",
|
||
"转换速度快,效率高"
|
||
]
|
||
},
|
||
{
|
||
"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": "pdf_merge",
|
||
"title": "PDF合并工具",
|
||
"desc": "合并多个PDF文件为一个",
|
||
"color": ("#8B5CF6", "#7C3AED"),
|
||
"icon": "pdf_merge",
|
||
"author": "刘强",
|
||
"upload_time": "2023-09-05",
|
||
"version": "极速版1.1.0",
|
||
"category": "文档处理",
|
||
"features": [
|
||
"拖拽排序PDF文件",
|
||
"自定义合并顺序",
|
||
"保留原始文档质量",
|
||
"支持大文件处理"
|
||
]
|
||
},
|
||
{
|
||
"id": "pdf_split",
|
||
"title": "PDF拆分工具",
|
||
"desc": "拆分PDF文件为多个文档",
|
||
"color": ("#EC4899", "#DB2777"),
|
||
"icon": "pdf_split",
|
||
"author": "陈晓",
|
||
"upload_time": "2023-09-18",
|
||
"version": "1.0.5",
|
||
"category": "文档处理",
|
||
"features": [
|
||
"按页码拆分PDF",
|
||
"按书签拆分PDF",
|
||
"自定义拆分范围",
|
||
"批量处理多个文件"
|
||
]
|
||
},
|
||
{
|
||
"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": [
|
||
"自动分析主色调",
|
||
"提取多种配色方案",
|
||
"支持导出配色代码",
|
||
"直观的颜色展示"
|
||
]
|
||
}
|
||
]
|
||
|
||
|
||
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;
|
||
}
|
||
|
||
#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);
|
||
}
|
||
""" |