Files
JoyD/AutoRobot/Windows/Robot/Web/src/DockLayout/DockLayout.vue

630 lines
20 KiB
Vue
Raw Normal View History

2025-10-31 23:58:26 +08:00
<template>
<div class="dock-layout" ref="dockLayoutRef" style="display: flex; flex-direction: column; position: relative; width: 100%; height: 100%;">
<!-- 停靠指示器组件 - 设置高z-index确保显示在最顶层 -->
<DockIndicator
:visible="showDockIndicator"
:target-rect="targetAreaRect"
:mouse-position="currentMousePosition"
:hide-edge-indicators="hideEdgeIndicators"
@zone-active="onDockZoneActive"
style="z-index: 9999;"
/>
2025-12-15 09:03:32 +08:00
<!-- 主区域使用Render组件统一渲染 -->
<div class="main-area-container" style="position: relative; width: 100%; height: 100%;">
<Render
:type="'Area'"
:config="mainAreaConfig"
ref="mainAreaRef"
/>
<!-- ResizeBar直接渲染在主区域容器中 -->
<ResizeBar
v-for="resizeBar in mainAreaResizeBars"
:key="resizeBar.id"
:target-id="resizeBar.targetId"
:direction="resizeBar.direction"
:min-size="resizeBar.minSize"
:max-size="resizeBar.maxSize"
:initial-size="resizeBar.initialSize"
@resize="(size) => handleMainAreaResizeBar(resizeBar.id, size)"
@resize-start="() => handleMainAreaResizeBarStart(resizeBar.id)"
@resize-end="() => handleMainAreaResizeBarEnd(resizeBar.id)"
:style="getMainAreaResizeBarStyle(resizeBar)"
/>
</div>
2025-11-19 13:57:51 +08:00
<!-- 浮动区域使用Render组件统一渲染 -->
<Render
v-for="area in floatingAreas"
:key="area.id"
2025-11-19 15:26:39 +08:00
:type="'Area'"
2025-11-19 13:57:51 +08:00
:config="area"
:style="{ zIndex: area.zIndex || zIndexManager.getFloatingAreaZIndex(area.id) }"
/>
</div>
2025-10-31 23:58:26 +08:00
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted, defineEmits } from 'vue'
2025-10-31 23:58:26 +08:00
import Area from './Area.vue';
import Panel from './Panel.vue';
import TabPage from './TabPage.vue';
import DockIndicator from './DockIndicator.vue';
2025-11-18 15:39:46 +08:00
import ResizeBar from './ResizeBar.vue';
2025-11-19 13:57:51 +08:00
import Render from './Render.vue';
2025-12-25 13:53:52 +08:00
import { zIndexManager } from './dockLayers';
import { eventBus, EVENT_TYPES } from './eventBus';
import { areaActions } from './handlers/AreaHandler';
import { dragStateActions } from './handlers/DragStateManager';
import { panelActions } from './handlers/PanelHandler';
import { tabPageActions } from './handlers/TabPageHandler';
import { globalEventActions } from './handlers/GlobalEventManager';
2025-11-18 13:48:13 +08:00
// 定义组件可以发出的事件
const emit = defineEmits([
'maximize', // 面板最大化事件
'toggleCollapse', // 折叠状态切换事件
'toggleToolbar', // 工具栏切换事件
'dragStart', // 拖拽开始事件
'dragMove', // 拖拽移动事件
'dragEnd' // 拖拽结束事件
])
// 主区域状态
const windowState = ref('最大化')
2025-12-15 09:03:32 +08:00
// 主区域配置
const mainAreaConfig = ref({
id: 'MainArea',
title: '主区域',
windowState: windowState.value,
2025-12-29 09:05:37 +08:00
showTitleBar: false,
children: {
type: 'TabPage',
children: []
}
2025-12-15 09:03:32 +08:00
})
// 浮动区域列表 - 每个area包含panels数组
const floatingAreas = ref([])
// 容器引用
const dockLayoutRef = ref(null)
// 主区域引用
const mainAreaRef = ref(null)
// 停靠指示器相关状态
const showDockIndicator = ref(false)
const currentMousePosition = ref({ x: 0, y: 0 })
const targetAreaRect = ref({ left: 0, top: 0, width: 0, height: 0 })
const activeDockZone = ref(null)
2025-11-18 15:39:46 +08:00
// 主区域ResizeBar列表
const mainAreaResizeBars = ref([])
// 检查主区域内是否有其他Area简化版
const hasAreasInMainContent = ref(false)
2025-12-29 09:05:37 +08:00
const shouldOperateAreaInsteadOfPanel = (areaId) => {
const area = floatingAreas.value.find(a => a.id === areaId);
if (!area) return false;
const childrenArray = Array.isArray(area.children) ? area.children : [area.children];
if (childrenArray.length !== 1) return false;
const tabPage = childrenArray[0];
if (tabPage.type !== 'TabPage') return false;
const tabPageChildren = tabPage.children;
const tabPageChildrenArray = Array.isArray(tabPageChildren) ? tabPageChildren : [tabPageChildren];
if (tabPageChildrenArray.length !== 1) return false;
// 检查TabPage的children是否是Panel组件
const childItem = tabPageChildrenArray[0];
if (childItem.type !== 'Panel') return false;
return true;
};
2025-12-15 09:03:32 +08:00
const onCloseFloatingArea = (event) => {
const id = event.areaId;
areaActions.closeFloatingArea(id);
const index = floatingAreas.value.findIndex(a => a.id === id);
if (index !== -1) {
floatingAreas.value.splice(index, 1);
}
};
2025-12-15 09:03:32 +08:00
const onUpdatePosition = (event) => {
const id = event.areaId;
const position = event;
const area = floatingAreas.value.find(a => a.id === id);
if (area) {
area.x = position.left;
area.y = position.top;
}
};
2025-12-15 09:03:32 +08:00
const onMaximize = (event) => {
const panelId = event.panelId;
2025-12-29 09:05:37 +08:00
const areaId = event.areaId;
if (shouldOperateAreaInsteadOfPanel(areaId)) {
areaActions.toggleMaximize(areaId);
} else {
panelActions.maximize(panelId);
}
};
2025-12-29 09:05:37 +08:00
const onPanelClose = (event) => {
2025-12-15 09:03:32 +08:00
const areaId = event.areaId;
const panelId = event.panelId;
2025-12-29 09:05:37 +08:00
if (shouldOperateAreaInsteadOfPanel(areaId)) {
onCloseFloatingArea({ areaId });
} else {
areaActions.closePanel(areaId, panelId);
const area = floatingAreas.value.find(a => a.id === areaId);
if (area && area.children) {
for (const child of area.children) {
if (child.type === 'TabPage' && child.children) {
// 处理TabPage的children可能是单个Panel或Panel数组
const isArray = Array.isArray(child.children);
const childrenArray = isArray ? child.children : [child.children];
// 检查每个子项是否为Panel组件
for (let i = 0; i < childrenArray.length; i++) {
const item = childrenArray[i];
if (item.type === 'Panel' && item.id === panelId) {
// 从数组中移除Panel
if (isArray) {
child.children.splice(i, 1);
} else {
// 如果是单个Panel移除整个TabPage
const tabPageIndex = area.children.indexOf(child);
if (tabPageIndex !== -1) {
area.children.splice(tabPageIndex, 1);
}
break;
}
// 如果Panel数组为空移除TabPage
if (child.children.length === 0) {
const tabPageIndex = area.children.indexOf(child);
if (tabPageIndex !== -1) {
area.children.splice(tabPageIndex, 1);
}
}
break;
}
}
break;
}
}
}
}
};
// 简单的拖拽事件处理
const handleMainAreaDragOver = (event) => {
event.preventDefault();
globalEventActions.handleDragOver('main-area', event);
};
const handleMainAreaDragLeave = () => {
globalEventActions.handleDragLeave('main-area');
};
const handleAreaDragOver = (event) => {
event.preventDefault();
globalEventActions.handleDragOver('floating-area', event);
};
2025-12-15 09:03:32 +08:00
const handleAreaDragLeave = (event) => {
2025-12-29 09:05:37 +08:00
globalEventActions.handleDragLeave('floating-area');
};
const onPanelDragStart = (event) => {
const areaId = event.areaId;
if (shouldOperateAreaInsteadOfPanel(areaId)) {
const area = floatingAreas.value.find(a => a.id === areaId);
const startLeft = area ? (area.x || 0) : 0;
const startTop = area ? (area.y || 0) : 0;
eventBus.emit(EVENT_TYPES.AREA_DRAG_START, {
dragId: event.dragId,
areaId: areaId,
event: {
clientX: event.position.x,
clientY: event.position.y
},
element: null,
position: event.position,
clientX: event.position.x,
clientY: event.position.y,
startLeft: startLeft,
startTop: startTop,
timestamp: event.timestamp
});
}
};
const onPanelDragMove = (event) => {
const areaId = event.areaId;
if (shouldOperateAreaInsteadOfPanel(areaId)) {
eventBus.emit(EVENT_TYPES.AREA_DRAG_MOVE, {
dragId: event.dragId,
areaId: areaId,
event: {
clientX: event.position.x,
clientY: event.position.y
},
position: event.position,
clientX: event.position.x,
clientY: event.position.y,
timestamp: event.timestamp
});
}
};
const onPanelDragEnd = (event) => {
const areaId = event.areaId;
if (shouldOperateAreaInsteadOfPanel(areaId)) {
const area = floatingAreas.value.find(a => a.id === areaId);
const left = area ? (area.x || 0) : 0;
const top = area ? (area.y || 0) : 0;
eventBus.emit(EVENT_TYPES.AREA_DRAG_END, {
dragId: event.dragId,
areaId: areaId,
finalPosition: {
x: left,
y: top
},
left: left,
top: top
});
}
};
// 其他事件处理方法
const onDockZoneActive = (zone) => {
activeDockZone.value = zone;
};
const onPanelMaximizeSync = ({ areaId, maximized }) => {
2025-12-15 09:03:32 +08:00
// 使用areaActions.updateState来更新区域的最大化状态
areaActions.updateState(areaId, { maximized });
};
2025-12-15 09:03:32 +08:00
const onAreaMerged = (event) => {
areaActions.handleAreaMerged(event.areaId);
};
2025-12-15 09:03:32 +08:00
// 标签页切换事件处理
const onTabChange = async (data) => {
try {
await tabPageActions.switch(data.tabPageId, data.areaId, data.fromTabPageId);
} catch (error) {
console.error('Failed to handle tab change:', error);
}
};
2025-12-15 09:03:32 +08:00
const onTabClose = async (data) => {
try {
await tabPageActions.requestClose(data.tabPageId, data.areaId);
} catch (error) {
console.error('Failed to handle tab close:', error);
}
};
2025-12-15 09:03:32 +08:00
const onTabAdd = async (data) => {
try {
await tabPageActions.create(data.areaId, data.config);
} catch (error) {
console.error('Failed to handle tab add:', error);
}
};
// ResizeBar相关处理方法
const handleMainAreaResizeBar = (id, size) => {
areaActions.handleResize(id, size);
};
const handleMainAreaResizeBarStart = (id) => {
areaActions.handleResizeStart(id);
};
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);
};
// 计算属性
const hideEdgeIndicators = computed(() => {
return !hasAreasInMainContent.value;
});
2025-12-15 09:03:32 +08:00
// 设置事件总线监听器
const setupEventListeners = () => {
2025-12-25 13:53:52 +08:00
// 创建一个数组来保存所有的取消订阅函数
const unsubscribeFunctions = [];
2025-12-15 09:03:32 +08:00
// Area相关事件
2025-12-26 13:09:35 +08:00
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' }));
2025-12-15 09:03:32 +08:00
// Tab相关事件
2025-12-26 13:09:35 +08:00
unsubscribeFunctions.push(eventBus.on(EVENT_TYPES.TAB_CHANGE, onTabChange, { componentId: 'dock-layout' }));
unsubscribeFunctions.push(eventBus.on(EVENT_TYPES.TAB_CLOSE, onTabClose, { componentId: 'dock-layout' }));
unsubscribeFunctions.push(eventBus.on(EVENT_TYPES.TAB_ADD, onTabAdd, { componentId: 'dock-layout' }));
2025-12-15 09:03:32 +08:00
// Panel相关事件
2025-12-26 13:09:35 +08:00
unsubscribeFunctions.push(eventBus.on(EVENT_TYPES.PANEL_TOGGLE_COLLAPSE, () => emit('toggleCollapse'), { componentId: 'dock-layout' }));
unsubscribeFunctions.push(eventBus.on(EVENT_TYPES.PANEL_MAXIMIZE, onMaximize, { componentId: 'dock-layout' }));
unsubscribeFunctions.push(eventBus.on(EVENT_TYPES.PANEL_CLOSE_REQUEST, onCloseFloatingArea, { componentId: 'dock-layout' }));
2025-12-29 09:05:37 +08:00
unsubscribeFunctions.push(eventBus.on(EVENT_TYPES.PANEL_CLOSE, onPanelClose, { componentId: 'dock-layout' }));
2025-12-26 13:09:35 +08:00
unsubscribeFunctions.push(eventBus.on(EVENT_TYPES.PANEL_TOGGLE_TOOLBAR, () => emit('toggleToolbar'), { componentId: 'dock-layout' }));
unsubscribeFunctions.push(eventBus.on(EVENT_TYPES.PANEL_MAXIMIZE_SYNC, onPanelMaximizeSync, { componentId: 'dock-layout' }));
2025-12-29 09:05:37 +08:00
unsubscribeFunctions.push(eventBus.on(EVENT_TYPES.PANEL_DRAG_START, onPanelDragStart, { componentId: 'dock-layout' }));
unsubscribeFunctions.push(eventBus.on(EVENT_TYPES.PANEL_DRAG_MOVE, onPanelDragMove, { componentId: 'dock-layout' }));
unsubscribeFunctions.push(eventBus.on(EVENT_TYPES.PANEL_DRAG_END, onPanelDragEnd, { componentId: 'dock-layout' }));
2025-12-15 09:03:32 +08:00
// Resize相关事件
2025-12-26 13:09:35 +08:00
unsubscribeFunctions.push(eventBus.on(EVENT_TYPES.RESIZE_START, () => emit('dragStart'), { componentId: 'dock-layout' }));
unsubscribeFunctions.push(eventBus.on(EVENT_TYPES.RESIZE_MOVE, () => emit('dragMove'), { componentId: 'dock-layout' }));
unsubscribeFunctions.push(eventBus.on(EVENT_TYPES.RESIZE_END, () => emit('dragEnd'), { componentId: 'dock-layout' }));
2025-12-15 09:03:32 +08:00
// Window相关事件
2025-12-25 13:53:52 +08:00
unsubscribeFunctions.push(eventBus.on(EVENT_TYPES.WINDOW_STATE_CHANGE, (event) => {
2025-12-15 09:03:32 +08:00
// 处理窗口状态变化
2025-12-26 13:09:35 +08:00
}, { componentId: 'dock-layout' }));
2025-12-15 09:03:32 +08:00
// 自定义事件
2025-12-29 09:05:37 +08:00
unsubscribeFunctions.push(eventBus.on(EVENT_TYPES.AREA_MERGED, onAreaMerged, { componentId: 'dock-layout' }));
unsubscribeFunctions.push(eventBus.on(EVENT_TYPES.DOCK_ZONE_ACTIVE, (event) => onDockZoneActive(event.zoneId), { componentId: 'dock-layout' }));
2025-12-25 13:53:52 +08:00
return unsubscribeFunctions;
};
2025-12-25 13:53:52 +08:00
// 保存取消订阅函数数组
const unsubscribeFunctions = ref([]);
// 清理函数
const cleanup = () => {
// 清理事件监听器和其他资源
console.log('🧹 开始清理DockLayout资源');
2025-12-29 09:05:37 +08:00
// 清理事件监听器
console.log('🔇 开始清理事件监听器');
unsubscribeFunctions.value.forEach(unsubscribe => unsubscribe());
unsubscribeFunctions.value = [];
// 清理浮动区域
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资源清理完成');
};
// 轻量级隐藏区域管理
const hiddenAreas = ref([]);
// 隐藏Area管理方法
2025-11-17 10:59:46 +08:00
// 将Area添加到隐藏列表
const addAreaToHiddenList = (area) => {
// 确保area有唯一标识符
if (!area.id) {
area.id = `hidden-area-${Date.now()}`
}
// 检查是否已经存在于隐藏列表中
const existingIndex = hiddenAreas.value.findIndex(h => h.id === area.id)
if (existingIndex === -1) {
// 添加到隐藏列表
hiddenAreas.value.push({
...area,
hiddenAt: new Date().toISOString()
})
}
}
// 移除重复的详细实现函数 - 使用轻量级代理
2025-11-18 15:39:46 +08:00
/**
* 处理并排停靠逻辑
* 当主区域内已有Area时压缩目标Area并创建并排布局
* @param {Object} sourceArea - 源Area对象
* @param {string} dockZone - 停靠方向
* @returns {Object} 处理结果 {success: boolean, message: string}
*/
// 轻量级并排停靠代理
const handleSideBySideDocking = () => '轻量级并排停靠代理'
// 轻量级ResizeBar添加代理
const addResizeBarForSideBySideLayout = () => '轻量级ResizeBar代理'
// 轻量级ResizeBar事件代理
const handleResizeBarResize = () => '轻量级ResizeBar调整代理'
// 轻量级ResizeBar尺寸调整代理
const handleResizeBarResizeStart = () => '轻量级ResizeBar调整开始代理'
const handleResizeBarResizeEnd = () => '轻量级ResizeBar调整结束代理'
// 轻量级尺寸调整代理
const handleHorizontalResize = () => '轻量级水平调整代理'
const handleVerticalResize = () => '轻量级垂直调整代理'
// 轻量级Area查找代理
const findFirstMainArea = () => '轻量级主区域查找代理'
const getOrCreateTargetArea = () => '轻量级目标区域获取代理'
// 轻量级并排布局代理
const createSideBySideLayout = () => '轻量级并排布局代理'
const compressTargetArea = () => '轻量级压缩目标区域代理'
const adjustSourceAreaForLayout = () => '轻量级布局调整代理'
// 添加浮动面板
const addFloatingPanel = (panel) => {
// 确保panel参数存在否则使用默认面板对象
const safePanel = panel || {
id: `panel-${Date.now()}`,
title: '新建面板',
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' }
]
}
};
2025-11-18 15:39:46 +08:00
const newArea = {
id: `area-${Date.now()}`,
x: 100 + Math.random() * 200,
y: 100 + Math.random() * 200,
width: 300,
height: 200,
zIndex: zIndexManager.getFloatingAreaZIndex(`area-${Date.now()}`),
// 使用children结构以兼容Render组件的渲染逻辑
children: {
type: 'TabPage',
children: [{
...safePanel,
id: `panel-${Date.now()}`,
title: safePanel.title || '新建面板',
type: 'Panel'
}]
}
2025-11-18 15:39:46 +08:00
}
floatingAreas.value.push(newArea)
return newArea.id
2025-11-18 15:39:46 +08:00
}
// 查找或创建主区域TabPage
const findOrCreateMainAreaTabPage = () => {
// 返回主区域的tabPage信息
return {
id: 'main-area-tabpage',
title: '主区域',
items: []
};
2025-11-18 15:39:46 +08:00
}
// 轻量级生命周期处理
onMounted(() => {
// 初始化轻量级状态
console.log('DockLayout component mounted');
2025-12-25 13:53:52 +08:00
unsubscribeFunctions.value = setupEventListeners();
})
2025-11-18 15:39:46 +08:00
// 组件卸载时清理资源
onUnmounted(() => {
// 清理事件监听器和其他资源
console.log('DockLayout component unmounted');
cleanup();
2025-12-25 13:53:52 +08:00
// 逐个移除事件监听器
unsubscribeFunctions.value.forEach(unsubscribe => unsubscribe());
unsubscribeFunctions.value = [];
})
// 暴露轻量级接口给父组件
defineExpose({
// 基础数据
2025-11-19 13:57:51 +08:00
floatingAreas,
hiddenAreas,
// 核心方法
2025-11-17 10:59:46 +08:00
addFloatingPanel,
2025-11-18 15:39:46 +08:00
findOrCreateMainAreaTabPage,
// 轻量级代理方法(功能保留但简化)
handleDockingEnding: () => '轻量级停靠处理代理',
handleEdgeDocking: () => '轻量级边缘停靠代理',
handleSideBySideDocking: () => '轻量级并排停靠代理',
// ResizeBar轻量级代理
addResizeBarForSideBySideLayout: () => '轻量级ResizeBar代理',
handleResizeBarResize: () => '轻量级ResizeBar调整代理',
// 隐藏列表轻量级代理
getHiddenAreas: () => '轻量级隐藏列表代理',
restoreAreaFromHidden: () => '轻量级恢复代理',
removeFromHiddenList: () => '轻量级移除代理',
clearHiddenList: () => '轻量级清空代理',
})
</script>
<style scoped>
.dock-layout {
position: relative;
width: 100%;
height: 100%;
overflow: visible;
}
2025-11-03 17:26:28 +08:00
/* 浮动区域样式已直接应用到Area组件 */
/* 添加浮动区域按钮样式 */
.add-floating-btn {
font-size: 14px;
cursor: pointer;
user-select: none;
}
.add-floating-btn:active {
transform: scale(0.98);
}
</style>