From 0c66be439a9b6d46828ef94c404890ff670d7590 Mon Sep 17 00:00:00 2001 From: zqm Date: Mon, 17 Nov 2025 16:55:03 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=AD=E5=BF=83=E5=8C=BA=E4=BE=9D=E9=9D=A0?= =?UTF-8?q?=E5=A4=9A=E4=B8=AATabPanel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Windows/Robot/Web/src/DockLayout/Area.vue | 130 +++++-- .../Robot/Web/src/DockLayout/DockLayout.vue | 348 +++++++++++++++--- .../Robot/Web/src/DockLayout/Panel.vue | 40 +- .../Robot/Web/src/DockLayout/ToDoList.md | 9 +- 4 files changed, 443 insertions(+), 84 deletions(-) diff --git a/AutoRobot/Windows/Robot/Web/src/DockLayout/Area.vue b/AutoRobot/Windows/Robot/Web/src/DockLayout/Area.vue index 8a03e20..3ae45d8 100644 --- a/AutoRobot/Windows/Robot/Web/src/DockLayout/Area.vue +++ b/AutoRobot/Windows/Robot/Web/src/DockLayout/Area.vue @@ -125,6 +125,7 @@ :collapsed="panel.collapsed" :toolbarExpanded="panel.toolbarExpanded" :maximized="panel.maximized" + :content="panel.content" @toggleCollapse="() => {}" @maximize="onPanelMaximize" @close="() => {}" @@ -262,7 +263,7 @@ const areaStyle = computed(() => { return style }) -const emit = defineEmits(['close', 'update:WindowState', 'update:position', 'dragover', 'dragleave', 'areaDragStart', 'areaDragMove', 'areaDragEnd']) +const emit = defineEmits(['close', 'update:WindowState', 'update:position', 'dragover', 'dragleave', 'areaDragStart', 'areaDragMove', 'areaDragEnd', 'area-merged']) // 处理Panel的最大化事件 const onPanelMaximize = (panelId) => { @@ -569,56 +570,121 @@ onMounted(() => { } }) -// 添加Area内容的方法,用于主区域的Area接收外部Area内容 -const appendAreaContent = (sourceArea) => { - console.log(`[Area] ${props.id} 接收到内容移动请求:`, sourceArea) + + +// 合并Area内容的方法,只保留合并逻辑 +const mergeAreaContent = (sourceArea) => { + console.log(`[Area] ${props.id} 接收到Area合并请求:`, sourceArea) if (!sourceArea) { - console.warn('[Area] 源Area为空,无法接收内容') + console.warn('[Area] 源Area为空,无法合并内容') return false } try { - // 清空之前的内容,确保只接收一个新的TabPage - receivedContent.value = [] + const isEmpty = receivedContent.value.length === 0 - // 处理源Area的tabPages,只接收第一个TabPage(用户要求只能接收一个TabPage) - if (sourceArea.tabPages && sourceArea.tabPages.length > 0) { - const tabPage = sourceArea.tabPages[0] // 只接收第一个TabPage - if (tabPage) { - // 确保所有Panel都处于最大化状态,显示还原按钮 - const maximizedPanels = (tabPage.panels || []).map(panel => ({ - ...panel, - maximized: true // 设置为最大化状态 - })) - - receivedContent.value.push({ - id: `received-${tabPage.id || Date.now() + Math.random()}`, - title: tabPage.title || '标签页', - tabPage: { - ...tabPage, - panels: maximizedPanels // 使用最大化状态的Panel - }, - panels: maximizedPanels + if (isEmpty) { + // 4.2.1 如果目标Area内容区为空,将源Area内容区的子组件添加到目标Area内容区 + console.log('[Area] 目标Area为空,添加源Area的子组件') + + // 处理源Area的所有tabPages + if (sourceArea.tabPages && sourceArea.tabPages.length > 0) { + sourceArea.tabPages.forEach((tabPage, tabIndex) => { + const newTabPageId = `merged-tabpage-${Date.now()}-${tabIndex}` + const newPanels = (tabPage.panels || []).map((panel, panelIndex) => { + const newPanelId = `merged-panel-${Date.now()}-${tabIndex}-${panelIndex}` + console.log(`[Area] 添加Panel: ${panel.id} -> ${newPanelId}`) + return { + ...panel, + id: newPanelId, + maximized: true + } + }) + + receivedContent.value.push({ + id: `received-${newTabPageId}`, + title: tabPage.title || `标签页${tabIndex + 1}`, + tabPage: { + ...tabPage, + id: newTabPageId, + panels: newPanels + }, + panels: newPanels + }) + + console.log(`[Area] 成功添加TabPage: ${tabPage.title} (${newPanels.length} 个Panel)`) }) - console.log(`[Area] 成功接收tabPage: ${tabPage.title},并设置Panel为最大化状态`) } + + // 触发事件通知父组件将源Area保存到隐藏列表 + emit('area-merged', { + sourceArea: sourceArea, + targetAreaId: props.id, + targetAreaHasContent: false, // 目标Area为空 + operation: 'add-children', + addedTabPages: receivedContent.value + }) + + return true + } else { - console.warn('[Area] 源Area中没有tabPages数据') + // 4.2.2 如果目标Area内容区已包含TabPage,将源Area的每个TabPage添加到目标Area的TabPage中 + console.log('[Area] 目标Area已有TabPage,合并TabPage标签页') + + // 获取第一个现有的TabPage作为合并目标 + const existingTabPage = receivedContent.value[0] + if (!existingTabPage) { + console.error('[Area] 现有TabPage不存在') + return false + } + + // 处理源Area的所有tabPages + if (sourceArea.tabPages && sourceArea.tabPages.length > 0) { + sourceArea.tabPages.forEach((sourceTabPage, tabIndex) => { + if (sourceTabPage && sourceTabPage.panels) { + // 为每个Panel创建新的唯一ID避免冲突 + const newPanels = sourceTabPage.panels.map((panel, panelIndex) => { + const newPanelId = `merged-panel-${Date.now()}-${tabIndex}-${panelIndex}` + console.log(`[Area] 合并Panel到现有TabPage: ${panel.id} -> ${newPanelId}`) + return { + ...panel, + id: newPanelId, + maximized: true + } + }) + + // 将新的Panel添加到现有TabPage + existingTabPage.tabPage.panels.push(...newPanels) + existingTabPage.panels.push(...newPanels) + + console.log(`[Area] 成功合并 ${newPanels.length} 个Panel到现有TabPage`) + } + }) + } + + // 触发事件通知父组件将源Area及其TabPage组件保存到隐藏列表 + emit('area-merged', { + sourceArea: sourceArea, + targetAreaId: props.id, + targetAreaHasContent: true, // 目标Area已有内容 + operation: 'merge-tabpages', + sourceTabPages: sourceArea.tabPages || [] + }) + + console.log(`[Area] 合并完成,现有TabPage共有 ${existingTabPage.tabPage.panels.length} 个Panel`) + return true } - console.log(`[Area] ${props.id} 当前接收内容数量: ${receivedContent.value.length}`) - return true - } catch (error) { - console.error('[Area] 接收外部内容时出错:', error) + console.error('[Area] 合并Area内容时出错:', error) return false } } // 暴露方法给父组件调用 defineExpose({ - appendAreaContent, + mergeAreaContent, // 合并Area内容的方法 id: props.id, title: props.title, isMaximized: isMaximized.value diff --git a/AutoRobot/Windows/Robot/Web/src/DockLayout/DockLayout.vue b/AutoRobot/Windows/Robot/Web/src/DockLayout/DockLayout.vue index 434e623..ff19353 100644 --- a/AutoRobot/Windows/Robot/Web/src/DockLayout/DockLayout.vue +++ b/AutoRobot/Windows/Robot/Web/src/DockLayout/DockLayout.vue @@ -20,6 +20,7 @@ :style="{ position: 'relative', width: '100%', height: '100%', zIndex: 1 }" @dragover="handleMainAreaDragOver" @dragleave="handleMainAreaDragLeave" + @area-merged="onAreaMerged" > @@ -76,6 +77,7 @@ :collapsed="panel.collapsed" :toolbarExpanded="panel.toolbarExpanded" :maximized="panel.maximized" + :content="panel.content" @toggleCollapse="onToggleCollapse" @maximize="onMaximize" @close="onClosePanel(area.id, panel.id)" @@ -180,6 +182,44 @@ const checkMainContentForAreas = () => { } } +/** + * 生成随机测试内容用于合并测试 + * @param {number} panelIndex - 面板索引 + * @returns {Object} 包含随机内容的对象 + */ +const generateRandomContent = (panelIndex) => { + const contentTypes = ['图表', '数据', '配置', '文档', '代码', '日志'] + const randomType = contentTypes[Math.floor(Math.random() * contentTypes.length)] + const colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7', '#DDA0DD'] + const randomColor = colors[Math.floor(Math.random() * colors.length)] + + return { + type: randomType, + color: randomColor, + index: panelIndex, + title: `${randomType}面板 ${panelIndex + 1}`, + data: generateSampleData(panelIndex), + timestamp: new Date().toLocaleString() + } +} + +/** + * 生成样本数据 + * @param {number} dataIndex - 数据索引 + * @returns {Array} 样本数据数组 + */ +const generateSampleData = (dataIndex) => { + const data = [] + for (let i = 0; i < 5; i++) { + data.push({ + label: `项目 ${String.fromCharCode(65 + i)}`, + value: Math.floor(Math.random() * 100) + 10, + id: `data-${dataIndex}-${i}` + }) + } + return data +} + // 添加新的浮动面板 const addFloatingPanel = () => { // 获取父容器尺寸以计算居中位置 @@ -217,13 +257,15 @@ const addFloatingPanel = () => { panels: [ { id: `panel-${areaIdCounter - 1}-1-1`, - title: `面板 1`, + title: `面板 ${areaIdCounter - 1}`, x: 0, y: 0, width: 100, height: 100, collapsed: false, - toolbarExpanded: false + toolbarExpanded: false, + // 添加随机测试内容以便合并测试 + content: generateRandomContent(areaIdCounter - 1) } ] } @@ -354,6 +396,41 @@ const addAreaToHiddenList = (area) => { } } +// 处理Area合并事件 +const onAreaMerged = (eventData) => { + console.log('处理Area合并事件:', eventData) + + const { sourceArea, targetAreaHasContent } = eventData + + // 获取源Area对象 + const sourceAreaObj = floatingAreas.value.find(a => a.id === sourceArea.id) + if (!sourceAreaObj) { + console.warn('找不到源Area:', sourceArea.id) + return + } + + // 根据目标Area内容状态执行不同的隐藏逻辑 + if (targetAreaHasContent) { + // 目标Area已有内容:保存源Area及其TabPage组件到隐藏列表 + console.log('目标Area已有内容,保存源Area和TabPage组件到隐藏列表') + addAreaToHiddenList({ + ...sourceAreaObj, + tabPages: [...sourceAreaObj.tabPages] // 深拷贝TabPages + }) + } else { + // 目标Area为空:仅保存源Area到隐藏列表 + console.log('目标Area为空,保存源Area到隐藏列表') + addAreaToHiddenList(sourceAreaObj) + } + + // 从浮动区域中移除已合并的源Area + const sourceIndex = floatingAreas.value.findIndex(a => a.id === sourceArea.id) + if (sourceIndex !== -1) { + floatingAreas.value.splice(sourceIndex, 1) + console.log('源Area已从浮动区域移除:', sourceArea.id) + } +} + // Panel拖拽开始 const onPanelDragStart = (areaId, event) => { const area = floatingAreas.value.find(a => a.id === areaId) @@ -909,7 +986,7 @@ const onPanelMaximizeSync = ({ areaId, maximized }) => { * 检查Area是否可以停靠到中心区域 * @param {Object} sourceArea - 源Area对象 * @param {Object} targetArea - 目标Area对象(主区域) - * @returns {Object} 验证结果 {canDock: boolean, reason: string} + * @returns {Object} 验证结果 {canDock: boolean, reason: string, strategy: string} */ const canDockToCenter = (sourceArea, targetArea) => { // 验证1:检查源Area是否包含TabPage @@ -949,69 +1026,248 @@ const canDockToCenter = (sourceArea, targetArea) => { } } - // 所有验证通过 + // 5.2. 新增验证:根据目标Area状态确定停靠策略 + // 检查主区域内容状态 + checkMainContentForAreas() + + // 如果主区域已有内容,判断停靠策略 + if (hasAreasInMainContent.value) { + // 检查主区域是否已有TabPage + const mainAreaElement = mainAreaRef.value?.$el + if (mainAreaElement) { + const existingTabPages = mainAreaElement.querySelectorAll('.tab-page, [class*="tab"]') + const hasExistingTabPage = existingTabPages.length > 0 + + if (hasExistingTabPage && sourceArea.tabPages.length === 1) { + // 场景5.2:源Area只有一个TabPage,目标Area也有TabPage + // 策略:合并TabPage标签页 + return { + canDock: true, + reason: '可以停靠 - 采用TabPage标签页合并策略', + strategy: 'merge-tabpages' + } + } else { + // 其他场景:采用简单Area内容移动策略 + return { + canDock: true, + reason: '可以停靠 - 采用简单移动策略', + strategy: 'simple-move' + } + } + } + } + + // 主区域为空的情况:采用简单移动策略 return { canDock: true, - reason: '验证通过,可以停靠到中心区域' + reason: '验证通过,可以停靠到中心区域', + strategy: 'simple-move' } } -// 第3步:中心停靠逻辑 +// 3.3 创建handleCenterDocking核心处理函数 /** - * 处理中心停靠的核心函数 - * @param {string} areaId - 源Area的ID + * 处理中心停靠逻辑 + * @param {string} sourceAreaId - 源Area的ID * @returns {Object} 处理结果 {success: boolean, message: string} */ -const handleCenterDocking = (areaId) => { +const handleCenterDocking = (sourceAreaId) => { + console.log('开始处理中心停靠:', sourceAreaId) + + // 1. 查找源Area + const sourceArea = floatingAreas.value.find(a => a.id === sourceAreaId) + if (!sourceArea) { + return { + success: false, + message: `未找到ID为 ${sourceAreaId} 的源Area` + } + } + + // 2. 验证停靠条件 + const validationResult = canDockToCenter(sourceArea, mainAreaRef.value) + if (!validationResult.canDock) { + return { + success: false, + message: `停靠验证失败: ${validationResult.reason}` + } + } + + console.log('停靠验证通过:', validationResult.reason) + 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) + // 3. 根据策略执行停靠逻辑 + if (validationResult.strategy === 'merge-tabpages') { + // 5.2. 策略:合并TabPage标签页 + return handleMergeTabpagesDocking(sourceArea) } else { - console.warn('主区域Ref不可用,无法移动内容') + // 默认策略:简单Area内容移动 + return handleSimpleAreaDocking(sourceArea) + } + } catch (error) { + console.error('停靠处理过程中发生错误:', error) + return { + success: false, + message: `停靠过程中发生错误: ${error.message}` + } + } +} + +// 5.2. 新增:处理TabPage标签页合并策略 +/** + * 处理TabPage标签页合并策略 + * 当源Area只有一个TabPage且目标Area已有TabPage时使用此策略 + * @param {Object} sourceArea - 源Area对象 + * @returns {Object} 处理结果 {success: boolean, message: string} + */ +// 合并TabPage标签页的核心处理函数 +const handleMergeTabpagesDocking = (sourceArea) => { + console.log(`[DockLayout] 开始合并TabPage标签页,源Area:`, sourceArea) + + try { + // 验证源Area是否有效 + if (!sourceArea || !sourceArea.tabPages || sourceArea.tabPages.length === 0) { + console.warn('[DockLayout] 源Area无效或没有TabPage,无法执行合并操作') + return null + } + + // 查找主区域的Area组件 + const mainArea = mainAreaRef.value + if (!mainArea) { + console.error('[DockLayout] 找不到主区域引用') + return null + } + + console.log('[DockLayout] 使用新的mergeAreaContent方法合并TabPage') + const success = mainArea.mergeAreaContent(sourceArea) + + if (success) { + const newTabPageId = `merged-${Date.now()}` + const panelsCount = sourceArea.tabPages[0]?.panels?.length || 0 + console.log(`[DockLayout] 成功合并 ${panelsCount} 个Panel到主区域TabPage: ${newTabPageId}`) + + return { + success: true, + newTabPageId: newTabPageId, + panelsAdded: panelsCount + } + } + + console.error('[DockLayout] mergeAreaContent方法执行失败') + return null + + } catch (error) { + console.error('[DockLayout] 合并TabPage时发生错误:', error) + return null + } +} + +/** + * 在主区域查找或创建TabPage + * @returns {Object} 查找结果 {success: boolean, message: string, tabPage?: Object} + */ +const findOrCreateMainAreaTabPage = () => { + try { + // 检查主区域是否已经接收了内容 + if (mainAreaRef.value && mainAreaRef.value.receivedContent) { + const existingContent = mainAreaRef.value.receivedContent + console.log('检查主区域已接收内容,数量:', existingContent.length) + + if (existingContent.length > 0) { + // 找到现有的TabPage内容 + const existingTabPage = existingContent[0] // 只使用第一个TabPage + console.log('找到现有的TabPage:', existingTabPage.title) + + return { + success: true, + message: '找到现有的TabPage', + tabPage: existingTabPage.tabPage, + rawTabPage: existingTabPage // 返回原始TabPage对象以供修改 + } + } } - // 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() + // 如果主区域没有内容,现在不再主动创建空TabPage + // 因为在合并逻辑中,Area会自动根据需要创建TabPage + console.log('主区域没有现有TabPage,将通过合并操作自动创建') return { success: true, - message: '成功停靠到中心区域' + message: '主区域为空,等待合并操作创建TabPage', + tabPage: null, + rawTabPage: null } } catch (error) { - console.error('中心停靠处理错误:', error) + console.error('查找TabPage时发生错误:', error) return { success: false, - message: `停靠处理出错:${error.message}` + message: `查找TabPage失败: ${error.message}` + } + } +} + +// 3.4 实现简单Area停靠策略(重构为使用合并逻辑) +/** + * 处理简单Area停靠策略(Area内容合并) + * 当主区域为空或不符合TabPage合并条件时使用此策略 + * @param {Object} sourceArea - 源Area对象 + * @returns {Object} 处理结果 {success: boolean, message: string} + */ +const handleSimpleAreaDocking = (sourceArea) => { + console.log('执行简单Area停靠策略(使用合并逻辑)') + + try { + // 1. 验证源Area结构 + if (!sourceArea.tabPages || sourceArea.tabPages.length === 0) { + return { + success: false, + message: '源Area不包含TabPage,无法进行停靠合并' + } + } + + console.log('源Area结构验证通过,包含', sourceArea.tabPages.length, '个TabPage') + + // 2. 使用mergeAreaContent方法将源Area合并到主区域 + if (mainAreaRef.value && typeof mainAreaRef.value.mergeAreaContent === 'function') { + console.log('通过mergeAreaContent方法合并源Area到主区域') + const result = mainAreaRef.value.mergeAreaContent(sourceArea) + + if (result) { + console.log('源Area成功合并到主区域,area-merged事件将处理隐藏列表操作') + + // 3. 更新主区域状态(合并完成后由事件处理函数处理隐藏列表) + nextTick(() => { + checkMainContentForAreas() + // 确保主区域最大化显示还原按钮 + if (mainAreaRef.value) { + mainAreaRef.value.WindowState = '最大化' + console.log('主区域状态已设置为最大化') + } + }) + + return { + success: true, + message: `成功合并 ${sourceArea.tabPages.length} 个TabPage到主区域` + } + } else { + return { + success: false, + message: 'mergeAreaContent方法执行失败' + } + } + } else { + console.error('主区域不支持mergeAreaContent方法') + return { + success: false, + message: '主区域不支持合并操作' + } + } + + } catch (error) { + console.error('简单Area停靠策略执行时发生错误:', error) + return { + success: false, + message: `简单Area停靠失败: ${error.message}` } } } diff --git a/AutoRobot/Windows/Robot/Web/src/DockLayout/Panel.vue b/AutoRobot/Windows/Robot/Web/src/DockLayout/Panel.vue index 592a868..f5b9a00 100644 --- a/AutoRobot/Windows/Robot/Web/src/DockLayout/Panel.vue +++ b/AutoRobot/Windows/Robot/Web/src/DockLayout/Panel.vue @@ -71,7 +71,41 @@ -
+
+
+
+

