#!/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