442 lines
16 KiB
Python
442 lines
16 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 MetaCore.data.project_manager import ProjectManager, Project
|
||
from MetaCore.ui.icon_manager import IconManager
|
||
from MetaCore.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.project_manager = project_manager
|
||
self.view_mode = "grid" # grid 或 list
|
||
self.projects = []
|
||
|
||
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, 0, 0, 0)
|
||
layout.setSpacing(0)
|
||
|
||
# 顶部工具栏
|
||
self.create_toolbar(layout)
|
||
|
||
# 搜索和过滤区域
|
||
self.create_search_filter_area(layout)
|
||
|
||
# 项目显示区域
|
||
self.create_project_display_area(layout)
|
||
|
||
def create_toolbar(self, layout):
|
||
"""创建顶部工具栏"""
|
||
toolbar_widget = QWidget()
|
||
toolbar_widget.setObjectName("toolbar")
|
||
toolbar_layout = QHBoxLayout(toolbar_widget)
|
||
toolbar_layout.setContentsMargins(30, 20, 30, 10)
|
||
|
||
# 面包屑导航容器
|
||
breadcrumb_container = QWidget()
|
||
breadcrumb_container.setObjectName("breadcrumbContainer")
|
||
breadcrumb_layout = QHBoxLayout(breadcrumb_container)
|
||
breadcrumb_layout.setContentsMargins(0, 0, 0, 0)
|
||
breadcrumb_layout.setSpacing(0)
|
||
|
||
self.home_label = QLabel("我的项目")
|
||
self.home_label.setObjectName("breadcrumbItem")
|
||
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)
|
||
|
||
breadcrumb_layout.addStretch()
|
||
|
||
toolbar_layout.addWidget(breadcrumb_container)
|
||
|
||
# 右侧操作按钮
|
||
actions_layout = QHBoxLayout()
|
||
|
||
# 导入项目按钮
|
||
self.import_project_btn = QPushButton("导入项目")
|
||
if IconManager.icon_exists('import'):
|
||
self.import_project_btn.setIcon(IconManager.get_icon('import'))
|
||
else:
|
||
self.import_project_btn = QPushButton("↑ 导入项目")
|
||
self.import_project_btn.setObjectName("importBtn")
|
||
self.import_project_btn.clicked.connect(self.import_project_requested.emit)
|
||
actions_layout.addWidget(self.import_project_btn)
|
||
|
||
# 创建项目按钮
|
||
self.create_project_btn = QPushButton("创建")
|
||
if IconManager.icon_exists('create'):
|
||
self.create_project_btn.setIcon(IconManager.get_icon('create'))
|
||
else:
|
||
self.create_project_btn = QPushButton("+ 创建")
|
||
self.create_project_btn.setObjectName("primaryBtn")
|
||
self.create_project_btn.clicked.connect(self.create_project_requested.emit)
|
||
actions_layout.addWidget(self.create_project_btn)
|
||
|
||
toolbar_layout.addLayout(actions_layout)
|
||
layout.addWidget(toolbar_widget)
|
||
|
||
def create_search_filter_area(self, layout):
|
||
"""创建搜索和过滤区域"""
|
||
# 内容头部 - 调整高度更紧凑
|
||
content_header = QWidget()
|
||
content_header.setObjectName("contentHeader")
|
||
header_layout = QHBoxLayout(content_header)
|
||
header_layout.setContentsMargins(30, 12, 30, 12) # 减少上下边距:20->12
|
||
|
||
# 标题
|
||
self.title_label = QLabel("项目概述")
|
||
self.title_label.setObjectName("contentTitle")
|
||
header_layout.addWidget(self.title_label)
|
||
|
||
header_layout.addStretch()
|
||
|
||
# 搜索输入框
|
||
self.search_input = QLineEdit()
|
||
self.search_input.setObjectName("searchInput")
|
||
self.search_input.setPlaceholderText("搜索项目...")
|
||
self.search_input.textChanged.connect(self.search_changed.emit)
|
||
self.search_input.setMaximumWidth(250) # 限制最大宽度
|
||
header_layout.addWidget(self.search_input)
|
||
|
||
# 视图控制按钮
|
||
view_controls = QHBoxLayout()
|
||
|
||
# 网格视图按钮
|
||
self.grid_view_btn = QPushButton()
|
||
if IconManager.icon_exists('grid_view'):
|
||
self.grid_view_btn.setIcon(IconManager.get_icon('grid_view', QSize(16, 16)))
|
||
self.grid_view_btn.setIconSize(QSize(16, 16))
|
||
else:
|
||
self.grid_view_btn.setText("⊞")
|
||
self.grid_view_btn.setObjectName("viewBtn")
|
||
self.grid_view_btn.setCheckable(True)
|
||
self.grid_view_btn.setChecked(True)
|
||
self.grid_view_btn.setToolTip("网格视图")
|
||
self.grid_view_btn.clicked.connect(lambda: self.set_view_mode("grid"))
|
||
view_controls.addWidget(self.grid_view_btn)
|
||
|
||
# 列表视图按钮
|
||
self.list_view_btn = QPushButton()
|
||
if IconManager.icon_exists('list_view'):
|
||
self.list_view_btn.setIcon(IconManager.get_icon('list_view', QSize(16, 16)))
|
||
self.list_view_btn.setIconSize(QSize(16, 16))
|
||
else:
|
||
self.list_view_btn.setText("☰")
|
||
self.list_view_btn.setObjectName("viewBtn")
|
||
self.list_view_btn.setCheckable(True)
|
||
self.list_view_btn.setToolTip("列表视图")
|
||
self.list_view_btn.clicked.connect(lambda: self.set_view_mode("list"))
|
||
view_controls.addWidget(self.list_view_btn)
|
||
|
||
# # 过滤按钮
|
||
# filter_btn = QPushButton("▼")
|
||
# filter_btn.setObjectName("viewBtn")
|
||
# filter_btn.setToolTip("过滤选项")
|
||
# view_controls.addWidget(filter_btn)
|
||
|
||
|
||
|
||
header_layout.addLayout(view_controls)
|
||
layout.addWidget(content_header)
|
||
|
||
|
||
|
||
def create_project_display_area(self, layout):
|
||
"""创建项目显示区域"""
|
||
# 滚动区域
|
||
self.scroll_area = QScrollArea()
|
||
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(30, 20, 30, 30)
|
||
self.projects_layout.setSpacing(20)
|
||
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)
|
||
self.current_label.setText(item_name)
|
||
|
||
# 更新主标题
|
||
# 根据不同的导航项设置不同的标题
|
||
title_mapping = {
|
||
"项目概述": "项目概述",
|
||
"项目管理": "项目管理",
|
||
"资源分类": "资源分类管理",
|
||
"资源管理": "资源管理",
|
||
"系统设置": "系统设置",
|
||
}
|
||
|
||
display_title = title_mapping.get(item_name, item_name)
|
||
self.title_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
|
||
|
||
# 更新按钮状态
|
||
self.grid_view_btn.setChecked(mode == "grid")
|
||
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 # 使用更小的边距
|
||
|
||
# 固定卡片宽度和间距
|
||
card_width = 280 # 固定卡片宽度
|
||
spacing = 20 # 卡片间距
|
||
|
||
# 计算能容纳的列数
|
||
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
|