2025-10-31 23:58:26 +08:00
|
|
|
|
<template>
|
2025-11-07 15:09:06 +08:00
|
|
|
|
<div class="dock-layout" ref="dockLayoutRef" style="display: flex; flex-direction: column; position: relative;">
|
2025-11-07 15:41:44 +08:00
|
|
|
|
<!-- 停靠指示器组件 - 设置高z-index确保显示在最顶层 -->
|
2025-11-13 17:08:21 +08:00
|
|
|
|
<!-- :visible="showDockIndicator" :visible="true" -->
|
2025-11-07 15:41:44 +08:00
|
|
|
|
<DockIndicator
|
2025-11-14 09:39:59 +08:00
|
|
|
|
:visible="showDockIndicator"
|
2025-11-07 15:41:44 +08:00
|
|
|
|
:target-rect="targetAreaRect"
|
|
|
|
|
|
:mouse-position="currentMousePosition"
|
|
|
|
|
|
@zone-active="onDockZoneActive"
|
|
|
|
|
|
style="z-index: 9999;"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
2025-11-13 10:50:22 +08:00
|
|
|
|
<!-- 主区域 - 添加ref引用 -->
|
2025-11-04 09:10:15 +08:00
|
|
|
|
<Area
|
|
|
|
|
|
:WindowState="windowState"
|
|
|
|
|
|
:showTitleBar="false"
|
|
|
|
|
|
title="主区域"
|
2025-11-07 14:56:09 +08:00
|
|
|
|
:style="{ position: 'relative', width: '100%', height: '100%', zIndex: 1 }"
|
2025-11-07 15:41:44 +08:00
|
|
|
|
@dragover="handleMainAreaDragOver"
|
|
|
|
|
|
@dragleave="handleMainAreaDragLeave"
|
2025-11-05 16:40:27 +08:00
|
|
|
|
>
|
2025-11-07 14:44:07 +08:00
|
|
|
|
</Area>
|
2025-11-07 14:56:09 +08:00
|
|
|
|
<!-- 浮动区域直接渲染,不使用额外的div包装 -->
|
2025-11-07 14:44:07 +08:00
|
|
|
|
<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',
|
2025-11-07 15:14:33 +08:00
|
|
|
|
zIndex: 1000,
|
2025-11-07 14:44:07 +08:00
|
|
|
|
background: 'rgba(255, 255, 255, 1)',
|
|
|
|
|
|
border: '1px solid #4f72b3'
|
|
|
|
|
|
} : {
|
|
|
|
|
|
position: 'absolute',
|
|
|
|
|
|
top: 0,
|
|
|
|
|
|
left: 0,
|
|
|
|
|
|
width: '100%',
|
|
|
|
|
|
height: '100%',
|
2025-11-07 15:14:33 +08:00
|
|
|
|
zIndex: 1000
|
2025-11-07 14:44:07 +08:00
|
|
|
|
}"
|
|
|
|
|
|
@close="onCloseFloatingArea(area.id)"
|
|
|
|
|
|
@update:position="onUpdatePosition(area.id, $event)"
|
|
|
|
|
|
@panelMaximizeSync="onPanelMaximizeSync"
|
|
|
|
|
|
>
|
|
|
|
|
|
<!-- 每个Area内渲染其包含的TabPages -->
|
|
|
|
|
|
<TabPage
|
|
|
|
|
|
v-for="tabPage in area.tabPages"
|
|
|
|
|
|
:key="tabPage.id"
|
|
|
|
|
|
:id="tabPage.id"
|
|
|
|
|
|
:title="tabPage.title"
|
|
|
|
|
|
:panels="tabPage.panels"
|
|
|
|
|
|
@tabDragStart="onTabDragStart(area.id, $event)"
|
|
|
|
|
|
@tabDragMove="onTabDragMove(area.id, $event)"
|
|
|
|
|
|
@tabDragEnd="onTabDragEnd"
|
2025-11-05 16:40:27 +08:00
|
|
|
|
>
|
2025-11-07 14:44:07 +08:00
|
|
|
|
<!-- 在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"
|
|
|
|
|
|
@toggleCollapse="onToggleCollapse"
|
|
|
|
|
|
@maximize="onMaximize"
|
|
|
|
|
|
@close="onClosePanel(area.id, panel.id)"
|
|
|
|
|
|
@toggleToolbar="onToggleToolbar"
|
|
|
|
|
|
@dragStart="onPanelDragStart(area.id, $event)"
|
|
|
|
|
|
@dragMove="onPanelDragMove(area.id, $event)"
|
|
|
|
|
|
@dragEnd="onPanelDragEnd"
|
2025-11-07 15:41:44 +08:00
|
|
|
|
@dragover="handleAreaDragOver"
|
|
|
|
|
|
@dragleave="handleAreaDragLeave"
|
2025-11-07 14:44:07 +08:00
|
|
|
|
/>
|
|
|
|
|
|
</TabPage>
|
2025-11-05 16:40:27 +08:00
|
|
|
|
</Area>
|
2025-11-02 17:19:53 +07:00
|
|
|
|
</div>
|
2025-10-31 23:58:26 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
2025-11-04 17:24:08 +08:00
|
|
|
|
import { ref, defineExpose, nextTick, watch } from 'vue'
|
2025-10-31 23:58:26 +08:00
|
|
|
|
import Area from './Area.vue';
|
2025-11-02 17:06:40 +07:00
|
|
|
|
import Panel from './Panel.vue';
|
2025-11-05 09:02:11 +08:00
|
|
|
|
import TabPage from './TabPage.vue';
|
2025-11-07 15:41:44 +08:00
|
|
|
|
import DockIndicator from './DockIndicator.vue';
|
2025-11-02 17:06:40 +07:00
|
|
|
|
|
|
|
|
|
|
// 主区域状态
|
2025-11-01 14:23:35 +07:00
|
|
|
|
const windowState = ref('最大化')
|
2025-11-02 17:06:40 +07:00
|
|
|
|
|
2025-11-04 10:53:22 +08:00
|
|
|
|
// 浮动区域列表 - 每个area包含panels数组
|
2025-11-02 17:06:40 +07:00
|
|
|
|
const floatingAreas = ref([])
|
|
|
|
|
|
|
2025-11-04 09:45:51 +08:00
|
|
|
|
// 容器引用
|
|
|
|
|
|
const dockLayoutRef = ref(null)
|
2025-11-02 17:06:40 +07:00
|
|
|
|
// 区域ID计数器
|
|
|
|
|
|
let areaIdCounter = 1
|
|
|
|
|
|
|
2025-11-07 15:41:44 +08:00
|
|
|
|
// 停靠指示器相关状态
|
|
|
|
|
|
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-04 11:05:12 +08:00
|
|
|
|
// Panel拖拽相关状态
|
|
|
|
|
|
const panelDragState = ref({
|
|
|
|
|
|
isDragging: false,
|
|
|
|
|
|
currentAreaId: null,
|
|
|
|
|
|
startClientPos: { x: 0, y: 0 },
|
|
|
|
|
|
startAreaPos: { x: 0, y: 0 }
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2025-11-06 14:57:30 +08:00
|
|
|
|
// TabPage拖拽相关状态
|
|
|
|
|
|
const tabDragState = ref({
|
|
|
|
|
|
isDragging: false,
|
|
|
|
|
|
currentAreaId: null,
|
|
|
|
|
|
startClientPos: { x: 0, y: 0 },
|
|
|
|
|
|
startAreaPos: { x: 0, y: 0 }
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2025-11-04 10:31:12 +08:00
|
|
|
|
// 添加新的浮动面板
|
|
|
|
|
|
const addFloatingPanel = () => {
|
2025-11-04 09:45:51 +08:00
|
|
|
|
// 获取父容器尺寸以计算居中位置
|
|
|
|
|
|
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)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-05 08:59:53 +08:00
|
|
|
|
// 1. 添加一个Area。使用Area的初始设置
|
2025-11-02 17:06:40 +07:00
|
|
|
|
const newArea = {
|
|
|
|
|
|
id: `floating-area-${areaIdCounter++}`,
|
|
|
|
|
|
title: `浮动区域 ${areaIdCounter - 1}`,
|
2025-11-04 09:45:51 +08:00
|
|
|
|
x: x,
|
|
|
|
|
|
y: y,
|
2025-11-02 17:06:40 +07:00
|
|
|
|
width: 300,
|
2025-11-04 09:45:51 +08:00
|
|
|
|
height: 250,
|
2025-11-03 17:26:28 +08:00
|
|
|
|
WindowState: '正常',
|
2025-11-04 10:53:22 +08:00
|
|
|
|
showTitleBar: true,
|
2025-11-05 08:59:53 +08:00
|
|
|
|
// 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`,
|
|
|
|
|
|
title: `面板 1`,
|
|
|
|
|
|
x: 0,
|
|
|
|
|
|
y: 0,
|
|
|
|
|
|
width: 100,
|
|
|
|
|
|
height: 100,
|
|
|
|
|
|
collapsed: false,
|
|
|
|
|
|
toolbarExpanded: false
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
2025-11-02 17:06:40 +07:00
|
|
|
|
}
|
|
|
|
|
|
floatingAreas.value.push(newArea)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-04 09:45:51 +08:00
|
|
|
|
// 更新区域位置
|
|
|
|
|
|
const onUpdatePosition = (id, position) => {
|
|
|
|
|
|
const area = floatingAreas.value.find(a => a.id === id)
|
|
|
|
|
|
if (area) {
|
|
|
|
|
|
area.x = position.left
|
|
|
|
|
|
area.y = position.top
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-02 17:06:40 +07:00
|
|
|
|
// 切换折叠状态
|
|
|
|
|
|
const onToggleCollapse = (id) => {
|
|
|
|
|
|
const area = floatingAreas.value.find(a => a.id === id)
|
|
|
|
|
|
if (area) {
|
|
|
|
|
|
area.collapsed = !area.collapsed
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 最大化/还原
|
2025-11-04 14:43:19 +08:00
|
|
|
|
const onMaximize = (panelId) => {
|
|
|
|
|
|
// 查找包含该面板的区域
|
|
|
|
|
|
for (const area of floatingAreas.value) {
|
2025-11-06 13:32:18 +08:00
|
|
|
|
// 正确处理层级结构: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 }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log('Panel最大化按钮触发,切换Area状态:', area.WindowState)
|
|
|
|
|
|
console.log('Panel最大化状态:', tabPage.panels[0].maximized)
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
2025-11-04 14:43:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-11-02 17:06:40 +07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-04 15:26:50 +08:00
|
|
|
|
// 关闭浮动区域 - 同时移除内容区的Panel
|
2025-11-02 17:06:40 +07:00
|
|
|
|
const onCloseFloatingArea = (id) => {
|
|
|
|
|
|
const index = floatingAreas.value.findIndex(a => a.id === id)
|
|
|
|
|
|
if (index !== -1) {
|
2025-11-04 15:26:50 +08:00
|
|
|
|
// 获取要移除的Area
|
|
|
|
|
|
const areaToRemove = floatingAreas.value[index]
|
|
|
|
|
|
|
|
|
|
|
|
// 清理Panel引用,确保Panel被正确移除
|
|
|
|
|
|
if (areaToRemove.panels) {
|
|
|
|
|
|
// 这里可以添加任何需要的Panel清理逻辑
|
|
|
|
|
|
console.log('移除Area时同步移除Panel:', areaToRemove.panels.map(p => p.id))
|
|
|
|
|
|
// 清空panels数组,确保Panel被正确移除
|
|
|
|
|
|
areaToRemove.panels = []
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 从数组中移除Area
|
2025-11-02 17:06:40 +07:00
|
|
|
|
floatingAreas.value.splice(index, 1)
|
2025-11-04 15:26:50 +08:00
|
|
|
|
console.log('成功关闭Area:', id)
|
2025-11-02 17:06:40 +07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-04 10:53:22 +08:00
|
|
|
|
// 关闭面板
|
|
|
|
|
|
const onClosePanel = (areaId, panelId) => {
|
|
|
|
|
|
const area = floatingAreas.value.find(a => a.id === areaId)
|
2025-11-06 13:32:18 +08:00
|
|
|
|
// 正确处理层级结构: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
|
|
|
|
|
|
}
|
2025-11-04 10:53:22 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-02 17:06:40 +07:00
|
|
|
|
// 切换工具栏
|
|
|
|
|
|
const onToggleToolbar = (id) => {
|
|
|
|
|
|
const area = floatingAreas.value.find(a => a.id === id)
|
|
|
|
|
|
if (area) {
|
|
|
|
|
|
area.toolbarExpanded = !area.toolbarExpanded
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-11-02 17:12:40 +07:00
|
|
|
|
|
2025-11-04 11:05:12 +08:00
|
|
|
|
// Panel拖拽开始
|
|
|
|
|
|
const onPanelDragStart = (areaId, event) => {
|
|
|
|
|
|
const area = floatingAreas.value.find(a => a.id === areaId)
|
2025-11-05 09:07:06 +08:00
|
|
|
|
// 只有当Area中只有一个TabPage且该TabPage中只有一个Panel时才允许通过Panel标题栏移动Area
|
|
|
|
|
|
if (area && area.tabPages && area.tabPages.length === 1 && area.tabPages[0].panels && area.tabPages[0].panels.length === 1) {
|
2025-11-04 11:05:12 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
2025-11-04 14:34:40 +08:00
|
|
|
|
console.log('Panel拖拽开始,移动Area:', areaId)
|
2025-11-07 15:41:44 +08:00
|
|
|
|
|
|
|
|
|
|
// 初始化鼠标位置跟踪
|
|
|
|
|
|
currentMousePosition.value = {
|
|
|
|
|
|
x: event.clientX,
|
|
|
|
|
|
y: event.clientY
|
|
|
|
|
|
}
|
2025-11-13 10:50:22 +08:00
|
|
|
|
|
|
|
|
|
|
// 拖拽开始时就显示指示器
|
|
|
|
|
|
showDockIndicator.value = true
|
|
|
|
|
|
|
|
|
|
|
|
// 使用dock-layout作为默认目标区域
|
|
|
|
|
|
if (dockLayoutRef.value) {
|
|
|
|
|
|
const rect = dockLayoutRef.value.getBoundingClientRect()
|
|
|
|
|
|
targetAreaRect.value = {
|
|
|
|
|
|
left: 0, // 使用相对于容器的位置(左上角)
|
|
|
|
|
|
top: 0,
|
|
|
|
|
|
width: rect.width,
|
|
|
|
|
|
height: rect.height
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-11-04 11:05:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Panel拖拽移动
|
|
|
|
|
|
const onPanelDragMove = (areaId, event) => {
|
|
|
|
|
|
if (panelDragState.value.isDragging && panelDragState.value.currentAreaId === areaId) {
|
|
|
|
|
|
const area = floatingAreas.value.find(a => a.id === areaId)
|
|
|
|
|
|
if (area) {
|
|
|
|
|
|
// 计算移动距离
|
|
|
|
|
|
const deltaX = event.clientX - panelDragState.value.startClientPos.x
|
|
|
|
|
|
const deltaY = event.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
|
2025-11-04 14:34:40 +08:00
|
|
|
|
|
2025-11-07 15:41:44 +08:00
|
|
|
|
// 更新鼠标位置
|
|
|
|
|
|
currentMousePosition.value = {
|
|
|
|
|
|
x: event.clientX,
|
|
|
|
|
|
y: event.clientY
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-04 14:34:40 +08:00
|
|
|
|
// 调试信息
|
|
|
|
|
|
console.log('Panel拖拽移动,Area新位置:', { x: newLeft, y: newTop })
|
2025-11-04 11:05:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Panel拖拽结束
|
|
|
|
|
|
const onPanelDragEnd = () => {
|
2025-11-04 14:34:40 +08:00
|
|
|
|
console.log('Panel拖拽结束')
|
2025-11-04 11:05:12 +08:00
|
|
|
|
panelDragState.value.isDragging = false
|
|
|
|
|
|
panelDragState.value.currentAreaId = null
|
2025-11-07 15:41:44 +08:00
|
|
|
|
|
|
|
|
|
|
// 隐藏停靠指示器
|
|
|
|
|
|
showDockIndicator.value = false
|
|
|
|
|
|
activeDockZone.value = null
|
|
|
|
|
|
|
|
|
|
|
|
// 如果有活动的停靠区域,可以在这里处理停靠逻辑
|
|
|
|
|
|
if (activeDockZone.value) {
|
|
|
|
|
|
console.log('停靠到区域:', activeDockZone.value)
|
|
|
|
|
|
// 这里可以实现具体的停靠逻辑
|
|
|
|
|
|
}
|
2025-11-04 11:05:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-06 14:57:30 +08:00
|
|
|
|
// 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) {
|
|
|
|
|
|
tabDragState.value.isDragging = true
|
|
|
|
|
|
tabDragState.value.currentAreaId = areaId
|
|
|
|
|
|
tabDragState.value.startClientPos = {
|
|
|
|
|
|
x: event.clientX,
|
|
|
|
|
|
y: event.clientY
|
|
|
|
|
|
}
|
|
|
|
|
|
tabDragState.value.startAreaPos = {
|
|
|
|
|
|
x: area.x,
|
|
|
|
|
|
y: area.y
|
|
|
|
|
|
}
|
|
|
|
|
|
console.log('TabPage拖拽开始,移动Area:', areaId)
|
2025-11-13 10:50:22 +08:00
|
|
|
|
|
|
|
|
|
|
// 初始化鼠标位置跟踪
|
|
|
|
|
|
currentMousePosition.value = {
|
|
|
|
|
|
x: event.clientX,
|
|
|
|
|
|
y: event.clientY
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 拖拽开始时就显示指示器
|
|
|
|
|
|
showDockIndicator.value = true
|
|
|
|
|
|
|
|
|
|
|
|
// 使用dock-layout作为默认目标区域
|
|
|
|
|
|
if (dockLayoutRef.value) {
|
|
|
|
|
|
const rect = dockLayoutRef.value.getBoundingClientRect()
|
|
|
|
|
|
targetAreaRect.value = {
|
|
|
|
|
|
left: 0, // 使用相对于容器的位置(左上角)
|
|
|
|
|
|
top: 0,
|
|
|
|
|
|
width: rect.width,
|
|
|
|
|
|
height: rect.height
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-11-06 14:57:30 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// TabPage拖拽移动
|
|
|
|
|
|
const onTabDragMove = (areaId, event) => {
|
|
|
|
|
|
if (tabDragState.value.isDragging && tabDragState.value.currentAreaId === areaId) {
|
|
|
|
|
|
const area = floatingAreas.value.find(a => a.id === areaId)
|
|
|
|
|
|
if (area) {
|
|
|
|
|
|
// 计算移动距离
|
|
|
|
|
|
const deltaX = event.clientX - tabDragState.value.startClientPos.x
|
|
|
|
|
|
const deltaY = event.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
|
|
|
|
|
|
|
2025-11-13 10:50:22 +08:00
|
|
|
|
// 更新鼠标位置
|
|
|
|
|
|
currentMousePosition.value = {
|
|
|
|
|
|
x: event.clientX,
|
|
|
|
|
|
y: event.clientY
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-06 14:57:30 +08:00
|
|
|
|
// 调试信息
|
|
|
|
|
|
console.log('TabPage拖拽移动,Area新位置:', { x: newLeft, y: newTop })
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// TabPage拖拽结束
|
|
|
|
|
|
const onTabDragEnd = () => {
|
|
|
|
|
|
console.log('TabPage拖拽结束')
|
|
|
|
|
|
tabDragState.value.isDragging = false
|
|
|
|
|
|
tabDragState.value.currentAreaId = null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-04 17:24:08 +08:00
|
|
|
|
// 监听floatingAreas变化,确保当Area最大化时,Panel也会自动最大化
|
|
|
|
|
|
watch(floatingAreas, (newAreas) => {
|
|
|
|
|
|
newAreas.forEach(area => {
|
2025-11-06 13:32:18 +08:00
|
|
|
|
// 正确处理层级结构: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 };
|
|
|
|
|
|
console.log(`Area ${area.id} 状态变化,同步Panel最大化状态为:`, isAreaMaximized);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-11-04 17:24:08 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}, { deep: true });
|
|
|
|
|
|
|
2025-11-07 15:41:44 +08:00
|
|
|
|
// 处理主区域的dragover事件
|
|
|
|
|
|
const handleMainAreaDragOver = (event) => {
|
|
|
|
|
|
event.preventDefault()
|
|
|
|
|
|
|
|
|
|
|
|
if (panelDragState.value.isDragging || tabDragState.value.isDragging) {
|
2025-11-13 10:50:22 +08:00
|
|
|
|
// 使用dock-layout作为基准获取位置和大小
|
|
|
|
|
|
let rect
|
|
|
|
|
|
if (dockLayoutRef.value) {
|
|
|
|
|
|
rect = dockLayoutRef.value.getBoundingClientRect()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 回退到使用事件目标
|
|
|
|
|
|
rect = event.currentTarget.getBoundingClientRect()
|
|
|
|
|
|
}
|
2025-11-07 15:41:44 +08:00
|
|
|
|
|
|
|
|
|
|
// 更新目标区域信息并显示停靠指示器
|
|
|
|
|
|
targetAreaRect.value = {
|
2025-11-13 10:50:22 +08:00
|
|
|
|
left: 0, // 使用相对于容器的位置(左上角)
|
|
|
|
|
|
top: 0,
|
2025-11-07 15:41:44 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-06 13:24:37 +08:00
|
|
|
|
// 处理Panel最大化同步事件
|
|
|
|
|
|
const onPanelMaximizeSync = ({ areaId, maximized }) => {
|
|
|
|
|
|
// 查找对应的Area
|
|
|
|
|
|
const area = floatingAreas.value.find(a => a.id === areaId);
|
2025-11-06 13:32:18 +08:00
|
|
|
|
// 正确处理层级结构: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-06 13:24:37 +08:00
|
|
|
|
console.log(`同步Area ${areaId} 的Panel最大化状态为:`, maximized);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-02 17:12:40 +07:00
|
|
|
|
// 暴露方法给父组件
|
|
|
|
|
|
defineExpose({
|
2025-11-04 10:31:12 +08:00
|
|
|
|
addFloatingPanel
|
2025-11-02 17:12:40 +07:00
|
|
|
|
})
|
2025-11-02 17:06:40 +07:00
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
.dock-layout {
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
2025-11-04 09:10:15 +08:00
|
|
|
|
overflow: visible;
|
2025-11-02 17:06:40 +07:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-03 17:26:28 +08:00
|
|
|
|
/* 浮动区域样式已直接应用到Area组件 */
|
2025-11-02 17:19:53 +07:00
|
|
|
|
|
2025-11-02 17:06:40 +07:00
|
|
|
|
/* 添加浮动区域按钮样式 */
|
|
|
|
|
|
.add-floating-btn {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
user-select: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.add-floating-btn:active {
|
|
|
|
|
|
transform: scale(0.98);
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|