修改内容区显示逻辑,改为配置驱动

This commit is contained in:
zqm
2025-12-26 17:12:36 +08:00
parent e89d3254e8
commit 09e4076635
5 changed files with 213 additions and 210 deletions

View File

@@ -50,7 +50,7 @@
@mousedown="onResizeStart('w', $event)"
></div>
<!-- 顶部标题栏 -->
<div v-if="showTitleBar" class="vs-title-bar" :class="{ 'cursor-move': !isMaximized }" @mousedown="onDragStart">
<div v-if="shouldShowTitleBar" class="vs-title-bar" :class="{ 'cursor-move': !isMaximized }" @mousedown="onDragStart">
<div class="vs-title-left">
<div class="vs-app-icon" aria-label="AppIcon">
<svg class="vs-icon" viewBox="0 0 22.4 22.4" aria-hidden="true">
@@ -100,40 +100,14 @@
<!-- 内容区域 -->
<div class="vs-content">
<!-- 优先显示receivedContent停靠的外部内容 -->
<template v-if="receivedContent && receivedContent.length > 0">
<!-- 调试信息 -->
<div v-if="false" style="position: absolute; top: 0; left: 0; background: red; color: white; padding: 5px; z-index: 9999;">
DEBUG: receivedContent长度 = {{ receivedContent.length }}
<div v-for="(item, index) in receivedContent" :key="`debug-${index}`">
TabPage {{ index }}: {{ item.tabPage.id }} - {{ item.tabPage.title }} ({{ item.tabPage.panels.length }} panels)
</div>
</div>
<TabPage
v-for="(item, index) in receivedContent"
:key="item.tabPage.id"
:id="item.tabPage.id"
:title="item.tabPage.title"
:panels="item.tabPage.panels"
:tabPosition="'bottom'"
@tabDragStart="() => {}"
@tabDragMove="() => {}"
@tabDragEnd="() => {}"
@maximize="onPanelMaximize"
/>
</template>
<!-- 如果没有receivedContent但有slot内容显示slot内容 -->
<template v-else-if="$slots.default">
<!-- 直接渲染插槽内容Render组件会自动处理children -->
<slot></slot>
</template>
</div>
</div>
</template>
<script setup>
import { defineProps, computed, ref, onMounted, onUnmounted, watch, defineExpose } from 'vue'
import { defineProps, computed, ref, onMounted, onUnmounted, watch, defineExpose, useSlots } from 'vue'
import { emitEvent, EVENT_TYPES, globalEventListenerManager } from './eventBus'
import TabPage from './TabPage.vue'
import Panel from './Panel.vue'
@@ -170,12 +144,44 @@ const originalPosition = ref({
// 保存最大化前的位置和大小,用于还原
const maximizedFromPosition = ref(null)
// 存储接收到的外部Area内容
const receivedContent = ref([])
// 不再需要存储接收到的外部Area内容改为通过children配置管理
// 组件引用
const areaRef = ref(null)
// 获取插槽
const slots = useSlots();
// 计算属性:是否显示标题栏
const shouldShowTitleBar = computed(() => {
// 基础条件props.showTitleBar为true
if (!props.showTitleBar) return false;
// 获取插槽内容
if (slots.default) {
const slotChildren = slots.default();
// 如果没有插槽内容,显示标题栏
if (slotChildren.length === 0) return true;
// 如果插槽内容不是TabPage显示标题栏
const firstChild = slotChildren[0];
if (firstChild.type.name !== 'TabPage') return true;
// 获取TabPage的插槽内容
const tabPageSlots = firstChild.children?.default ? firstChild.children.default() : [];
// 如果TabPage包含多个Panel显示标题栏
if (tabPageSlots.length !== 1) return true;
// 如果TabPage只包含一个Panel不显示标题栏
return false;
}
// 默认显示标题栏
return true;
});
// 拖拽相关状态
const isDragging = ref(false)
const dragStartPos = ref({ x: 0, y: 0 })
@@ -269,9 +275,20 @@ const onPanelMaximize = (panelId) => {
// // console.log('🔸 Area接收最大化事件 - Panel ID:', panelId)
// 检查内容区是否只有一个Panel
const panelChildren = $slots.default ? $slots.default() : []
const isSinglePanel = panelChildren.length === 1
// // console.log('🔸 检查是否单Panel模式:', { panelChildren: panelChildren.length, isSinglePanel })
const slotChildren = slots.default ? slots.default() : []
let isSinglePanel = false
// 查找TabPage组件
const tabPages = slotChildren.filter(child => child.type.name === 'TabPage')
if (tabPages.length === 1) {
// 获取TabPage的插槽内容
const tabPageSlots = tabPages[0].children?.default ? tabPages[0].children.default() : []
// 如果TabPage只有一个Panel认为是单Panel模式
isSinglePanel = tabPageSlots.length === 1
}
// // console.log('🔸 检查是否单Panel模式:', { tabPages: tabPages.length, isSinglePanel })
if (isSinglePanel) {
// // console.log('🔸 单Panel模式切换Area最大化状态')
@@ -659,133 +676,27 @@ const mergeAreaContent = (sourceArea) => {
}
try {
const isEmpty = receivedContent.value.length === 0
if (isEmpty) {
// 4.2.1 如果目标Area内容区为空将源Area内容区的子组件添加到目标Area内容区
// console.log('[Area] 目标Area为空添加源Area的子组件')
// 处理源Area的所有tabPages支持两种模式tabPages和children
let tabPagesData = []
if (sourceArea.children) {
// 统一处理children结构
const childrenArray = Array.isArray(sourceArea.children) ? sourceArea.children : [sourceArea.children]
for (const child of childrenArray) {
if (child.type === 'TabPage' && child.children) {
tabPagesData.push({
id: child.id,
title: child.title,
panels: Array.isArray(child.children) ? child.children : (child.children.type === 'Panel' ? [child.children] : [])
// 发送合并请求事件,让父组件处理配置修改
emitEvent(EVENT_TYPES.AREA_MERGE_REQUEST, {
sourceArea: sourceArea,
targetAreaId: props.id
}, {
source: { component: 'Area', areaId: props.id }
})
}
}
}
if (tabPagesData.length > 0) {
tabPagesData.forEach((tabPage, tabIndex) => {
// 保持原有的tabPage ID确保Vue组件状态连续性
const tabPageId = `merged-tabpage-${tabPage.id}`
const newPanels = (tabPage.panels || []).map((panel, panelIndex) => {
// 保持原有Panel ID不变确保Vue响应式和状态稳定性
// console.log(`[Area] 添加Panel: ${panel.id}`)
return {
...panel,
maximized: true
}
})
receivedContent.value.push({
id: `received-${tabPageId}`,
title: tabPage.title || `标签页${tabIndex + 1}`,
tabPage: {
...tabPage,
id: tabPageId,
panels: newPanels
},
panels: newPanels
})
// console.log(`[Area] 成功添加TabPage: ${tabPage.title} (${newPanels.length} 个Panel)`)
})
}
// 触发事件通知父组件将源Area保存到隐藏列表
emitEvent(EVENT_TYPES.AREA_MERGED, {
sourceArea: sourceArea,
targetAreaId: props.id,
targetAreaHasContent: false, // 目标Area为空
operation: 'add-children',
addedTabPages: receivedContent.value
targetAreaHasContent: false, // 简化处理,由父组件判断
operation: 'merge-children',
sourceTabPages: sourceArea.children ? [sourceArea.children] : []
}, {
source: { component: 'Area', areaId: props.id }
})
return true
} else {
// 4.2.3 如果目标Area已有TabPage合并TabPage标签页
// console.log('[Area] 目标Area已有TabPage合并TabPage标签页')
// 获取第一个现有的TabPage作为合并目标
const existingTabPage = receivedContent.value[0]
if (!existingTabPage) {
// console.error('[Area] 现有TabPage不存在')
return false
}
// 处理源Area的所有children统一使用children结构
let tabPagesData = []
if (sourceArea.children) {
// 统一处理children结构
const childrenArray = Array.isArray(sourceArea.children) ? sourceArea.children : [sourceArea.children]
for (const child of childrenArray) {
if (child.type === 'TabPage' && child.children) {
tabPagesData.push({
id: child.id,
title: child.title,
panels: Array.isArray(child.children) ? child.children : (child.children.type === 'Panel' ? [child.children] : [])
})
}
}
}
if (tabPagesData.length > 0) {
tabPagesData.forEach((sourceTabPage, tabIndex) => {
if (sourceTabPage && sourceTabPage.panels) {
// 保持原有Panel ID不变避免Vue组件重新创建和状态丢失
const newPanels = sourceTabPage.panels.map((panel, panelIndex) => {
// // console.log(`[Area] 合并Panel到现有TabPage: ${panel.id}`)
return {
...panel,
maximized: true
}
})
// 将新的Panel添加到现有TabPage保持ID连续性
existingTabPage.tabPage.panels.push(...newPanels)
// existingTabPage.panels 是旧引用,保持结构一致性但避免重复添加
// // console.log(`[Area] 成功合并 ${newPanels.length} 个Panel到现有TabPage`)
}
})
}
// 触发事件通知父组件将源Area及其TabPage组件保存到隐藏列表
emitEvent(EVENT_TYPES.AREA_MERGED, {
sourceArea: sourceArea,
targetAreaId: props.id,
targetAreaHasContent: true, // 目标Area已有内容
operation: 'merge-tabpages',
sourceTabPages: tabPagesData
}, {
source: { component: 'Area', areaId: props.id }
})
// 更新完成
// // console.log(`[Area] 合并完成现有TabPage共有 ${existingTabPage.tabPage.panels.length} 个Panel`)
return true
}
} catch (error) {
// console.error('[Area] 合并Area内容时出错:', error)
return false

View File

@@ -225,6 +225,37 @@ const handleMainAreaResizeBarEnd = (id) => {
areaActions.handleResizeEnd(id);
};
// 处理Area合并请求
const handleAreaMergeRequest = (data) => {
const { sourceArea, targetAreaId } = data;
// 查找目标Area
const targetArea = floatingAreas.value.find(area => area.id === targetAreaId);
if (!targetArea) return;
// 直接修改目标Area的children配置
if (!targetArea.children) {
targetArea.children = [];
}
// 处理源Area的children
if (sourceArea.children) {
const childrenArray = Array.isArray(sourceArea.children) ? sourceArea.children : [sourceArea.children];
childrenArray.forEach(child => {
if (child.type === 'TabPage') {
// 添加到目标Area的children中
if (!Array.isArray(targetArea.children)) {
targetArea.children = [targetArea.children];
}
targetArea.children.push(child);
}
});
}
// 从floatingAreas中移除源Area
floatingAreas.value = floatingAreas.value.filter(area => area.id !== sourceArea.id);
};
const getMainAreaResizeBarStyle = (resizeBar) => {
return areaActions.getResizeBarStyle(resizeBar);
};
@@ -243,6 +274,7 @@ const setupEventListeners = () => {
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' }));
unsubscribeFunctions.push(eventBus.on(EVENT_TYPES.AREA_MERGE_REQUEST, handleAreaMergeRequest, { componentId: 'dock-layout' }));
// Tab相关事件
unsubscribeFunctions.push(eventBus.on(EVENT_TYPES.TAB_CHANGE, onTabChange, { componentId: 'dock-layout' }));

View File

@@ -397,22 +397,40 @@ const onDragStart = (e) => {
console.log(`[Panel:${props.id}] 开始拖拽, dragId: ${currentDragId}`)
// 2. 使用事件总线触发拖拽开始事件,包含统一的 dragId 和标准化数据格式
const areaId = getCurrentAreaId();
// 2. 使用事件总线触发面板拖拽开始事件,包含统一的 dragId 和标准化数据格式
emitEvent(EVENT_TYPES.PANEL_DRAG_START, {
dragId: currentDragId,
panelId: props.id,
areaId: getCurrentAreaId(),
areaId: areaId,
position: { x: e.clientX, y: e.clientY },
timestamp: Date.now()
}, {
source: { component: 'Panel', panelId: props.id, dragId: currentDragId }
})
// 3. 防止文本选择和默认行为
// 3. 同时触发Area拖拽开始事件实现通过Panel标题栏拖拽Area
emitEvent(EVENT_TYPES.AREA_DRAG_START, {
dragId: currentDragId,
areaId: areaId,
event: e,
element: null,
position: { x: e.clientX, y: e.clientY },
clientX: e.clientX,
clientY: e.clientY,
startLeft: 0,
startTop: 0,
timestamp: Date.now()
}, {
source: { component: 'Panel', panelId: props.id, dragId: currentDragId }
})
// 4. 防止文本选择和默认行为
e.preventDefault()
e.stopPropagation()
// 4. 添加Document事件监听器使用一次性变量避免内存泄漏
// 5. 添加Document事件监听器使用一次性变量避免内存泄漏
addDocumentDragListeners()
}
};
@@ -424,16 +442,31 @@ const onDragMove = (e) => {
e.preventDefault();
e.stopPropagation();
// 使用事件总线触发拖拽移动事件,包含 dragId
const areaId = getCurrentAreaId();
// 1. 使用事件总线触发面板拖拽移动事件,包含 dragId
emitEvent(EVENT_TYPES.PANEL_DRAG_MOVE, {
dragId: currentDragId,
panelId: props.id,
areaId: getCurrentAreaId(),
areaId: areaId,
position: { x: e.clientX, y: e.clientY },
timestamp: Date.now()
}, {
source: { component: 'Panel', panelId: props.id, dragId: currentDragId }
})
// 2. 同时触发Area拖拽移动事件实现通过Panel标题栏拖拽Area
emitEvent(EVENT_TYPES.AREA_DRAG_MOVE, {
dragId: currentDragId,
areaId: areaId,
event: e,
position: { x: e.clientX, y: e.clientY },
clientX: e.clientX,
clientY: e.clientY,
timestamp: Date.now()
}, {
source: { component: 'Panel', panelId: props.id, dragId: currentDragId }
})
}
};
@@ -444,20 +477,33 @@ const onDragEnd = () => {
console.log(`[Panel:${props.id}] 结束拖拽, dragId: ${currentDragId}`)
// 使用事件总线触发拖拽结束事件,包含 dragId
const areaId = getCurrentAreaId();
// 1. 使用事件总线触发面板拖拽结束事件,包含 dragId
emitEvent(EVENT_TYPES.PANEL_DRAG_END, {
dragId: currentDragId,
panelId: props.id,
areaId: getCurrentAreaId(),
areaId: areaId,
timestamp: Date.now()
}, {
source: { component: 'Panel', panelId: props.id, dragId: currentDragId }
})
// 使用统一的清理方法,确保一致性和完整性
// 2. 同时触发Area拖拽结束事件实现通过Panel标题栏拖拽Area
emitEvent(EVENT_TYPES.AREA_DRAG_END, {
dragId: currentDragId,
areaId: areaId,
event: null,
position: null,
timestamp: Date.now()
}, {
source: { component: 'Panel', panelId: props.id, dragId: currentDragId }
})
// 3. 使用统一的清理方法,确保一致性和完整性
cleanupDragEventListeners()
// 重置 dragId
// 4. 重置 dragId
currentDragId = null
}
};

View File

@@ -87,10 +87,16 @@
<!-- Tab页内容区域 -->
<div class="tab-content">
<!-- 直接渲染插槽内容 -->
<slot></slot>
<!-- 使用Render组件渲染children配置 -->
<div v-for="child in Array.isArray(children) ? children : [children]" :key="child.id" style="width: 100%; height: 100%;">
<Render
:type="child.type"
:config="child"
/>
</div>
<!-- 空状态提示 -->
<div v-if="slotItems.length === 0" class="tab-empty">
<div v-if="!children || (Array.isArray(children) && children.length === 0)" class="tab-empty">
<span>没有可显示的内容</span>
</div>
</div>
@@ -126,10 +132,9 @@
</template>
<script setup>
import { defineProps, ref, onMounted, onUnmounted, computed, useSlots } from 'vue'
import { defineProps, ref, onMounted, onUnmounted, computed } from 'vue'
import { emitEvent, EVENT_TYPES } from './eventBus'
const slots = useSlots()
import Render from './Render.vue'
const props = defineProps({
id: { type: String, required: true },
@@ -157,34 +162,40 @@ let isDragging = false
let dragIndex = -1
let currentDragId = null
// 计算属性:获取插槽项的props
// 计算属性:获取子组件项的props
const slotItems = computed(() => {
if (!slots.default) return []
const slotChildren = slots.default()
return slotChildren.map(child => child?.props || {})
if (!props.children) return []
const childrenArray = Array.isArray(props.children) ? props.children : [props.children]
return childrenArray.map(child => child || {})
})
// 计算属性:控制标签栏的显示
const shouldShowTabs = computed(() => {
// 显示标签栏的条件showTabs为true且有子组件
return props.showTabs && slots.default && slots.default().length > 0
// 显示标签栏的条件showTabs为true且有多个子组件
if (!props.children) return false
const childrenCount = Array.isArray(props.children) ? props.children.length : 1
return props.showTabs && childrenCount > 1
})
// 设置激活的标签页
const setActiveTab = (index) => {
const slotChildren = slots.default ? slots.default() : []
if (index >= 0 && index < slotChildren.length) {
if (!props.children) return
const childrenArray = Array.isArray(props.children) ? props.children : [props.children]
if (index >= 0 && index < childrenArray.length) {
activeTabIndex.value = index
emitEvent(EVENT_TYPES.TAB_CHANGE, { index, tab: slotChildren[index] })
emitEvent(EVENT_TYPES.TAB_CHANGE, { index, tab: childrenArray[index] })
}
}
// 组件挂载后,如果有子组件且没有激活的标签,默认激活第一个
onMounted(() => {
const slotChildren = slots.default ? slots.default() : []
if (slotChildren.length > 0 && activeTabIndex.value === -1) {
if (props.children) {
const childrenCount = Array.isArray(props.children) ? props.children.length : 1
if (childrenCount > 0 && activeTabIndex.value === -1) {
setActiveTab(0)
}
}
})
// 关闭标签页
@@ -202,13 +213,20 @@ const onTabDragStart = (index, event) => {
// 生成统一的 dragId
currentDragId = `tabpage_${props.id}_${index}_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`
// 获取tabId
let tabId = null
if (props.children) {
const childrenArray = Array.isArray(props.children) ? props.children : [props.children]
tabId = childrenArray[index]?.id || `tab-${index}`
}
// 传递标签页索引和鼠标位置,包含 dragId
emitEvent(EVENT_TYPES.TAB_DRAG_START, {
dragId: currentDragId,
clientX: event.clientX,
clientY: event.clientY,
tabIndex: index,
tabId: $slots.default()[index]?.props?.id
tabId: tabId
}, {
source: { component: 'TabPage', tabPageId: props.id, tabIndex: index, dragId: currentDragId }
})

View File

@@ -612,20 +612,6 @@ class DragStateManager {
console.log('🔍 _onDragEvent 接收到的数据:', { eventType, dragId, componentType, data });
// 从事件数据中提取 dragId如果没有则根据组件类型推断
let actualDragId = dragId;
if (!actualDragId) {
if (data.panelId) {
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()}_${Math.random().toString(36).substring(2, 9)}`;
} else if (data.areaId) {
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) {
@@ -638,6 +624,11 @@ class DragStateManager {
}
}
// 从事件数据中提取 dragId如果没有则使用统一的生成方法
let actualDragId = dragId || this._generateDragId(actualComponentType);
console.log('🔍 _onDragEvent 处理后的 dragId:', { originalDragId: dragId, actualDragId });
// 准备标准化的拖拽数据
const dragData = {
...data,
@@ -1799,15 +1790,20 @@ class DragStateManager {
...options
} = eventData;
// 检查是否有其他活跃拖拽
if (this.activeDrags.size > 0) {
console.warn('检测到其他活跃拖拽,跳过创建新拖拽状态');
// 如果已经有活跃拖拽直接返回现有拖拽的dragId
return Array.from(this.activeDrags.keys())[0];
// 使用传入的 dragId如果没有则生成新的
const dragId = incomingDragId || this._generateDragId(type);
// 检查是否已存在相同 dragId 的拖拽状态
if (this.activeDrags.has(dragId)) {
console.log(`⚠️ 拖拽状态已存在: ${dragId},跳过创建`);
return dragId;
}
// 使用传入的 dragId如果没有则生成新的
const dragId = incomingDragId || `area-${areaId}-${Date.now()}`;
// 检查是否有其他活跃拖拽
if (this.activeDrags.size > 0) {
console.warn('检测到其他活跃拖拽,暂停之前的拖拽');
this.cancelAllDrags();
}
// 创建拖拽状态
const dragState = new DragState(dragId, DRAG_AREA_TYPES.AREA, element, {