2025-10-31 21:58:33 +08:00
|
|
|
|
<template>
|
2025-11-05 13:06:11 +08:00
|
|
|
|
<div class="panel bg-white overflow-hidden"
|
2025-11-20 09:44:51 +08:00
|
|
|
|
:style="{ width: '100%', height: '100%' }"
|
|
|
|
|
|
:data-panel-id="id">
|
2025-10-31 21:58:33 +08:00
|
|
|
|
<div class="flex flex-col h-full">
|
|
|
|
|
|
<!-- 标题栏 -->
|
2025-11-04 11:05:12 +08:00
|
|
|
|
<div class="title-bar h-6 bg-[#435d9c] text-white px-2 flex items-center justify-between select-none cursor-move"
|
2025-11-04 11:08:34 +08:00
|
|
|
|
@mousedown="onDragStart">
|
2025-10-31 21:58:33 +08:00
|
|
|
|
<div class="flex items-center">
|
|
|
|
|
|
<span class="text-xs">{{ title }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="title-bar-buttons flex items-center gap-0.5">
|
|
|
|
|
|
<button class="button-icon p-[2px] rounded hover:opacity-100 opacity-80"
|
2025-11-14 16:43:00 +08:00
|
|
|
|
@click.stop="onToggleCollapse"
|
|
|
|
|
|
@mousedown.stop
|
2025-10-31 21:58:33 +08:00
|
|
|
|
aria-label="折叠/展开">
|
|
|
|
|
|
<!-- 向下小三角,使用内联SVG避免样式作用域问题 -->
|
|
|
|
|
|
<svg width="11" height="11" viewBox="0 0 11 11" aria-hidden="true">
|
|
|
|
|
|
<polygon points="5.5,8 2,3.5 9,3.5" fill="#cbd6ff" />
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button class="button-icon p-[2px] rounded hover:opacity-100 opacity-80"
|
2025-11-14 16:43:00 +08:00
|
|
|
|
@click.stop="onMaximize"
|
|
|
|
|
|
@mousedown.stop
|
2025-11-04 15:45:10 +08:00
|
|
|
|
:aria-label="maximized ? '还原' : '最大化'">
|
|
|
|
|
|
<!-- 最大化图标 -->
|
|
|
|
|
|
<template v-if="!maximized">
|
|
|
|
|
|
<!-- 最大化图标:外框 + 内部线条 -->
|
|
|
|
|
|
<svg class="icon-square-svg" width="11" height="11" viewBox="0 0 11 11" aria-hidden="true">
|
2025-11-04 16:46:24 +08:00
|
|
|
|
<rect x="0.5" y="0.5" width="10" height="10" fill="#cbd6ff" stroke="#8ea3d8" stroke-width="1" />
|
|
|
|
|
|
<rect x="3" y="3" width="5" height="1" fill="#b8c6ff" />
|
|
|
|
|
|
<rect x="1" y="3" width="8.5" height="6.5" fill="#435d9c" />
|
|
|
|
|
|
</svg>
|
2025-11-04 15:45:10 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
<!-- 还原图标 -->
|
|
|
|
|
|
<template v-else>
|
|
|
|
|
|
<svg class="icon-square-svg" width="11" height="11" viewBox="0 0 11 11" aria-hidden="true">
|
2025-11-04 16:46:24 +08:00
|
|
|
|
<path
|
|
|
|
|
|
fill="#CED4DD"
|
|
|
|
|
|
fill-rule="evenodd"
|
|
|
|
|
|
clip-rule="evenodd"
|
|
|
|
|
|
d="M1 4 L4 4 L4 1 L11 1 L11 8 L8 8 L8 11 L1 11 Z
|
|
|
|
|
|
M5 4 L5 3 L10 3 L10 7 L8 7 L8 4 Z
|
|
|
|
|
|
M2 6 L12.6 5 L7 6 L7 10 L2 10 Z" />
|
2025-11-04 15:45:10 +08:00
|
|
|
|
</svg>
|
|
|
|
|
|
</template>
|
2025-10-31 21:58:33 +08:00
|
|
|
|
</button>
|
|
|
|
|
|
<button class="button-icon p-[2px] rounded hover:opacity-100 opacity-80"
|
2025-11-14 16:43:00 +08:00
|
|
|
|
@click.stop="onClose"
|
|
|
|
|
|
@mousedown.stop
|
2025-10-31 21:58:33 +08:00
|
|
|
|
aria-label="关闭">
|
|
|
|
|
|
<!-- 关闭图标(X),内联SVG确保1px线条 -->
|
|
|
|
|
|
<svg width="11" height="11" viewBox="0 0 11 11" aria-hidden="true">
|
|
|
|
|
|
<line x1="2" y1="2" x2="9" y2="9" stroke="#e6efff" stroke-width="1" />
|
|
|
|
|
|
<line x1="2" y1="9" x2="9" y2="2" stroke="#e6efff" stroke-width="1" />
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 工具栏:位于标题栏下方,右侧扩展钮 -->
|
|
|
|
|
|
<div class="toolbar h-6 bg-[#d5e2f6] text-[#2c3e7a] px-2 flex items-center justify-between border-b border-[#c7d2ea]">
|
|
|
|
|
|
<div class="toolbar-left flex items-center gap-2">
|
|
|
|
|
|
<span class="text-xs">工具栏</span>
|
|
|
|
|
|
<button v-if="toolbarExpanded" class="toolbar-button px-2 py-0.5 text-xs bg-white/60 rounded hover:bg-white">示例按钮</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<button class="toolbar-toggle px-2 py-0.5 text-xs rounded hover:bg-white/40"
|
|
|
|
|
|
@click="onToggleToolbar"
|
|
|
|
|
|
aria-label="展开工具栏">
|
|
|
|
|
|
<i class="fa-solid" :class="toolbarExpanded ? 'fa-angles-left' : 'fa-angles-right'"></i>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-11-18 15:39:46 +08:00
|
|
|
|
<!-- 内容区:可折叠,添加滚动条 -->
|
|
|
|
|
|
<div class="content-area bg-[#f5f7fb] flex-1 p-4 overflow-auto min-h-0" v-show="!collapsed">
|
2025-11-17 16:55:03 +08:00
|
|
|
|
<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>
|
2025-10-31 21:58:33 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
2025-11-20 09:44:51 +08:00
|
|
|
|
import { defineProps, onMounted, onUnmounted } from 'vue';
|
2025-12-25 13:53:52 +08:00
|
|
|
|
import {
|
2025-11-20 09:44:51 +08:00
|
|
|
|
eventBus,
|
|
|
|
|
|
EVENT_TYPES,
|
|
|
|
|
|
emitEvent,
|
2025-12-25 13:53:52 +08:00
|
|
|
|
onEvent
|
|
|
|
|
|
} from './eventBus';
|
2025-10-31 21:58:33 +08:00
|
|
|
|
|
|
|
|
|
|
// 定义组件属性
|
|
|
|
|
|
const props = defineProps({
|
|
|
|
|
|
id: {
|
|
|
|
|
|
type: String,
|
|
|
|
|
|
required: true
|
|
|
|
|
|
},
|
|
|
|
|
|
title: {
|
|
|
|
|
|
type: String,
|
|
|
|
|
|
default: ''
|
|
|
|
|
|
},
|
|
|
|
|
|
x: {
|
|
|
|
|
|
type: Number,
|
|
|
|
|
|
default: 0
|
|
|
|
|
|
},
|
|
|
|
|
|
y: {
|
|
|
|
|
|
type: Number,
|
|
|
|
|
|
default: 0
|
|
|
|
|
|
},
|
|
|
|
|
|
width: {
|
|
|
|
|
|
type: Number,
|
|
|
|
|
|
default: 300
|
|
|
|
|
|
},
|
|
|
|
|
|
height: {
|
|
|
|
|
|
type: Number,
|
|
|
|
|
|
default: 180
|
|
|
|
|
|
},
|
|
|
|
|
|
collapsed: {
|
|
|
|
|
|
type: Boolean,
|
|
|
|
|
|
default: false
|
|
|
|
|
|
},
|
|
|
|
|
|
toolbarExpanded: {
|
|
|
|
|
|
type: Boolean,
|
|
|
|
|
|
default: false
|
2025-11-04 15:45:10 +08:00
|
|
|
|
},
|
|
|
|
|
|
maximized: {
|
|
|
|
|
|
type: Boolean,
|
|
|
|
|
|
default: false
|
2025-11-17 16:55:03 +08:00
|
|
|
|
},
|
|
|
|
|
|
content: {
|
|
|
|
|
|
type: Object,
|
|
|
|
|
|
default: null
|
2025-11-20 09:44:51 +08:00
|
|
|
|
},
|
|
|
|
|
|
// 移除areaId属性,因为面板会被拖拽到不同区域
|
|
|
|
|
|
// 改为通过DOM动态获取当前所在区域
|
2025-10-31 21:58:33 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2025-11-20 10:21:05 +08:00
|
|
|
|
// 事件订阅管理 - 使用Set避免key冲突,并添加唯一标识符
|
|
|
|
|
|
const subscriptions = new Set();
|
|
|
|
|
|
const subscriptionRegistry = new Map(); // 用于追踪订阅详细信息
|
2025-10-31 21:58:33 +08:00
|
|
|
|
|
2025-11-20 09:44:51 +08:00
|
|
|
|
// 动态获取当前面板所在的Area ID
|
|
|
|
|
|
const getCurrentAreaId = () => {
|
|
|
|
|
|
// 通过DOM向上查找最近的Area容器
|
|
|
|
|
|
const panelElement = document.querySelector(`[data-panel-id="${props.id}"]`);
|
|
|
|
|
|
if (panelElement) {
|
|
|
|
|
|
const areaElement = panelElement.closest('[data-area-id]');
|
|
|
|
|
|
if (areaElement) {
|
|
|
|
|
|
return areaElement.getAttribute('data-area-id');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 备用方法:通过父组件查找
|
|
|
|
|
|
// 向上查找vs-area容器
|
|
|
|
|
|
const parentElement = document.querySelector(`[data-panel-id="${props.id}"]`)?.parentElement;
|
|
|
|
|
|
if (parentElement) {
|
|
|
|
|
|
const areaElement = parentElement.closest('.vs-area');
|
|
|
|
|
|
if (areaElement) {
|
|
|
|
|
|
return areaElement.getAttribute('data-area-id');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.warn(`[Panel:${props.id}] 无法找到当前所在的Area,Panel可能未正确挂载`);
|
|
|
|
|
|
return null;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 事件处理函数 - 使用事件总线
|
2025-10-31 21:58:33 +08:00
|
|
|
|
const onToggleCollapse = () => {
|
2025-11-20 09:44:51 +08:00
|
|
|
|
console.log(`[Panel:${props.id}] 触发折叠/展开事件`)
|
|
|
|
|
|
emitEvent(EVENT_TYPES.PANEL_TOGGLE_COLLAPSE, {
|
|
|
|
|
|
panelId: props.id,
|
|
|
|
|
|
areaId: getCurrentAreaId(),
|
|
|
|
|
|
currentState: props.collapsed
|
2025-12-26 13:09:35 +08:00
|
|
|
|
}, {
|
|
|
|
|
|
source: { component: 'Panel', panelId: props.id }
|
2025-11-20 09:44:51 +08:00
|
|
|
|
})
|
2025-10-31 21:58:33 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const onMaximize = () => {
|
2025-11-20 09:44:51 +08:00
|
|
|
|
console.log(`[Panel:${props.id}] 触发最大化事件`)
|
|
|
|
|
|
emitEvent(EVENT_TYPES.PANEL_MAXIMIZE, {
|
|
|
|
|
|
panelId: props.id,
|
|
|
|
|
|
areaId: getCurrentAreaId(),
|
|
|
|
|
|
currentState: props.maximized
|
2025-12-26 13:09:35 +08:00
|
|
|
|
}, {
|
|
|
|
|
|
source: { component: 'Panel', panelId: props.id }
|
2025-11-20 09:44:51 +08:00
|
|
|
|
})
|
2025-10-31 21:58:33 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const onClose = () => {
|
2025-11-20 09:44:51 +08:00
|
|
|
|
// console.log(`[Panel:${props.id}] 触发关闭请求事件`)
|
|
|
|
|
|
// 首先发送关闭请求,让父组件可以处理确认逻辑
|
|
|
|
|
|
emitEvent(EVENT_TYPES.PANEL_CLOSE_REQUEST, {
|
|
|
|
|
|
panelId: props.id,
|
|
|
|
|
|
areaId: getCurrentAreaId(),
|
|
|
|
|
|
panelTitle: props.title,
|
|
|
|
|
|
requestTime: Date.now()
|
2025-12-26 13:09:35 +08:00
|
|
|
|
}, {
|
|
|
|
|
|
source: { component: 'Panel', panelId: props.id }
|
2025-11-20 09:44:51 +08:00
|
|
|
|
})
|
2025-10-31 21:58:33 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const onToggleToolbar = () => {
|
2025-11-20 09:44:51 +08:00
|
|
|
|
console.log(`[Panel:${props.id}] 触发工具栏切换事件`)
|
|
|
|
|
|
emitEvent(EVENT_TYPES.PANEL_TOGGLE_TOOLBAR, {
|
|
|
|
|
|
panelId: props.id,
|
|
|
|
|
|
areaId: getCurrentAreaId(),
|
|
|
|
|
|
currentState: props.toolbarExpanded
|
2025-12-26 13:09:35 +08:00
|
|
|
|
}, {
|
|
|
|
|
|
source: { component: 'Panel', panelId: props.id }
|
2025-11-20 09:44:51 +08:00
|
|
|
|
})
|
2025-10-31 21:58:33 +08:00
|
|
|
|
};
|
2025-11-04 11:05:12 +08:00
|
|
|
|
|
|
|
|
|
|
// 拖拽相关状态
|
2025-11-20 10:21:05 +08:00
|
|
|
|
let isDragging = false
|
2025-12-26 13:09:35 +08:00
|
|
|
|
let currentDragId = null
|
2025-11-20 10:21:05 +08:00
|
|
|
|
|
|
|
|
|
|
// 全局内存泄漏保护机制
|
|
|
|
|
|
if (!window.__panelMemoryProtection) {
|
|
|
|
|
|
window.__panelMemoryProtection = {
|
|
|
|
|
|
// 存储所有面板组件实例追踪信息
|
|
|
|
|
|
panelInstances: new Map(),
|
|
|
|
|
|
|
|
|
|
|
|
// 定时检测内存泄漏(开发环境)
|
|
|
|
|
|
startLeakDetection() {
|
|
|
|
|
|
if (import.meta.env.DEV) {
|
|
|
|
|
|
setInterval(() => {
|
|
|
|
|
|
this.detectMemoryLeaks()
|
|
|
|
|
|
}, 30000) // 每30秒检测一次
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 检测内存泄漏
|
|
|
|
|
|
detectMemoryLeaks() {
|
|
|
|
|
|
const activePanels = window.__panelDragHandlers ? window.__panelDragHandlers.size : 0
|
|
|
|
|
|
const registeredPanels = this.panelInstances.size
|
|
|
|
|
|
|
|
|
|
|
|
if (activePanels !== registeredPanels) {
|
|
|
|
|
|
console.warn(`[内存泄漏检测] 发现面板内存不一致 - 活动拖拽: ${activePanels}, 注册实例: ${registeredPanels}`)
|
|
|
|
|
|
|
|
|
|
|
|
// 清理 orphaned handlers
|
|
|
|
|
|
if (window.__panelDragHandlers && activePanels > 0) {
|
|
|
|
|
|
window.__panelDragHandlers.forEach((handlers, panelId) => {
|
|
|
|
|
|
if (!this.panelInstances.has(panelId)) {
|
|
|
|
|
|
console.warn(`[内存泄漏检测] 清理orphaned handler: ${panelId}`)
|
|
|
|
|
|
document.removeEventListener('mousemove', handlers.dragMoveHandler, false)
|
|
|
|
|
|
document.removeEventListener('mouseup', handlers.dragEndHandler, false)
|
|
|
|
|
|
document.removeEventListener('mouseleave', handlers.dragEndHandler, false)
|
|
|
|
|
|
window.__panelDragHandlers.delete(panelId)
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 注册面板实例
|
|
|
|
|
|
registerPanel(panelId) {
|
|
|
|
|
|
this.panelInstances.set(panelId, {
|
|
|
|
|
|
createdAt: Date.now(),
|
|
|
|
|
|
lastActivity: Date.now()
|
|
|
|
|
|
})
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 注销面板实例
|
|
|
|
|
|
unregisterPanel(panelId) {
|
|
|
|
|
|
this.panelInstances.delete(panelId)
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 更新活动状态
|
|
|
|
|
|
updateActivity(panelId) {
|
|
|
|
|
|
const panel = this.panelInstances.get(panelId)
|
|
|
|
|
|
if (panel) {
|
|
|
|
|
|
panel.lastActivity = Date.now()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 启动内存泄漏检测
|
|
|
|
|
|
window.__panelMemoryProtection.startLeakDetection()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 添加Document拖拽事件监听器
|
|
|
|
|
|
*/
|
|
|
|
|
|
const addDocumentDragListeners = () => {
|
|
|
|
|
|
// 移除可能存在的旧监听器
|
|
|
|
|
|
cleanupDragEventListeners()
|
|
|
|
|
|
|
|
|
|
|
|
// 使用组件实例标识符确保清理正确性
|
|
|
|
|
|
const componentId = `panel_${props.id}`
|
|
|
|
|
|
const dragMoveHandler = (e) => onDragMove(e)
|
|
|
|
|
|
const dragEndHandler = (e) => onDragEnd(e)
|
|
|
|
|
|
|
|
|
|
|
|
// 将处理函数绑定到组件作用域,避免匿名函数导致的清理问题
|
|
|
|
|
|
if (!window.__panelDragHandlers) {
|
|
|
|
|
|
window.__panelDragHandlers = new Map()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
window.__panelDragHandlers.set(componentId, {
|
|
|
|
|
|
dragMoveHandler,
|
|
|
|
|
|
dragEndHandler
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
document.addEventListener('mousemove', dragMoveHandler, false)
|
|
|
|
|
|
document.addEventListener('mouseup', dragEndHandler, false)
|
|
|
|
|
|
document.addEventListener('mouseleave', dragEndHandler, false)
|
|
|
|
|
|
|
|
|
|
|
|
if (import.meta.env.DEV) {
|
|
|
|
|
|
console.log(`[Panel:${props.id}] Document拖拽事件监听器已添加: ${componentId}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 清理Document拖拽事件监听器
|
|
|
|
|
|
*/
|
|
|
|
|
|
const cleanupDragEventListeners = () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const componentId = `panel_${props.id}`
|
|
|
|
|
|
|
|
|
|
|
|
// 从全局拖拽处理函数映射中获取处理函数
|
|
|
|
|
|
const handlers = window.__panelDragHandlers?.get(componentId)
|
|
|
|
|
|
|
|
|
|
|
|
if (handlers) {
|
|
|
|
|
|
// 使用正确的处理函数引用进行清理
|
|
|
|
|
|
document.removeEventListener('mousemove', handlers.dragMoveHandler, false)
|
|
|
|
|
|
document.removeEventListener('mouseup', handlers.dragEndHandler, false)
|
|
|
|
|
|
document.removeEventListener('mouseleave', handlers.dragEndHandler, false)
|
|
|
|
|
|
|
|
|
|
|
|
// 从映射中移除
|
|
|
|
|
|
window.__panelDragHandlers.delete(componentId)
|
|
|
|
|
|
|
|
|
|
|
|
// 清理映射,如果为空则删除整个映射
|
|
|
|
|
|
if (window.__panelDragHandlers.size === 0) {
|
|
|
|
|
|
delete window.__panelDragHandlers
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (import.meta.env.DEV) {
|
|
|
|
|
|
console.log(`[Panel:${props.id}] Document拖拽事件监听器已清理: ${componentId}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 立即重置拖拽状态,确保清理完整性
|
|
|
|
|
|
isDragging = false
|
|
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.warn(`[Panel:${props.id}] 清理拖拽事件监听器时出错:`, error)
|
|
|
|
|
|
|
|
|
|
|
|
// 发生错误时仍然重置状态
|
|
|
|
|
|
isDragging = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-11-04 11:05:12 +08:00
|
|
|
|
|
|
|
|
|
|
// 拖拽开始
|
|
|
|
|
|
const onDragStart = (e) => {
|
|
|
|
|
|
// 只有当点击的是标题栏区域(不是按钮)时才触发拖拽
|
|
|
|
|
|
if (!e.target.closest('.title-bar-buttons') && !e.target.closest('button')) {
|
2025-11-20 10:21:05 +08:00
|
|
|
|
// 1. 立即重置之前的拖拽状态
|
|
|
|
|
|
isDragging = false
|
2025-12-26 13:09:35 +08:00
|
|
|
|
currentDragId = null
|
2025-11-20 10:21:05 +08:00
|
|
|
|
cleanupDragEventListeners()
|
|
|
|
|
|
|
|
|
|
|
|
isDragging = true
|
2025-11-20 09:44:51 +08:00
|
|
|
|
|
2025-12-26 13:09:35 +08:00
|
|
|
|
// 生成统一的 dragId
|
|
|
|
|
|
currentDragId = `panel_${props.id}_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`[Panel:${props.id}] 开始拖拽, dragId: ${currentDragId}`)
|
|
|
|
|
|
|
2025-12-26 17:12:36 +08:00
|
|
|
|
const areaId = getCurrentAreaId();
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 使用事件总线触发面板拖拽开始事件,包含统一的 dragId 和标准化数据格式
|
2025-11-20 09:44:51 +08:00
|
|
|
|
emitEvent(EVENT_TYPES.PANEL_DRAG_START, {
|
2025-12-26 13:09:35 +08:00
|
|
|
|
dragId: currentDragId,
|
2025-11-20 09:44:51 +08:00
|
|
|
|
panelId: props.id,
|
2025-12-26 17:12:36 +08:00
|
|
|
|
areaId: areaId,
|
2025-11-20 09:44:51 +08:00
|
|
|
|
position: { x: e.clientX, y: e.clientY },
|
|
|
|
|
|
timestamp: Date.now()
|
|
|
|
|
|
}, {
|
2025-12-26 13:09:35 +08:00
|
|
|
|
source: { component: 'Panel', panelId: props.id, dragId: currentDragId }
|
2025-11-20 09:44:51 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
2025-12-26 17:12:36 +08:00
|
|
|
|
// 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. 防止文本选择和默认行为
|
2025-11-20 10:21:05 +08:00
|
|
|
|
e.preventDefault()
|
|
|
|
|
|
e.stopPropagation()
|
2025-11-04 11:08:34 +08:00
|
|
|
|
|
2025-12-26 17:12:36 +08:00
|
|
|
|
// 5. 添加Document事件监听器,使用一次性变量避免内存泄漏
|
2025-11-20 10:21:05 +08:00
|
|
|
|
addDocumentDragListeners()
|
2025-11-04 11:05:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 拖拽移动
|
|
|
|
|
|
const onDragMove = (e) => {
|
2025-12-26 13:09:35 +08:00
|
|
|
|
if (isDragging && currentDragId) {
|
2025-11-04 14:34:40 +08:00
|
|
|
|
// 防止文本选择和默认行为
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
e.stopPropagation();
|
2025-11-20 09:44:51 +08:00
|
|
|
|
|
2025-12-26 17:12:36 +08:00
|
|
|
|
const areaId = getCurrentAreaId();
|
|
|
|
|
|
|
|
|
|
|
|
// 1. 使用事件总线触发面板拖拽移动事件,包含 dragId
|
2025-11-20 09:44:51 +08:00
|
|
|
|
emitEvent(EVENT_TYPES.PANEL_DRAG_MOVE, {
|
2025-12-26 13:09:35 +08:00
|
|
|
|
dragId: currentDragId,
|
2025-11-20 09:44:51 +08:00
|
|
|
|
panelId: props.id,
|
2025-12-26 17:12:36 +08:00
|
|
|
|
areaId: areaId,
|
2025-11-20 09:44:51 +08:00
|
|
|
|
position: { x: e.clientX, y: e.clientY },
|
|
|
|
|
|
timestamp: Date.now()
|
|
|
|
|
|
}, {
|
2025-12-26 13:09:35 +08:00
|
|
|
|
source: { component: 'Panel', panelId: props.id, dragId: currentDragId }
|
2025-11-20 09:44:51 +08:00
|
|
|
|
})
|
2025-12-26 17:12:36 +08:00
|
|
|
|
|
|
|
|
|
|
// 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 }
|
|
|
|
|
|
})
|
2025-11-04 11:05:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 拖拽结束
|
|
|
|
|
|
const onDragEnd = () => {
|
2025-12-26 13:09:35 +08:00
|
|
|
|
if (isDragging && currentDragId) {
|
2025-11-04 11:05:12 +08:00
|
|
|
|
isDragging = false;
|
2025-11-20 09:44:51 +08:00
|
|
|
|
|
2025-12-26 13:09:35 +08:00
|
|
|
|
console.log(`[Panel:${props.id}] 结束拖拽, dragId: ${currentDragId}`)
|
|
|
|
|
|
|
2025-12-26 17:12:36 +08:00
|
|
|
|
const areaId = getCurrentAreaId();
|
|
|
|
|
|
|
|
|
|
|
|
// 1. 使用事件总线触发面板拖拽结束事件,包含 dragId
|
2025-11-20 09:44:51 +08:00
|
|
|
|
emitEvent(EVENT_TYPES.PANEL_DRAG_END, {
|
2025-12-26 13:09:35 +08:00
|
|
|
|
dragId: currentDragId,
|
2025-11-20 09:44:51 +08:00
|
|
|
|
panelId: props.id,
|
2025-12-26 17:12:36 +08:00
|
|
|
|
areaId: areaId,
|
2025-11-20 09:44:51 +08:00
|
|
|
|
timestamp: Date.now()
|
|
|
|
|
|
}, {
|
2025-12-26 13:09:35 +08:00
|
|
|
|
source: { component: 'Panel', panelId: props.id, dragId: currentDragId }
|
2025-11-20 09:44:51 +08:00
|
|
|
|
})
|
2025-11-04 11:08:34 +08:00
|
|
|
|
|
2025-12-26 17:12:36 +08:00
|
|
|
|
// 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. 使用统一的清理方法,确保一致性和完整性
|
2025-11-20 10:21:05 +08:00
|
|
|
|
cleanupDragEventListeners()
|
2025-12-26 13:09:35 +08:00
|
|
|
|
|
2025-12-26 17:12:36 +08:00
|
|
|
|
// 4. 重置 dragId
|
2025-12-26 13:09:35 +08:00
|
|
|
|
currentDragId = null
|
2025-11-04 11:05:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
};
|
2025-11-20 09:44:51 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 监听面板关闭事件,更新组件状态(可选)
|
|
|
|
|
|
*/
|
|
|
|
|
|
const setupEventListeners = () => {
|
2025-11-20 10:21:05 +08:00
|
|
|
|
try {
|
|
|
|
|
|
// 监听面板最大化同步事件
|
|
|
|
|
|
const unsubscribeMaximizeSync = onEvent(EVENT_TYPES.PANEL_MAXIMIZE_SYNC, (data) => {
|
|
|
|
|
|
if (data.panelId === props.id) {
|
|
|
|
|
|
console.log(`[Panel:${props.id}] 收到最大化同步事件`)
|
|
|
|
|
|
}
|
2025-12-26 13:09:35 +08:00
|
|
|
|
}, { componentId: `panel-${props.id}` })
|
2025-11-20 10:21:05 +08:00
|
|
|
|
|
|
|
|
|
|
const subscriptionId = `maximizeSync_${props.id}_${Date.now()}`
|
|
|
|
|
|
subscriptions.add(unsubscribeMaximizeSync)
|
|
|
|
|
|
subscriptionRegistry.set(subscriptionId, {
|
|
|
|
|
|
unsubscribe: unsubscribeMaximizeSync,
|
|
|
|
|
|
name: 'maximizeSync',
|
|
|
|
|
|
createdAt: Date.now()
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`[Panel:${props.id}] 事件监听器注册完成,ID: ${subscriptionId}`)
|
|
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error(`[Panel:${props.id}] 注册事件监听器失败:`, error)
|
|
|
|
|
|
}
|
2025-11-20 09:44:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2025-11-20 10:21:05 +08:00
|
|
|
|
* 增强版清理事件监听器,返回清理结果统计
|
2025-11-20 09:44:51 +08:00
|
|
|
|
*/
|
|
|
|
|
|
const cleanupEventListeners = () => {
|
2025-11-20 10:21:05 +08:00
|
|
|
|
const cleanupResult = {
|
|
|
|
|
|
eventSubscriptions: 0,
|
|
|
|
|
|
documentListeners: 0,
|
|
|
|
|
|
errors: []
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (import.meta.env.DEV) {
|
|
|
|
|
|
console.log(`[Panel:${props.id}] 开始清理所有事件监听器...`)
|
2025-11-20 09:44:51 +08:00
|
|
|
|
}
|
2025-11-20 10:21:05 +08:00
|
|
|
|
|
|
|
|
|
|
// 1. 清理事件订阅
|
|
|
|
|
|
let unsubscribeCount = 0
|
|
|
|
|
|
const subscriptionsToCleanup = Array.from(subscriptions)
|
|
|
|
|
|
|
|
|
|
|
|
subscriptionsToCleanup.forEach((subscription, index) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (subscription && typeof subscription === 'function') {
|
|
|
|
|
|
subscription() // 执行取消订阅函数
|
|
|
|
|
|
unsubscribeCount++
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.warn(`[Panel:${props.id}] 发现无效的订阅函数,索引: ${index}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.warn(`[Panel:${props.id}] 取消订阅时出错,索引: ${index}:`, error)
|
|
|
|
|
|
cleanupResult.errors.push(`取消订阅错误 (${index}): ${error.message}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 清空订阅集合和注册表
|
|
|
|
|
|
subscriptions.clear()
|
|
|
|
|
|
subscriptionRegistry.clear()
|
|
|
|
|
|
|
|
|
|
|
|
cleanupResult.eventSubscriptions = unsubscribeCount
|
|
|
|
|
|
if (import.meta.env.DEV) {
|
|
|
|
|
|
console.log(`[Panel:${props.id}] 已清理 ${unsubscribeCount} 个事件订阅`)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error(`[Panel:${props.id}] 清理事件订阅时发生严重错误:`, error)
|
|
|
|
|
|
cleanupResult.errors.push(`事件订阅清理错误: ${error.message}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 2. 清理Document事件监听器
|
|
|
|
|
|
cleanupDragEventListeners()
|
|
|
|
|
|
cleanupResult.documentListeners = 3 // mousemove, mouseup, mouseleave
|
|
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error(`[Panel:${props.id}] 清理Document事件监听器时发生错误:`, error)
|
|
|
|
|
|
cleanupResult.errors.push(`Document事件清理错误: ${error.message}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 重置状态
|
|
|
|
|
|
try {
|
|
|
|
|
|
isDragging = false
|
|
|
|
|
|
if (import.meta.env.DEV) {
|
|
|
|
|
|
console.log(`[Panel:${props.id}] 拖拽状态已重置`)
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
cleanupResult.errors.push(`状态重置错误: ${error.message}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 4. 输出清理结果摘要
|
|
|
|
|
|
if (import.meta.env.DEV) {
|
|
|
|
|
|
const totalErrors = cleanupResult.errors.length
|
|
|
|
|
|
if (totalErrors > 0) {
|
|
|
|
|
|
console.warn(`[Panel:${props.id}] 清理完成,存在 ${totalErrors} 个错误:`, cleanupResult.errors)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.log(`[Panel:${props.id}] 清理完全成功,无错误`)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return cleanupResult
|
2025-11-20 09:44:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 生命周期钩子
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
|
console.log(`[Panel:${props.id}] 组件已挂载`)
|
|
|
|
|
|
|
2025-11-20 10:21:05 +08:00
|
|
|
|
// 注册到全局内存保护机制
|
|
|
|
|
|
if (window.__panelMemoryProtection) {
|
|
|
|
|
|
window.__panelMemoryProtection.registerPanel(`panel_${props.id}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-20 09:44:51 +08:00
|
|
|
|
// 启用调试模式(开发环境)
|
|
|
|
|
|
if (import.meta.env.DEV) {
|
|
|
|
|
|
eventBus.setDebugMode(true)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 设置事件监听器
|
|
|
|
|
|
setupEventListeners()
|
2025-11-20 10:21:05 +08:00
|
|
|
|
|
|
|
|
|
|
// 更新活动状态
|
|
|
|
|
|
if (window.__panelMemoryProtection) {
|
|
|
|
|
|
window.__panelMemoryProtection.updateActivity(`panel_${props.id}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (import.meta.env.DEV) {
|
|
|
|
|
|
console.log(`[Panel:${props.id}] 所有监听器设置完成`)
|
|
|
|
|
|
}
|
2025-11-20 09:44:51 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
|
|
|
console.log(`[Panel:${props.id}] 组件即将卸载`)
|
|
|
|
|
|
|
2025-11-20 10:21:05 +08:00
|
|
|
|
// 立即注销全局内存保护机制
|
|
|
|
|
|
if (window.__panelMemoryProtection) {
|
|
|
|
|
|
window.__panelMemoryProtection.unregisterPanel(`panel_${props.id}`)
|
|
|
|
|
|
}
|
2025-11-20 09:44:51 +08:00
|
|
|
|
|
2025-11-20 10:21:05 +08:00
|
|
|
|
try {
|
|
|
|
|
|
// 1. 立即设置标志位,防止新的异步操作
|
2025-11-20 09:44:51 +08:00
|
|
|
|
isDragging = false
|
2025-11-20 10:21:05 +08:00
|
|
|
|
|
|
|
|
|
|
// 2. 同步清理所有可以直接清理的资源
|
|
|
|
|
|
const cleanupResult = cleanupEventListeners()
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 记录清理结果
|
|
|
|
|
|
if (import.meta.env.DEV) {
|
|
|
|
|
|
console.log(`[Panel:${props.id}] 组件清理结果:`, cleanupResult)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 4. 清理超时保护 - 简化版本,防止无限等待
|
|
|
|
|
|
const cleanupTimeout = setTimeout(() => {
|
|
|
|
|
|
console.warn(`[Panel:${props.id}] 清理超时,但继续卸载`)
|
|
|
|
|
|
}, 200) // 缩短超时时间
|
|
|
|
|
|
|
|
|
|
|
|
// 5. 清理超时定时器,确保不会泄露
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
clearTimeout(cleanupTimeout)
|
|
|
|
|
|
}, 250)
|
|
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error(`[Panel:${props.id}] 清理过程中出现异常:`, error)
|
|
|
|
|
|
|
|
|
|
|
|
// 即使出现异常,也要尝试强制清理
|
|
|
|
|
|
try {
|
|
|
|
|
|
isDragging = false
|
|
|
|
|
|
// 强制清理事件监听器
|
|
|
|
|
|
cleanupEventListeners()
|
|
|
|
|
|
} catch (forceError) {
|
|
|
|
|
|
console.error(`[Panel:${props.id}] 强制清理也失败:`, forceError)
|
|
|
|
|
|
}
|
2025-11-20 09:44:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
})
|
2025-10-31 21:58:33 +08:00
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
/* 面板基础样式 */
|
|
|
|
|
|
.panel {
|
2025-11-04 15:45:10 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
2025-10-31 21:58:33 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 图标样式优化 */
|
|
|
|
|
|
.icon-square-svg {
|
|
|
|
|
|
/* 优化SVG渲染,避免1px边框显示过粗的问题 */
|
|
|
|
|
|
shape-rendering: crispEdges;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-18 15:39:46 +08:00
|
|
|
|
/* 内容区域滚动条样式 */
|
|
|
|
|
|
.content-area {
|
|
|
|
|
|
/* 确保滚动条正确显示 */
|
|
|
|
|
|
scrollbar-width: thin;
|
|
|
|
|
|
scrollbar-color: #c7d2ea #f5f7fb;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Webkit浏览器滚动条样式 */
|
|
|
|
|
|
.content-area::-webkit-scrollbar {
|
|
|
|
|
|
width: 12px;
|
|
|
|
|
|
height: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.content-area::-webkit-scrollbar-track {
|
|
|
|
|
|
background: #f5f7fb;
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.content-area::-webkit-scrollbar-thumb {
|
|
|
|
|
|
background: linear-gradient(to bottom, #d0d6ea, #c0c7e0);
|
|
|
|
|
|
border: 1px solid #b0b6d6;
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.content-area::-webkit-scrollbar-thumb:hover {
|
|
|
|
|
|
background: linear-gradient(to bottom, #c1c7e2, #b2b8d9);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.content-area::-webkit-scrollbar-corner {
|
|
|
|
|
|
background: #f5f7fb;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-31 21:58:33 +08:00
|
|
|
|
/* 禁用可能存在的旧伪元素样式 */
|
|
|
|
|
|
:deep(.icon-square::before),
|
|
|
|
|
|
:deep(.icon-square::after) {
|
|
|
|
|
|
content: none !important;
|
|
|
|
|
|
display: none !important;
|
|
|
|
|
|
border: 0 !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|