Files
nl-utils/v1/views/app_center.py
2025-06-07 23:43:57 +08:00

1137 lines
39 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
}
"""