From 8e472d4497fa33e78c7d3c6901adcdd5ce5fb837 Mon Sep 17 00:00:00 2001 From: zqm Date: Mon, 17 Nov 2025 10:59:46 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8B=96=E6=8B=BDArea=E5=81=9C=E9=9D=A0?= =?UTF-8?q?=E4=B8=AD=E5=BF=83=E5=8C=BA=E5=9F=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Windows/Robot/Web/src/DockLayout/Area.vue | 153 ++++++- .../Web/src/DockLayout/DockIndicator.vue | 1 - .../Robot/Web/src/DockLayout/DockLayout.vue | 388 +++++++++++++++++- .../Robot/Web/src/DockLayout/ToDoList.md | 9 +- 4 files changed, 525 insertions(+), 26 deletions(-) diff --git a/AutoRobot/Windows/Robot/Web/src/DockLayout/Area.vue b/AutoRobot/Windows/Robot/Web/src/DockLayout/Area.vue index 498365a..8a03e20 100644 --- a/AutoRobot/Windows/Robot/Web/src/DockLayout/Area.vue +++ b/AutoRobot/Windows/Robot/Web/src/DockLayout/Area.vue @@ -99,14 +99,52 @@
- - + + + +
\ No newline at end of file diff --git a/AutoRobot/Windows/Robot/Web/src/DockLayout/DockIndicator.vue b/AutoRobot/Windows/Robot/Web/src/DockLayout/DockIndicator.vue index b8b3c56..75e619c 100644 --- a/AutoRobot/Windows/Robot/Web/src/DockLayout/DockIndicator.vue +++ b/AutoRobot/Windows/Robot/Web/src/DockLayout/DockIndicator.vue @@ -412,7 +412,6 @@ { } } +// 隐藏Area管理方法 - 第1步实现 +// 将Area添加到隐藏列表 +const addAreaToHiddenList = (area) => { + // 确保area有唯一标识符 + if (!area.id) { + area.id = `hidden-area-${Date.now()}` + } + + // 检查是否已经存在于隐藏列表中 + const existingIndex = hiddenAreas.value.findIndex(h => h.id === area.id) + if (existingIndex === -1) { + // 添加到隐藏列表 + hiddenAreas.value.push({ + ...area, + hiddenAt: new Date().toISOString() + }) + console.log('Area已添加到隐藏列表:', area.id) + } else { + console.warn('Area已在隐藏列表中:', area.id) + } +} + // Panel拖拽开始 const onPanelDragStart = (areaId, event) => { const area = floatingAreas.value.find(a => a.id === areaId) @@ -436,17 +461,23 @@ const onPanelDragMove = (areaId, event) => { const onPanelDragEnd = () => { panelDragState.value.isDragging = false + const currentAreaId = panelDragState.value.currentAreaId panelDragState.value.currentAreaId = null + // 3.1 在onPanelDragEnd方法中添加中心停靠检测 + if (activeDockZone.value === 'center' && currentAreaId) { + // 处理中心停靠 + const result = handleCenterDocking(currentAreaId) + if (!result.success) { + console.warn('中心停靠失败:', result.message) + } else { + console.log('中心停靠成功:', result.message) + } + } + // 隐藏停靠指示器 showDockIndicator.value = false activeDockZone.value = null - - // 如果有活动的停靠区域,可以在这里处理停靠逻辑 - if (activeDockZone.value) { - - // 这里可以实现具体的停靠逻辑 - } } // Area拖拽开始 @@ -454,6 +485,24 @@ const onAreaDragStart = (areaId, event) => { const area = floatingAreas.value.find(a => a.id === areaId) if (!area) return + // 设置拖拽状态(类似TabPage拖拽) + panelDragState.value.isDragging = true + panelDragState.value.currentAreaId = areaId + panelDragState.value.startClientPos = { + x: event.clientX, + y: event.clientY + } + panelDragState.value.startAreaPos = { + x: area.x, + y: area.y + } + + // 初始化鼠标位置跟踪 + currentMousePosition.value = { + x: event.clientX, + y: event.clientY + } + // 拖拽开始时显示指示器 showDockIndicator.value = true @@ -477,12 +526,33 @@ const onAreaDragStart = (areaId, event) => { // Area拖拽移动 const onAreaDragMove = (areaId, event) => { + // 更新鼠标位置 + currentMousePosition.value = { + x: event.clientX, + y: event.clientY + } + // 根据鼠标位置动态更新停靠区域 updateDockZoneByMousePosition(event.clientX, event.clientY) } // Area拖拽结束 const onAreaDragEnd = (areaId, event) => { + // 清理拖拽状态 + panelDragState.value.isDragging = false + panelDragState.value.currentAreaId = null + + // 3.1 在onAreaDragEnd方法中添加中心停靠检测 + if (activeDockZone.value === 'center') { + // 处理中心停靠 + const result = handleCenterDocking(areaId) + if (!result.success) { + console.warn('Area中心停靠失败:', result.message) + } else { + console.log('Area中心停靠成功:', result.message) + } + } + // 隐藏停靠指示器 showDockIndicator.value = false activeDockZone.value = null @@ -595,8 +665,20 @@ const onTabDragMove = (areaId, event) => { const onTabDragEnd = () => { tabDragState.value.isDragging = false + const currentAreaId = tabDragState.value.currentAreaId tabDragState.value.currentAreaId = null + // 3.2 在onTabDragEnd方法中添加中心停靠检测 + if (activeDockZone.value === 'center' && currentAreaId) { + // 处理中心停靠 + const result = handleCenterDocking(currentAreaId) + if (!result.success) { + console.warn('中心停靠失败:', result.message) + } else { + console.log('中心停靠成功:', result.message) + } + } + // 隐藏停靠指示器 showDockIndicator.value = false activeDockZone.value = null @@ -642,7 +724,22 @@ watch(floatingAreas, () => { // 当主区域内没有其他Area时,隐藏外部边缘指示器,只显示中心指示器 -// 根据鼠标位置动态更新停靠区域 +// 第5步:优化UI指示器显示逻辑(当主区域为空时) +/** + * 检查主区域是否为空 + * @returns {boolean} 主区域是否为空 + */ +const isMainAreaEmpty = () => { + checkMainContentForAreas() + return !hasAreasInMainContent.value +} + +/** + * 根据鼠标位置动态更新停靠区域(优化版) + * 当主区域为空时,只显示中心指示器 + * @param {number} mouseX - 鼠标X坐标 + * @param {number} mouseY - 鼠标Y坐标 + */ const updateDockZoneByMousePosition = (mouseX, mouseY) => { if (!dockLayoutRef.value || !targetAreaRect.value) return @@ -658,26 +755,43 @@ const updateDockZoneByMousePosition = (mouseX, mouseY) => { let newActiveZone = null + // 检查主区域是否为空 + const mainAreaEmpty = isMainAreaEmpty() + if (relativeX >= 0 && relativeX <= 1 && relativeY >= 0 && relativeY <= 1) { // 鼠标在目标区域内 - if (relativeY <= threshold) { - newActiveZone = 'top' - } else if (relativeY >= 1 - threshold) { - newActiveZone = 'bottom' - } else if (relativeX <= threshold) { - newActiveZone = 'left' - } else if (relativeX >= 1 - threshold) { - newActiveZone = 'right' - } else if (relativeX >= 0.4 && relativeX <= 0.6 && relativeY >= 0.4 && relativeY <= 0.6) { - // 中心区域 (40%-60% 的中心区域) - newActiveZone = 'center' + if (mainAreaEmpty) { + // 主区域为空时:只允许停靠到中心区域 + if (relativeX >= 0.3 && relativeX <= 0.7 && relativeY >= 0.3 && relativeY <= 0.7) { + // 扩大的中心区域 (30%-70% 的中心区域) + newActiveZone = 'center' + } + } else { + // 主区域不为空时:显示所有指示器选项 + if (relativeY <= threshold) { + newActiveZone = 'top' + } else if (relativeY >= 1 - threshold) { + newActiveZone = 'bottom' + } else if (relativeX <= threshold) { + newActiveZone = 'left' + } else if (relativeX >= 1 - threshold) { + newActiveZone = 'right' + } else if (relativeX >= 0.4 && relativeX <= 0.6 && relativeY >= 0.4 && relativeY <= 0.6) { + // 中心区域 (40%-60% 的中心区域) + newActiveZone = 'center' + } } } // 只有当停靠区域改变时才更新,减少不必要的重新渲染 if (activeDockZone.value !== newActiveZone) { activeDockZone.value = newActiveZone - + // 如果激活的区域不为空,显示指示器;如果为空,隐藏指示器 + if (newActiveZone) { + showDockIndicator.value = true + } else { + showDockIndicator.value = false + } } } @@ -790,9 +904,243 @@ const onPanelMaximizeSync = ({ areaId, maximized }) => { } } +// 第2步:Area验证逻辑 - 目标Area内容区为空的情况 +/** + * 检查Area是否可以停靠到中心区域 + * @param {Object} sourceArea - 源Area对象 + * @param {Object} targetArea - 目标Area对象(主区域) + * @returns {Object} 验证结果 {canDock: boolean, reason: string} + */ +const canDockToCenter = (sourceArea, targetArea) => { + // 验证1:检查源Area是否包含TabPage + if (!sourceArea) { + return { + canDock: false, + reason: '源Area不存在' + } + } + + if (!sourceArea.tabPages || sourceArea.tabPages.length === 0) { + return { + canDock: false, + reason: '源Area不包含TabPage,无法停靠' + } + } + + // 验证2:检查目标Area(主区域)- 支持Area级别停靠 + if (targetArea === mainAreaRef.value) { + // Area级别停靠支持:将源Area的内容移动到主区域 + // 不再要求主区域完全为空,允许Area停靠 + console.log('Area级别停靠:源Area可停靠到主区域') + } else { + // 对于其他目标Area的验证逻辑可以后续扩展 + return { + canDock: false, + reason: '暂不支持停靠到非主区域' + } + } + + // 验证3:确保包含TabPage的Area才能停靠到中心 + const sourceTabPage = sourceArea.tabPages[0] + if (!sourceTabPage.panels || sourceTabPage.panels.length === 0) { + return { + canDock: false, + reason: '源Area的TabPage不包含Panel,无法停靠' + } + } + + // 所有验证通过 + return { + canDock: true, + reason: '验证通过,可以停靠到中心区域' + } +} + +// 第3步:中心停靠逻辑 +/** + * 处理中心停靠的核心函数 + * @param {string} areaId - 源Area的ID + * @returns {Object} 处理结果 {success: boolean, message: string} + */ +const handleCenterDocking = (areaId) => { + try { + // 3.4 查找源Area + const sourceArea = floatingAreas.value.find(a => a.id === areaId) + if (!sourceArea) { + return { + success: false, + message: '找不到指定的Area' + } + } + + // 2.1-2.5 验证是否可以停靠到中心 + const validation = canDockToCenter(sourceArea, mainAreaRef.value) + if (!validation.canDock) { + return { + success: false, + message: `验证失败:${validation.reason}` + } + } + + // 3.4 实现Area子组件移动到主区域的逻辑 + // 将Area的内容移动到主区域(通过主区域Ref处理) + if (mainAreaRef.value && mainAreaRef.value.appendAreaContent) { + mainAreaRef.value.appendAreaContent(sourceArea) + } else { + console.warn('主区域Ref不可用,无法移动内容') + } + + // 3.5 实现Area添加到隐藏列表的逻辑 + addAreaToHiddenList(sourceArea) + + // 3.6 实现Area从浮动区域移除的逻辑 + const index = floatingAreas.value.findIndex(a => a.id === areaId) + if (index !== -1) { + floatingAreas.value.splice(index, 1) + } + + // 3.7 更新主区域和浮动区域的响应式状态 + checkMainContentForAreas() + + return { + success: true, + message: '成功停靠到中心区域' + } + + } catch (error) { + console.error('中心停靠处理错误:', error) + return { + success: false, + message: `停靠处理出错:${error.message}` + } + } +} + +// 第4步:在defineExpose中暴露隐藏列表管理方法 +/** + * 获取隐藏Area列表 + * @returns {Array} 隐藏Area列表 + */ +const getHiddenAreas = () => { + return [...hiddenAreas.value] +} + +/** + * 从隐藏列表恢复Area到浮动区域 + * @param {string} areaId - Area的ID + * @returns {Object} 恢复结果 {success: boolean, message: string} + */ +const restoreAreaFromHidden = (areaId) => { + try { + const hiddenIndex = hiddenAreas.value.findIndex(area => area.id === areaId) + if (hiddenIndex === -1) { + return { + success: false, + message: '找不到指定的隐藏Area' + } + } + + const areaToRestore = hiddenAreas.value[hiddenIndex] + + // 检查是否已存在相同的Area(避免重复) + const existingIndex = floatingAreas.value.findIndex(area => area.id === areaId) + if (existingIndex !== -1) { + return { + success: false, + message: 'Area已存在于浮动区域中' + } + } + + // 恢复Area到浮动区域 + floatingAreas.value.push(areaToRestore) + + // 从隐藏列表移除 + hiddenAreas.value.splice(hiddenIndex, 1) + + // 更新状态 + checkMainContentForAreas() + + return { + success: true, + message: 'Area已成功恢复到浮动区域' + } + + } catch (error) { + console.error('恢复Area时出错:', error) + return { + success: false, + message: `恢复失败:${error.message}` + } + } +} + +/** + * 从隐藏列表移除Area(不恢复) + * @param {string} areaId - Area的ID + * @returns {Object} 移除结果 {success: boolean, message: string} + */ +const removeFromHiddenList = (areaId) => { + try { + const hiddenIndex = hiddenAreas.value.findIndex(area => area.id === areaId) + if (hiddenIndex === -1) { + return { + success: false, + message: '找不到指定的隐藏Area' + } + } + + // 从隐藏列表移除 + hiddenAreas.value.splice(hiddenIndex, 1) + + return { + success: true, + message: 'Area已从隐藏列表中移除' + } + + } catch (error) { + console.error('移除Area时出错:', error) + return { + success: false, + message: `移除失败:${error.message}` + } + } +} + +/** + * 清空隐藏列表 + * @returns {Object} 清空结果 {success: boolean, message: string, removedCount: number} + */ +const clearHiddenList = () => { + try { + const count = hiddenAreas.value.length + hiddenAreas.value.splice(0, hiddenAreas.value.length) + + return { + success: true, + message: `已清空隐藏列表,移除${count}个Area`, + removedCount: count + } + + } catch (error) { + console.error('清空隐藏列表时出错:', error) + return { + success: false, + message: `清空失败:${error.message}`, + removedCount: 0 + } + } +} + // 暴露方法给父组件 defineExpose({ - addFloatingPanel + // 原有方法 + addFloatingPanel, + + // 第4步:隐藏列表管理方法 + getHiddenAreas, + restoreAreaFromHidden, + removeFromHiddenList, + clearHiddenList }) diff --git a/AutoRobot/Windows/Robot/Web/src/DockLayout/ToDoList.md b/AutoRobot/Windows/Robot/Web/src/DockLayout/ToDoList.md index 151deba..cbf87e7 100644 --- a/AutoRobot/Windows/Robot/Web/src/DockLayout/ToDoList.md +++ b/AutoRobot/Windows/Robot/Web/src/DockLayout/ToDoList.md @@ -35,5 +35,10 @@ 1. 当一个Panel被拖动时,显示停靠指示器。 2. 当拖动Panel到指示器时,显示停靠区。 3. 当主区域内没有其他Area时,隐藏外部边缘指示器、中心区域容器,只显示中心指示器。 -4. 当将Area拖动到中心指示器时 -4.1. 如果Area只有一个直接子组件(TabPage),则将Area的子组件停靠到中心区域。这个Area保存到DockLayout的的隐藏列表中。 +4. 当将源Panel拖动到中心指示器时 +4.1. 如果源Area只有一个直接子组件(TabPage),且目标Area内容区为空,则将源Area的子组件停靠到中心区域。这个源Area保存到DockLayout的的隐藏列表中。 +5. 当将源Area拖动到中心指示器时 +5.1. 如果源Area只有一个直接子组件(TabPage),且目标Area内容区为空,则将源Area的子组件停靠到中心区域。这个源Area保存到DockLayout的的隐藏列表中。 +5.2. 如果源Area只有一个直接子组件(TabPage),且目标Area内容区已经包含一个TabPage,则将源Area的TabPage组件的每个标签页移动并添加到目标Area的Tabpage中。这个源Area保存到DockLayout的的隐藏列表中。 + +