From e89d3254e8d1e4110855578974bfcb03ae1beccb Mon Sep 17 00:00:00 2001 From: zqm Date: Fri, 26 Dec 2025 14:59:20 +0800 Subject: [PATCH] =?UTF-8?q?=E6=89=BE=E4=B8=8D=E5=88=B0=E5=8C=BA=E5=9F=9F?= =?UTF-8?q?=E6=8B=96=E6=8B=BD=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Robot/Web/src/DockLayout/DockLayout.vue | 61 - .../Robot/Web/src/DockLayout/README.md | 1081 +++++++++++++++-- .../DockLayout/handlers/DragStateManager.js | 61 +- 3 files changed, 1041 insertions(+), 162 deletions(-) diff --git a/AutoRobot/Windows/Robot/Web/src/DockLayout/DockLayout.vue b/AutoRobot/Windows/Robot/Web/src/DockLayout/DockLayout.vue index bc7aef6..2615707 100644 --- a/AutoRobot/Windows/Robot/Web/src/DockLayout/DockLayout.vue +++ b/AutoRobot/Windows/Robot/Web/src/DockLayout/DockLayout.vue @@ -102,55 +102,6 @@ const mainAreaResizeBars = ref([]) // 检查主区域内是否有其他Area(简化版) const hasAreasInMainContent = ref(false) -// 代理到事件处理器的方法 -const onPanelDragStart = (areaId, event) => { - dragStateActions.onPanelDragStart(areaId, event); -}; - -const onPanelDragMove = (areaId, event) => { - dragStateActions.onPanelDragMove(areaId, event); -}; - -const onPanelDragEnd = () => { - dragStateActions.onPanelDragEnd(); -}; - -const onPanelDragStartFromTabPage = (event) => { - dragStateActions.onPanelDragStartFromTabPage(event); -}; - -const onPanelDragMoveFromTabPage = (event) => { - dragStateActions.onPanelDragMoveFromTabPage(event); -}; - -const onPanelDragEndFromTabPage = () => { - dragStateActions.onPanelDragEndFromTabPage(); -}; - -const onAreaDragStart = (event) => { - dragStateActions.onAreaDragStart(event); -}; - -const onAreaDragMove = (event) => { - dragStateActions.onAreaDragMove(event); -}; - -const onAreaDragEnd = (event) => { - dragStateActions.onAreaDragEnd(event); -}; - -const onTabDragStart = (event) => { - dragStateActions.onTabDragStart(event); -}; - -const onTabDragMove = (event) => { - dragStateActions.onTabDragMove(event); -}; - -const onTabDragEnd = () => { - dragStateActions.onTabDragEnd(); -}; - // Area相关事件处理 const onCloseFloatingArea = (event) => { const id = event.areaId; @@ -289,9 +240,6 @@ const setupEventListeners = () => { const unsubscribeFunctions = []; // Area相关事件 - unsubscribeFunctions.push(eventBus.on(EVENT_TYPES.AREA_DRAG_START, onAreaDragStart, { componentId: 'dock-layout' })); - unsubscribeFunctions.push(eventBus.on(EVENT_TYPES.AREA_DRAG_MOVE, onAreaDragMove, { componentId: 'dock-layout' })); - unsubscribeFunctions.push(eventBus.on(EVENT_TYPES.AREA_DRAG_END, onAreaDragEnd, { componentId: 'dock-layout' })); unsubscribeFunctions.push(eventBus.on(EVENT_TYPES.AREA_POSITION_UPDATE, onUpdatePosition, { componentId: 'dock-layout' })); unsubscribeFunctions.push(eventBus.on(EVENT_TYPES.AREA_DRAG_OVER, handleAreaDragOver, { componentId: 'dock-layout' })); unsubscribeFunctions.push(eventBus.on(EVENT_TYPES.AREA_DRAG_LEAVE, handleAreaDragLeave, { componentId: 'dock-layout' })); @@ -300,9 +248,6 @@ const setupEventListeners = () => { unsubscribeFunctions.push(eventBus.on(EVENT_TYPES.TAB_CHANGE, onTabChange, { componentId: 'dock-layout' })); unsubscribeFunctions.push(eventBus.on(EVENT_TYPES.TAB_CLOSE, onTabClose, { componentId: 'dock-layout' })); unsubscribeFunctions.push(eventBus.on(EVENT_TYPES.TAB_ADD, onTabAdd, { componentId: 'dock-layout' })); - unsubscribeFunctions.push(eventBus.on(EVENT_TYPES.TAB_DRAG_START, onTabDragStart, { componentId: 'dock-layout' })); - unsubscribeFunctions.push(eventBus.on(EVENT_TYPES.TAB_DRAG_MOVE, onTabDragMove, { componentId: 'dock-layout' })); - unsubscribeFunctions.push(eventBus.on(EVENT_TYPES.TAB_DRAG_END, onTabDragEnd, { componentId: 'dock-layout' })); // Panel相关事件 unsubscribeFunctions.push(eventBus.on(EVENT_TYPES.PANEL_TOGGLE_COLLAPSE, () => emit('toggleCollapse'), { componentId: 'dock-layout' })); @@ -310,12 +255,6 @@ const setupEventListeners = () => { unsubscribeFunctions.push(eventBus.on(EVENT_TYPES.PANEL_CLOSE_REQUEST, onCloseFloatingArea, { componentId: 'dock-layout' })); unsubscribeFunctions.push(eventBus.on(EVENT_TYPES.PANEL_CLOSE, onClosePanel, { componentId: 'dock-layout' })); unsubscribeFunctions.push(eventBus.on(EVENT_TYPES.PANEL_TOGGLE_TOOLBAR, () => emit('toggleToolbar'), { componentId: 'dock-layout' })); - unsubscribeFunctions.push(eventBus.on(EVENT_TYPES.PANEL_DRAG_START, onPanelDragStart, { componentId: 'dock-layout' })); - unsubscribeFunctions.push(eventBus.on(EVENT_TYPES.PANEL_DRAG_MOVE, onPanelDragMove, { componentId: 'dock-layout' })); - unsubscribeFunctions.push(eventBus.on(EVENT_TYPES.PANEL_DRAG_END, onPanelDragEnd, { componentId: 'dock-layout' })); - unsubscribeFunctions.push(eventBus.on(EVENT_TYPES.PANEL_DRAG_START_FROM_TABPAGE, onPanelDragStartFromTabPage, { componentId: 'dock-layout' })); - unsubscribeFunctions.push(eventBus.on(EVENT_TYPES.PANEL_DRAG_MOVE_FROM_TABPAGE, onPanelDragMoveFromTabPage, { componentId: 'dock-layout' })); - unsubscribeFunctions.push(eventBus.on(EVENT_TYPES.PANEL_DRAG_END_FROM_TABPAGE, onPanelDragEndFromTabPage, { componentId: 'dock-layout' })); unsubscribeFunctions.push(eventBus.on(EVENT_TYPES.PANEL_MAXIMIZE_SYNC, onPanelMaximizeSync, { componentId: 'dock-layout' })); // Resize相关事件 diff --git a/AutoRobot/Windows/Robot/Web/src/DockLayout/README.md b/AutoRobot/Windows/Robot/Web/src/DockLayout/README.md index 0252ccd..372ca2a 100644 --- a/AutoRobot/Windows/Robot/Web/src/DockLayout/README.md +++ b/AutoRobot/Windows/Robot/Web/src/DockLayout/README.md @@ -1,120 +1,1029 @@ -# DockLayout 组件说明 +# DockLayout 可停靠布局系统 -本文档描述在 `src/DockLayout/` 目录下将实现的停靠布局组件的目标、核心概念与数据结构约定,用于指导后续组件开发与联调。 +## 项目概述 -## 目标概述 -- 提供 IDE/工作台类的停靠布局能力:支持上/下/左/右/中心五个面板区。 -- 每个面板区内部以 TabGroup 组织多个子面板(标签页),支持切换、关闭、最小化、浮动等扩展。 -- 建立面板区之间的“大小影响关系”,通过受影响/被影响/待更新三类列表配合队列批处理,保证尺寸与边界在增删、折叠、拖拽后稳定更新。 +DockLayout 是一个基于 Vue 3 的可停靠布局系统,提供了灵活的面板管理、拖拽停靠、标签页切换、窗口调整大小等功能。该系统采用事件驱动架构,支持浮动面板、停靠面板、并排布局等多种布局模式,适用于需要复杂界面布局的应用场景。 -## 核心概念 -- 面板区(Area):指 Top/Bottom/Left/Right/Center 五个区域之一,是 DockLayout 的一级容器。 -- 子面板区(SubArea):面板区内部承载 TabGroup 的容器,负责布局与滚动等;一个面板区至少包含一个子面板区。 -- 子面板(Panel):以标签页形式存在的具体内容面板,如“解决方案”、“属性”、“终端”、“编辑器”等。 -- TabGroup:负责管理同一面板区内的多个子面板(标签),包含新增、关闭、激活等行为;所有面板区均以 TabGroup 作为统一的内容组织方式。 +### 核心特性 -## 面板区与列表约定 -每个面板区都维护三类列表,用于描述与调度尺寸更新: -- 受影响列表(influence):本面板区会影响到的其他面板区集合。 -- 被影响列表(influencedBy):会影响到本面板区的其他面板区集合。 -- 待更新列表(pendingUpdates):当本区状态变化(新增/删除/折叠/展开/拖拽)后,入队待批处理的面板区集合。 +- **灵活的面板管理**:支持浮动面板、停靠面板、标签页面板 +- **拖拽停靠**:支持面板拖拽到不同位置进行停靠 +- **动态调整大小**:支持通过拖拽边框或调整条改变面板大小 +- **标签页系统**:支持多标签页切换、拖拽重排 +- **并排布局**:支持面板并排显示,可动态调整比例 +- **事件驱动架构**:采用事件总线实现组件间通信 +- **内存保护机制**:自动清理过期资源,防止内存泄漏 +- **性能监控**:内置性能监控和集成测试框架 -这三类列表的存在旨在: -- 明确依赖方向,避免双向更新导致的循环与遗漏。 -- 通过队列式批处理,多次局部变更合并为一次稳定的全局尺寸更新过程。 +## 系统架构 -## 数据结构草案 -> 以下为建议的数据结构,具体实现时可与现有 store(如 `dockPanelStore.js`)对齐。 +### 整体架构 -```ts -// 面板区枚举 -export type PanelPosition = 'left' | 'right' | 'top' | 'bottom' | 'center'; +``` +DockLayout +├── 核心组件层 +│ ├── DockLayout.vue # 根组件,布局管理器 +│ ├── Area.vue # 面板区容器 +│ ├── TabPage.vue # 标签页容器 +│ ├── Panel.vue # 面板组件 +│ ├── Render.vue # 动态组件渲染器 +│ ├── DockIndicator.vue # 停靠指示器 +│ └── ResizeBar.vue # 调整条组件 +├── 事件处理层 +│ ├── DragStateManager.js # 拖拽状态管理 +│ ├── EventBusManager.js # 事件总线管理 +│ ├── AreaHandler.js # Area事件处理 +│ ├── PanelHandler.js # Panel事件处理 +│ ├── TabPageHandler.js # TabPage事件处理 +│ ├── GlobalEventManager.js # 全局事件管理 +│ └── IntegrationTester.js # 集成测试 +├── 工具层 +│ ├── eventBus.js # 增强事件总线 +│ ├── dockLayers.js # Z-index层级管理 +│ └── types.d.ts # TypeScript类型定义 +└── 文档层 + ├── DockIndicator指示器命名约定.md + └── ToDoList.md +``` -// 影响关系项 -export interface InfluenceEntry { - position: PanelPosition; // 目标面板区位置 - influence: boolean; // 是否产生影响(用于过滤/开关) +### 架构设计原则 + +1. **分层架构**:组件层、事件层、工具层清晰分离 +2. **事件驱动**:所有组件间通信通过事件总线实现 +3. **单例模式**:关键管理器采用单例模式确保全局唯一 +4. **内存保护**:自动清理过期资源,防止内存泄漏 +5. **类型安全**:使用TypeScript提供类型检查 +6. **可扩展性**:模块化设计,易于扩展新功能 + +## 核心功能模块 + +### 1. 布局管理模块 + +**组件**:[DockLayout.vue](file:///d:/Projects/trunk/JoyD/AutoRobot/Windows/Robot/Web/src/DockLayout/DockLayout.vue) + +**功能**: +- 管理所有浮动面板和隐藏面板 +- 提供面板添加、移除、查找接口 +- 协调组件间的事件通信 +- 资源清理和生命周期管理 + +**核心接口**: +```javascript +{ + // 数据 + floatingAreas: ref([]), // 浮动面板列表 + hiddenAreas: ref([]), // 隐藏面板列表 + + // 方法 + addFloatingPanel(panel), // 添加浮动面板 + findOrCreateMainAreaTabPage(), // 查找或创建主区域标签页 + handleDockingEnding(), // 停靠结束处理 + handleEdgeDocking(), // 边缘停靠处理 + handleSideBySideDocking(), // 并排停靠处理 } +``` -// 子面板(标签页) -export interface Panel { - id: string; - title: string; - icon?: string; - content?: unknown; // 组件或渲染函数引用 - collapsed?: boolean; +### 2. 面板区模块 + +**组件**:[Area.vue](file:///d:/Projects/trunk/JoyD/AutoRobot/Windows/Robot/Web/src/DockLayout/Area.vue) + +**功能**: +- 面板区容器,支持浮动和停靠两种模式 +- 拖拽移动(带边界约束) +- 调整大小(8个方向) +- 最大化/还原/关闭 +- 单面板时显示标题栏 + +**核心特性**: +- 初始尺寸:300px × 250px,居中显示 +- 最大化时填充父容器 +- 拖拽时不超出父容器边界 +- 支持边框拖拽改变大小 +- 单面板时标题栏与Panel联动 + +**事件系统**: +```javascript +EVENT_TYPES = { + AREA_DRAG_START, // 面板拖拽开始 + AREA_DRAG_MOVE, // 面板拖拽移动 + AREA_DRAG_END, // 面板拖拽结束 + AREA_RESIZE_START, // 面板调整开始 + AREA_RESIZE_MOVE, // 面板调整移动 + AREA_RESIZE_END, // 面板调整结束 + AREA_MAXIMIZE, // 面板最大化 + AREA_RESTORE, // 面板还原 + AREA_CLOSE, // 面板关闭 + AREA_POSITION_UPDATE, // 面板位置更新 } +``` -// 子面板区(TabGroup 容器) -export interface SubArea { - id: string; - tabGroupId: string; // 绑定的 TabGroup 标识 - panels: Panel[]; // 标签页集合 +### 3. 标签页模块 + +**组件**:[TabPage.vue](file:///d:/Projects/trunk/JoyD/AutoRobot/Windows/Robot/Web/src/DockLayout/TabPage.vue) + +**功能**: +- 多标签页容器 +- 标签页切换 +- 标签页拖拽重排 +- 标签页关闭 +- 支持4个方向(上/右/下/左) + +**样式特性**: +- 背景颜色:#5D6B99 +- 激活标签:#F5CC84,文字黑色 +- 未激活标签:与Area背景相同,文字白色 +- 激活标签不显示关闭按钮 +- 激活标签鼠标显示移动,否则显示手型 + +**方向支持**: +- `top`:标签在上方(默认) +- `right`:标签在右侧 +- `bottom`:标签在下方 +- `left`:标签在左侧 + +### 4. 面板模块 + +**组件**:[Panel.vue](file:///d:/Projects/trunk/JoyD/AutoRobot/Windows/Robot/Web/src/DockLayout/Panel.vue) + +**功能**: +- 单个面板内容容器 +- 填充父容器 +- 最大化/还原 +- 拖拽(单面板时拖动标题栏相当于拖动Area) + +**核心特性**: +- 始终填充父容器 +- 最大化时图标变为还原图标 +- 单面板时与Area标题栏联动 + +### 5. 停靠指示器模块 + +**组件**:[DockIndicator.vue](file:///d:/Projects/trunk/JoyD/AutoRobot/Windows/Robot/Web/src/DockLayout/DockIndicator.vue) + +**功能**: +- 提供可视化的停靠位置指示 +- 支持全局边缘停靠 +- 支持中心区域停靠 +- 支持子区域停靠 +- 动态显示停靠预览 + +**指示器分类**: + +1. **外部边缘指示器**(全局停靠) + - `global-top-indicator`:全局顶部 + - `global-right-indicator`:全局右侧 + - `global-bottom-indicator`:全局底部 + - `global-left-indicator`:全局左侧 + +2. **中心区域指示器** + - `center-dock-zone`:中心停靠区 + - `center-edge-top`:中心上边缘 + - `center-edge-right`:中心右边缘 + - `center-edge-bottom`:中心下边缘 + - `center-edge-left`:中心左边缘 + +3. **子区域指示器** + - `center-top-area`:中心上方区域 + - `center-right-area`:中心右侧区域 + - `center-bottom-area`:中心下方区域 + - `center-left-area`:中心左侧区域 + +4. **独立中心指示器** + - `center-main-indicator`:独立中心指示器(z-index: 10000) + +**命名约定**:详见 [DockIndicator指示器命名约定.md](file:///d:/Projects/trunk/JoyD/AutoRobot/Windows/Robot/Web/src/DockLayout/DockIndicator指示器命名约定.md) + +### 6. 调整条模块 + +**组件**:[ResizeBar.vue](file:///d:/Projects/trunk/JoyD/AutoRobot/Windows/Robot/Web/src/DockLayout/ResizeBar.vue) + +**功能**: +- 在并排布局中提供尺寸调整功能 +- 支持水平和垂直两种方向 +- 实时调整面板大小 +- 支持最小/最大尺寸限制 + +**事件系统**: +```javascript +EVENT_TYPES = { + RESIZE_START, // 调整开始 + RESIZE_MOVE, // 调整移动 + RESIZE_END, // 调整结束 } +``` -// 面板区(一级容器) -export interface PanelArea { - position: PanelPosition; - subAreas: SubArea[]; // 一个或多个子面板区 - activeTabIndex?: number; // 当前激活标签索引 - influence: InfluenceEntry[]; // 受影响列表 - influencedBy: InfluenceEntry[]; // 被影响列表 - pendingUpdates: Set; // 待更新列表(去重队列) +**使用场景**: +- 外部边缘停靠后的并排布局 +- 两个Area之间的尺寸调整 +- 保持比例总和为1 - // 尺寸与边界(用于绝对定位计算) - x: number; - y: number; - width: number; - height: number; +### 7. 拖拽状态管理模块 - // 面板内比例(按区维度) - widthRatios?: number[]; - heightRatios?: number[]; +**处理器**:[DragStateManager.js](file:///d:/Projects/trunk/JoyD/AutoRobot/Windows/Robot/Web/src/DockLayout/handlers/DragStateManager.js) + +**功能**: +- 管理拖拽状态 +- 跟踪拖拽位置和速度 +- 冲突检测和解决 +- 性能监控 + +**核心功能**: +```javascript +class DragStateManager { + // 状态管理 + startDrag(dragId, source, position) + updateDrag(dragId, position) + endDrag(dragId) + + // 冲突检测 + checkConflict(dragId) + resolveConflict(dragId, conflictId) + + // 性能监控 + startPerformanceMonitor(operation) + endPerformanceMonitor(monitorId) } +``` -// DockLayout 根状态 +### 8. 事件总线管理模块 + +**处理器**:[EventBusManager.js](file:///d:/Projects/trunk/JoyD/AutoRobot/Windows/Robot/Web/src/DockLayout/handlers/EventBusManager.js) + +**功能**: +- 统一事件路由和分发 +- 全局错误处理 +- 事件去重 +- 性能监控 + +**核心功能**: +```javascript +class EventBusManager { + // 事件管理 + registerEventRoute(eventType, handler) + unregisterEventRoute(eventType) + routeEvent(eventType, data) + + // 监控 + startMonitoring(eventId, eventData) + completeMonitoring(eventId) + getExecutionStats() +} +``` + +### 9. 全局事件管理模块 + +**处理器**:[GlobalEventManager.js](file:///d:/Projects/trunk/JoyD/AutoRobot/Windows/Robot/Web/src/DockLayout/handlers/GlobalEventManager.js) + +**功能**: +- 系统级事件管理 +- 跨组件通信 +- 事件链执行 +- 性能监控和告警 + +**核心功能**: +```javascript +class GlobalEventManager { + // 系统事件 + handleSystemInit(data) + handleSystemError(data) + handlePerformanceThresholdExceeded(data) + + // 跨组件通信 + broadcast(message, data, targetComponents) + request(targetComponent, action, payload) + + // 事件链 + createEventChain(chainName, events) + getEventChainStatus() +} +``` + +### 10. Area事件处理模块 + +**处理器**:[AreaHandler.js](file:///d:/Projects/trunk/JoyD/AutoRobot/Windows/Robot/Web/src/DockLayout/handlers/AreaHandler.js) + +**功能**: +- 处理Area相关事件 +- Area状态管理 +- 停靠逻辑实现 +- 并排布局创建 + +**核心功能**: +```javascript +class AreaHandler { + // Area管理 + handleAreaCreated(data) + handleAreaDestroyed(data) + handleAreaDragStart(data) + handleAreaDragEnd(data) + + // 停靠逻辑 + handleEdgeDocking(data) + handleSideBySideDocking(data) + createSideBySideLayout(sourceArea, targetArea, direction) +} +``` + +### 11. Panel事件处理模块 + +**处理器**:[PanelHandler.js](file:///d:/Projects/trunk/JoyD/AutoRobot/Windows/Robot/Web/src/DockLayout/handlers/PanelHandler.js) + +**功能**: +- 处理Panel相关事件 +- Panel状态管理 +- Panel生命周期管理 + +**核心功能**: +```javascript +class PanelHandler { + // Panel管理 + handlePanelCreated(data) + handlePanelDestroyed(data) + handlePanelDragStart(data) + handlePanelDragEnd(data) + + // 状态管理 + getPanelState(panelId) + updatePanelState(panelId, updates) +} +``` + +### 12. TabPage事件处理模块 + +**处理器**:[TabPageHandler.js](file:///d:/Projects/trunk/JoyD/AutoRobot/Windows/Robot/Web/src/DockLayout/handlers/TabPageHandler.js) + +**功能**: +- 处理TabPage相关事件 +- TabPage状态管理 +- 标签页拖拽和切换 +- 标签页组合并 + +**核心功能**: +```javascript +class TabPageHandler { + // TabPage管理 + handleTabPageCreated(data) + handleTabPageDestroyed(data) + handleTabPageDragStart(data) + handleTabPageDragEnd(data) + + // 标签页操作 + handleTabPageSwitch(data) + handleTabPageCloseRequest(data) + handleTabPageGroupMerge(data) +} +``` + +### 13. 集成测试模块 + +**处理器**:[IntegrationTester.js](file:///d:/Projects/trunk/JoyD/AutoRobot/Windows/Robot/Web/src/DockLayout/handlers/IntegrationTester.js) + +**功能**: +- 集成测试框架 +- 性能监控 +- 测试报告生成 + +**核心功能**: +```javascript +class IntegrationTester { + // 测试执行 + runTestSuite(testSuite) + runSingleTest(testCase) + + // 性能监控 + startPerformanceMonitor() + endPerformanceMonitor() + getPerformanceStats() +} +``` + +## 关键技术栈及依赖 + +### 核心技术栈 + +- **Vue 3**:渐进式JavaScript框架 + - Composition API + - 响应式系统 + - 组件化开发 + +- **TypeScript**:JavaScript超集 + - 类型安全 + - 接口定义 + - 类型推断 + +- **Vite**:下一代前端构建工具 + - 快速热更新 + - 优化的生产构建 + - 原生ES模块支持 + +### 依赖库 + +根据项目package.json分析: + +```json +{ + "dependencies": { + "vue": "^3.x.x", + "pinia": "^2.x.x" // 状态管理 + }, + "devDependencies": { + "vite": "^5.x.x", + "typescript": "^5.x.x" + } +} +``` + +### 内部依赖 + +DockLayout模块内部依赖: +- [eventBus.js](file:///d:/Projects/trunk/JoyD/AutoRobot/Windows/Robot/Web/src/DockLayout/eventBus.js):增强事件总线 +- [dockLayers.js](file:///d:/Projects/trunk/JoyD/AutoRobot/Windows/Robot/Web/src/DockLayout/dockLayers.js):Z-index层级管理 +- [types.d.ts](file:///d:/Projects/trunk/JoyD/AutoRobot/Windows/Robot/Web/src/DockLayout/types.d.ts):类型定义 + +## 项目目录结构 + +``` +DockLayout/ +├── handlers/ # 事件处理器目录 +│ ├── DragStateManager.js # 拖拽状态管理器 +│ ├── EventBusManager.js # 事件总线管理器 +│ ├── AreaHandler.js # Area事件处理器 +│ ├── PanelHandler.js # Panel事件处理器 +│ ├── TabPageHandler.js # TabPage事件处理器 +│ ├── GlobalEventManager.js # 全局事件管理器 +│ └── IntegrationTester.js # 集成测试器 +├── Area.vue # 面板区组件 +├── DockIndicator.vue # 停靠指示器组件 +├── DockLayout.vue # 根组件 +├── Panel.vue # 面板组件 +├── Render.vue # 动态组件渲染器 +├── ResizeBar.vue # 调整条组件 +├── TabPage.vue # 标签页组件 +├── dockLayers.js # Z-index层级管理 +├── eventBus.js # 增强事件总线 +├── types.d.ts # TypeScript类型定义 +├── DockIndicator指示器命名约定.md # 指示器命名约定文档 +└── ToDoList.md # 待办事项文档 +``` + +## 组件设计 + +### 组件层次结构 + +``` +DockLayout (根组件) +├── Render (动态组件渲染器) +│ ├── Area (面板区) +│ │ ├── TabPage (标签页) +│ │ │ └── Panel (面板) +│ │ └── ResizeBar (调整条) +│ └── DockIndicator (停靠指示器) +``` + +### 组件通信机制 + +1. **父子通信**:props和emit +2. **兄弟通信**:事件总线(eventBus) +3. **跨层级通信**:事件总线(eventBus) +4. **全局通信**:全局事件管理器(GlobalEventManager) + +### 组件生命周期 + +1. **创建阶段**: + - 组件初始化 + - 事件监听器注册 + - 状态初始化 + +2. **运行阶段**: + - 事件处理 + - 状态更新 + - UI渲染 + +3. **销毁阶段**: + - 事件监听器清理 + - 状态清理 + - 资源释放 + +## 事件系统 + +### 事件总线架构 + +DockLayout采用增强的事件总线系统,支持以下特性: + +1. **优先级队列**:事件按优先级处理 +2. **去重机制**:防止重复事件 +3. **性能监控**:跟踪事件处理时间 +4. **错误处理**:全局错误捕获 + +### 事件分类 + +#### 1. 系统级事件 + +```javascript +GLOBAL_EVENT_TYPES = { + SYSTEM_INIT: 'system.init', + SYSTEM_READY: 'system.ready', + SYSTEM_DESTROY: 'system.destroy', + SYSTEM_ERROR: 'system.error', + SYSTEM_PERFORMANCE: 'system.performance', +} +``` + +#### 2. Area事件 + +```javascript +AREA_EVENT_TYPES = { + AREA_CREATED: 'area.created', + AREA_DESTROYED: 'area.destroyed', + AREA_DRAG_START: 'area.drag.start', + AREA_DRAG_MOVE: 'area.drag.move', + AREA_DRAG_END: 'area.drag.end', + AREA_RESIZE_START: 'area.resize.start', + AREA_RESIZE_MOVE: 'area.resize.move', + AREA_RESIZE_END: 'area.resize.end', + AREA_MAXIMIZE: 'area.maximize', + AREA_RESTORE: 'area.restore', + AREA_CLOSE: 'area.close', + AREA_POSITION_UPDATE: 'area.position.update', +} +``` + +#### 3. Panel事件 + +```javascript +PANEL_EVENT_TYPES = { + PANEL_CREATED: 'panel.created', + PANEL_DESTROYED: 'panel.destroyed', + PANEL_DRAG_START: 'panel.drag.start', + PANEL_DRAG_MOVE: 'panel.drag.move', + PANEL_DRAG_END: 'panel.drag.end', + PANEL_MAXIMIZE: 'panel.maximize', + PANEL_RESTORE: 'panel.restore', + PANEL_CLOSE: 'panel.close', +} +``` + +#### 4. TabPage事件 + +```javascript +TABPAGE_EVENT_TYPES = { + TABPAGE_CREATED: 'tabPage.created', + TABPAGE_DESTROYED: 'tabPage.destroyed', + TABPAGE_SWITCH: 'tabPage.switch', + TABPAGE_CLOSE_REQUEST: 'tabPage.close.request', + TABPAGE_CLOSE: 'tabPage.close', + TABPAGE_DRAG_START: 'tabPage.drag.start', + TABPAGE_DRAG_MOVE: 'tabPage.drag.move', + TABPAGE_DRAG_END: 'tabPage.drag.end', + TABPAGE_GROUP_MERGE: 'tabPage.group.merge', + TABPAGE_ACTIVATED: 'tabPage.activated', + TABPAGE_DEACTIVATED: 'tabPage.deactivated', +} +``` + +#### 5. ResizeBar事件 + +```javascript +RESIZE_EVENT_TYPES = { + RESIZE_START: 'resize.start', + RESIZE_MOVE: 'resize.move', + RESIZE_END: 'resize.end', +} +``` + +### 事件流程示例 + +#### 面板拖拽流程 + +``` +1. 用户开始拖拽面板 + ↓ +2. Panel触发 PANEL_DRAG_START 事件 + ↓ +3. DragStateManager记录拖拽状态 + ↓ +4. EventBusManager路由事件到相关处理器 + ↓ +5. AreaHandler处理拖拽逻辑 + ↓ +6. DockIndicator显示停靠指示器 + ↓ +7. 用户拖拽面板移动 + ↓ +8. Panel触发 PANEL_DRAG_MOVE 事件 + ↓ +9. 更新拖拽状态和UI + ↓ +10. 用户释放面板 + ↓ +11. Panel触发 PANEL_DRAG_END 事件 + ↓ +12. 执行停靠逻辑(如果停靠到指示器) + ↓ +13. 清理拖拽状态 +``` + +## 类型定义 + +### 核心类型 + +**文件**:[types.d.ts](file:///d:/Projects/trunk/JoyD/AutoRobot/Windows/Robot/Web/src/DockLayout/types.d.ts) + +#### 1. 面板位置类型 + +```typescript +export type PanelPosition = 'left' | 'right' | 'top' | 'bottom' | 'center' | 'floating'; +``` + +#### 2. 面板元数据 + +```typescript export interface PanelMeta { id: string; title: string; icon?: string; - component?: unknown; // 非 DOM 实例的渲染引用或组件名 + component?: unknown; initialPosition?: PanelPosition; tags?: string[]; flags?: { collapsed?: boolean; floating?: boolean }; } +``` -export interface DockLayoutState { - areas: Record; - allPanels: Record; // 全局面板列表(非 DOM 实例) +#### 3. 面板区 + +```typescript +export interface PanelArea { + id: string; + parentAreaId?: string; + position: PanelPosition; + subAreas: SubArea[]; + activeTabIndex?: number; + influence: InfluenceEntry[]; + influencedBy: InfluenceEntry[]; + pendingUpdates: Set; + x: number; + y: number; + width: number; + height: number; + widthRatios: number; + heightRatios: number; } ``` -## 全局面板列表(allPanels) -- 定义:全局面板列表 `allPanels: Record`,仅保存子面板元数据(非 DOM 实例)。 -- 作用:单一事实来源、去重与索引、跨区域移动一致性、持久化/导入导出、快速检索。 -- 基本 API(约定):`registerPanel(meta)`, `unregisterPanel(panelId)`, `getPanelById(panelId)`, `listPanels()`, `updatePanelMeta(panelId, patch)`。 -- 与 TabGroup/面板区协作:面板区仅保存面板 `id` 列表;渲染时通过 `id` 访问 `allPanels` 中的 meta;尺寸队列更新只处理边界与比例,不涉及 DOM 实例。 -- 示例字段:`id`, `title`, `icon`, `component`, `initialPosition`, `tags`, `flags`(如 `collapsed`, `floating`)。 +#### 4. 子面板区 -## 更新流程(队列式批处理) -- 触发条件:当一个面板区发生增删面板、折叠/展开、拖拽分割条、切换 Tab 等事件。 -- 入队:将“变化的面板区”加入其 `pendingUpdates` 以及受影响链上的目标区(或采用全局 Set 以去重)。 -- 处理:从队列弹出一个区,先计算该区边界与尺寸,再按“受影响列表”逐个更新目标区的子面板尺寸,过程中将新产生的更新继续入队。 -- 终止:队列为空或达到安全迭代上限(防循环保护),保证一次批处理中所有关联区都被稳定更新。 +```typescript +export interface SubArea { + id: string; + position: PanelPosition; + panels: Panel[]; + activePanelId?: string; +} +``` -## TabGroup 行为约定 -- API:`addTab(panel)`, `closeTab(panelId)`, `activateTab(index)`。 -- 事件:`tabAdded`, `tabClosed`, `tabActivated`(用于通知面板区更新队列)。 -- 视觉:标签栏 + 内容区,支持拖拽排序、上下文菜单等扩展能力。 +#### 5. 面板 -## 目录规划(建议) -- `DockLayoutContainer.vue`:顶层容器(挂载五大面板区、调度队列)。 -- `Area.vue`:单个面板区容器(承载一个或多个子面板区)。 -- `TabGroup.vue`(或复用现有 `components/TabGroup.js`):标签页组织与交互。 -- `influence.ts`:影响关系与队列工具(`enqueue`/`processQueue`)。 -- `types.ts`:类型定义(如上草案)。 -- `README.md`:本文档。 +```typescript +export interface Panel { + id: string; + title: string; + component?: unknown; + isMaximized: boolean; + isMinimized: boolean; +} +``` -## 备注 -- 后续将对接已有的布局协调器(如 `LayoutCoordinator`)与 store(如 `dockPanelStore.js`)的尺寸计算接口。 -- 若需要在本目录中实现独立组件,也可提供适配层以与现有容器组件互通。 \ No newline at end of file +## 测试和监控 + +### 集成测试 + +**文件**:[IntegrationTester.js](file:///d:/Projects/trunk/JoyD/AutoRobot/Windows/Robot/Web/src/DockLayout/handlers/IntegrationTester.js) + +#### 测试配置 + +```javascript +TEST_CONFIG = { + performanceThresholds: { + eventEmitTime: 10, // 事件发射时间阈值(毫秒) + eventHandleTime: 50, // 事件处理时间阈值(毫秒) + memoryUsage: 50 * 1024 * 1024, // 内存使用阈值(50MB) + cpuUsage: 80, // CPU使用率阈值(%) + dragFPS: 30, // 拖拽帧率阈值 + dragDuration: 3000, // 拖拽持续时间阈值(毫秒) + concurrentEvents: 100 // 并发事件数量阈值 + }, + + testCases: { + panel: { + createPanel: { priority: 1, timeout: 5000 }, + destroyPanel: { priority: 1, timeout: 5000 }, + maximizePanel: { priority: 1, timeout: 3000 }, + minimizePanel: { priority: 1, timeout: 3000 }, + restorePanel: { priority: 1, timeout: 3000 }, + closePanel: { priority: 1, timeout: 5000 } + }, + tabPage: { + createTabPage: { priority: 1, timeout: 5000 }, + switchTabPage: { priority: 1, timeout: 3000 }, + closeTabPage: { priority: 1, timeout: 5000 }, + mergeTabPage: { priority: 1, timeout: 5000 } + }, + area: { + createArea: { priority: 1, timeout: 5000 }, + dragArea: { priority: 1, timeout: 5000 }, + resizeArea: { priority: 1, timeout: 5000 }, + maximizeArea: { priority: 1, timeout: 3000 }, + restoreArea: { priority: 1, timeout: 3000 } + }, + drag: { + startDrag: { priority: 1, timeout: 3000 }, + moveDrag: { priority: 1, timeout: 5000 }, + endDrag: { priority: 1, timeout: 3000 }, + dockToEdge: { priority: 1, timeout: 5000 }, + dockToCenter: { priority: 1, timeout: 5000 } + } + } +} +``` + +#### 性能监控 + +```javascript +class PerformanceMonitor { + start() // 开始监控 + stop() // 停止监控 + recordMetric(name, value) // 记录指标 + getMetrics() // 获取指标 + checkThresholds() // 检查阈值 +} +``` + +### 内存保护 + +所有处理器都实现了内存保护机制: + +1. **自动清理**:定期清理过期数据 +2. **数量限制**:限制最大对象数量 +3. **历史清理**:限制历史记录大小 +4. **泄漏检测**:检测和报告内存泄漏 + +## 使用指南 + +### 基本使用 + +#### 1. 添加浮动面板 + +```javascript +import { ref } from 'vue'; + +const floatingAreas = ref([]); + +// 添加浮动面板 +const addFloatingPanel = (panel) => { + floatingAreas.value.push({ + id: `area-${Date.now()}`, + x: 100, + y: 100, + width: 300, + height: 250, + panels: [panel] + }); +}; +``` + +#### 2. 创建面板 + +```javascript +const panel = { + id: 'panel-1', + title: '示例面板', + component: 'ExampleComponent' +}; +``` + +#### 3. 监听事件 + +```javascript +import { eventBus } from './eventBus'; + +// 监听面板拖拽开始 +eventBus.on('panel.drag.start', (data) => { + console.log('面板拖拽开始:', data); +}); + +// 监听面板停靠 +eventBus.on('panel.dock', (data) => { + console.log('面板停靠:', data); +}); +``` + +### 高级使用 + +#### 1. 自定义停靠逻辑 + +```javascript +import { eventBus } from './eventBus'; + +// 监听停靠事件 +eventBus.on('panel.dock', (data) => { + const { panel, targetArea, position } = data; + + // 自定义停靠逻辑 + if (position === 'center') { + // 中心停靠逻辑 + } else if (position === 'edge') { + // 边缘停靠逻辑 + } +}); +``` + +#### 2. 性能监控 + +```javascript +import { getIntegrationTester } from './handlers/IntegrationTester'; + +const tester = getIntegrationTester(); + +// 开始性能监控 +tester.startPerformanceMonitor(); + +// 执行测试 +await tester.runTestSuite('panel'); + +// 获取性能报告 +const report = tester.getPerformanceReport(); +console.log(report); +``` + +#### 3. 自定义事件处理器 + +```javascript +import { eventBus } from './eventBus'; + +// 注册自定义事件处理器 +eventBus.on('custom.event', (data) => { + console.log('自定义事件:', data); +}, { + priority: 1, + deduplication: { type: 'TTL_BASED', ttl: 100 } +}); +``` + +## 最佳实践 + +### 1. 组件开发 + +- 使用Composition API编写组件 +- 遵循单一职责原则 +- 合理拆分组件 +- 使用TypeScript提供类型安全 + +### 2. 事件处理 + +- 使用事件总线进行组件通信 +- 合理设置事件优先级 +- 启用事件去重 +- 及时清理事件监听器 + +### 3. 性能优化 + +- 避免频繁的状态更新 +- 使用computed缓存计算结果 +- 合理使用v-show和v-if +- 及时清理过期数据 + +### 4. 内存管理 + +- 在组件卸载时清理资源 +- 使用单例模式避免重复创建 +- 定期清理过期数据 +- 监控内存使用情况 + +### 5. 错误处理 + +- 使用try-catch捕获错误 +- 记录错误日志 +- 提供友好的错误提示 +- 实现错误恢复机制 + +## 常见问题 + +### Q1: 如何添加自定义面板? + +A: 创建面板组件,然后在DockLayout中添加浮动面板或停靠面板。 + +```javascript +const customPanel = { + id: 'custom-panel', + title: '自定义面板', + component: CustomPanelComponent +}; +``` + +### Q2: 如何实现自定义停靠逻辑? + +A: 监听停靠事件,在事件处理器中实现自定义逻辑。 + +```javascript +eventBus.on('panel.dock', (data) => { + // 自定义停靠逻辑 +}); +``` + +### Q3: 如何优化性能? + +A: +- 使用事件去重 +- 及时清理过期数据 +- 避免频繁的状态更新 +- 使用性能监控工具 + +### Q4: 如何处理内存泄漏? + +A: +- 在组件卸载时清理事件监听器 +- 使用单例模式避免重复创建 +- 定期清理过期数据 +- 使用内存监控工具 + +### Q5: 如何调试事件? + +A: +- 启用调试模式 +- 使用事件监控工具 +- 查看事件日志 +- 使用浏览器开发者工具 + +## 更新日志 + +### v1.8 +- 修正DockIndicator文档,移除不准确的描述 +- 增强中文沟通用词建议 +- 修正独立中心指示器z-index为10000 + +### v1.7 +- 实现将中心指示器移到与中心区域容器同级 +- 保持中心指示器位置相对于中心区域容器正中央 +- 更新层级结构图 + +### v1.6 +- 根据实际代码情况更新文档结构 +- 移除独立中心指示器相关内容 +- 更新层级结构图 + +### v1.3 +- 添加独立中心指示器相关命名约定 +- 更新层级结构图 +- 更新中文沟通用词建议 + +### v1.2 +- 添加子区域指示器功能实现记录 +- 更新文档以反映半透明依靠区功能的完成状态 + +### v1.1 +- 修正层级结构图,添加完整的外部边缘指示器 +- 在层级结构图中标注当前实际使用的类名 +- 完善命名体系的一致性 + +### v1.0 +- 初始版本,建立基础命名约定体系 + +## 贡献指南 + +欢迎贡献代码、报告问题或提出改进建议。 + +### 开发环境 + +1. 克隆项目 +2. 安装依赖:`npm install` +3. 启动开发服务器:`npm run dev` +4. 运行测试:`npm run test` + +### 代码规范 + +- 使用ESLint进行代码检查 +- 使用Prettier进行代码格式化 +- 遵循Vue 3风格指南 +- 编写单元测试和集成测试 + +### 提交规范 + +- 使用清晰的提交信息 +- 遵循Conventional Commits规范 +- 提交前运行测试 +- 更新相关文档 + +## 许可证 + +本项目采用 MIT 许可证。 + +## 联系方式 + +如有问题或建议,请联系项目维护者。 + +--- + +**文档版本**:v1.0 +**创建日期**:2024年 +**最后更新**:2024年 +**适用范围**:DockLayout 可停靠布局系统 diff --git a/AutoRobot/Windows/Robot/Web/src/DockLayout/handlers/DragStateManager.js b/AutoRobot/Windows/Robot/Web/src/DockLayout/handlers/DragStateManager.js index 9046eb9..4b64326 100644 --- a/AutoRobot/Windows/Robot/Web/src/DockLayout/handlers/DragStateManager.js +++ b/AutoRobot/Windows/Robot/Web/src/DockLayout/handlers/DragStateManager.js @@ -610,18 +610,22 @@ class DragStateManager { try { const { eventType = data.type, dragId, componentType, sourceElement } = data; + console.log('🔍 _onDragEvent 接收到的数据:', { eventType, dragId, componentType, data }); + // 从事件数据中提取 dragId,如果没有则根据组件类型推断 let actualDragId = dragId; if (!actualDragId) { if (data.panelId) { - actualDragId = `panel_${data.panelId}_${data.timestamp || Date.now()}`; + actualDragId = `panel_${data.panelId}_${data.timestamp || Date.now()}_${Math.random().toString(36).substring(2, 9)}`; } else if (data.tabIndex !== undefined) { - actualDragId = `tabpage_${data.tabId}_${data.tabIndex}_${data.timestamp || Date.now()}`; + actualDragId = `tabpage_${data.tabId}_${data.tabIndex}_${data.timestamp || Date.now()}_${Math.random().toString(36).substring(2, 9)}`; } else if (data.areaId) { - actualDragId = `area_${data.areaId}_${data.timestamp || Date.now()}`; + actualDragId = `area_${data.areaId}_${data.timestamp || Date.now()}_${Math.random().toString(36).substring(2, 9)}`; } } + console.log('🔍 _onDragEvent 处理后的 dragId:', { originalDragId: dragId, actualDragId }); + // 推断组件类型 let actualComponentType = componentType; if (!actualComponentType) { @@ -686,9 +690,19 @@ class DragStateManager { async _handleDragStart(data) { const { dragId, componentType, sourceElement, position, options = {} } = data; - // 如果没有提供 dragId,生成一个唯一的 dragId + console.log('🔍 _handleDragStart 接收到的数据:', { dragId, componentType, position, data }); + + // 使用传入的 dragId,如果没有则生成一个唯一的 dragId const actualDragId = dragId || this._generateDragId(componentType); + console.log('🔍 _handleDragStart 处理后的 dragId:', { originalDragId: dragId, actualDragId }); + + // 检查是否已存在相同 dragId 的拖拽状态 + if (this.activeDrags.has(actualDragId)) { + console.log(`⚠️ 拖拽状态已存在: ${actualDragId},跳过创建`); + return actualDragId; + } + // 创建拖拽状态 const dragState = new DragState(actualDragId, componentType, sourceElement, options); dragState.startPosition = { ...position }; @@ -1277,6 +1291,18 @@ class DragStateManager { dragId, reason: 'manager_cancelled' }); + + // 立即从活跃拖拽中移除,不依赖异步事件处理 + this.activeDrags.delete(dragId); + + // 清除反馈 + this._hideDragFeedback(dragId); + + // 从冲突检测器注销 + this.conflictDetector.unregisterDrag(dragId); + + // 清理拖拽目标 + this.dragTargets.delete(dragId); } }); @@ -1295,6 +1321,7 @@ class DragStateManager { element, type = 'panel', position = { x: 0, y: 0 }, + dragId: incomingDragId, ...options } = eventData; @@ -1304,8 +1331,8 @@ class DragStateManager { this.cancelAllDrags(); } - // 创建拖拽ID - const dragId = `panel-${panelId}-${Date.now()}`; + // 使用传入的 dragId,如果没有则生成新的 + const dragId = incomingDragId || `panel-${panelId}-${Date.now()}`; // 创建拖拽状态 const dragState = new DragState(dragId, DRAG_AREA_TYPES.PANEL, element, { @@ -1487,11 +1514,12 @@ class DragStateManager { tabPageId, element, tabIndex, + dragId: incomingDragId, ...options } = eventData; - // 创建拖拽ID - const dragId = `tabpage-${tabPageId}-${Date.now()}`; + // 使用传入的 dragId,如果没有则生成新的 + const dragId = incomingDragId || `tabpage-${tabPageId}-${Date.now()}`; // 创建拖拽状态 const dragState = new DragState(dragId, DRAG_AREA_TYPES.TABPAGE, element, { @@ -1767,17 +1795,19 @@ class DragStateManager { element, type = 'area', position = { x: 0, y: 0 }, + dragId: incomingDragId, ...options } = eventData; // 检查是否有其他活跃拖拽 if (this.activeDrags.size > 0) { - console.warn('检测到其他活跃拖拽,暂停之前的拖拽'); - this.cancelAllDrags(); + console.warn('检测到其他活跃拖拽,跳过创建新拖拽状态'); + // 如果已经有活跃拖拽,直接返回现有拖拽的dragId + return Array.from(this.activeDrags.keys())[0]; } - // 创建拖拽ID - const dragId = `area-${areaId}-${Date.now()}`; + // 使用传入的 dragId,如果没有则生成新的 + const dragId = incomingDragId || `area-${areaId}-${Date.now()}`; // 创建拖拽状态 const dragState = new DragState(dragId, DRAG_AREA_TYPES.AREA, element, { @@ -1900,10 +1930,12 @@ class DragStateManager { ...options } = eventData; + console.log('🔍 onAreaDragEnd 接收到的数据:', { dragId, areaId, eventData }); + // 查找拖拽状态 const activeDrag = this._findDragByIdOrAreaId(dragId, areaId); if (!activeDrag) { - console.warn('找不到区域拖拽状态:', areaId); + console.warn('找不到区域拖拽状态:', { dragId, areaId, activeDrags: Array.from(this.activeDrags.keys()) }); return false; } @@ -1950,7 +1982,7 @@ class DragStateManager { } // 立即创建单例实例,避免并发初始化问题 -const dragStateManager = new DragStateManager(); +let dragStateManager = new DragStateManager(); // 便捷操作函数 export const dragStateActions = { @@ -2097,7 +2129,6 @@ export const dragStateActions = { } catch (e) { console.warn('销毁拖拽状态管理器时出错:', e); } - dragStateManager = null; console.log('🗑️ 拖拽状态管理器已销毁'); } }