中心区依靠多个TabPanel

This commit is contained in:
zqm
2025-11-17 16:55:03 +08:00
parent 8e472d4497
commit 0c66be439a
4 changed files with 443 additions and 84 deletions

View File

@@ -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

View File

@@ -20,6 +20,7 @@
:style="{ position: 'relative', width: '100%', height: '100%', zIndex: 1 }"
@dragover="handleMainAreaDragOver"
@dragleave="handleMainAreaDragLeave"
@area-merged="onAreaMerged"
>
<!-- 这里可以放置主区域的内容 -->
</Area>
@@ -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}`
}
}
}

View File

@@ -71,7 +71,41 @@
</div>
<!-- 内容区可折叠 -->
<div class="content-area bg-[#f5f7fb] flex-1" v-show="!collapsed"></div>
<div class="content-area bg-[#f5f7fb] flex-1 p-4" v-show="!collapsed">
<div v-if="content" class="panel-content">
<div class="mb-4">
<h3 class="text-lg font-bold mb-2" :style="{ color: content.color }">
{{ content.title }}
</h3>
<p class="text-sm text-gray-600 mb-3">
类型{{ content.type }} | 创建时间{{ content.timestamp }}
</p>
</div>
<div class="grid grid-cols-1 gap-3">
<div
v-for="(item, index) in content.data"
:key="item.id"
class="data-item p-3 border rounded-lg shadow-sm"
:style="{
borderLeftColor: content.color,
borderLeftWidth: '4px'
}"
>
<div class="flex justify-between items-center">
<span class="font-medium">{{ item.label }}</span>
<span class="text-sm font-bold" :style="{ color: content.color }">
{{ item.value }}
</span>
</div>
</div>
</div>
</div>
<div v-else class="text-gray-500 text-center">
<p>暂无内容</p>
<div class="mt-2 text-xs">面板ID: {{ id }} - 标题: {{ title }}</div>
</div>
</div>
</div>
</div>
</template>
@@ -116,6 +150,10 @@ const props = defineProps({
maximized: {
type: Boolean,
default: false
},
content: {
type: Object,
default: null
}
});

View File

@@ -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的的隐藏列表中。