373 lines
14 KiB
Python
373 lines
14 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
项目区域组件
|
||
"""
|
||
|
||
from PyQt5.QtWidgets import *
|
||
from PyQt5.QtCore import *
|
||
from PyQt5.QtGui import *
|
||
from typing import List
|
||
|
||
from data.project_manager import ProjectManager, Project
|
||
from ui.icon_manager import IconManager
|
||
from ui.project_card import ProjectCard
|
||
|
||
class ProjectArea(QWidget):
|
||
"""项目区域组件"""
|
||
|
||
# 信号定义
|
||
search_changed = pyqtSignal(str)
|
||
create_project_requested = pyqtSignal()
|
||
import_project_requested = pyqtSignal()
|
||
|
||
def __init__(self, project_manager: ProjectManager):
|
||
super().__init__()
|
||
self.setObjectName("projectArea")
|
||
self.setAttribute(Qt.WA_StyledBackground, True)
|
||
self.project_manager = project_manager
|
||
self.view_mode = "grid" # grid 或 list
|
||
self.projects = []
|
||
self.grid_view_btn = None
|
||
self.list_view_btn = None
|
||
|
||
self.init_ui()
|
||
self.connect_signals()
|
||
|
||
# 初始加载项目
|
||
self.update_projects(self.project_manager.get_all_projects())
|
||
|
||
def init_ui(self):
|
||
"""初始化UI"""
|
||
layout = QVBoxLayout(self)
|
||
layout.setContentsMargins(0, 30, 24, 30)
|
||
layout.setSpacing(0)
|
||
|
||
# 主内容容器,方便区分背景颜色
|
||
self.content_widget = QWidget()
|
||
self.content_widget.setObjectName("projectAreaContent")
|
||
self.content_layout = QVBoxLayout(self.content_widget)
|
||
self.content_layout.setContentsMargins(0, 0, 0, 0)
|
||
self.content_layout.setSpacing(0)
|
||
|
||
layout.addWidget(self.content_widget)
|
||
|
||
# 顶部工具栏
|
||
self.create_toolbar(self.content_layout)
|
||
|
||
# 项目显示区域
|
||
self.create_project_display_area(self.content_layout)
|
||
|
||
def create_toolbar(self, layout):
|
||
"""创建顶部工具区域"""
|
||
header_widget = QWidget()
|
||
header_widget.setObjectName("contentHeader")
|
||
header_layout = QHBoxLayout(header_widget)
|
||
header_layout.setContentsMargins(36, 26, 36, 24)
|
||
header_layout.setSpacing(0)
|
||
|
||
breadcrumb_container = QWidget()
|
||
breadcrumb_container.setObjectName("breadcrumbContainer")
|
||
breadcrumb_layout = QHBoxLayout(breadcrumb_container)
|
||
breadcrumb_layout.setContentsMargins(0, 0, 0, 0)
|
||
breadcrumb_layout.setSpacing(8)
|
||
|
||
self.home_label = QLabel("我的项目")
|
||
self.home_label.setObjectName("breadcrumbBase")
|
||
breadcrumb_layout.addWidget(self.home_label)
|
||
|
||
self.separator_label = QLabel("/")
|
||
self.separator_label.setObjectName("breadcrumbSeparator")
|
||
breadcrumb_layout.addWidget(self.separator_label)
|
||
|
||
self.current_label = QLabel("项目概述")
|
||
self.current_label.setObjectName("breadcrumbCurrent")
|
||
breadcrumb_layout.addWidget(self.current_label)
|
||
|
||
header_layout.addWidget(breadcrumb_container)
|
||
header_layout.addStretch()
|
||
|
||
self.search_input = QLineEdit()
|
||
self.search_input.setObjectName("searchInput")
|
||
self.search_input.setPlaceholderText("搜索项目...")
|
||
self.search_input.setClearButtonEnabled(True)
|
||
if IconManager.icon_exists('search'):
|
||
search_action = self.search_input.addAction(IconManager.get_icon('search', QSize(30, 30)), QLineEdit.TrailingPosition)
|
||
self.search_input.setFixedWidth(508)
|
||
self.search_input.textChanged.connect(self.search_changed.emit)
|
||
header_layout.addWidget(self.search_input, 0, Qt.AlignRight)
|
||
|
||
layout.addWidget(header_widget)
|
||
|
||
def create_project_display_area(self, layout):
|
||
"""创建项目显示区域"""
|
||
# 滚动区域
|
||
self.scroll_area = QScrollArea()
|
||
self.scroll_area.setObjectName("projectScrollArea")
|
||
self.scroll_area.setFrameShape(QFrame.NoFrame)
|
||
self.scroll_area.setWidgetResizable(True)
|
||
self.scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
||
self.scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
|
||
|
||
# 项目容器
|
||
self.projects_container = QWidget()
|
||
self.projects_container.setObjectName("projectsContainer")
|
||
|
||
# 网格布局
|
||
self.projects_layout = QGridLayout(self.projects_container)
|
||
self.projects_layout.setContentsMargins(36, 24, 6, 18)
|
||
self.projects_layout.setSpacing(24)
|
||
self.projects_layout.setAlignment(Qt.AlignTop) # 设置顶部对齐
|
||
|
||
# 响应式列数
|
||
self.grid_columns = 4 # 默认列数
|
||
self.update_grid_columns()
|
||
|
||
self.scroll_area.setWidget(self.projects_container)
|
||
layout.addWidget(self.scroll_area)
|
||
|
||
def connect_signals(self):
|
||
"""连接信号"""
|
||
# 项目管理器信号
|
||
self.project_manager.project_added.connect(self.on_project_added)
|
||
self.project_manager.project_removed.connect(self.on_project_removed)
|
||
self.project_manager.project_updated.connect(self.on_project_updated)
|
||
|
||
def update_projects(self, projects: List[Project]):
|
||
"""更新项目显示"""
|
||
self.projects = projects
|
||
self.refresh_project_display()
|
||
|
||
def update_navigation(self, section: str, item_name: str):
|
||
"""更新导航显示"""
|
||
# 更新面包屑导航
|
||
self.home_label.setText(section)
|
||
|
||
# 根据不同的导航项设置显示标题
|
||
title_mapping = {
|
||
"项目概述": "项目概述",
|
||
"项目管理": "项目管理",
|
||
"资源分类": "资源分类管理",
|
||
"资源管理": "资源管理",
|
||
"系统设置": "系统设置",
|
||
}
|
||
|
||
display_title = title_mapping.get(item_name, item_name)
|
||
self.current_label.setText(display_title)
|
||
|
||
def refresh_project_display(self):
|
||
"""刷新项目显示"""
|
||
# 清除现有项目卡片
|
||
for i in reversed(range(self.projects_layout.count())):
|
||
child = self.projects_layout.itemAt(i).widget()
|
||
if child:
|
||
child.setParent(None)
|
||
|
||
if not self.projects:
|
||
# 显示空状态
|
||
self.show_empty_state()
|
||
else:
|
||
# 添加项目卡片
|
||
if self.view_mode == "grid":
|
||
self.show_grid_view()
|
||
else:
|
||
self.show_list_view()
|
||
|
||
# 强制更新布局和几何信息
|
||
self._update_layout_geometry()
|
||
|
||
def show_grid_view(self):
|
||
"""显示网格视图"""
|
||
# 使用响应式列数
|
||
columns = self.grid_columns
|
||
|
||
for i, project in enumerate(self.projects):
|
||
row = i // columns
|
||
col = i % columns
|
||
|
||
project_card = ProjectCard(project, self.project_manager)
|
||
self.projects_layout.addWidget(project_card, row, col, Qt.AlignTop) # 顶部对齐
|
||
|
||
def show_list_view(self):
|
||
"""显示列表视图"""
|
||
for i, project in enumerate(self.projects):
|
||
project_card = ProjectCard(project, self.project_manager, view_mode="list")
|
||
self.projects_layout.addWidget(project_card, i, 0, Qt.AlignTop) # 顶部对齐
|
||
|
||
def show_empty_state(self):
|
||
"""显示空状态"""
|
||
empty_widget = QWidget()
|
||
empty_layout = QVBoxLayout(empty_widget)
|
||
empty_layout.setAlignment(Qt.AlignCenter)
|
||
|
||
# 空状态图标
|
||
empty_icon = QLabel("📁")
|
||
empty_icon.setStyleSheet("font-size: 48px; color: #666666;")
|
||
empty_icon.setAlignment(Qt.AlignCenter)
|
||
empty_layout.addWidget(empty_icon)
|
||
|
||
# 空状态文字
|
||
empty_text = QLabel("没有找到匹配的项目")
|
||
empty_text.setStyleSheet("font-size: 16px; color: #888888; margin-top: 10px;")
|
||
empty_text.setAlignment(Qt.AlignCenter)
|
||
empty_layout.addWidget(empty_text)
|
||
|
||
self.projects_layout.addWidget(empty_widget, 0, 0, 1, 4)
|
||
|
||
def set_view_mode(self, mode: str):
|
||
"""设置视图模式"""
|
||
self.view_mode = mode
|
||
|
||
# 更新按钮状态
|
||
if self.grid_view_btn:
|
||
self.grid_view_btn.setChecked(mode == "grid")
|
||
if self.list_view_btn:
|
||
self.list_view_btn.setChecked(mode == "list")
|
||
|
||
# 刷新显示
|
||
self.refresh_project_display()
|
||
|
||
def focus_search(self):
|
||
"""聚焦搜索框"""
|
||
self.search_input.setFocus()
|
||
|
||
def on_project_added(self, project: Project):
|
||
"""项目添加事件"""
|
||
# 如果当前显示的是项目概述或项目管理,则添加新项目
|
||
if hasattr(self, 'current_filter') and self.current_filter in ["overview", "management"]:
|
||
self.projects.insert(0, project)
|
||
self.refresh_project_display()
|
||
|
||
def on_project_removed(self, project_id: int):
|
||
"""项目删除事件"""
|
||
self.projects = [p for p in self.projects if p.id != project_id]
|
||
self.refresh_project_display()
|
||
|
||
def on_project_updated(self, project: Project):
|
||
"""项目更新事件"""
|
||
for i, p in enumerate(self.projects):
|
||
if p.id == project.id:
|
||
self.projects[i] = project
|
||
break
|
||
self.refresh_project_display()
|
||
|
||
def update_grid_columns(self):
|
||
"""更新网格列数 - 针对固定大小卡片优化"""
|
||
# 获取可用宽度,尝试多种方式确保准确性
|
||
width = 0
|
||
if hasattr(self, 'scroll_area') and self.scroll_area.width() > 0:
|
||
# 优先使用滚动区域的宽度
|
||
width = self.scroll_area.viewport().width()
|
||
elif hasattr(self, 'projects_container') and self.projects_container.width() > 0:
|
||
# 备选:使用项目容器的宽度
|
||
width = self.projects_container.width()
|
||
elif hasattr(self, 'scroll_area'):
|
||
# 最后备选:使用滚动区域的宽度
|
||
width = self.scroll_area.width()
|
||
|
||
# 确保宽度有效
|
||
if width <= 0:
|
||
# 如果仍然无效,使用默认列数
|
||
if not hasattr(self, 'grid_columns'):
|
||
self.grid_columns = 1
|
||
return False
|
||
|
||
# 固定卡片大小的计算(280px宽度,20px间距)
|
||
available_width = width - 60 # 减去左右边距
|
||
|
||
# 确保可用宽度为正数
|
||
if available_width <= 0:
|
||
available_width = width - 20 # 使用更小的边距
|
||
|
||
# 固定卡片宽度和间距 - 匹配 Figma 设计
|
||
card_width = 280 # 固定卡片宽度,匹配当前卡片尺寸
|
||
spacing = 24 # 卡片间距,匹配更新后的间距
|
||
|
||
# 计算能容纳的列数
|
||
if available_width < card_width:
|
||
columns = 1
|
||
else:
|
||
# 计算能容纳的列数:(可用宽度 + 间距) / (卡片宽度 + 间距)
|
||
columns = (available_width + spacing) // (card_width + spacing)
|
||
columns = max(1, columns)
|
||
|
||
# 限制最大列数,避免过多列
|
||
columns = min(columns, 6)
|
||
|
||
# 检查列数是否发生变化
|
||
old_columns = getattr(self, 'grid_columns', 4)
|
||
if old_columns != columns:
|
||
self.grid_columns = columns
|
||
return True # 返回True表示列数发生了变化
|
||
else:
|
||
self.grid_columns = columns
|
||
return False
|
||
|
||
def resizeEvent(self, event):
|
||
"""窗口大小变化事件"""
|
||
super().resizeEvent(event)
|
||
|
||
# 立即处理缩小情况,延迟处理放大情况
|
||
if hasattr(self, '_last_width'):
|
||
current_width = event.size().width()
|
||
is_shrinking = current_width < self._last_width
|
||
|
||
if is_shrinking:
|
||
# 缩小时立即处理,确保响应性
|
||
self._handle_resize()
|
||
self._last_width = current_width
|
||
return
|
||
|
||
# 延迟处理,避免频繁调用(主要用于放大情况)
|
||
if hasattr(self, '_resize_timer'):
|
||
self._resize_timer.stop()
|
||
|
||
self._resize_timer = QTimer()
|
||
self._resize_timer.setSingleShot(True)
|
||
self._resize_timer.timeout.connect(self._handle_resize)
|
||
self._resize_timer.start(50) # 减少延迟:100ms->50ms
|
||
|
||
# 记录当前宽度
|
||
self._last_width = event.size().width()
|
||
|
||
def _handle_resize(self):
|
||
"""处理窗口大小变化"""
|
||
if hasattr(self, 'projects') and self.view_mode == "grid":
|
||
# 强制更新滚动区域几何信息
|
||
if hasattr(self, 'scroll_area'):
|
||
self.scroll_area.updateGeometry()
|
||
QApplication.processEvents()
|
||
|
||
# 更新列数
|
||
columns_changed = self.update_grid_columns()
|
||
|
||
if columns_changed:
|
||
# 列数变化,完全重新布局
|
||
self.refresh_project_display()
|
||
else:
|
||
# 列数没变,但仍需要更新布局以适应新的宽度
|
||
self._update_layout_geometry()
|
||
# 强制重新计算卡片大小
|
||
self._update_card_sizes()
|
||
|
||
def _update_layout_geometry(self):
|
||
"""更新布局几何信息"""
|
||
if hasattr(self, 'projects_container'):
|
||
# 强制更新布局
|
||
self.projects_container.updateGeometry()
|
||
self.projects_layout.update()
|
||
|
||
# 确保滚动区域正确更新
|
||
if hasattr(self, 'scroll_area'):
|
||
self.scroll_area.updateGeometry()
|
||
|
||
# 强制重新计算滚动区域大小
|
||
QApplication.processEvents()
|
||
self.scroll_area.ensureVisible(0, 0)
|
||
|
||
def _update_card_sizes(self):
|
||
"""更新项目卡片大小 - 固定大小卡片无需动态调整"""
|
||
# 由于卡片现在是固定大小(280x240),不再需要动态调整
|
||
# 这个方法保留为空,以保持接口兼容性
|
||
pass
|