Files
JoyD/AutoRobot/Windows/Robot/Web/src/DockLayout/DockLayout.vue
2025-11-19 16:06:50 +08:00

2369 lines
74 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="dock-layout" ref="dockLayoutRef" style="display: flex; flex-direction: column; position: relative;">
<!-- 停靠指示器组件 - 设置高z-index确保显示在最顶层 -->
<!-- :visible="showDockIndicator" :visible="true" -->
<DockIndicator
:visible="showDockIndicator"
:target-rect="targetAreaRect"
:mouse-position="currentMousePosition"
:hide-edge-indicators="hideEdgeIndicators"
@zone-active="onDockZoneActive"
style="z-index: 9999;"
/>
<!-- 主区域 - 添加ref引用 -->
<Area
ref="mainAreaRef"
:WindowState="windowState"
:showTitleBar="false"
title="主区域"
:style="{ position: 'relative', width: '100%', height: '100%', zIndex: 1 }"
@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"
: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>
</Area>
<!-- 浮动区域使用Render组件统一渲染 -->
<Render
v-for="area in floatingAreas"
:key="area.id"
:type="'Area'"
:config="area"
:style="{ zIndex: area.zIndex || zIndexManager.getFloatingAreaZIndex(area.id) }"
@close="() => onCloseFloatingArea(area.id)"
@update:position="(position) => onUpdatePosition(area.id, position)"
@panelMaximizeSync="onPanelMaximizeSync"
@areaDragStart="(event) => onAreaDragStart(area.id, event)"
@areaDragMove="(event) => onAreaDragMove(area.id, event)"
@areaDragEnd="(event) => onAreaDragEnd(area.id, event)"
@tab-change="onTabChange"
@tab-close="onTabClose"
@tab-add="onTabAdd"
@tabDragStart="(event) => onTabDragStart(area.id, event)"
@tabDragMove="(event) => onTabDragMove(area.id, event)"
@tabDragEnd="onTabDragEnd"
@toggleCollapse="(panelId) => $emit('toggleCollapse', panelId)"
@maximize="(panelId) => onMaximize(panelId)"
@closePanel="(panelId) => onClosePanel(area.id, panelId)"
@toggleToolbar="(panelId) => $emit('toggleToolbar', panelId)"
@dragStart="(event) => onPanelDragStartFromTabPage(area.id, event)"
@dragMove="(event) => onPanelDragMoveFromTabPage(area.id, event)"
@dragEnd="onPanelDragEndFromTabPage"
@dragover="handleAreaDragOver"
@dragleave="handleAreaDragLeave"
/>
</div>
</template>
<script setup>
import { ref, defineExpose, defineEmits, nextTick, watch, computed, onMounted } from 'vue'
import Area from './Area.vue';
import Panel from './Panel.vue';
import TabPage from './TabPage.vue';
import DockIndicator from './DockIndicator.vue';
import ResizeBar from './ResizeBar.vue';
import Render from './Render.vue';
import { Z_INDEX_LAYERS, zIndexManager } from './dockLayers.js';
// 定义组件可以发出的事件
const emit = defineEmits([
'maximize', // 面板最大化事件
'toggleCollapse', // 折叠状态切换事件
'toggleToolbar', // 工具栏切换事件
'dragStart', // 拖拽开始事件
'dragMove', // 拖拽移动事件
'dragEnd' // 拖拽结束事件
])
// 主区域状态
const windowState = ref('最大化')
// 浮动区域列表 - 每个area包含panels数组
const floatingAreas = ref([])
// 隐藏区域列表 - 存储被隐藏的Area
const hiddenAreas = ref([])
// 容器引用
const dockLayoutRef = ref(null)
// 主区域引用
const mainAreaRef = ref(null)
// 区域ID计数器
let areaIdCounter = 1
// 停靠指示器相关状态
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)
// 检查主区域内是否有其他AreaTabPage和Panel等子组件
const hasAreasInMainContent = ref(false)
// 主区域ResizeBar列表
const mainAreaResizeBars = ref([])
// 计算是否隐藏外部边缘指示器
const hideEdgeIndicators = computed(() => {
// 当主区域内没有其他Area时隐藏外部边缘指示器
return !hasAreasInMainContent.value
})
// Panel拖拽相关状态
const panelDragState = ref({
isDragging: false,
currentAreaId: null,
startClientPos: { x: 0, y: 0 },
startAreaPos: { x: 0, y: 0 }
})
// TabPage拖拽相关状态
const tabDragState = ref({
isDragging: false,
currentAreaId: null,
startClientPos: { x: 0, y: 0 },
startAreaPos: { x: 0, y: 0 }
})
// 处理从TabPage转发来的Panel拖拽事件
const onPanelDragStartFromTabPage = (areaId, event) => {
// console.log('🔸 DockLayout收到从TabPage转发的Panel拖拽事件:', { areaId, event })
// 直接调用原始的onPanelDragStart处理函数
onPanelDragStart(areaId, event)
}
// 处理从TabPage转发来的Panel拖拽移动事件
const onPanelDragMoveFromTabPage = (areaId, event) => {
// console.log('🔸 DockLayout收到从TabPage转发的Panel拖拽移动事件:', { areaId, event })
// 直接调用原始的onPanelDragMove处理函数
onPanelDragMove(areaId, event)
}
// 处理从TabPage转发来的Panel拖拽结束事件
const onPanelDragEndFromTabPage = () => {
// console.log('🔸 DockLayout收到从TabPage转发的Panel拖拽结束事件')
// 直接调用原始的onPanelDragEnd处理函数
onPanelDragEnd()
}
// 检测主区域内是否有其他Area
const checkMainContentForAreas = () => {
if (!mainAreaRef.value) {
hasAreasInMainContent.value = false
return
}
try {
// 获取主区域的DOM元素
const mainAreaElement = mainAreaRef.value.$el
if (!mainAreaElement) {
hasAreasInMainContent.value = false
return
}
// 检查主区域内容区内是否包含vs-area元素子Area
const childAreas = mainAreaElement.querySelectorAll('.vs-area')
const hasChildAreas = childAreas.length > 1 // 排除主区域本身
// 检查主区域内容区内是否包含TabPage
const tabPages = mainAreaElement.querySelectorAll('.tab-page, [class*="tab"]')
// 检查主区域内容区内是否包含Panel
const panels = mainAreaElement.querySelectorAll('.panel, [class*="panel"]')
// 如果有任何子Area、TabPage或Panel则认为主区域内有其他Area
hasAreasInMainContent.value = hasChildAreas || tabPages.length > 0 || panels.length > 0
} catch (error) {
// console.error('检查主区域内容时出错:', error)
hasAreasInMainContent.value = false
}
}
/**
* 生成随机测试内容用于合并测试
* @param {number} panelIndex - 面板索引
* @returns {Object} 包含随机内容的对象
*/
const generateRandomContent = (panelIndex) => {
const contentTypes = ['图表', '数据', '配置', '文档', '代码', '日志']
const randomType = contentTypes[Math.floor(Math.random() * contentTypes.length)]
const colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7', '#DDA0DD']
const randomColor = colors[Math.floor(Math.random() * colors.length)]
return {
type: randomType,
color: randomColor,
index: panelIndex,
title: `${randomType}面板 ${panelIndex + 1}`,
data: generateSampleData(panelIndex),
timestamp: new Date().toLocaleString()
}
}
/**
* 生成样本数据
* @param {number} dataIndex - 数据索引
* @returns {Array} 样本数据数组
*/
const generateSampleData = (dataIndex) => {
const data = []
for (let i = 0; i < 5; i++) {
data.push({
label: `项目 ${String.fromCharCode(65 + i)}`,
value: Math.floor(Math.random() * 100) + 10,
id: `data-${dataIndex}-${i}`
})
}
return data
}
// 添加新的浮动面板
const addFloatingPanel = () => {
// 获取父容器尺寸以计算居中位置
let x = 50 + (areaIdCounter - 2) * 20
let y = 50 + (areaIdCounter - 2) * 20
// 如果容器已渲染,计算居中位置
if (dockLayoutRef.value) {
const containerRect = dockLayoutRef.value.getBoundingClientRect()
const width = 280
const height = 200
x = Math.floor((containerRect.width - width) / 2)
y = Math.floor((containerRect.height - height) / 2)
}
// 获取当前ID并递增
const currentId = areaIdCounter++
const areaId = `floating-area-${currentId}`
// 使用z-index管理器为新浮动区域分配z-index
const newZIndex = zIndexManager.getFloatingAreaZIndex(areaId)
// 直接创建符合Render config的数据结构确保响应式同步
const newArea = {
id: areaId,
title: `浮动区域 ${currentId}`,
x: x,
y: y,
width: 280,
height: 200,
windowState: '正常',
showTitleBar: true,
resizable: true,
draggable: true,
zIndex: newZIndex, // 使用z-index管理器分配的层级
// 使用Render期望的children结构
children: [
{
type: 'TabPage',
id: `tabpage-${currentId}-1`,
title: `标签页 1`,
tabPosition: 'bottom',
children: {
type: 'Panel',
items: [
{
type: 'Panel',
id: `panel-${currentId}-1-1`,
title: `面板 ${currentId}`,
x: 0,
y: 0,
width: 280,
height: 200,
collapsed: false,
toolbarExpanded: false,
maximized: false,
content: generateRandomContent(currentId)
}
]
}
}
]
}
floatingAreas.value.push(newArea)
// console.log('✅ 创建浮动面板成功:', newArea.id, 'z-index:', newZIndex)
// console.log('🔍 浮动面板数据结构:', JSON.stringify(newArea, null, 2))
// 使用nextTick确保DOM更新后再进行后续操作
nextTick(() => {
// console.log('🔍 浮动面板已添加到DOMfloatingAreas长度:', floatingAreas.value.length)
})
}
// 更新区域位置
const onUpdatePosition = (id, position) => {
const area = floatingAreas.value.find(a => a.id === id)
if (area) {
area.x = position.left
area.y = position.top
}
}
// 切换折叠状态
const onToggleCollapse = (id) => {
const area = floatingAreas.value.find(a => a.id === id)
if (area) {
area.collapsed = !area.collapsed
}
}
// 最大化/还原
const onMaximize = (panelId) => {
// 查找包含该面板的区域
for (const area of floatingAreas.value) {
if (area.children) {
for (const child of area.children) {
if (child.type === 'TabPage' && child.children && child.children.type === 'Panel') {
const panels = child.children.items || []
if (panels.length === 1 && panels[0].id === panelId) {
// 当区域只包含一个Panel时切换Area和Panel的最大化状态
const isCurrentlyMaximized = area.windowState === '最大化' || area.windowState === 'maximized'
if (isCurrentlyMaximized) {
// 切换为正常状态
area.windowState = '正常'
// 确保Panel也恢复正常状态 - 使用展开运算符创建新对象确保响应式
child.children.items[0] = { ...panels[0], maximized: false }
} else {
// 切换为最大化状态
area.windowState = '最大化'
// 同时最大化Panel - 使用展开运算符创建新对象确保响应式
child.children.items[0] = { ...panels[0], maximized: true }
// 激活浮动区域,将其置于最顶层
zIndexManager.activateFloatingArea(area.id)
area.zIndex = zIndexManager.getFloatingAreaZIndex(area.id, true)
}
break
}
}
}
}
}
}
// 关闭浮动区域 - 同时移除内容区的Panel
const onCloseFloatingArea = (id) => {
const index = floatingAreas.value.findIndex(a => a.id === id)
if (index !== -1) {
// 获取要移除的Area
const areaToRemove = floatingAreas.value[index]
// 从z-index管理器中移除该区域的层级管理
zIndexManager.removeFloatingArea(id)
// 清理Panel引用确保Panel被正确移除
if (areaToRemove.panels) {
// 这里可以添加任何需要的Panel清理逻辑
// 清空panels数组确保Panel被正确移除
areaToRemove.panels = []
}
// 从数组中移除Area
floatingAreas.value.splice(index, 1)
}
}
// 关闭面板 - 适配children数据结构
const onClosePanel = (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 && child.children.type === 'Panel') {
const panels = child.children.items || []
const panelIndex = panels.findIndex(p => p.id === panelId)
if (panelIndex !== -1) {
panels.splice(panelIndex, 1)
// 如果区域内没有面板了,可以考虑关闭整个区域
if (panels.length === 0) {
onCloseFloatingArea(areaId)
}
break
}
}
}
}
}
// 切换工具栏
const onToggleToolbar = (id) => {
const area = floatingAreas.value.find(a => a.id === id)
if (area) {
area.toolbarExpanded = !area.toolbarExpanded
}
}
// 隐藏Area管理方法 - 第1步实现
// 将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()
})
// console.log('Area已添加到隐藏列表:', area.id)
} else {
// console.warn('Area已在隐藏列表中:', area.id)
}
}
// 处理Area合并事件
const onAreaMerged = (eventData) => {
// console.log('处理Area合并事件:', eventData)
const { sourceArea, targetAreaHasContent } = eventData
// 获取源Area对象
const sourceAreaObj = floatingAreas.value.find(a => a.id === sourceArea.id)
if (!sourceAreaObj) {
// console.warn('找不到源Area:', sourceArea.id)
return
}
// 根据目标Area内容状态执行不同的隐藏逻辑
if (targetAreaHasContent) {
// 目标Area已有内容保存源Area及其TabPage组件到隐藏列表
// console.log('目标Area已有内容保存源Area和TabPage组件到隐藏列表')
addAreaToHiddenList({
...sourceAreaObj,
tabPages: [...sourceAreaObj.tabPages] // 深拷贝TabPages
})
} else {
// 目标Area为空仅保存源Area到隐藏列表
// console.log('目标Area为空保存源Area到隐藏列表')
addAreaToHiddenList(sourceAreaObj)
}
// 从浮动区域中移除已合并的源Area
const sourceIndex = floatingAreas.value.findIndex(a => a.id === sourceArea.id)
if (sourceIndex !== -1) {
floatingAreas.value.splice(sourceIndex, 1)
// console.log('源Area已从浮动区域移除:', sourceArea.id)
}
}
// Panel拖拽开始
const onPanelDragStart = (areaId, event) => {
// console.log('=== PANEL拖拽开始调试 ===')
// console.log('areaId:', areaId)
// console.log('event:', event)
// console.log('event.clientX:', event?.clientX)
// console.log('event.clientY:', event?.clientY)
// console.log('event.panelId:', event?.panelId)
const area = floatingAreas.value.find(a => a.id === areaId)
// console.log('找到的area:', area)
// console.log('area.tabPages.length:', area?.tabPages?.length)
// console.log('area.tabPages[0].panels.length:', area?.tabPages?.[0]?.panels?.length)
// 只有当Area中只有一个TabPage且该TabPage中只有一个Panel时才允许通过Panel标题栏移动Area
if (area && area.tabPages && area.tabPages.length === 1 && area.tabPages[0].panels && area.tabPages[0].panels.length === 1) {
// 检查event是否为对象格式来自Panel.vue
const clientX = event.clientX || (typeof event === 'object' ? event.clientX : event?.clientX)
const clientY = event.clientY || (typeof event === 'object' ? event.clientY : event?.clientY)
if (clientX === undefined || clientY === undefined) {
// console.error('无法获取有效的鼠标位置信息:', event)
return
}
panelDragState.value.isDragging = true
panelDragState.value.currentAreaId = areaId
panelDragState.value.startClientPos = {
x: clientX,
y: clientY
}
panelDragState.value.startAreaPos = {
x: area.x,
y: area.y
}
// 初始化鼠标位置跟踪
currentMousePosition.value = {
x: clientX,
y: clientY
}
// 拖拽开始时就显示指示器
showDockIndicator.value = true
// 检查主区域内是否有其他Area
checkMainContentForAreas()
// 使用dock-layout作为默认目标区域
if (dockLayoutRef.value) {
const rect = dockLayoutRef.value.getBoundingClientRect()
targetAreaRect.value = {
left: 0, // 使用相对于容器的位置(左上角)
top: 0,
width: rect.width,
height: rect.height
}
// 拖拽开始时立即更新停靠区域
updateDockZoneByMousePosition(clientX, clientY)
}
}
}
// Panel拖拽移动
const onPanelDragMove = (areaId, event) => {
// console.log('=== PANEL拖拽移动调试 ===')
// console.log('areaId:', areaId)
// console.log('panelDragState.isDragging:', panelDragState.value.isDragging)
// console.log('panelDragState.currentAreaId:', panelDragState.value.currentAreaId)
// console.log('event:', event)
// console.log('event.clientX:', event?.clientX)
// console.log('event.clientY:', event?.clientY)
if (panelDragState.value.isDragging && panelDragState.value.currentAreaId === areaId) {
const area = floatingAreas.value.find(a => a.id === areaId)
if (area) {
// 检查event是否为对象格式来自Panel.vue
const clientX = event.clientX || (typeof event === 'object' ? event.clientX : event?.clientX)
const clientY = event.clientY || (typeof event === 'object' ? event.clientY : event?.clientY)
if (clientX === undefined || clientY === undefined) {
return
}
// 计算移动距离
const deltaX = clientX - panelDragState.value.startClientPos.x
const deltaY = clientY - panelDragState.value.startClientPos.y
// 计算新位置
let newLeft = panelDragState.value.startAreaPos.x + deltaX
let newTop = panelDragState.value.startAreaPos.y + deltaY
// 确保不超出父容器边界
if (dockLayoutRef.value) {
const parentRect = dockLayoutRef.value.getBoundingClientRect()
// 严格边界检查
newLeft = Math.max(0, Math.min(newLeft, parentRect.width - area.width))
newTop = Math.max(0, Math.min(newTop, parentRect.height - area.height))
}
// 更新位置
area.x = newLeft
area.y = newTop
// 更新鼠标位置
currentMousePosition.value = {
x: clientX,
y: clientY
}
// 根据鼠标位置动态更新停靠区域
updateDockZoneByMousePosition(clientX, clientY)
// 调试信息(已注释)
}
}
}
// Panel拖拽结束
const onPanelDragEnd = () => {
// console.log('=== PANEL拖拽结束调试 ===')
// console.log('panelDragState.isDragging:', panelDragState.value.isDragging)
// console.log('panelDragState.currentAreaId:', panelDragState.value.currentAreaId)
// console.log('activeDockZone:', activeDockZone.value)
// console.log('currentMousePosition:', currentMousePosition.value)
panelDragState.value.isDragging = false
const currentAreaId = panelDragState.value.currentAreaId
panelDragState.value.currentAreaId = null
// 3.1 在onPanelDragEnd方法中添加中心停靠检测
// 确保只有在独立中心指示器区域内释放才执行停靠
if (activeDockZone.value === 'center' && currentAreaId && isMouseInCenterIndicator(currentMousePosition.value.x, currentMousePosition.value.y)) {
// 处理中心停靠
const result = handleCenterDocking(currentAreaId)
if (!result.success) {
// console.warn('中心停靠失败:', result.message)
} else {
// console.log('中心停靠成功:', result.message)
}
}
// 隐藏停靠指示器
showDockIndicator.value = false
activeDockZone.value = null
}
// Area拖拽开始
const onAreaDragStart = (areaId, event) => {
const area = floatingAreas.value.find(a => a.id === areaId)
if (!area) return
// 设置拖拽状态类似TabPage拖拽
panelDragState.value.isDragging = true
panelDragState.value.currentAreaId = areaId
panelDragState.value.startClientPos = {
x: event.clientX,
y: event.clientY
}
panelDragState.value.startAreaPos = {
x: area.x,
y: area.y
}
// 初始化鼠标位置跟踪
currentMousePosition.value = {
x: event.clientX,
y: event.clientY
}
// 拖拽开始时显示指示器
showDockIndicator.value = true
// 检查主区域内是否有其他Area
checkMainContentForAreas()
// 使用dock-layout作为默认目标区域
if (dockLayoutRef.value) {
const rect = dockLayoutRef.value.getBoundingClientRect()
targetAreaRect.value = {
left: 0,
top: 0,
width: rect.width,
height: rect.height
}
// 拖拽开始时立即更新停靠区域
updateDockZoneByMousePosition(event.clientX, event.clientY)
}
}
// Area拖拽移动
const onAreaDragMove = (areaId, event) => {
// 更新鼠标位置
currentMousePosition.value = {
x: event.clientX,
y: event.clientY
}
// 根据鼠标位置动态更新停靠区域
updateDockZoneByMousePosition(event.clientX, event.clientY)
}
// Area拖拽结束
const onAreaDragEnd = (areaId, event) => {
// console.log('[DockLayout] Area拖拽结束:', { areaId, event, activeDockZone: activeDockZone.value })
// 清理拖拽状态
panelDragState.value.isDragging = false
panelDragState.value.currentAreaId = null
// 3.1 在onAreaDragEnd方法中添加中心停靠检测
// 确保只有在独立中心指示器区域内释放才执行停靠
if (activeDockZone.value === 'center' && isMouseInCenterIndicator(currentMousePosition.value.x, currentMousePosition.value.y)) {
// console.log('[DockLayout] 检测到中心停靠区域,鼠标位置:', currentMousePosition.value)
// 处理中心停靠
const result = handleCenterDocking(areaId)
if (!result.success) {
// console.warn('Area中心停靠失败:', result.message)
} else {
// console.log('Area中心停靠成功:', result.message)
}
} else {
// console.log('[DockLayout] 未检测到有效的中心停靠:', {
// activeDockZone: activeDockZone.value,
// mousePosition: currentMousePosition.value,
// isInCenter: isMouseInCenterIndicator(currentMousePosition.value?.x || 0, currentMousePosition.value?.y || 0)
// })
}
// 隐藏停靠指示器
showDockIndicator.value = false
activeDockZone.value = null
}
// TabPage拖拽开始
const onTabDragStart = (areaId, event) => {
const area = floatingAreas.value.find(a => a.id === areaId)
// 只有当Area中只有一个TabPage时才允许通过TabPage的页标签移动Area
if (area && area.tabPages && area.tabPages.length === 1) {
// 检查event是否为对象格式来自TabPage.vue
const clientX = event.clientX || (typeof event === 'object' ? event.x : event?.x)
const clientY = event.clientY || (typeof event === 'object' ? event.y : event?.y)
if (clientX === undefined || clientY === undefined) {
// console.error('无法获取有效的鼠标位置信息:', event)
return
}
tabDragState.value.isDragging = true
tabDragState.value.currentAreaId = areaId
tabDragState.value.startClientPos = {
x: clientX,
y: clientY
}
tabDragState.value.startAreaPos = {
x: area.x,
y: area.y
}
// 初始化鼠标位置跟踪
currentMousePosition.value = {
x: clientX,
y: clientY
}
// 拖拽开始时就显示指示器
showDockIndicator.value = true
// 检查主区域内是否有其他Area
checkMainContentForAreas()
// 使用dock-layout作为默认目标区域
if (dockLayoutRef.value) {
const rect = dockLayoutRef.value.getBoundingClientRect()
targetAreaRect.value = {
left: 0, // 使用相对于容器的位置(左上角)
top: 0,
width: rect.width,
height: rect.height
}
// 拖拽开始时立即更新停靠区域
updateDockZoneByMousePosition(clientX, clientY)
}
}
}
// TabPage拖拽移动
const onTabDragMove = (areaId, event) => {
if (tabDragState.value.isDragging && tabDragState.value.currentAreaId === areaId) {
const area = floatingAreas.value.find(a => a.id === areaId)
if (area) {
// 检查event是否为对象格式来自TabPage.vue
const clientX = event.clientX || (typeof event === 'object' ? event.x : event?.x)
const clientY = event.clientY || (typeof event === 'object' ? event.y : event?.y)
if (clientX === undefined || clientY === undefined) {
return
}
// 计算移动距离
const deltaX = clientX - tabDragState.value.startClientPos.x
const deltaY = clientY - tabDragState.value.startClientPos.y
// 计算新位置
let newLeft = tabDragState.value.startAreaPos.x + deltaX
let newTop = tabDragState.value.startAreaPos.y + deltaY
// 确保不超出父容器边界
if (dockLayoutRef.value) {
const parentRect = dockLayoutRef.value.getBoundingClientRect()
// 严格边界检查
newLeft = Math.max(0, Math.min(newLeft, parentRect.width - area.width))
newTop = Math.max(0, Math.min(newTop, parentRect.height - area.height))
}
// 更新位置
area.x = newLeft
area.y = newTop
// 更新鼠标位置
currentMousePosition.value = {
x: clientX,
y: clientY
}
// 根据鼠标位置动态更新停靠区域
updateDockZoneByMousePosition(clientX, clientY)
// 调试信息
}
}
}
// TabPage拖拽结束
const onTabDragEnd = () => {
tabDragState.value.isDragging = false
const currentAreaId = tabDragState.value.currentAreaId
tabDragState.value.currentAreaId = null
// 3.2 在onTabDragEnd方法中添加中心停靠检测
// 确保只有在独立中心指示器区域内释放才执行停靠
if (activeDockZone.value === 'center' && currentAreaId && isMouseInCenterIndicator(currentMousePosition.value.x, currentMousePosition.value.y)) {
// 处理中心停靠
const result = handleCenterDocking(currentAreaId)
if (!result.success) {
// console.warn('中心停靠失败:', result.message)
} else {
// console.log('中心停靠成功:', result.message)
}
}
// 隐藏停靠指示器
showDockIndicator.value = false
activeDockZone.value = null
}
// 监听floatingAreas变化确保当Area最大化时Panel也会自动最大化
watch(floatingAreas, (newAreas) => {
newAreas.forEach(area => {
// 正确处理层级结构Area -> TabPage -> Panel
if (area.tabPages) {
for (const tabPage of area.tabPages) {
// 当区域只包含一个Panel且Area状态变为最大化时Panel也应该最大化
if (tabPage.panels && tabPage.panels.length === 1) {
const isAreaMaximized = area.WindowState === '最大化' || area.WindowState === 'maximized';
const isPanelMaximized = tabPage.panels[0].maximized;
// 如果状态不一致更新Panel的maximized属性
if (isAreaMaximized !== isPanelMaximized) {
tabPage.panels[0] = { ...tabPage.panels[0], maximized: isAreaMaximized };
}
}
}
}
});
}, { deep: true });
// 组件挂载后检查主区域内容
onMounted(() => {
// 延迟执行确保DOM完全渲染
setTimeout(() => {
checkMainContentForAreas()
}, 100)
})
// 监听主区域内容变化
watch(floatingAreas, () => {
// 当浮动区域变化时,重新检查主区域内容
setTimeout(() => {
checkMainContentForAreas()
}, 100)
}, { deep: true })
// 当主区域内没有其他Area时隐藏外部边缘指示器只显示中心指示器
// 第5步优化UI指示器显示逻辑当主区域为空时
/**
* 检查主区域是否为空
* @returns {boolean} 主区域是否为空
*/
const isMainAreaEmpty = () => {
checkMainContentForAreas()
return !hasAreasInMainContent.value
}
/**
* 根据鼠标位置动态更新停靠区域(优化版)
* 当主区域为空时,只显示中心指示器
* @param {number} mouseX - 鼠标X坐标
* @param {number} mouseY - 鼠标Y坐标
*/
const updateDockZoneByMousePosition = (mouseX, mouseY) => {
if (!dockLayoutRef.value || !targetAreaRect.value) return
try {
const rect = dockLayoutRef.value.getBoundingClientRect()
const { left, top, width, height } = targetAreaRect.value
// 计算鼠标相对于目标区域的相对位置 (0-1)
const relativeX = (mouseX - rect.left - left) / width
const relativeY = (mouseY - rect.top - top) / height
// 定义各个区域的阈值
const threshold = 0.2 // 20% 边缘区域
let newActiveZone = null
// 检查主区域是否为空
const mainAreaEmpty = isMainAreaEmpty()
if (relativeX >= 0 && relativeX <= 1 && relativeY >= 0 && relativeY <= 1) {
// 鼠标在目标区域内
if (mainAreaEmpty) {
// 主区域为空时:只允许停靠到中心区域
// 扩大中心区域检测范围到更宽容的范围
const centerRadius = 0.25 // 中心区域半径25%
const centerDistance = Math.sqrt(
Math.pow(relativeX - 0.5, 2) + Math.pow(relativeY - 0.5, 2)
)
if (centerDistance <= centerRadius) {
newActiveZone = 'center'
// console.log('[DockLayout] 主区域为空,检测到中心停靠区域:', {
// mouseX, mouseY,
// relativeX: relativeX.toFixed(3),
// relativeY: relativeY.toFixed(3),
// centerDistance: centerDistance.toFixed(3),
// centerRadius: centerRadius,
// newActiveZone
// })
}
} else {
// 主区域不为空时:显示所有指示器选项
if (relativeY <= threshold) {
newActiveZone = 'top'
} else if (relativeY >= 1 - threshold) {
newActiveZone = 'bottom'
} else if (relativeX <= threshold) {
newActiveZone = 'left'
} else if (relativeX >= 1 - threshold) {
newActiveZone = 'right'
} else if (relativeX >= 0.4 && relativeX <= 0.6 && relativeY >= 0.4 && relativeY <= 0.6) {
// 中心区域 (40%-60% 的中心区域)
newActiveZone = 'center'
}
}
}
// 只有当停靠区域改变时才更新,减少不必要的重新渲染
if (activeDockZone.value !== newActiveZone) {
const oldZone = activeDockZone.value
activeDockZone.value = newActiveZone
// console.log('[DockLayout] 停靠区域更新:', { from: oldZone, to: newActiveZone })
// 如果激活的区域不为空,显示指示器;如果为空,隐藏指示器
// 但在拖拽过程中,始终保持中心指示器显示
const isDragging = panelDragState.value.isDragging || tabDragState.value.isDragging
if (newActiveZone || isDragging) {
showDockIndicator.value = true
} else {
showDockIndicator.value = false
}
}
} catch (error) {
// console.warn('更新停靠区域时出错:', error)
}
}
// 处理主区域的dragover事件
const handleMainAreaDragOver = (event) => {
event.preventDefault()
if (panelDragState.value.isDragging || tabDragState.value.isDragging) {
// 使用dock-layout作为基准获取位置和大小
let rect
if (dockLayoutRef.value) {
rect = dockLayoutRef.value.getBoundingClientRect()
} else {
// 回退到使用事件目标
rect = event.currentTarget.getBoundingClientRect()
}
// 更新目标区域信息并显示停靠指示器
targetAreaRect.value = {
left: 0, // 使用相对于容器的位置(左上角)
top: 0,
width: rect.width,
height: rect.height
}
showDockIndicator.value = true
// 更新鼠标位置
currentMousePosition.value = {
x: event.clientX,
y: event.clientY
}
}
}
// 处理主区域的dragleave事件
const handleMainAreaDragLeave = () => {
// 检查鼠标是否真的离开了区域(可能只是进入了子元素)
setTimeout(() => {
const activeElement = document.activeElement
const dockLayout = dockLayoutRef.value
// 如果活动元素不是dockLayout的后代隐藏指示器
// 但如果在拖拽过程中,不立即隐藏指示器
const isDragging = panelDragState.value.isDragging || tabDragState.value.isDragging
if (!isDragging && (!dockLayout || (activeElement && !dockLayout.contains(activeElement)))) {
showDockIndicator.value = false
activeDockZone.value = null
}
}, 50)
}
// 处理浮动区域的dragover事件
const handleAreaDragOver = (event, areaId) => {
event.preventDefault()
if (panelDragState.value.isDragging || tabDragState.value.isDragging) {
// 避免自身停靠到自身
if (areaId !== panelDragState.value.currentAreaId && areaId !== tabDragState.value.currentAreaId) {
// 获取目标区域的位置和大小
const areaElement = event.currentTarget
const rect = areaElement.getBoundingClientRect()
// 更新目标区域信息并显示停靠指示器
targetAreaRect.value = {
left: rect.left,
top: rect.top,
width: rect.width,
height: rect.height
}
showDockIndicator.value = true
// 更新鼠标位置
currentMousePosition.value = {
x: event.clientX,
y: event.clientY
}
}
}
}
// 处理浮动区域的dragleave事件
const handleAreaDragLeave = () => {
// 延迟检查,避免快速移动时的闪烁
setTimeout(() => {
const activeElement = document.activeElement
const dockLayout = dockLayoutRef.value
// 如果活动元素不是dockLayout的后代隐藏指示器
// 但如果在拖拽过程中,不立即隐藏指示器
const isDragging = panelDragState.value.isDragging || tabDragState.value.isDragging
if (!isDragging && (!dockLayout || (activeElement && !dockLayout.contains(activeElement)))) {
showDockIndicator.value = false
activeDockZone.value = null
}
}, 50)
}
// 处理停靠区域激活事件
const onDockZoneActive = (zone) => {
activeDockZone.value = zone
}
// 处理Panel最大化同步事件
const onPanelMaximizeSync = ({ areaId, maximized }) => {
// 查找对应的Area
const area = floatingAreas.value.find(a => a.id === areaId);
// 正确处理层级结构Area -> TabPage -> Panel
if (area && area.tabPages && area.tabPages.length === 1 && area.tabPages[0].panels && area.tabPages[0].panels.length === 1) {
// 更新TabPage中Panel的maximized状态
area.tabPages[0].panels[0] = { ...area.tabPages[0].panels[0], maximized };
}
}
// 第2步Area验证逻辑 - 目标Area内容区为空的情况
/**
* 检查鼠标是否在独立中心指示器区域内
* @param {number} mouseX - 鼠标X坐标
* @param {number} mouseY - 鼠标Y坐标
* @returns {boolean} 是否在独立中心指示器区域内
*/
const isMouseInCenterIndicator = (mouseX, mouseY) => {
if (!dockLayoutRef.value) return false
try {
// 查找独立中心指示器元素
const centerIndicator = dockLayoutRef.value.querySelector('.center-main-indicator')
if (!centerIndicator) {
// console.log('未找到.center-main-indicator元素')
return false
}
// 检查元素是否可见
const style = window.getComputedStyle(centerIndicator)
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') {
// console.log('独立中心指示器被隐藏')
return false
}
const rect = centerIndicator.getBoundingClientRect()
const isInIndicator = mouseX >= rect.left && mouseX <= rect.right &&
mouseY >= rect.top && mouseY <= rect.bottom
// console.log('独立中心指示器检测:', {
// mouseX, mouseY,
// indicatorRect: rect,
// isInIndicator,
// showDockIndicator: showDockIndicator.value,
// activeDockZone: activeDockZone.value
// })
return isInIndicator
} catch (error) {
// console.warn('检查独立中心指示器区域时出错:', error)
return false
}
}
/**
* 检查Area是否可以停靠到中心区域
* @param {Object} sourceArea - 源Area对象
* @param {Object} targetArea - 目标Area对象主区域
* @returns {Object} 验证结果 {canDock: boolean, reason: string, strategy: string}
*/
const canDockToCenter = (sourceArea, targetArea) => {
// 验证1检查源Area是否包含TabPage
if (!sourceArea) {
return {
canDock: false,
reason: '源Area不存在'
}
}
if (!sourceArea.tabPages || sourceArea.tabPages.length === 0) {
return {
canDock: false,
reason: '源Area不包含TabPage无法停靠'
}
}
// 验证2检查目标Area主区域- 支持Area级别停靠
if (targetArea === mainAreaRef.value) {
// Area级别停靠支持将源Area的内容移动到主区域
// 不再要求主区域完全为空允许Area停靠
// console.log('Area级别停靠源Area可停靠到主区域')
} else {
// 对于其他目标Area的验证逻辑可以后续扩展
return {
canDock: false,
reason: '暂不支持停靠到非主区域'
}
}
// 验证3确保包含TabPage的Area才能停靠到中心
const sourceTabPage = sourceArea.tabPages[0]
if (!sourceTabPage.panels || sourceTabPage.panels.length === 0) {
return {
canDock: false,
reason: '源Area的TabPage不包含Panel无法停靠'
}
}
// 5.2. 新增验证根据目标Area状态确定停靠策略
// 检查主区域内容状态
checkMainContentForAreas()
// 如果主区域已有内容,判断停靠策略
if (hasAreasInMainContent.value) {
// 检查主区域是否已有TabPage
const mainAreaElement = mainAreaRef.value?.$el
if (mainAreaElement) {
const existingTabPages = mainAreaElement.querySelectorAll('.tab-page, [class*="tab"]')
const hasExistingTabPage = existingTabPages.length > 0
if (hasExistingTabPage && sourceArea.tabPages.length === 1) {
// 场景5.2源Area只有一个TabPage目标Area也有TabPage
// 策略合并TabPage标签页
return {
canDock: true,
reason: '可以停靠 - 采用TabPage标签页合并策略',
strategy: 'merge-tabpages'
}
} else {
// 其他场景采用简单Area内容移动策略
return {
canDock: true,
reason: '可以停靠 - 采用简单移动策略',
strategy: 'simple-move'
}
}
}
}
// 主区域为空的情况:采用简单移动策略
return {
canDock: true,
reason: '验证通过可以停靠到中心区域',
strategy: 'simple-move'
}
}
// 3.3 创建handleCenterDocking核心处理函数
/**
* 处理中心停靠逻辑
* @param {string} sourceAreaId - 源Area的ID
* @returns {Object} 处理结果 {success: boolean, message: string}
*/
const handleCenterDocking = (sourceAreaId) => {
// console.log('开始处理中心停靠:', sourceAreaId)
// 1. 查找源Area
const sourceArea = floatingAreas.value.find(a => a.id === sourceAreaId)
if (!sourceArea) {
return {
success: false,
message: `未找到ID为 ${sourceAreaId} 的源Area`
}
}
// 2. 验证停靠条件
const validationResult = canDockToCenter(sourceArea, mainAreaRef.value)
if (!validationResult.canDock) {
return {
success: false,
message: `停靠验证失败: ${validationResult.reason}`
}
}
// console.log('停靠验证通过:', validationResult.reason)
try {
// 3. 根据策略执行停靠逻辑
if (validationResult.strategy === 'merge-tabpages') {
// 5.2. 策略合并TabPage标签页
return handleMergeTabpagesDocking(sourceArea)
} else {
// 默认策略简单Area内容移动
return handleSimpleAreaDocking(sourceArea)
}
} catch (error) {
// console.error('停靠处理过程中发生错误:', error)
return {
success: false,
message: `停靠过程中发生错误: ${error.message}`
}
}
}
// 5.2. 新增处理TabPage标签页合并策略
/**
* 处理TabPage标签页合并策略
* 当源Area只有一个TabPage且目标Area已有TabPage时使用此策略
* @param {Object} sourceArea - 源Area对象
* @returns {Object} 处理结果 {success: boolean, message: string}
*/
// 合并TabPage标签页的核心处理函数
const handleMergeTabpagesDocking = (sourceArea) => {
// console.log(`[DockLayout] 开始合并TabPage标签页源Area:`, sourceArea)
try {
// 验证源Area是否有效
if (!sourceArea || !sourceArea.tabPages || sourceArea.tabPages.length === 0) {
// console.warn('[DockLayout] 源Area无效或没有TabPage无法执行合并操作')
return null
}
// 查找主区域的Area组件
const mainArea = mainAreaRef.value
if (!mainArea) {
// console.error('[DockLayout] 找不到主区域引用')
return null
}
// console.log('[DockLayout] 使用新的mergeAreaContent方法合并TabPage')
const success = mainArea.mergeAreaContent(sourceArea)
if (success) {
const newTabPageId = `merged-${Date.now()}`
const panelsCount = sourceArea.tabPages[0]?.panels?.length || 0
// console.log(`[DockLayout] 成功合并 ${panelsCount} 个Panel到主区域TabPage: ${newTabPageId}`)
return {
success: true,
newTabPageId: newTabPageId,
panelsAdded: panelsCount
}
}
// console.error('[DockLayout] mergeAreaContent方法执行失败')
return null
} catch (error) {
// console.error('[DockLayout] 合并TabPage时发生错误:', error)
return null
}
}
/**
* 在主区域查找或创建TabPage
* @returns {Object} 查找结果 {success: boolean, message: string, tabPage?: Object}
*/
const findOrCreateMainAreaTabPage = () => {
try {
// 检查主区域是否已经接收了内容
if (mainAreaRef.value && mainAreaRef.value.receivedContent) {
const existingContent = mainAreaRef.value.receivedContent
// console.log('检查主区域已接收内容数量:', existingContent.length)
if (existingContent.length > 0) {
// 找到现有的TabPage内容
const existingTabPage = existingContent[0] // 只使用第一个TabPage
// console.log('找到现有的TabPage:', existingTabPage.title)
return {
success: true,
message: '找到现有的TabPage',
tabPage: existingTabPage.tabPage,
rawTabPage: existingTabPage // 返回原始TabPage对象以供修改
}
}
}
// 如果主区域没有内容现在不再主动创建空TabPage
// 因为在合并逻辑中Area会自动根据需要创建TabPage
// console.log('主区域没有现有TabPage将通过合并操作自动创建')
return {
success: true,
message: '主区域为空等待合并操作创建TabPage',
tabPage: null,
rawTabPage: null
}
} catch (error) {
// console.error('查找TabPage时发生错误:', error)
return {
success: false,
message: `查找TabPage失败: ${error.message}`
}
}
}
// 3.4 实现简单Area停靠策略重构为使用合并逻辑
/**
* 处理简单Area停靠策略Area内容合并
* 当主区域为空或不符合TabPage合并条件时使用此策略
* @param {Object} sourceArea - 源Area对象
* @returns {Object} 处理结果 {success: boolean, message: string}
*/
const handleSimpleAreaDocking = (sourceArea) => {
// console.log('执行简单Area停靠策略使用合并逻辑')
try {
// 1. 验证源Area结构
if (!sourceArea.tabPages || sourceArea.tabPages.length === 0) {
return {
success: false,
message: '源Area不包含TabPage无法进行停靠合并'
}
}
// console.log('源Area结构验证通过包含', sourceArea.tabPages.length, '个TabPage')
// 2. 使用mergeAreaContent方法将源Area合并到主区域
if (mainAreaRef.value && typeof mainAreaRef.value.mergeAreaContent === 'function') {
// console.log('通过mergeAreaContent方法合并源Area到主区域')
const result = mainAreaRef.value.mergeAreaContent(sourceArea)
if (result) {
// console.log('源Area成功合并到主区域area-merged事件将处理隐藏列表操作')
// 3. 更新主区域状态(合并完成后由事件处理函数处理隐藏列表)
nextTick(() => {
checkMainContentForAreas()
// 确保主区域最大化显示还原按钮
if (mainAreaRef.value) {
mainAreaRef.value.WindowState = '最大化'
// console.log('主区域状态已设置为最大化')
}
})
return {
success: true,
message: `成功合并 ${sourceArea.tabPages.length} 个TabPage到主区域`
}
} else {
return {
success: false,
message: 'mergeAreaContent方法执行失败'
}
}
} else {
// console.error('主区域不支持mergeAreaContent方法')
return {
success: false,
message: '主区域不支持合并操作'
}
}
} catch (error) {
// console.error('简单Area停靠策略执行时发生错误:', error)
return {
success: false,
message: `简单Area停靠失败: ${error.message}`
}
}
}
// 第4步在defineExpose中暴露隐藏列表管理方法
/**
* 获取隐藏Area列表
* @returns {Array} 隐藏Area列表
*/
const getHiddenAreas = () => {
return [...hiddenAreas.value]
}
/**
* 从隐藏列表恢复Area到浮动区域
* @param {string} areaId - Area的ID
* @returns {Object} 恢复结果 {success: boolean, message: string}
*/
const restoreAreaFromHidden = (areaId) => {
try {
const hiddenIndex = hiddenAreas.value.findIndex(area => area.id === areaId)
if (hiddenIndex === -1) {
return {
success: false,
message: '找不到指定的隐藏Area'
}
}
const areaToRestore = hiddenAreas.value[hiddenIndex]
// 检查是否已存在相同的Area避免重复
const existingIndex = floatingAreas.value.findIndex(area => area.id === areaId)
if (existingIndex !== -1) {
return {
success: false,
message: 'Area已存在于浮动区域中'
}
}
// 恢复Area到浮动区域
floatingAreas.value.push(areaToRestore)
// 从隐藏列表移除
hiddenAreas.value.splice(hiddenIndex, 1)
// 更新状态
checkMainContentForAreas()
return {
success: true,
message: 'Area已成功恢复到浮动区域'
}
} catch (error) {
// console.error('恢复Area时出错:', error)
return {
success: false,
message: `恢复失败:${error.message}`
}
}
}
/**
* 从隐藏列表移除Area不恢复
* @param {string} areaId - Area的ID
* @returns {Object} 移除结果 {success: boolean, message: string}
*/
const removeFromHiddenList = (areaId) => {
try {
const hiddenIndex = hiddenAreas.value.findIndex(area => area.id === areaId)
if (hiddenIndex === -1) {
return {
success: false,
message: '找不到指定的隐藏Area'
}
}
// 从隐藏列表移除
hiddenAreas.value.splice(hiddenIndex, 1)
return {
success: true,
message: 'Area已从隐藏列表中移除'
}
} catch (error) {
// console.error('移除Area时出错:', error)
return {
success: false,
message: `移除失败:${error.message}`
}
}
}
/**
* 清空隐藏列表
* @returns {Object} 清空结果 {success: boolean, message: string, removedCount: number}
*/
const clearHiddenList = () => {
try {
const count = hiddenAreas.value.length
hiddenAreas.value.splice(0, hiddenAreas.value.length)
return {
success: true,
message: `已清空隐藏列表,移除${count}个Area`,
removedCount: count
}
} catch (error) {
// console.error('清空隐藏列表时出错:', error)
return {
success: false,
message: `清空失败:${error.message}`,
removedCount: 0
}
}
}
/**
* 统一停靠结束处理函数
* 根据拖拽类型和目标区域执行相应的停靠逻辑
* @param {string} dragType - 拖拽类型 ('Panel', 'Area', 'TabPage')
* @param {string} sourceAreaId - 源Area的ID
* @param {Object} options - 可选参数
* @returns {Object} 处理结果 {success: boolean, message: string, strategy?: string}
*/
const handleDockingEnding = (dragType, sourceAreaId, options = {}) => {
// console.log(`[DockLayout] 处理停靠结束事件: type=${dragType}, areaId=${sourceAreaId}`)
try {
// 确保只有在正确的区域内释放才执行停靠
if (!activeDockZone.value || !sourceAreaId) {
return {
success: false,
message: '缺少停靠区域或源Area ID'
}
}
// 根据停靠区域类型执行不同的停靠逻辑
switch (activeDockZone.value) {
case 'center':
// 中心停靠
const centerResult = handleCenterDocking(sourceAreaId)
if (!centerResult.success) {
// console.warn('中心停靠失败:', centerResult.message)
} else {
// console.log('中心停靠成功:', centerResult.message)
}
return centerResult
case 'top':
case 'bottom':
case 'left':
case 'right':
// 外部边缘停靠
const edgeResult = handleEdgeDocking(sourceAreaId, activeDockZone.value)
if (!edgeResult.success) {
// console.warn('外部边缘停靠失败:', edgeResult.message)
} else {
// console.log('外部边缘停靠成功:', edgeResult.message)
}
return edgeResult
default:
return {
success: false,
message: `不支持的停靠区域: ${activeDockZone.value}`
}
}
} catch (error) {
// console.error('[DockLayout] 停靠处理过程中发生错误:', error)
return {
success: false,
message: `停靠过程中发生错误: ${error.message}`
}
}
}
/**
* 处理外部边缘停靠逻辑
* @param {string} sourceAreaId - 源Area的ID
* @param {string} dockZone - 停靠方向 ('top', 'bottom', 'left', 'right')
* @returns {Object} 处理结果 {success: boolean, message: string, strategy?: string}
*/
const handleEdgeDocking = (sourceAreaId, dockZone) => {
// console.log(`[DockLayout] 开始外部边缘停靠: areaId=${sourceAreaId}, zone=${dockZone}`)
try {
// 1. 查找源Area
const sourceArea = floatingAreas.value.find(a => a.id === sourceAreaId)
if (!sourceArea) {
return {
success: false,
message: `未找到ID为 ${sourceAreaId} 的源Area`
}
}
// 2. 验证停靠条件
const validationResult = canDockToEdge(sourceArea, dockZone)
if (!validationResult.canDock) {
return {
success: false,
message: `边缘停靠验证失败: ${validationResult.reason}`
}
}
// console.log('边缘停靠验证通过:', validationResult.reason)
// 3. 根据主区域状态选择处理策略
if (validationResult.strategy === 'side-by-side') {
// 执行并排停靠策略
return handleSideBySideDocking(sourceArea, dockZone)
} else {
return {
success: false,
message: `暂不支持的边缘停靠策略: ${validationResult.strategy}`
}
}
} catch (error) {
// console.error('[DockLayout] 外部边缘停靠处理过程中发生错误:', error)
return {
success: false,
message: `外部边缘停靠过程中发生错误: ${error.message}`
}
}
}
/**
* 检查外部边缘停靠条件
* @param {Object} sourceArea - 源Area对象
* @param {string} dockZone - 停靠方向
* @returns {Object} 验证结果 {canDock: boolean, reason: string, strategy: string}
*/
const canDockToEdge = (sourceArea, dockZone) => {
// 验证1检查源Area是否有效
if (!sourceArea) {
return {
canDock: false,
reason: '源Area不存在'
}
}
if (!sourceArea.tabPages || sourceArea.tabPages.length === 0) {
return {
canDock: false,
reason: '源Area不包含TabPage无法停靠'
}
}
// 验证2检查停靠方向
const validZones = ['top', 'bottom', 'left', 'right']
if (!validZones.includes(dockZone)) {
return {
canDock: false,
reason: `不支持的停靠方向: ${dockZone}`
}
}
// 验证3检查主区域内容状态
checkMainContentForAreas()
if (hasAreasInMainContent.value) {
// 主区域有内容:采用并排停靠策略
return {
canDock: true,
reason: '主区域已有内容采用并排停靠策略',
strategy: 'side-by-side'
}
} else {
// 主区域无内容创建目标Area后执行并排停靠
return {
canDock: true,
reason: '主区域无内容将创建目标Area后执行并排停靠',
strategy: 'side-by-side'
}
}
}
/**
* 处理并排停靠逻辑
* 当主区域内已有Area时压缩目标Area并创建并排布局
* @param {Object} sourceArea - 源Area对象
* @param {string} dockZone - 停靠方向
* @returns {Object} 处理结果 {success: boolean, message: string}
*/
const handleSideBySideDocking = (sourceArea, dockZone) => {
// console.log(`[DockLayout] 开始并排停靠: areaId=${sourceArea.id}, zone=${dockZone}`)
try {
// 1. 检查主区域内是否有现有Area
checkMainContentForAreas()
let targetArea = null
if (hasAreasInMainContent.value) {
// 主区域内已有Area找到第一个作为目标
targetArea = findFirstMainArea()
// console.log('[DockLayout] 找到现有目标Area:', targetArea?.id)
} else {
// 主区域内无Area从隐藏列表获取或创建新的目标Area
// console.log('[DockLayout] 主区域内无现有Area创建目标Area')
const restoreResult = getOrCreateTargetArea()
if (!restoreResult.success) {
return restoreResult
}
targetArea = restoreResult.targetArea
}
if (!targetArea) {
return {
success: false,
message: '未找到有效的目标Area'
}
}
// 2. 压缩目标Area并创建并排布局
const layoutResult = createSideBySideLayout(sourceArea, targetArea, dockZone)
if (!layoutResult.success) {
return layoutResult
}
// 3. 添加ResizeBar支持
const resizeBarResult = addResizeBarForSideBySideLayout(sourceArea, targetArea, dockZone)
if (!resizeBarResult.success) {
// console.warn('[DockLayout] ResizeBar添加失败:', resizeBarResult.message)
// ResizeBar失败不影响核心功能继续执行
} else {
// console.log('[DockLayout] ResizeBar添加成功')
}
// 4. 更新源Area状态
const sourceIndex = floatingAreas.value.findIndex(a => a.id === sourceArea.id)
if (sourceIndex !== -1) {
// 将源Area从浮动区域移除添加到主区域
const [removedArea] = floatingAreas.value.splice(sourceIndex, 1)
// console.log('[DockLayout] 源Area已从浮动区域移除')
// 将源Area添加到主区域的并排布局中
// 这里需要根据实际的布局结构进行调整
// console.log('[DockLayout] 源Area将添加到主区域并排布局')
}
// 5. 更新主区域状态
nextTick(() => {
checkMainContentForAreas()
})
return {
success: true,
message: `成功执行${dockZone}方向并排停靠`,
strategy: 'side-by-side',
sourceAreaId: sourceArea.id,
targetAreaId: targetArea.id
}
} catch (error) {
// console.error('[DockLayout] 并排停靠处理过程中发生错误:', error)
return {
success: false,
message: `并排停靠过程中发生错误: ${error.message}`
}
}
}
/**
* 在并排布局中添加ResizeBar支持
* @param {Object} sourceArea - 源Area对象
* @param {Object} targetArea - 目标Area对象
* @param {string} dockZone - 停靠方向
* @returns {Object} 处理结果 {success: boolean, message: string}
*/
const addResizeBarForSideBySideLayout = (sourceArea, targetArea, dockZone) => {
// console.log(`[DockLayout] 为并排布局添加ResizeBar: ${dockZone}`)
try {
// 根据停靠方向确定ResizeBar的方向
const isHorizontal = ['left', 'right'].includes(dockZone)
const resizeBarDirection = isHorizontal ? 'vertical' : 'horizontal'
// console.log(`[DockLayout] ResizeBar方向设置: ${resizeBarDirection}`)
// 创建ResizeBar配置
const resizeBarConfig = {
id: `resizebar-${sourceArea.id}-${targetArea.id}-${Date.now()}`,
targetId: `${sourceArea.id}-${targetArea.id}`,
direction: resizeBarDirection,
sourceAreaId: sourceArea.id,
targetAreaId: targetArea.id,
dockZone: dockZone,
minSize: 150,
maxSize: undefined,
initialSize: isHorizontal ? 300 : 200
}
// 将ResizeBar配置添加到主区域的resizeBars列表中
mainAreaResizeBars.value.push(resizeBarConfig)
// console.log('[DockLayout] ResizeBar配置已添加到主区域')
return {
success: true,
message: `ResizeBar配置添加成功方向: ${resizeBarDirection}`
}
} catch (error) {
// console.error('[DockLayout] 添加ResizeBar时发生错误:', error)
return {
success: false,
message: `添加ResizeBar失败: ${error.message}`
}
}
}
/**
* 处理主区域ResizeBar尺寸调整事件
* @param {string} resizeBarId - ResizeBar的ID
* @param {number} newSize - 新的尺寸
*/
const handleMainAreaResizeBar = (resizeBarId, newSize) => {
try {
// console.log(`[DockLayout] 主区域ResizeBar调整: id=${resizeBarId}, size=${newSize}`)
// 找到对应的ResizeBar配置
const resizeBar = mainAreaResizeBars.value.find(rb => rb.id === resizeBarId)
if (!resizeBar) {
// console.warn(`[DockLayout] 未找到ResizeBar: ${resizeBarId}`)
return
}
// 根据停靠方向计算比例变化
const sourceArea = floatingAreas.value.find(a => a.id === resizeBar.sourceAreaId)
const targetArea = floatingAreas.value.find(a => a.id === resizeBar.targetAreaId)
if (!sourceArea || !targetArea) {
// console.warn('[DockLayout] 未找到源Area或目标Area')
return
}
// 根据停靠方向计算新的比例
if (['left', 'right'].includes(resizeBar.dockZone)) {
// 水平并排:左右布局
handleHorizontalResizeWithSize(sourceArea, targetArea, newSize, resizeBar.dockZone)
} else {
// 垂直并排:上下布局
handleVerticalResizeWithSize(sourceArea, targetArea, newSize, resizeBar.dockZone)
}
} catch (error) {
// console.error('[DockLayout] 处理主区域ResizeBar调整时发生错误:', error)
}
}
/**
* 处理主区域ResizeBar调整开始事件
* @param {string} resizeBarId - ResizeBar的ID
*/
const handleMainAreaResizeBarStart = (resizeBarId) => {
// console.log(`[DockLayout] 主区域ResizeBar调整开始: ${resizeBarId}`)
}
/**
* 处理主区域ResizeBar调整结束事件
* @param {string} resizeBarId - ResizeBar的ID
*/
const handleMainAreaResizeBarEnd = (resizeBarId) => {
// console.log(`[DockLayout] 主区域ResizeBar调整结束: ${resizeBarId}`)
}
/**
* 获取主区域ResizeBar的样式
* @param {Object} resizeBar - ResizeBar配置
* @returns {Object} CSS样式对象
*/
const getMainAreaResizeBarStyle = (resizeBar) => {
try {
const baseStyle = {
position: 'absolute',
zIndex: 50
}
if (resizeBar.direction === 'horizontal') {
// 水平ResizeBar左右调整
return {
...baseStyle,
top: '0',
right: '0',
width: '4px',
height: '100%',
cursor: 'col-resize'
}
} else {
// 垂直ResizeBar上下调整
return {
...baseStyle,
bottom: '0',
left: '0',
width: '100%',
height: '4px',
cursor: 'row-resize'
}
}
} catch (error) {
// console.error('[DockLayout] 获取ResizeBar样式时发生错误:', error)
return { position: 'absolute' }
}
}
/**
* 处理ResizeBar尺寸调整事件
* @param {Object} delta - 调整量
* @param {Object} sourceArea - 源Area对象
* @param {Object} targetArea - 目标Area对象
* @param {string} dockZone - 停靠方向
*/
const handleResizeBarResize = (delta, sourceArea, targetArea, dockZone) => {
try {
// console.log(`[DockLayout] ResizeBar调整: delta=`, delta, `zone=${dockZone}`)
// 根据停靠方向计算新的尺寸比例
if (['left', 'right'].includes(dockZone)) {
// 水平并排:左右布局
handleHorizontalResize(sourceArea, targetArea, delta.x)
} else {
// 垂直并排:上下布局
handleVerticalResize(sourceArea, targetArea, delta.y)
}
// 更新Area的ratio属性以保持布局持久化
updateAreasRatio(sourceArea, targetArea, dockZone)
} catch (error) {
// console.error('[DockLayout] 处理ResizeBar调整时发生错误:', error)
}
}
/**
* 处理水平方向(左右)尺寸调整
*/
const handleHorizontalResize = (sourceArea, targetArea, deltaX) => {
// 实现水平尺寸调整逻辑
const sourceRatio = sourceArea.ratio || 0.5
const targetRatio = targetArea.ratio || 0.5
const totalRatio = sourceRatio + targetRatio
// 计算新的比例保持总和为1
const newSourceRatio = Math.max(0.1, Math.min(0.9, sourceRatio + deltaX / 1000))
const newTargetRatio = totalRatio - newSourceRatio
// 确保最小比例限制
if (newSourceRatio >= 0.1 && newTargetRatio >= 0.1) {
sourceArea.ratio = newSourceRatio
targetArea.ratio = newTargetRatio
// console.log(`[DockLayout] 水平调整完成: source=${newSourceRatio.toFixed(2)}, target=${newTargetRatio.toFixed(2)}`)
}
}
/**
* 处理垂直方向(上下)尺寸调整
*/
const handleVerticalResize = (sourceArea, targetArea, deltaY) => {
// 实现垂直尺寸调整逻辑
const sourceRatio = sourceArea.ratio || 0.5
const targetRatio = targetArea.ratio || 0.5
const totalRatio = sourceRatio + targetRatio
// 计算新的比例保持总和为1
const newSourceRatio = Math.max(0.1, Math.min(0.9, sourceRatio + deltaY / 1000))
const newTargetRatio = totalRatio - newSourceRatio
// 确保最小比例限制
if (newSourceRatio >= 0.1 && newTargetRatio >= 0.1) {
sourceArea.ratio = newSourceRatio
targetArea.ratio = newTargetRatio
// console.log(`[DockLayout] 垂直调整完成: source=${newSourceRatio.toFixed(2)}, target=${newTargetRatio.toFixed(2)}`)
}
}
/**
* 更新Area的尺寸比例
*/
const updateAreasRatio = (sourceArea, targetArea, dockZone) => {
// 确保比例总和为1
const sourceRatio = sourceArea.ratio || 0.5
const targetRatio = targetArea.ratio || 0.5
const totalRatio = sourceRatio + targetRatio
if (totalRatio !== 1) {
const sourceNewRatio = sourceRatio / totalRatio
const targetNewRatio = targetRatio / totalRatio
sourceArea.ratio = sourceNewRatio
targetArea.ratio = targetNewRatio
// console.log(`[DockLayout] 比例调整: source=${sourceNewRatio.toFixed(2)}, target=${targetNewRatio.toFixed(2)}`)
}
}
/**
* 处理ResizeBar调整开始事件
*/
const handleResizeBarResizeStart = (sourceArea, targetArea) => {
// console.log('[DockLayout] ResizeBar调整开始')
// 可以在这里添加视觉反馈或其他开始时的处理
}
/**
* 处理ResizeBar调整结束事件
*/
const handleResizeBarResizeEnd = (sourceArea, targetArea) => {
// console.log('[DockLayout] ResizeBar调整结束')
// 可以在这里添加调整完成后的处理,如保存布局状态
}
/**
* 处理水平方向(左右)尺寸调整 - 带指定大小
* @param {Object} sourceArea - 源Area对象
* @param {Object} targetArea - 目标Area对象
* @param {number} newSize - 新的尺寸
* @param {string} dockZone - 停靠方向
*/
const handleHorizontalResizeWithSize = (sourceArea, targetArea, newSize, dockZone) => {
try {
// console.log(`[DockLayout] 水平尺寸调整: newSize=${newSize}, zone=${dockZone}`)
// 计算总宽度(假设容器宽度)
const containerWidth = 800 // 默认容器宽度,实际项目中应该动态获取
const minSize = 150
// 确保大小在有效范围内
const adjustedSize = Math.max(minSize, Math.min(containerWidth - minSize, newSize))
// 计算比例
const sourceRatio = adjustedSize / containerWidth
const targetRatio = 1 - sourceRatio
// 确保最小比例限制
if (sourceRatio >= 0.1 && targetRatio >= 0.1) {
sourceArea.ratio = sourceRatio
targetArea.ratio = targetRatio
sourceArea.width = adjustedSize
targetArea.width = containerWidth - adjustedSize
// console.log(`[DockLayout] 水平调整完成: source=${sourceRatio.toFixed(2)}, target=${targetRatio.toFixed(2)}`)
}
} catch (error) {
// console.error('[DockLayout] 水平尺寸调整时发生错误:', error)
}
}
/**
* 处理垂直方向(上下)尺寸调整 - 带指定大小
* @param {Object} sourceArea - 源Area对象
* @param {Object} targetArea - 目标Area对象
* @param {number} newSize - 新的尺寸
* @param {string} dockZone - 停靠方向
*/
const handleVerticalResizeWithSize = (sourceArea, targetArea, newSize, dockZone) => {
try {
// console.log(`[DockLayout] 垂直尺寸调整: newSize=${newSize}, zone=${dockZone}`)
// 计算总高度(假设容器高度)
const containerHeight = 600 // 默认容器高度,实际项目中应该动态获取
const minSize = 150
// 确保大小在有效范围内
const adjustedSize = Math.max(minSize, Math.min(containerHeight - minSize, newSize))
// 计算比例
const sourceRatio = adjustedSize / containerHeight
const targetRatio = 1 - sourceRatio
// 确保最小比例限制
if (sourceRatio >= 0.1 && targetRatio >= 0.1) {
sourceArea.ratio = sourceRatio
targetArea.ratio = targetRatio
sourceArea.height = adjustedSize
targetArea.height = containerHeight - adjustedSize
// console.log(`[DockLayout] 垂直调整完成: source=${sourceRatio.toFixed(2)}, target=${targetRatio.toFixed(2)}`)
}
} catch (error) {
// console.error('[DockLayout] 垂直尺寸调整时发生错误:', error)
}
}
/**
* 查找主区域内的第一个Area
* @returns {Object|null} 找到的Area对象或null
*/
const findFirstMainArea = () => {
try {
// 这里需要根据实际的DOM结构或数据结构调整
// 目前返回null需要根据实际实现来完善
// console.log('[DockLayout] 查找主区域内的第一个Area')
return null
} catch (error) {
// console.error('[DockLayout] 查找主区域Area时发生错误:', error)
return null
}
}
/**
* 获取或创建目标Area
* @returns {Object} 处理结果 {success: boolean, message: string, targetArea?: Object}
*/
const getOrCreateTargetArea = () => {
try {
// console.log('[DockLayout] 获取或创建目标Area')
// 1. 尝试从隐藏列表获取Area
const hiddenResult = getHiddenAreas()
if (hiddenResult.length > 0) {
// 取第一个隐藏的Area作为目标Area
const targetArea = hiddenResult[0]
const restoreResult = restoreAreaFromHidden(targetArea.id)
if (restoreResult.success) {
// console.log('[DockLayout] 从隐藏列表恢复目标Area成功:', targetArea.id)
return {
success: true,
message: '成功从隐藏列表获取目标Area',
targetArea: targetArea
}
}
}
// 2. 如果没有隐藏Area创建新的Area
// console.log('[DockLayout] 创建新的目标Area')
const newAreaId = `area-${Date.now()}`
const newArea = {
id: newAreaId,
x: 0,
y: 0,
width: 300,
height: 250,
tabPages: [], // 初始为空,通过主区域内容填充
ratio: 0.5
}
// 将新Area添加到浮动区域
floatingAreas.value.push(newArea)
// console.log('[DockLayout] 创建新目标Area成功:', newAreaId)
return {
success: true,
message: '成功创建新的目标Area',
targetArea: newArea
}
} catch (error) {
// console.error('[DockLayout] 获取或创建目标Area时发生错误:', error)
return {
success: false,
message: `获取或创建目标Area失败: ${error.message}`
}
}
}
/**
* 创建并排布局
* @param {Object} sourceArea - 源Area对象
* @param {Object} targetArea - 目标Area对象
* @param {string} dockZone - 停靠方向
* @returns {Object} 处理结果 {success: boolean, message: string}
*/
const createSideBySideLayout = (sourceArea, targetArea, dockZone) => {
try {
// console.log(`[DockLayout] 创建并排布局: ${dockZone}`)
// 1. 设置初始比例
sourceArea.ratio = 0.5
targetArea.ratio = 0.5
// 2. 压缩目标Area的空间
const compressResult = compressTargetArea(targetArea, dockZone)
if (!compressResult.success) {
return compressResult
}
// 3. 调整源Area的位置和大小以适应并排布局
adjustSourceAreaForLayout(sourceArea, targetArea, dockZone)
// console.log('[DockLayout] 并排布局创建完成')
return {
success: true,
message: '并排布局创建成功'
}
} catch (error) {
// console.error('[DockLayout] 创建并排布局时发生错误:', error)
return {
success: false,
message: `创建并排布局失败: ${error.message}`
}
}
}
/**
* 压缩目标Area的空间
* @param {Object} targetArea - 目标Area对象
* @param {string} dockZone - 停靠方向
* @returns {Object} 处理结果 {success: boolean, message: string}
*/
const compressTargetArea = (targetArea, dockZone) => {
try {
// console.log(`[DockLayout] 压缩目标Area空间: ${dockZone}`)
// 根据停靠方向计算新的尺寸
let newWidth = targetArea.width
let newHeight = targetArea.height
if (['left', 'right'].includes(dockZone)) {
// 左右停靠:压缩宽度
newWidth = Math.max(150, targetArea.width * 0.6) // 最小宽度150px
} else {
// 上下停靠:压缩高度
newHeight = Math.max(150, targetArea.height * 0.6) // 最小高度150px
}
targetArea.width = newWidth
targetArea.height = newHeight
// console.log(`[DockLayout] 目标Area压缩完成: ${newWidth}x${newHeight}`)
return {
success: true,
message: '目标Area空间压缩成功'
}
} catch (error) {
// console.error('[DockLayout] 压缩目标Area空间时发生错误:', error)
return {
success: false,
message: `压缩目标Area空间失败: ${error.message}`
}
}
}
/**
* 调整源Area以适应并排布局
* @param {Object} sourceArea - 源Area对象
* @param {Object} targetArea - 目标Area对象
* @param {string} dockZone - 停靠方向
*/
const adjustSourceAreaForLayout = (sourceArea, targetArea, dockZone) => {
try {
// console.log(`[DockLayout] 调整源Area适应布局: ${dockZone}`)
// 根据停靠方向调整源Area的尺寸
if (['left', 'right'].includes(dockZone)) {
// 左右停靠设置源Area的宽度
sourceArea.width = targetArea.width
sourceArea.height = targetArea.height
// 设置源Area的位置并排放置
if (dockZone === 'left') {
sourceArea.x = 0
sourceArea.y = targetArea.y
} else {
sourceArea.x = targetArea.x + targetArea.width
sourceArea.y = targetArea.y
}
} else {
// 上下停靠设置源Area的高度
sourceArea.width = targetArea.width
sourceArea.height = targetArea.height
// 设置源Area的位置并排放置
if (dockZone === 'top') {
sourceArea.x = targetArea.x
sourceArea.y = 0
} else {
sourceArea.x = targetArea.x
sourceArea.y = targetArea.y + targetArea.height
}
}
// console.log(`[DockLayout] 源Area调整完成: ${sourceArea.x}, ${sourceArea.y}, ${sourceArea.width}x${sourceArea.height}`)
} catch (error) {
// console.error('[DockLayout] 调整源Area布局时发生错误:', error)
}
}
// 暴露数据和方法给父组件
defineExpose({
// 数据属性
floatingAreas,
hiddenAreas,
// 原有方法
addFloatingPanel,
// 统一停靠处理
handleDockingEnding,
handleEdgeDocking,
handleSideBySideDocking,
// 隐藏列表管理方法
getHiddenAreas,
restoreAreaFromHidden,
removeFromHiddenList,
clearHiddenList,
// ResizeBar相关方法
addResizeBarForSideBySideLayout,
handleResizeBarResize,
handleResizeBarResizeStart,
handleResizeBarResizeEnd,
// ResizeBar尺寸调整方法
handleHorizontalResizeWithSize,
handleVerticalResizeWithSize,
// 查找方法
findOrCreateMainAreaTabPage,
findFirstMainArea,
})
</script>
<style scoped>
.dock-layout {
position: relative;
width: 100%;
height: 100%;
overflow: visible;
}
/* 浮动区域样式已直接应用到Area组件 */
/* 添加浮动区域按钮样式 */
.add-floating-btn {
font-size: 14px;
cursor: pointer;
user-select: none;
}
.add-floating-btn:active {
transform: scale(0.98);
}
</style>