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

436 lines
14 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;">
<!-- 主区域 -->
<Area
:WindowState="windowState"
:showTitleBar="false"
title="主区域"
:style="{ position: 'relative', width: '100%', height: '100%', zIndex: 1 }"
>
</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"
>
<!-- 每个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"
>
<!-- 在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"
/>
</TabPage>
</Area>
</div>
2025-10-31 23:58:26 +08:00
</template>
<script setup>
import { ref, defineExpose, nextTick, watch } 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';
// 主区域状态
const windowState = ref('最大化')
// 浮动区域列表 - 每个area包含panels数组
const floatingAreas = ref([])
// 容器引用
const dockLayoutRef = ref(null)
// 区域ID计数器
let areaIdCounter = 1
// 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 }
})
// 添加新的浮动面板
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`,
title: `面板 1`,
x: 0,
y: 0,
width: 100,
height: 100,
collapsed: false,
toolbarExpanded: false
}
]
}
]
}
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 }
}
console.log('Panel最大化按钮触发切换Area状态:', area.WindowState)
console.log('Panel最大化状态:', tabPage.panels[0].maximized)
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清理逻辑
console.log('移除Area时同步移除Panel:', areaToRemove.panels.map(p => p.id))
// 清空panels数组确保Panel被正确移除
areaToRemove.panels = []
}
// 从数组中移除Area
floatingAreas.value.splice(index, 1)
console.log('成功关闭Area:', id)
}
}
// 关闭面板
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
}
}
// 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) {
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
}
console.log('Panel拖拽开始移动Area:', areaId)
}
}
// 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
// 调试信息
console.log('Panel拖拽移动Area新位置:', { x: newLeft, y: newTop })
}
}
}
// Panel拖拽结束
const onPanelDragEnd = () => {
console.log('Panel拖拽结束')
panelDragState.value.isDragging = false
panelDragState.value.currentAreaId = 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) {
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)
}
}
// 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
// 调试信息
console.log('TabPage拖拽移动Area新位置:', { x: newLeft, y: newTop })
}
}
}
// TabPage拖拽结束
const onTabDragEnd = () => {
console.log('TabPage拖拽结束')
tabDragState.value.isDragging = false
tabDragState.value.currentAreaId = 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 };
console.log(`Area ${area.id} 状态变化同步Panel最大化状态为:`, isAreaMaximized);
}
}
}
}
});
}, { deep: true });
// 处理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 };
console.log(`同步Area ${areaId} 的Panel最大化状态为:`, maximized);
}
}
// 暴露方法给父组件
defineExpose({
addFloatingPanel
})
</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>