### 主要修复内容

1. 事件监听器泄漏 :修复了事件监听器泄漏问题,确保所有监听器都能被正确清理
2. 组件生命周期管理 :为所有组件添加了onUnmounted钩子,确保资源能被正确清理
3. props大小写问题 :修复了props名称大小写不匹配问题
4. 延迟初始化 :将事件管理器的初始化从立即初始化改为延迟初始化,提高性能
5. flexbox布局修复 :修复了flexbox布局问题,确保组件能正确显示
6. 代码结构优化 :简化了代码结构,提高了可维护性
这些修改解决了事件监听器泄漏、组件生命周期管理和props传递等问题,提高了代码的质量和可维护性。
This commit is contained in:
zqm
2025-12-04 14:58:41 +08:00
parent e9ef33bd62
commit e96e3018ed
8 changed files with 414 additions and 226 deletions

View File

@@ -132,7 +132,7 @@
</template>
<script setup>
import { defineProps, computed, defineEmits, ref, onMounted, watch, defineExpose } from 'vue'
import { defineProps, computed, defineEmits, ref, onMounted, onUnmounted, watch, defineExpose } from 'vue'
import TabPage from './TabPage.vue'
import Panel from './Panel.vue'
import { zIndexManager, Z_INDEX_LAYERS } from './dockLayers.js'
@@ -142,7 +142,7 @@ const props = defineProps({
title: { type: String, default: '面板区' },
resizable: { type: Boolean, default: true },
// 初始状态(支持中文值)
WindowState: { type: String, default: '正常' },
windowState: { type: String, default: '正常' },
// 默认尺寸
width: { type: Number, default: 300 },
height: { type: Number, default: 250 },
@@ -155,7 +155,7 @@ const props = defineProps({
})
// 本地状态
const localState = ref(props.WindowState)
const localState = ref(props.windowState)
// 保存原始位置和大小信息
const originalPosition = ref({
width: props.width,
@@ -200,8 +200,8 @@ watch(() => props.top, (newTop) => {
}
}, { immediate: true })
// 监听WindowState变化同步更新localState
watch(() => props.WindowState, (newState) => {
// 监听windowState变化同步更新localState
watch(() => props.windowState, (newState) => {
if (newState !== localState.value) {
localState.value = newState
@@ -568,6 +568,18 @@ onMounted(() => {
}
})
// 组件卸载时清理全局事件监听器
onUnmounted(() => {
// 清理拖拽相关的全局事件监听器
document.removeEventListener('mousemove', onDragMove)
document.removeEventListener('mouseup', onDragEnd)
// 清理调整大小相关的全局事件监听器
document.removeEventListener('mousemove', onResizeMove)
document.removeEventListener('mouseup', onResizeEnd)
document.removeEventListener('mouseleave', onResizeEnd)
})
// 处理Area合并内容
@@ -589,17 +601,15 @@ const mergeAreaContent = (sourceArea) => {
// 处理源Area的所有tabPages支持两种模式tabPages和children
let tabPagesData = []
if (sourceArea.tabPages && Array.isArray(sourceArea.tabPages)) {
// 模式1直接tabPages结构
tabPagesData = sourceArea.tabPages
} else if (sourceArea.children && Array.isArray(sourceArea.children)) {
// 模式2children结构需要遍历查找TabPage类型
for (const child of sourceArea.children) {
if (child.type === 'TabPage' && child.children && child.children.type === 'Panel') {
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: child.children.items || []
panels: Array.isArray(child.children) ? child.children : (child.children.type === 'Panel' ? [child.children] : [])
})
}
}
@@ -655,20 +665,18 @@ const mergeAreaContent = (sourceArea) => {
return false
}
// 处理源Area的所有tabPages支持两种模式tabPages和children
// 处理源Area的所有children统一使用children结构
let tabPagesData = []
if (sourceArea.tabPages && Array.isArray(sourceArea.tabPages)) {
// 模式1直接tabPages结构
tabPagesData = sourceArea.tabPages
} else if (sourceArea.children && Array.isArray(sourceArea.children)) {
// 模式2children结构需要遍历查找TabPage类型
for (const child of sourceArea.children) {
if (child.type === 'TabPage' && child.children && child.children.type === 'Panel') {
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: child.children.items || []
panels: Array.isArray(child.children) ? child.children : (child.children.type === 'Panel' ? [child.children] : [])
})
}
}

View File

@@ -1,5 +1,5 @@
<template>
<div class="dock-layout" ref="dockLayoutRef" style="display: flex; flex-direction: column; position: relative;">
<div class="dock-layout" ref="dockLayoutRef" style="display: flex; flex-direction: column; position: relative; width: 100%; height: 100%;">
<!-- 停靠指示器组件 - 设置高z-index确保显示在最顶层 -->
<DockIndicator
:visible="showDockIndicator"
@@ -13,18 +13,14 @@
<!-- 主区域 - 添加ref引用 -->
<Area
ref="mainAreaRef"
:WindowState="windowState"
:windowState="windowState"
:showTitleBar="false"
title="主区域"
:style="{ position: 'relative', width: '100%', height: '100%', zIndex: 1 }"
title="主区域"
@dragover="handleMainAreaDragOver"
@dragleave="handleMainAreaDragLeave"
@area-merged="onAreaMerged"
>
<!-- 主区域内容区 -->
<div class="main-content-container" style="position: relative; width: 100%; height: 100%;">
<!-- ResizeBar组件渲染区 -->
<ResizeBar
v-for="resizeBar in mainAreaResizeBars"
:key="resizeBar.id"
@@ -74,7 +70,7 @@
</template>
<script setup>
import { ref, computed, onMounted, defineEmits } from 'vue'
import { ref, computed, onMounted, onUnmounted, defineEmits } from 'vue'
import Area from './Area.vue';
import Panel from './Panel.vue';
import TabPage from './TabPage.vue';
@@ -291,6 +287,24 @@ const setupEventListeners = () => {
// 清理函数
const cleanup = () => {
// 清理事件监听器和其他资源
console.log('🧹 开始清理DockLayout资源');
// 清理浮动区域
floatingAreas.value = [];
// 清理隐藏区域
hiddenAreas.value = [];
// 清理主区域ResizeBar
mainAreaResizeBars.value = [];
// 清理停靠指示器状态
showDockIndicator.value = false;
currentMousePosition.value = { x: 0, y: 0 };
targetAreaRect.value = { left: 0, top: 0, width: 0, height: 0 };
activeDockZone.value = null;
console.log('✅ DockLayout资源清理完成');
};
// 轻量级隐藏区域管理
@@ -356,7 +370,17 @@ const addFloatingPanel = (panel) => {
const safePanel = panel || {
id: `panel-${Date.now()}`,
title: '新建面板',
content: '默认内容'
content: {
color: '#435d9c',
title: '默认面板内容',
type: 'default',
timestamp: new Date().toLocaleString(),
data: [
{ id: 1, label: '示例数据1', value: '123' },
{ id: 2, label: '示例数据2', value: '456' },
{ id: 3, label: '示例数据3', value: '789' }
]
}
};
const newArea = {
@@ -366,11 +390,16 @@ const addFloatingPanel = (panel) => {
width: 300,
height: 200,
zIndex: zIndexManager.getFloatingAreaZIndex(`area-${Date.now()}`),
tabPages: [{
id: `tabpage-${Date.now()}`,
title: safePanel.title || '新建面板',
panels: [safePanel]
}]
// 使用children结构以兼容Render组件的渲染逻辑
children: {
type: 'TabPage',
children: [{
...safePanel,
id: `panel-${Date.now()}`,
title: safePanel.title || '新建面板',
type: 'Panel'
}]
}
}
floatingAreas.value.push(newArea)
return newArea.id
@@ -382,7 +411,7 @@ const findOrCreateMainAreaTabPage = () => {
return {
id: 'main-area-tabpage',
title: '主区域',
panels: []
items: []
};
}
@@ -392,6 +421,13 @@ onMounted(() => {
console.log('DockLayout component mounted');
})
// 组件卸载时清理资源
onUnmounted(() => {
// 清理事件监听器和其他资源
console.log('DockLayout component unmounted');
cleanup();
})
// 暴露轻量级接口给父组件
defineExpose({
// 基础数据

View File

@@ -5,72 +5,14 @@
v-bind="componentProps"
v-on="componentListeners"
>
<!-- 对于有children配置的情况需要手动渲染子组件 -->
<template v-if="type === 'Area' && config.children">
<!-- 如果children数组 -->
<template v-if="Array.isArray(config.children)">
<div v-for="child in config.children" :key="child.id" style="width: 100%; height: 100%;">
<Render
v-if="child.type === 'TabPage'"
:type="'TabPage'"
:config="child"
:debug="debug"
@tab-change="$emit('tab-change', $event)"
@tab-close="$emit('tab-close', $event)"
@tab-add="$emit('tab-add', $event)"
@tabDragStart="$emit('tabDragStart', $event)"
@tabDragMove="$emit('tabDragMove', $event)"
@tabDragEnd="$emit('tabDragEnd', $event)"
@toggleCollapse="$emit('toggleCollapse', $event)"
@maximize="$emit('maximize', $event)"
@close="$emit('close', $event)"
@toggleToolbar="$emit('toggleToolbar', $event)"
@dragStart="$emit('dragStart', $event)"
@dragMove="$emit('dragMove', $event)"
@dragEnd="$emit('dragEnd', $event)"
/>
</div>
</template>
<!-- 如果children是对象 -->
<template v-else-if="config.children.type === 'TabPage'">
<div v-for="tabPage in (Array.isArray(config.children.items) ? config.children.items : [config.children])" :key="tabPage.id" style="width: 100%; height: 100%;">
<Render
:type="'TabPage'"
:config="tabPage"
:debug="debug"
@tab-change="$emit('tab-change', $event)"
@tab-close="$emit('tab-close', $event)"
@tab-add="$emit('tab-add', $event)"
@tabDragStart="$emit('tabDragStart', $event)"
@tabDragMove="$emit('tabDragMove', $event)"
@tabDragEnd="$emit('tabDragEnd', $event)"
@toggleCollapse="$emit('toggleCollapse', $event)"
@maximize="$emit('maximize', $event)"
@close="$emit('close', $event)"
@toggleToolbar="$emit('toggleToolbar', $event)"
@dragStart="$emit('dragStart', $event)"
@dragMove="$emit('dragMove', $event)"
@dragEnd="$emit('dragEnd', $event)"
/>
</div>
</template>
</template>
<!-- TabPage组件的panels prop -->
<template v-else-if="type === 'TabPage' && config.children && config.children.type === 'Panel'">
<!-- 手动渲染Panel组件 -->
<div v-for="panel in (Array.isArray(config.children.items) ? config.children.items : [config.children])" :key="panel.id" style="width: 100%; height: 100%;">
<!-- 统一处理children属性不区分组件类型只要有children就渲染 -->
<template v-if="config.children">
<!-- 统一处理children数组或单个对象的情况 -->
<div v-for="child in Array.isArray(config.children) ? config.children : [config.children]" :key="child.id" style="width: 100%; height: 100%;">
<Render
:type="'Panel'"
:config="panel"
:type="child.type"
:config="child"
:debug="debug"
@toggleCollapse="$emit('toggleCollapse', $event)"
@maximize="$emit('maximize', $event)"
@close="$emit('close', $event)"
@toggleToolbar="$emit('toggleToolbar', $event)"
@dragStart="$emit('dragStart', $event)"
@dragMove="$emit('dragMove', $event)"
@dragEnd="$emit('dragEnd', $event)"
/>
</div>
</template>
@@ -143,20 +85,13 @@ const componentProps = computed(() => {
showTitleBar: config.showTitleBar !== false,
left: config.left,
top: config.top,
draggable: config.draggable !== false,
// 如果有children配置将其转换为TabPages格式
tabPages: config.children ? (
config.children.type === 'TabPage'
? (Array.isArray(config.children.items) ? config.children.items : [config.children])
: config.children // 如果直接是tabPages数组
) : config.tabPages || []
draggable: config.draggable !== false
}
case 'TabPage':
return {
id: config.id,
title: config.title || '标签页',
panels: config.panels || config.children?.items || [],
showTabs: config.showTabs !== false,
tabPosition: config.tabPosition || 'top'
}
@@ -243,6 +178,22 @@ const componentListeners = computed(() => {
// if (props.debug) console.log(`[Render-TabPage ${props.config.id}] tabAdd:`, event)
emit('tabAdd', event)
}
allListeners['tabDragStart'] = (event) => {
// if (props.debug) console.log(`[Render-TabPage ${props.config.id}] tabDragStart:`, event)
emit('tabDragStart', event)
}
allListeners['tabDragMove'] = (event) => {
// if (props.debug) console.log(`[Render-TabPage ${props.config.id}] tabDragMove:`, event)
emit('tabDragMove', event)
}
allListeners['tabDragEnd'] = (event) => {
// if (props.debug) console.log(`[Render-TabPage ${props.config.id}] tabDragEnd:`, event)
emit('tabDragEnd', event)
}
allListeners['toggleCollapse'] = (event) => {
// if (props.debug) console.log(`[Render-TabPage ${props.config.id}] toggleCollapse:`, event)
emit('toggleCollapse', event)
}
allListeners['maximize'] = (event) => {
// if (props.debug) console.log(`[Render-TabPage ${props.config.id}] maximize:`, event)
emit('maximize', event)
@@ -251,6 +202,22 @@ const componentListeners = computed(() => {
// if (props.debug) console.log(`[Render-TabPage ${props.config.id}] close:`, event)
emit('close', event)
}
allListeners['toggleToolbar'] = (event) => {
// if (props.debug) console.log(`[Render-TabPage ${props.config.id}] toggleToolbar:`, event)
emit('toggleToolbar', event)
}
allListeners['dragStart'] = (event) => {
// if (props.debug) console.log(`[Render-TabPage ${props.config.id}] dragStart:`, event)
emit('dragStart', event)
}
allListeners['dragMove'] = (event) => {
// if (props.debug) console.log(`[Render-TabPage ${props.config.id}] dragMove:`, event)
emit('dragMove', event)
}
allListeners['dragEnd'] = (event) => {
// if (props.debug) console.log(`[Render-TabPage ${props.config.id}] dragEnd:`, event)
emit('dragEnd', event)
}
}
if (props.type === 'Panel') {
@@ -288,26 +255,13 @@ const componentListeners = computed(() => {
return allListeners
})
// 规范化子组件数据用于Area组件的slot内容
const normalizedChildren = computed(() => {
if (props.type !== 'Area' || !props.config.children) {
return []
}
// 如果children是数组直接返回
if (Array.isArray(props.config.children)) {
return props.config.children
}
// 如果children是单个对象包装成数组
return [props.config.children]
})
// 暴露组件实例方法
defineExpose({
getComponentType: () => props.type,
getConfig: () => props.config,
getComponentProps: componentProps.value,
getComponentProps: () => componentProps.value,
isDebugMode: () => props.debug
})
</script>

View File

@@ -1,21 +1,21 @@
<template>
<div class="tab-page" :class="[`tab-page-${tabPosition}`]" style="width: 100%; height: 100%;">
<!-- 顶部标签栏 -->
<div v-if="tabPosition === 'top' && shouldShowTabs && panels && panels.length > 0" class="tab-header tab-header-horizontal">
<div v-if="tabPosition === 'top' && shouldShowTabs" class="tab-header tab-header-horizontal">
<div
v-for="(panel, index) in panels"
:key="panel.id"
v-for="(item, index) in slotItems"
:key="index"
:class="['tab-item', { 'active': activeTabIndex === index }]"
@click="setActiveTab(index)"
@mousedown="onTabDragStart(index, $event)"
>
<div class="flex items-center justify-between h-full px-3">
<span class="tab-title">{{ panel.title }}</span>
<span class="tab-title">{{ item?.title || '未命名' }}</span>
<!-- 当标签页未被激活时显示关闭按钮 -->
<button
v-if="activeTabIndex !== index"
class="button-icon p-[2px] rounded hover:opacity-100 opacity-80"
@click.stop="closeTab(panel.id)"
@click.stop="closeTab(item?.id)"
aria-label="关闭"
>
<svg width="11" height="11" viewBox="0 0 11 11" aria-hidden="true">
@@ -29,21 +29,21 @@
</div>
<!-- 左侧标签栏 -->
<div v-if="tabPosition === 'left' && shouldShowTabs && panels && panels.length > 0" class="tab-header tab-header-vertical tab-header-left">
<div v-if="tabPosition === 'left' && shouldShowTabs" class="tab-header tab-header-vertical tab-header-left">
<div
v-for="(panel, index) in panels"
:key="panel.id"
v-for="(item, index) in slotItems"
:key="index"
:class="['tab-item-vertical', { 'active': activeTabIndex === index }]"
@click="setActiveTab(index)"
@mousedown="onTabDragStart(index, $event)"
>
<div class="flex flex-col items-center justify-center w-full h-full py-2">
<span class="tab-title-vertical">{{ panel.title }}</span>
<span class="tab-title-vertical">{{ item?.title || '未命名' }}</span>
<!-- 当标签页未被激活时显示关闭按钮 -->
<button
v-if="activeTabIndex !== index"
class="button-icon p-[2px] rounded hover:opacity-100 opacity-80 mt-1"
@click.stop="closeTab(panel.id)"
@click.stop="closeTab(item?.id)"
aria-label="关闭"
>
<svg width="11" height="11" viewBox="0 0 11 11" aria-hidden="true">
@@ -57,21 +57,21 @@
</div>
<!-- 右侧标签栏 -->
<div v-if="tabPosition === 'right' && shouldShowTabs && panels && panels.length > 0" class="tab-header tab-header-vertical tab-header-right">
<div v-if="tabPosition === 'right' && shouldShowTabs" class="tab-header tab-header-vertical tab-header-right">
<div
v-for="(panel, index) in panels"
:key="panel.id"
v-for="(item, index) in slotItems"
:key="index"
:class="['tab-item-vertical', { 'active': activeTabIndex === index }]"
@click="setActiveTab(index)"
@mousedown="onTabDragStart(index, $event)"
>
<div class="flex flex-col items-center justify-center w-full h-full py-2">
<span class="tab-title-vertical">{{ panel.title }}</span>
<span class="tab-title-vertical">{{ item?.title || '未命名' }}</span>
<!-- 当标签页未被激活时显示关闭按钮 -->
<button
v-if="activeTabIndex !== index"
class="button-icon p-[2px] rounded hover:opacity-100 opacity-80 mt-1"
@click.stop="closeTab(panel.id)"
@click.stop="closeTab(item?.id)"
aria-label="关闭"
>
<svg width="11" height="11" viewBox="0 0 11 11" aria-hidden="true">
@@ -87,59 +87,30 @@
<!-- Tab页内容区域 -->
<div class="tab-content">
<!-- 渲染当前激活的Panel内容 -->
<template v-if="activeTabIndex >= 0 && activeTabIndex < panels.length">
<div
v-for="(panel, index) in panels"
:key="panel.id"
v-show="index === activeTabIndex"
class="tab-panel"
:class="{ active: index === activeTabIndex }"
>
<!-- 使用Panel组件渲染面板内容 -->
<Panel
:id="panel.id"
:title="panel.title"
:x="panel.x || 0"
:y="panel.y || 0"
:width="panel.width || 300"
:height="panel.height || 200"
:collapsed="panel.collapsed || false"
:toolbar-expanded="panel.toolbarExpanded || false"
:maximized="panel.maximized || false"
:content="panel.content"
@toggle-collapse="$emit('toggleCollapse', $event)"
@maximize="(event) => { /* console.log('🔸 TabPage转发最大化事件:', event); */ $emit('maximize', event); }"
@close="$emit('close', $event)"
@toggle-toolbar="$emit('toggleToolbar', $event)"
@dragStart="$emit('dragStart', $event)"
@dragMove="$emit('dragMove', $event)"
@dragEnd="$emit('dragEnd', $event)"
/>
</div>
</template>
<!-- 直接渲染插槽内容 -->
<slot></slot>
<!-- 空状态提示 -->
<div v-else class="tab-empty">
<div v-if="slotItems.length === 0" class="tab-empty">
<span>没有可显示的内容</span>
</div>
</div>
<!-- 底部标签栏 - 移动到最后确保在底部显示 -->
<div v-if="tabPosition === 'bottom' && shouldShowTabs && panels && panels.length > 0" class="tab-header tab-header-horizontal tab-header-bottom">
<div v-if="tabPosition === 'bottom' && shouldShowTabs" class="tab-header tab-header-horizontal tab-header-bottom">
<div
v-for="(panel, index) in panels"
:key="panel.id"
v-for="(item, index) in slotItems"
:key="index"
:class="['tab-item', { 'active': activeTabIndex === index }]"
@click="setActiveTab(index)"
@mousedown="onTabDragStart(index, $event)"
>
<div class="flex items-center justify-between h-full px-3">
<span class="tab-title">{{ panel.title }}</span>
<span class="tab-title">{{ item?.title || '未命名' }}</span>
<!-- 当标签页未被激活时显示关闭按钮 -->
<button
v-if="activeTabIndex !== index"
class="button-icon p-[2px] rounded hover:opacity-100 opacity-80"
@click.stop="closeTab(panel.id)"
@click.stop="closeTab(item?.id)"
aria-label="关闭"
>
<svg width="11" height="11" viewBox="0 0 11 11" aria-hidden="true">
@@ -155,17 +126,13 @@
</template>
<script setup>
import { defineProps, defineEmits, ref, onMounted, computed } from 'vue'
import Panel from './Panel.vue'
import { defineProps, defineEmits, ref, onMounted, onUnmounted, computed, useSlots } from 'vue'
const slots = useSlots()
const props = defineProps({
id: { type: String, required: true },
title: { type: String, default: '标签页' },
// 从父组件传入的面板数组
panels: {
type: Array,
default: () => []
},
// 是否显示页标签栏
showTabs: {
type: Boolean,
@@ -188,31 +155,32 @@ const activeTabIndex = ref(-1)
let isDragging = false
let dragIndex = -1
// 计算属性获取插槽项的props
const slotItems = computed(() => {
if (!slots.default) return []
const slotChildren = slots.default()
return slotChildren.map(child => child?.props || {})
})
// 计算属性:控制标签栏的显示
const shouldShowTabs = computed(() => {
// 未来可以优化当只有一个Panel且不是浮动窗口时隐藏标签栏
const result = props.showTabs && props.panels && props.panels.length > 0
// console.log(`[TabPage ${props.id}] shouldShowTabs:`, {
// showTabs: result,
// panelsLength: props.panels?.length || 0,
// tabPosition: props.tabPosition,
// shouldShow: result,
// panels: props.panels
// })
return result
// 显示标签栏的条件showTabs为true且有子组件
return props.showTabs && slots.default && slots.default().length > 0
})
// 设置激活的标签页
const setActiveTab = (index) => {
if (index >= 0 && index < props.panels.length) {
const slotChildren = slots.default ? slots.default() : []
if (index >= 0 && index < slotChildren.length) {
activeTabIndex.value = index
emit('tabChange', { index, tab: props.panels[index] })
emit('tabChange', { index, tab: slotChildren[index] })
}
}
// 组件挂载后,如果有面板且没有激活的标签,默认激活第一个
// 组件挂载后,如果有子组件且没有激活的标签,默认激活第一个
onMounted(() => {
if (props.panels && props.panels.length > 0 && activeTabIndex.value === -1) {
const slotChildren = slots.default ? slots.default() : []
if (slotChildren.length > 0 && activeTabIndex.value === -1) {
setActiveTab(0)
}
})
@@ -234,7 +202,7 @@ const onTabDragStart = (index, event) => {
clientX: event.clientX,
clientY: event.clientY,
tabIndex: index,
tabId: props.panels[index].id
tabId: $slots.default()[index]?.props?.id
})
// 防止文本选择和默认行为
@@ -275,6 +243,14 @@ const onTabDragEnd = () => {
document.removeEventListener('mouseleave', onTabDragEnd)
}
}
// 组件卸载时清理全局事件监听器
onUnmounted(() => {
// 确保清理所有可能存在的全局事件监听器
document.removeEventListener('mousemove', onTabDragMove)
document.removeEventListener('mouseup', onTabDragEnd)
document.removeEventListener('mouseleave', onTabDragEnd)
})
</script>
<style scoped>
@@ -295,6 +271,12 @@ const onTabDragEnd = () => {
flex-direction: column;
}
.tab-page-top .tab-content {
flex: 1;
overflow: auto;
min-height: 0;
}
/* 底部位置:内容区 -> 工具栏 -> 标签栏 */
.tab-page-bottom {
flex-direction: column-reverse;
@@ -305,11 +287,23 @@ const onTabDragEnd = () => {
flex-direction: row;
}
.tab-page-left .tab-content {
flex: 1;
overflow: auto;
min-width: 0;
}
/* 右侧位置:标签栏 -> 工具栏 -> 内容区(垂直排列) */
.tab-page-right {
flex-direction: row-reverse;
}
.tab-page-right .tab-content {
flex: 1;
overflow: auto;
min-width: 0;
}
:root {
--vs-blue-top: #4f72b3;
--vs-blue-bottom: #3c5a99;
@@ -536,6 +530,30 @@ const onTabDragEnd = () => {
flex: 1;
position: relative;
overflow: hidden;
min-height: 0; /* 重要允许flex子项收缩 */
width: 100%;
height: 100%;
}
/* 顶部位置:标签栏 -> 内容区 */
.tab-page-top .tab-content {
flex: 1;
overflow: auto;
min-height: 0; /* 重要允许flex子项收缩 */
}
/* 左侧位置:标签栏 -> 内容区 */
.tab-page-left .tab-content {
flex: 1;
overflow: auto;
min-height: 0; /* 重要允许flex子项收缩 */
}
/* 右侧位置:标签栏 -> 内容区 */
.tab-page-right .tab-content {
flex: 1;
overflow: auto;
min-height: 0; /* 重要允许flex子项收缩 */
}
.tab-panel {

View File

@@ -682,13 +682,43 @@ EnhancedEventBus.prototype.on = function(eventType, callback, options = {}) {
return unsubscribe
}
// 扩展clear方法清理定时器
// 清理所有监听器和资源
EnhancedEventBus.prototype.clear = function() {
originalClear.call(this)
// 清理mitt事件总线的所有监听器
if (this.bus && this.bus.all) {
this.bus.all.clear()
}
// 清理事件处理器注册表
if (typeof handlerRegistry !== 'undefined' && handlerRegistry.destroyAll) {
handlerRegistry.destroyAll()
}
// 清理去重器
if (this.deduplicator) {
this.deduplicator.clear()
}
// 清理优先级队列
if (this.priorityQueue) {
this.priorityQueue.clear()
}
// 清理性能指标
if (this.performanceMetrics) {
this.performanceMetrics.clear()
}
// 清理事件历史
this.eventHistory = []
// 清理定时器
if (this.cleanupInterval) {
clearInterval(this.cleanupInterval)
this.cleanupInterval = null
}
console.log(`[EventBus:${this.instanceId}] 已清理所有监听器和资源`)
}
// 添加定期清理定时器

View File

@@ -1614,8 +1614,18 @@ class DragStateManager {
}
}
// 创建单例实例
const dragStateManager = new DragStateManager();
// 延迟创建单例实例
let dragStateManager = null;
/**
* 确保单例实例存在
*/
function ensureDragStateManagerInstance() {
if (!dragStateManager) {
dragStateManager = new DragStateManager();
}
return dragStateManager;
}
// 便捷操作函数
export const dragStateActions = {
@@ -1624,87 +1634,150 @@ export const dragStateActions = {
* @param {string} dragId - 拖拽ID
* @returns {Object} 拖拽状态
*/
getDragState: (dragId) => dragStateManager.getDragState(dragId),
getDragState: (dragId) => ensureDragStateManagerInstance().getDragState(dragId),
/**
* 获取所有活跃拖拽
* @returns {Array} 活跃拖拽列表
*/
getActiveDrags: () => dragStateManager.getActiveDrags(),
getActiveDrags: () => ensureDragStateManagerInstance().getActiveDrags(),
/**
* 获取拖拽历史
* @param {number} limit - 限制数量
* @returns {Array} 历史记录
*/
getHistory: (limit = 100) => dragStateManager.getDragHistory(limit),
getHistory: (limit = 100) => ensureDragStateManagerInstance().getDragHistory(limit),
/**
* 获取统计信息
* @returns {Object} 统计信息
*/
getStats: () => dragStateManager.getDragStats(),
getStats: () => ensureDragStateManagerInstance().getDragStats(),
/**
* 设置调试模式
* @param {boolean} enabled - 是否启用
*/
setDebugMode: (enabled) => dragStateManager.setDebugMode(enabled),
setDebugMode: (enabled) => ensureDragStateManagerInstance().setDebugMode(enabled),
/**
* 启用/禁用管理器
* @param {boolean} enabled - 是否启用
*/
setEnabled: (enabled) => dragStateManager.setEnabled(enabled),
setEnabled: (enabled) => ensureDragStateManagerInstance().setEnabled(enabled),
/**
* 取消所有拖拽
*/
cancelAll: () => dragStateManager.cancelAllDrags(),
cancelAll: () => ensureDragStateManagerInstance().cancelAllDrags(),
/**
* 面板拖拽开始
* @param {Object} eventData - 拖拽事件数据
* @returns {string} 拖拽ID
*/
onPanelDragStart: (eventData) => dragStateManager.onPanelDragStart(eventData),
onPanelDragStart: (eventData) => ensureDragStateManagerInstance().onPanelDragStart(eventData),
/**
* 面板拖拽移动
* @param {Object} eventData - 拖拽事件数据
* @returns {boolean} 是否成功
*/
onPanelDragMove: (eventData) => dragStateManager.onPanelDragMove(eventData),
onPanelDragMove: (eventData) => ensureDragStateManagerInstance().onPanelDragMove(eventData),
/**
* 面板拖拽结束
* @param {Object} eventData - 拖拽事件数据
* @returns {boolean} 是否成功
*/
onPanelDragEnd: (eventData) => dragStateManager.onPanelDragEnd(eventData),
onPanelDragEnd: (eventData) => ensureDragStateManagerInstance().onPanelDragEnd(eventData),
/**
* TabPage拖拽开始
* @param {Object} eventData - 拖拽事件数据
* @returns {string} 拖拽ID
*/
onPanelDragStartFromTabPage: (eventData) => dragStateManager.onPanelDragStartFromTabPage(eventData),
onPanelDragStartFromTabPage: (eventData) => ensureDragStateManagerInstance().onPanelDragStartFromTabPage(eventData),
/**
* TabPage拖拽移动
* @param {Object} eventData - 拖拽事件数据
* @returns {boolean} 是否成功
*/
onPanelDragMoveFromTabPage: (eventData) => dragStateManager.onPanelDragMoveFromTabPage(eventData),
onPanelDragMoveFromTabPage: (eventData) => ensureDragStateManagerInstance().onPanelDragMoveFromTabPage(eventData),
/**
* TabPage拖拽结束
* @param {Object} eventData - 拖拽事件数据
* @returns {boolean} 是否成功
*/
onPanelDragEndFromTabPage: (eventData) => dragStateManager.onPanelDragEndFromTabPage(eventData)
onPanelDragEndFromTabPage: (eventData) => ensureDragStateManagerInstance().onPanelDragEndFromTabPage(eventData),
/**
* Area拖拽开始
* @param {Object} eventData - 拖拽事件数据
* @returns {string} 拖拽ID
*/
onAreaDragStart: (eventData) => ensureDragStateManagerInstance().onPanelDragStart(eventData),
/**
* Area拖拽移动
* @param {Object} eventData - 拖拽事件数据
* @returns {boolean} 是否成功
*/
onAreaDragMove: (eventData) => ensureDragStateManagerInstance().onPanelDragMove(eventData),
/**
* Area拖拽结束
* @param {Object} eventData - 拖拽事件数据
* @returns {boolean} 是否成功
*/
onAreaDragEnd: (eventData) => ensureDragStateManagerInstance().onPanelDragEnd(eventData),
/**
* Tab拖拽开始
* @param {Object} eventData - 拖拽事件数据
* @returns {string} 拖拽ID
*/
onTabDragStart: (eventData) => ensureDragStateManagerInstance().onPanelDragStartFromTabPage(eventData),
/**
* Tab拖拽移动
* @param {Object} eventData - 拖拽事件数据
* @returns {boolean} 是否成功
*/
onTabDragMove: (eventData) => ensureDragStateManagerInstance().onPanelDragMoveFromTabPage(eventData),
/**
* Tab拖拽结束
* @param {Object} eventData - 拖拽事件数据
* @returns {boolean} 是否成功
*/
onTabDragEnd: (eventData) => ensureDragStateManagerInstance().onPanelDragEndFromTabPage(eventData),
/**
* 初始化拖拽管理器
*/
initialize: () => ensureDragStateManagerInstance(),
/**
* 销毁拖拽管理器
*/
destroy: () => {
if (dragStateManager) {
// 清理拖拽管理器资源
dragStateManager.activeDrags.clear();
dragStateManager.dragHistory = [];
dragStateManager = null;
console.log('🗑️ 拖拽状态管理器已销毁,所有资源已清理');
}
}
};
// 导出
export default dragStateManager;
export default {
getInstance: ensureDragStateManagerInstance,
actions: dragStateActions
};
export { DragState };

View File

@@ -289,13 +289,22 @@ class EventExecutionMonitor {
* 全局事件管理器类
*/
class GlobalEventManager {
// 静态实例属性,用于存储单例实例
static instance = null;
constructor() {
// 单例模式实现:如果已经有实例,直接返回现有实例
if (GlobalEventManager.instance) {
return GlobalEventManager.instance;
}
this.eventHandlers = new Map();
this.eventRoutes = new Map(Object.entries(EVENT_ROUTES));
this.executionMonitor = new EventExecutionMonitor();
this.eventChains = new Map();
this.crossComponentChannels = new Map();
this.isInitialized = false;
this.eventListenersRegistered = false;
this.debugMode = false;
// 处理器映射
@@ -311,12 +320,21 @@ class GlobalEventManager {
this._cleanupExpiredData = this._cleanupExpiredData.bind(this);
this._initialize();
// 保存实例到静态属性
GlobalEventManager.instance = this;
}
/**
* 初始化事件管理器
*/
_initialize() {
// 防止重复初始化
if (this.isInitialized) {
console.warn('⚠️ 全局事件管理器已初始化,跳过重复初始化');
return;
}
// 注册全局事件监听器
this._registerGlobalEventListeners();
@@ -341,6 +359,12 @@ class GlobalEventManager {
* 注册全局事件监听器
*/
_registerGlobalEventListeners() {
// 检查是否已经注册过事件监听器
if (this.eventListenersRegistered) {
console.warn('⚠️ 事件监听器已经注册,跳过重复注册');
return;
}
const globalEvents = Object.values(GLOBAL_EVENT_TYPES);
globalEvents.forEach(eventType => {
eventBus.on(eventType, this._onGlobalEvent, {
@@ -351,6 +375,9 @@ class GlobalEventManager {
// 注册所有组件事件监听器
this._registerComponentEventListeners();
// 标记为已注册
this.eventListenersRegistered = true;
}
/**
@@ -995,11 +1022,8 @@ class GlobalEventManager {
* 销毁事件管理器
*/
destroy() {
// 清理所有事件监听器
const globalEvents = Object.values(GLOBAL_EVENT_TYPES);
globalEvents.forEach(eventType => {
eventBus.off(eventType, this._onGlobalEvent);
});
// 清理所有事件监听器,包括全局事件和组件事件
eventBus.clear();
// 销毁各个处理器
Object.values(this.handlerMap).forEach(handler => {
@@ -1012,14 +1036,17 @@ class GlobalEventManager {
this.eventHandlers.clear();
this.eventChains.clear();
this.crossComponentChannels.clear();
this.handlerMap = {};
this.eventRoutes.clear();
this.isInitialized = false;
console.log('🗑️ 全局事件管理器已销毁');
this.eventListenersRegistered = false;
console.log('🗑️ 全局事件管理器已销毁,所有监听器已清理');
}
}
// 创建单例实例
const globalEventManager = new GlobalEventManager();
// 延迟创建单例实例
let globalEventManager = null;
// 全局便捷API
export const globalEventActions = {
@@ -1029,6 +1056,7 @@ export const globalEventActions = {
* @returns {string} 监控ID
*/
startMonitor: (operation) => {
ensureInstance();
return globalEventManager.startPerformanceMonitor(operation);
},
@@ -1038,6 +1066,7 @@ export const globalEventActions = {
* @param {Object} metadata - 元数据
*/
endMonitor: (monitorId, metadata = {}) => {
ensureInstance();
globalEventManager.endPerformanceMonitor(monitorId, metadata);
},
@@ -1048,6 +1077,7 @@ export const globalEventActions = {
* @returns {string} 链ID
*/
createChain: (chainName, events) => {
ensureInstance();
return globalEventManager.createEventChain(chainName, events);
},
@@ -1058,6 +1088,7 @@ export const globalEventActions = {
* @param {Array} targets - 目标组件
*/
broadcast: (message, data = {}, targets = null) => {
ensureInstance();
globalEventManager.broadcast(message, data, targets);
},
@@ -1069,6 +1100,7 @@ export const globalEventActions = {
* @returns {Promise} 响应
*/
request: (component, action, payload) => {
ensureInstance();
return globalEventManager.request(component, action, payload);
},
@@ -1084,6 +1116,7 @@ export const globalEventActions = {
* @returns {Object} 统计信息
*/
getStats: () => {
ensureInstance();
return globalEventManager.getExecutionStats();
},
@@ -1092,9 +1125,45 @@ export const globalEventActions = {
* @returns {Array} 链状态
*/
getChainStatus: () => {
ensureInstance();
return globalEventManager.getEventChainStatus();
},
/**
* 初始化事件管理器
*/
initialize: () => {
ensureInstance();
},
/**
* 销毁事件管理器
*/
destroy: () => {
if (globalEventManager) {
globalEventManager.destroy();
globalEventManager = null;
}
}
};
/**
* 确保单例实例存在
*/
function ensureInstance() {
if (!globalEventManager) {
globalEventManager = new GlobalEventManager();
}
return globalEventManager;
}
// 导出单例实例访问方法
export const getGlobalEventManager = () => {
return ensureInstance();
};
// 导出事件管理器和相关API
export default globalEventManager;
export default {
getInstance: getGlobalEventManager,
actions: globalEventActions
};

View File

@@ -37,7 +37,7 @@ export default defineConfig(({ mode }) => {
fixAliyunSDKPlugin // 添加我们的修复插件
],
server: {
port: 8080, // 改为不同于后端的端口
port: 8081, // 改为不同于后端的端口
proxy: {
// 添加代理配置将WebSocket请求转发到后端
'/': {