2025-10-20 09:04:09 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-if="panels.length > 0"
|
|
|
|
|
|
:class="containerClasses"
|
|
|
|
|
|
:style="containerStyle"
|
|
|
|
|
|
class="overflow-hidden"
|
|
|
|
|
|
>
|
|
|
|
|
|
<template v-for="(panel, index) in panels" :key="panel.id">
|
|
|
|
|
|
<DockPanel
|
|
|
|
|
|
:panel="panel"
|
|
|
|
|
|
:position="position"
|
|
|
|
|
|
:index="index"
|
|
|
|
|
|
@close="closePanel(panel.id)"
|
|
|
|
|
|
@float="floatPanel(panel.id)"
|
|
|
|
|
|
@dock="dockPanel"
|
|
|
|
|
|
@toggleCollapse="toggleCollapse(panel.id)"
|
|
|
|
|
|
@titleBarDragStart="handlePanelTitleBarDragStart(panel.id, $event)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<!-- 面板间分隔条 - 不显示在最后一个面板旁 -->
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-if="index < panels.length - 1"
|
|
|
|
|
|
:class="separatorClasses"
|
|
|
|
|
|
@mousedown="startPanelResize(panel.id, index, $event)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
|
|
|
|
|
import { computed } from 'vue';
|
|
|
|
|
|
import DockPanel from './DockPanel.js';
|
|
|
|
|
|
|
|
|
|
|
|
// 定义组件属性
|
|
|
|
|
|
const props = defineProps({
|
|
|
|
|
|
// 面板位置(left, right, top, bottom)
|
|
|
|
|
|
position: {
|
|
|
|
|
|
type: String,
|
|
|
|
|
|
required: true,
|
|
|
|
|
|
validator: (value) => ['left', 'right', 'top', 'bottom'].includes(value)
|
|
|
|
|
|
},
|
|
|
|
|
|
// 面板列表
|
|
|
|
|
|
panels: {
|
|
|
|
|
|
type: Array,
|
|
|
|
|
|
default: () => []
|
|
|
|
|
|
},
|
|
|
|
|
|
// 区域尺寸(宽度或高度)
|
|
|
|
|
|
size: {
|
|
|
|
|
|
type: Number,
|
|
|
|
|
|
required: true
|
|
|
|
|
|
},
|
|
|
|
|
|
// 面板尺寸比例
|
|
|
|
|
|
sizeRatios: {
|
|
|
|
|
|
type: Array,
|
|
|
|
|
|
default: () => []
|
|
|
|
|
|
},
|
|
|
|
|
|
// store实例
|
|
|
|
|
|
store: {
|
|
|
|
|
|
type: Object,
|
|
|
|
|
|
required: true
|
2025-10-23 22:09:49 +08:00
|
|
|
|
},
|
|
|
|
|
|
// 绝对定位边界(可选):{ x, y, width, height }
|
|
|
|
|
|
bounds: {
|
|
|
|
|
|
type: Object,
|
|
|
|
|
|
required: false,
|
|
|
|
|
|
default: null
|
2025-10-20 09:04:09 +08:00
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 定义组件事件
|
|
|
|
|
|
const emit = defineEmits([
|
|
|
|
|
|
'closePanel',
|
|
|
|
|
|
'floatPanel',
|
|
|
|
|
|
'dockPanel',
|
|
|
|
|
|
'toggleCollapse',
|
|
|
|
|
|
'titleBarDragStart',
|
|
|
|
|
|
'panelResizeStart'
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
// 根据位置计算容器类名
|
|
|
|
|
|
const containerClasses = computed(() => {
|
|
|
|
|
|
const baseClasses = 'overflow-hidden bg-white';
|
|
|
|
|
|
|
|
|
|
|
|
const positionClasses = {
|
|
|
|
|
|
left: 'flex flex-col border-r border-gray-300 panel-area-left',
|
|
|
|
|
|
right: 'flex flex-col border-l border-gray-300 panel-area-right',
|
|
|
|
|
|
top: 'flex border-b border-gray-300 panel-area-top',
|
|
|
|
|
|
bottom: 'flex border-t border-gray-300 panel-area-bottom'
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-10-23 22:09:49 +08:00
|
|
|
|
const absoluteClass = props.bounds ? ' absolute' : '';
|
|
|
|
|
|
return `${baseClasses} ${positionClasses[props.position]}${absoluteClass}`;
|
2025-10-20 09:04:09 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 根据位置计算容器样式
|
|
|
|
|
|
const containerStyle = computed(() => {
|
2025-10-23 22:09:49 +08:00
|
|
|
|
// 若提供了绝对定位边界,则使用top/left/width/height
|
|
|
|
|
|
if (props.bounds) {
|
|
|
|
|
|
const { x = 0, y = 0, width = 0, height = 0 } = props.bounds;
|
|
|
|
|
|
return {
|
|
|
|
|
|
position: 'absolute',
|
|
|
|
|
|
top: `${y}px`,
|
|
|
|
|
|
left: `${x}px`,
|
|
|
|
|
|
width: `${width}px`,
|
|
|
|
|
|
height: `${height}px`
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-20 09:04:09 +08:00
|
|
|
|
const isVertical = ['left', 'right'].includes(props.position);
|
|
|
|
|
|
const styleProperty = isVertical ? 'width' : 'height';
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
[styleProperty]: `${props.size}px`
|
|
|
|
|
|
};
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 根据位置计算分隔条类名
|
|
|
|
|
|
const separatorClasses = computed(() => {
|
|
|
|
|
|
const isVertical = ['left', 'right'].includes(props.position);
|
|
|
|
|
|
|
|
|
|
|
|
if (isVertical) {
|
|
|
|
|
|
// 左右面板中的子面板是垂直排列的,应该使用水平分隔条
|
|
|
|
|
|
return 'h-1 cursor-row-resize z-30 dock-panel-separator dock-panel-separator-horizontal';
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 顶部底部面板中的子面板是水平排列的,应该使用垂直分隔条
|
|
|
|
|
|
return 'w-1 cursor-col-resize z-30 dock-panel-separator dock-panel-separator-vertical';
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 处理关闭面板
|
|
|
|
|
|
function closePanel(panelId) {
|
|
|
|
|
|
emit('closePanel', panelId);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 处理浮动面板
|
|
|
|
|
|
function floatPanel(panelId) {
|
|
|
|
|
|
emit('floatPanel', panelId);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 处理停靠面板
|
|
|
|
|
|
function dockPanel(panelId, position) {
|
|
|
|
|
|
emit('dockPanel', panelId, position);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 处理折叠面板
|
|
|
|
|
|
function toggleCollapse(panelId) {
|
|
|
|
|
|
emit('toggleCollapse', panelId);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 处理面板标题栏拖拽开始
|
|
|
|
|
|
function handlePanelTitleBarDragStart(panelId, event) {
|
|
|
|
|
|
emit('titleBarDragStart', panelId, event);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 处理面板调整大小开始
|
|
|
|
|
|
function startPanelResize(panelId, panelIndex, event) {
|
|
|
|
|
|
emit('panelResizeStart', props.position, panelId, panelIndex, event);
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
/* PanelArea特有的作用域样式 */
|
|
|
|
|
|
/* 共享样式已在styles/panel-shared.css中定义 */
|
|
|
|
|
|
</style>
|