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

1423 lines
42 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;">
<!-- 停靠指示器组件 - 设置高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"
2025-11-17 16:55:03 +08:00
@area-merged="onAreaMerged"
>
<!-- 这里可以放置主区域的内容 -->
</Area>
<!-- 浮动区域直接渲染不使用额外的div包装 -->
<Area
v-for="area in floatingAreas"
:key="area.id"
:id="area.id"
:title="area.title"
v-model:WindowState="area.WindowState"
:showTitleBar="true"
:width="area.width"
:height="area.height"
:left="area.WindowState !== '最大化' ? area.x : undefined"
:top="area.WindowState !== '最大化' ? area.y : undefined"
:style="area.WindowState !== '最大化' ? {
position: 'absolute',
zIndex: 1000,
background: 'rgba(255, 255, 255, 1)',
border: '1px solid #4f72b3'
} : {
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
zIndex: 1000
}"
@close="onCloseFloatingArea(area.id)"
@update:position="onUpdatePosition(area.id, $event)"
@panelMaximizeSync="onPanelMaximizeSync"
@areaDragStart="onAreaDragStart(area.id, $event)"
@areaDragMove="onAreaDragMove(area.id, $event)"
@areaDragEnd="onAreaDragEnd(area.id, $event)"
>
<!-- 每个Area内渲染其包含的TabPages -->
<TabPage
v-for="tabPage in area.tabPages"
:key="tabPage.id"
:id="tabPage.id"
:title="tabPage.title"
:panels="tabPage.panels"
:tabPosition="'bottom'"
@tabDragStart="onTabDragStart(area.id, $event)"
@tabDragMove="onTabDragMove(area.id, $event)"
@tabDragEnd="onTabDragEnd"
>
<!-- 在TabPage内渲染其包含的Panels -->
<Panel
v-for="panel in tabPage.panels"
:key="panel.id"
:id="panel.id"
:title="panel.title"
:collapsed="panel.collapsed"
:toolbarExpanded="panel.toolbarExpanded"
:maximized="panel.maximized"
2025-11-17 16:55:03 +08:00
:content="panel.content"
@toggleCollapse="onToggleCollapse"
@maximize="onMaximize"
@close="onClosePanel(area.id, panel.id)"
@toggleToolbar="onToggleToolbar"
@dragStart="onPanelDragStart(area.id, $event)"
@dragMove="onPanelDragMove(area.id, $event)"
@dragEnd="onPanelDragEnd"
@dragover="handleAreaDragOver"
@dragleave="handleAreaDragLeave"
/>
</TabPage>
</Area>
</div>
2025-10-31 23:58:26 +08:00
</template>
<script setup>
import { ref, defineExpose, nextTick, watch, computed, onMounted } 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';
// 主区域状态
const windowState = ref('最大化')
// 浮动区域列表 - 每个area包含panels数组
const floatingAreas = ref([])
2025-11-17 10:59:46 +08:00
// 隐藏区域列表 - 存储被隐藏的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)
// 计算是否隐藏外部边缘指示器
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 }
})
// 检测主区域内是否有其他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
}
}
2025-11-17 16:55:03 +08:00
/**
* 生成随机测试内容用于合并测试
* @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 = 300
const height = 250
x = Math.floor((containerRect.width - width) / 2)
y = Math.floor((containerRect.height - height) / 2)
}
// 1. 添加一个Area。使用Area的初始设置
const newArea = {
id: `floating-area-${areaIdCounter++}`,
title: `浮动区域 ${areaIdCounter - 1}`,
x: x,
y: y,
width: 300,
height: 250,
2025-11-03 17:26:28 +08:00
WindowState: '正常',
showTitleBar: true,
// 2. 向Area添加一个TabPage。TabPage的初始设置为填充满父容器
tabPages: [
{
id: `tabpage-${areaIdCounter - 1}-1`,
title: `标签页 1`,
// TabPage填充满父容器
width: 100,
height: 100,
// 3. 向TabPage添加一个Panel。Panel的初始设置为填充满父容器
panels: [
{
id: `panel-${areaIdCounter - 1}-1-1`,
2025-11-17 16:55:03 +08:00
title: `面板 ${areaIdCounter - 1}`,
x: 0,
y: 0,
width: 100,
height: 100,
collapsed: false,
2025-11-17 16:55:03 +08:00
toolbarExpanded: false,
// 添加随机测试内容以便合并测试
content: generateRandomContent(areaIdCounter - 1)
}
]
}
]
}
floatingAreas.value.push(newArea)
}
// 更新区域位置
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) {
// 正确处理层级结构Area -> TabPage -> Panel
if (area.tabPages) {
for (const tabPage of area.tabPages) {
if (tabPage.panels && tabPage.panels.length === 1 && tabPage.panels[0].id === panelId) {
// 当区域只包含一个Panel时切换Area和Panel的最大化状态
const isCurrentlyMaximized = area.WindowState === '最大化' || area.WindowState === 'maximized'
// 使用Vue推荐的方式更新响应式数据
if (isCurrentlyMaximized) {
// 切换为正常状态
area.WindowState = '正常'
// 确保Panel也恢复正常状态 - 使用展开运算符创建新对象确保响应式
tabPage.panels[0] = { ...tabPage.panels[0], maximized: false }
} else {
// 切换为最大化状态
area.WindowState = '最大化'
// 同时最大化Panel - 使用展开运算符创建新对象确保响应式
tabPage.panels[0] = { ...tabPage.panels[0], maximized: true }
}
break
}
}
}
}
}
// 关闭浮动区域 - 同时移除内容区的Panel
const onCloseFloatingArea = (id) => {
const index = floatingAreas.value.findIndex(a => a.id === id)
if (index !== -1) {
// 获取要移除的Area
const areaToRemove = floatingAreas.value[index]
// 清理Panel引用确保Panel被正确移除
if (areaToRemove.panels) {
// 这里可以添加任何需要的Panel清理逻辑
// 清空panels数组确保Panel被正确移除
areaToRemove.panels = []
}
// 从数组中移除Area
floatingAreas.value.splice(index, 1)
}
}
// 关闭面板
const onClosePanel = (areaId, panelId) => {
const area = floatingAreas.value.find(a => a.id === areaId)
// 正确处理层级结构Area -> TabPage -> Panel
if (area && area.tabPages) {
for (const tabPage of area.tabPages) {
if (tabPage.panels) {
const panelIndex = tabPage.panels.findIndex(p => p.id === panelId)
if (panelIndex !== -1) {
tabPage.panels.splice(panelIndex, 1)
// 如果区域内没有面板了,可以考虑关闭整个区域
if (tabPage.panels.length === 0) {
onCloseFloatingArea(areaId)
}
break
}
}
}
}
}
// 切换工具栏
const onToggleToolbar = (id) => {
const area = floatingAreas.value.find(a => a.id === id)
if (area) {
area.toolbarExpanded = !area.toolbarExpanded
}
}
2025-11-17 10:59:46 +08:00
// 隐藏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)
}
}
2025-11-17 16:55:03 +08:00
// 处理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) => {
const area = floatingAreas.value.find(a => a.id === areaId)
// 只有当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.x : event?.x)
const clientY = event.clientY || (typeof event === 'object' ? event.y : event?.y)
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) => {
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.x : event?.x)
const clientY = event.clientY || (typeof event === 'object' ? event.y : event?.y)
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 = () => {
panelDragState.value.isDragging = false
2025-11-17 10:59:46 +08:00
const currentAreaId = panelDragState.value.currentAreaId
panelDragState.value.currentAreaId = null
2025-11-17 10:59:46 +08:00
// 3.1 在onPanelDragEnd方法中添加中心停靠检测
if (activeDockZone.value === 'center' && currentAreaId) {
// 处理中心停靠
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
2025-11-17 10:59:46 +08:00
// 设置拖拽状态类似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) => {
2025-11-17 10:59:46 +08:00
// 更新鼠标位置
currentMousePosition.value = {
x: event.clientX,
y: event.clientY
}
// 根据鼠标位置动态更新停靠区域
updateDockZoneByMousePosition(event.clientX, event.clientY)
}
// Area拖拽结束
const onAreaDragEnd = (areaId, event) => {
2025-11-17 10:59:46 +08:00
// 清理拖拽状态
panelDragState.value.isDragging = false
panelDragState.value.currentAreaId = null
// 3.1 在onAreaDragEnd方法中添加中心停靠检测
if (activeDockZone.value === 'center') {
// 处理中心停靠
const result = handleCenterDocking(areaId)
if (!result.success) {
console.warn('Area中心停靠失败:', result.message)
} else {
console.log('Area中心停靠成功:', result.message)
}
}
// 隐藏停靠指示器
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
2025-11-17 10:59:46 +08:00
const currentAreaId = tabDragState.value.currentAreaId
tabDragState.value.currentAreaId = null
2025-11-17 10:59:46 +08:00
// 3.2 在onTabDragEnd方法中添加中心停靠检测
if (activeDockZone.value === 'center' && currentAreaId) {
// 处理中心停靠
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时隐藏外部边缘指示器只显示中心指示器
2025-11-17 10:59:46 +08:00
// 第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
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
2025-11-17 10:59:46 +08:00
// 检查主区域是否为空
const mainAreaEmpty = isMainAreaEmpty()
if (relativeX >= 0 && relativeX <= 1 && relativeY >= 0 && relativeY <= 1) {
// 鼠标在目标区域内
2025-11-17 10:59:46 +08:00
if (mainAreaEmpty) {
// 主区域为空时:只允许停靠到中心区域
if (relativeX >= 0.3 && relativeX <= 0.7 && relativeY >= 0.3 && relativeY <= 0.7) {
// 扩大的中心区域 (30%-70% 的中心区域)
newActiveZone = 'center'
}
} 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) {
activeDockZone.value = newActiveZone
2025-11-17 10:59:46 +08:00
// 如果激活的区域不为空,显示指示器;如果为空,隐藏指示器
if (newActiveZone) {
showDockIndicator.value = true
} else {
showDockIndicator.value = false
}
}
}
// 处理主区域的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的后代隐藏指示器
if (!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的后代隐藏指示器
if (!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 };
}
}
2025-11-17 10:59:46 +08:00
// 第2步Area验证逻辑 - 目标Area内容区为空的情况
/**
* 检查Area是否可以停靠到中心区域
* @param {Object} sourceArea - 源Area对象
* @param {Object} targetArea - 目标Area对象主区域
2025-11-17 16:55:03 +08:00
* @returns {Object} 验证结果 {canDock: boolean, reason: string, strategy: string}
2025-11-17 10:59:46 +08:00
*/
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无法停靠'
}
}
2025-11-17 16:55:03 +08:00
// 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'
}
}
}
}
// 主区域为空的情况:采用简单移动策略
2025-11-17 10:59:46 +08:00
return {
canDock: true,
2025-11-17 16:55:03 +08:00
reason: '验证通过,可以停靠到中心区域',
strategy: 'simple-move'
2025-11-17 10:59:46 +08:00
}
}
2025-11-17 16:55:03 +08:00
// 3.3 创建handleCenterDocking核心处理函数
2025-11-17 10:59:46 +08:00
/**
2025-11-17 16:55:03 +08:00
* 处理中心停靠逻辑
* @param {string} sourceAreaId - 源Area的ID
2025-11-17 10:59:46 +08:00
* @returns {Object} 处理结果 {success: boolean, message: string}
*/
2025-11-17 16:55:03 +08:00
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)
2025-11-17 10:59:46 +08:00
try {
2025-11-17 16:55:03 +08:00
// 3. 根据策略执行停靠逻辑
if (validationResult.strategy === 'merge-tabpages') {
// 5.2. 策略合并TabPage标签页
return handleMergeTabpagesDocking(sourceArea)
2025-11-17 10:59:46 +08:00
} else {
2025-11-17 16:55:03 +08:00
// 默认策略简单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对象以供修改
}
}
2025-11-17 10:59:46 +08:00
}
2025-11-17 16:55:03 +08:00
// 如果主区域没有内容现在不再主动创建空TabPage
// 因为在合并逻辑中Area会自动根据需要创建TabPage
console.log('主区域没有现有TabPage将通过合并操作自动创建')
2025-11-17 10:59:46 +08:00
return {
success: true,
2025-11-17 16:55:03 +08:00
message: '主区域为空等待合并操作创建TabPage',
tabPage: null,
rawTabPage: null
2025-11-17 10:59:46 +08:00
}
} catch (error) {
2025-11-17 16:55:03 +08:00
console.error('查找TabPage时发生错误:', error)
2025-11-17 10:59:46 +08:00
return {
success: false,
2025-11-17 16:55:03 +08:00
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}`
2025-11-17 10:59:46 +08:00
}
}
}
// 第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
}
}
}
// 暴露方法给父组件
defineExpose({
2025-11-17 10:59:46 +08:00
// 原有方法
addFloatingPanel,
// 第4步隐藏列表管理方法
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>