+ {{ content.title }} +

+

+ 类型:{{ content.type }} | 创建时间:{{ content.timestamp }} +

+
+ +
+
+
+ {{ item.label }} + + {{ item.value }} + +
+
+
+
+
+

暂无内容

+
面板ID: {{ id }} - 标题: {{ title }}
+
+
@@ -116,6 +150,10 @@ const props = defineProps({ maximized: { type: Boolean, default: false + }, + content: { + type: Object, + default: null } }); diff --git a/AutoRobot/Windows/Robot/Web/src/DockLayout/ToDoList.md b/AutoRobot/Windows/Robot/Web/src/DockLayout/ToDoList.md index cbf87e7..c056ad5 100644 --- a/AutoRobot/Windows/Robot/Web/src/DockLayout/ToDoList.md +++ b/AutoRobot/Windows/Robot/Web/src/DockLayout/ToDoList.md @@ -35,10 +35,9 @@ 1. 当一个Panel被拖动时,显示停靠指示器。 2. 当拖动Panel到指示器时,显示停靠区。 3. 当主区域内没有其他Area时,隐藏外部边缘指示器、中心区域容器,只显示中心指示器。 -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的的隐藏列表中。 +4. 当将源Area拖动到中心指示器时 +4.1. Area的merge行为只接受一个Area参数. +4.1. 如果目标Area内容区为空,则将源Area内容区的子组件添加到目标Area内容区。这个源Area保存到DockLayout的的隐藏列表中。 +4.2. 如果目标Area内容区已经包含一个TabPage,则将源Area的TabPage组件的每个标签页添加到目标Area的Tabpage中。这个源Area和源Area的TabPage组件保存到DockLayout的的隐藏列表中。