抽取浮动窗口为独立Vue3组件Panel.js并更新DockLayoutTest.vue引用
This commit is contained in:
211
AutoRobot/Windows/Robot/Web/src/DockLayout/Panel.js
Normal file
211
AutoRobot/Windows/Robot/Web/src/DockLayout/Panel.js
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
import { ref, computed, defineComponent, onMounted, onUnmounted } from 'vue';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 浮动面板组件
|
||||||
|
* 提供可拖拽、最大化、最小化、折叠和工具栏扩展功能的窗口组件
|
||||||
|
*/
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'Panel',
|
||||||
|
props: {
|
||||||
|
panel: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
validator: (value) => {
|
||||||
|
return value && typeof value === 'object' && 'id' in value && 'title' in value;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
hostRef: {
|
||||||
|
type: Object,
|
||||||
|
default: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['close', 'toggleCollapse', 'toggleToolbar', 'maximize'],
|
||||||
|
setup(props, { emit }) {
|
||||||
|
// 响应式状态管理
|
||||||
|
const isDragging = ref(false);
|
||||||
|
const dragStartPos = ref({ x: 0, y: 0 });
|
||||||
|
const panelPosition = ref({
|
||||||
|
x: props.panel.x || 100,
|
||||||
|
y: props.panel.y || 100
|
||||||
|
});
|
||||||
|
const panelSize = ref({
|
||||||
|
width: props.panel.width || 300,
|
||||||
|
height: props.panel.height || 180
|
||||||
|
});
|
||||||
|
|
||||||
|
// 计算面板样式
|
||||||
|
const panelStyle = computed(() => {
|
||||||
|
const style = {
|
||||||
|
position: 'absolute',
|
||||||
|
top: `${panelPosition.value.y}px`,
|
||||||
|
left: `${panelPosition.value.x}px`,
|
||||||
|
width: `${panelSize.value.width}px`,
|
||||||
|
height: `${panelSize.value.height}px`,
|
||||||
|
backgroundColor: 'white',
|
||||||
|
border: '1px solid #e5e7eb',
|
||||||
|
borderRadius: '4px',
|
||||||
|
boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1)',
|
||||||
|
overflow: 'hidden'
|
||||||
|
};
|
||||||
|
|
||||||
|
// 最大化状态
|
||||||
|
if (props.panel.maximized) {
|
||||||
|
const host = props.hostRef?.value;
|
||||||
|
const rect = host?.getBoundingClientRect();
|
||||||
|
if (rect) {
|
||||||
|
style.top = '0px';
|
||||||
|
style.left = '0px';
|
||||||
|
style.width = `${rect.width - 2}px`;
|
||||||
|
style.height = `${rect.height - 2}px`;
|
||||||
|
} else {
|
||||||
|
style.top = '0px';
|
||||||
|
style.left = '0px';
|
||||||
|
style.width = '800px';
|
||||||
|
style.height = '600px';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return style;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 处理标题栏拖拽
|
||||||
|
const handleTitleBarMouseDown = (event) => {
|
||||||
|
if (props.panel.maximized) return;
|
||||||
|
|
||||||
|
isDragging.value = true;
|
||||||
|
dragStartPos.value = {
|
||||||
|
x: event.clientX - panelPosition.value.x,
|
||||||
|
y: event.clientY - panelPosition.value.y
|
||||||
|
};
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理窗口关闭
|
||||||
|
const handleClose = () => {
|
||||||
|
emit('close', props.panel.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理折叠/展开
|
||||||
|
const handleToggleCollapse = () => {
|
||||||
|
emit('toggleCollapse', props.panel.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理工具栏展开/收起
|
||||||
|
const handleToggleToolbar = () => {
|
||||||
|
emit('toggleToolbar', props.panel.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理最大化/还原
|
||||||
|
const handleMaximize = () => {
|
||||||
|
emit('maximize', props.panel.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 鼠标移动事件处理
|
||||||
|
const handleMouseMove = (event) => {
|
||||||
|
if (isDragging.value && !props.panel.maximized) {
|
||||||
|
panelPosition.value = {
|
||||||
|
x: event.clientX - dragStartPos.value.x,
|
||||||
|
y: event.clientY - dragStartPos.value.y
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 鼠标松开事件处理
|
||||||
|
const handleMouseUp = () => {
|
||||||
|
isDragging.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 生命周期钩子
|
||||||
|
onMounted(() => {
|
||||||
|
// 同步外部面板状态
|
||||||
|
if (props.panel.x !== undefined) panelPosition.value.x = props.panel.x;
|
||||||
|
if (props.panel.y !== undefined) panelPosition.value.y = props.panel.y;
|
||||||
|
if (props.panel.width !== undefined) panelSize.value.width = props.panel.width;
|
||||||
|
if (props.panel.height !== undefined) panelSize.value.height = props.panel.height;
|
||||||
|
|
||||||
|
// 添加全局事件监听
|
||||||
|
document.addEventListener('mousemove', handleMouseMove);
|
||||||
|
document.addEventListener('mouseup', handleMouseUp);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
// 清理全局事件监听
|
||||||
|
document.removeEventListener('mousemove', handleMouseMove);
|
||||||
|
document.removeEventListener('mouseup', handleMouseUp);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
isDragging,
|
||||||
|
panelStyle,
|
||||||
|
handleTitleBarMouseDown,
|
||||||
|
handleClose,
|
||||||
|
handleToggleCollapse,
|
||||||
|
handleToggleToolbar,
|
||||||
|
handleMaximize
|
||||||
|
};
|
||||||
|
},
|
||||||
|
template: `
|
||||||
|
<div class="panel-container" :style="panelStyle">
|
||||||
|
<div class="flex flex-col h-full">
|
||||||
|
<!-- 标题栏 -->
|
||||||
|
<div
|
||||||
|
class="h-6 bg-[#435d9c] text-white px-2 flex items-center justify-between select-none"
|
||||||
|
@mousedown="handleTitleBarMouseDown"
|
||||||
|
>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span class="text-xs">{{ panel.title }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-0.5">
|
||||||
|
<button
|
||||||
|
class="p-[2px] rounded hover:opacity-100 opacity-80"
|
||||||
|
@click.stop="handleToggleCollapse"
|
||||||
|
aria-label="折叠/展开"
|
||||||
|
>
|
||||||
|
<span class="icon-triangle-down"></span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="p-[2px] rounded hover:opacity-100 opacity-80"
|
||||||
|
@click.stop="handleMaximize"
|
||||||
|
aria-label="最大化"
|
||||||
|
>
|
||||||
|
<svg class="icon-square-svg" width="11" height="11" viewBox="0 0 11 11" aria-hidden="true">
|
||||||
|
<!-- 外框与底色(仅1px边框) -->
|
||||||
|
<rect x="0.5" y="0.5" width="10" height="10" fill="#cbd6ff" stroke="#8ea3d8" stroke-width="1" />
|
||||||
|
<!-- 两行填充:上行1px浅蓝,下行标题栏色 -->
|
||||||
|
<rect x="1" y="3" width="8.5" height="6.5" fill="#435d9c" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="p-[2px] rounded hover:opacity-100 opacity-80"
|
||||||
|
@click.stop="handleClose"
|
||||||
|
aria-label="关闭"
|
||||||
|
>
|
||||||
|
<span class="icon-x"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 工具栏 -->
|
||||||
|
<div class="h-6 bg-[#d5e2f6] text-[#2c3e7a] px-2 flex items-center justify-between border-b border-[#c7d2ea]">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-xs">工具栏</span>
|
||||||
|
<button v-if="panel.toolbarExpanded" class="px-2 py-0.5 text-xs bg-white/60 rounded hover:bg-white">示例按钮</button>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="px-2 py-0.5 text-xs rounded hover:bg-white/40"
|
||||||
|
@click.stop="handleToggleToolbar"
|
||||||
|
aria-label="展开工具栏"
|
||||||
|
>
|
||||||
|
<i class="fa-solid" :class="panel.toolbarExpanded ? 'fa-angles-left' : 'fa-angles-right'"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 内容区 -->
|
||||||
|
<div class="bg-[#f5f7fb] flex-1" v-show="!panel.collapsed">
|
||||||
|
<slot name="content"></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
});
|
||||||
@@ -44,56 +44,31 @@
|
|||||||
|
|
||||||
<!-- 预留主区域(暂不放面板) -->
|
<!-- 预留主区域(暂不放面板) -->
|
||||||
<div ref="panelHost" class="flex-1 w-full h-[calc(100%-4rem)] relative bg-gray-100">
|
<div ref="panelHost" class="flex-1 w-full h-[calc(100%-4rem)] relative bg-gray-100">
|
||||||
<!-- 浮动面板渲染区 -->
|
<!-- 浮动面板渲染区 - 使用Panel组件 -->
|
||||||
<div
|
<Panel
|
||||||
v-for="panel in floatingPanels"
|
v-for="panel in floatingPanels"
|
||||||
:key="panel.id"
|
:key="panel.id"
|
||||||
class="absolute bg-white shadow rounded border overflow-hidden"
|
:panel="panel"
|
||||||
:style="{ top: panel.y + 'px', left: panel.x + 'px', width: panel.width + 'px', height: panel.height + 'px' }"
|
:hostRef="panelHost"
|
||||||
|
@close="closePanel"
|
||||||
|
@toggleCollapse="toggleCollapse"
|
||||||
|
@toggleToolbar="toggleToolbarExpand"
|
||||||
|
@maximize="maximizePanel"
|
||||||
>
|
>
|
||||||
<div class="flex flex-col h-full">
|
<template #content>
|
||||||
<!-- 标题栏:小三角移到右侧,压缩高度与间距 -->
|
<!-- 面板内容区域 -->
|
||||||
<div class="h-6 bg-[#435d9c] text-white px-2 flex items-center justify-between select-none">
|
<div class="p-4">
|
||||||
<div class="flex items-center">
|
<div class="text-center text-gray-500 text-sm">面板内容区域 - {{ panel.id }}</div>
|
||||||
<span class="text-xs">{{ panel.title }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-0.5">
|
|
||||||
<button class="p-[2px] rounded hover:opacity-100 opacity-80" @click="toggleCollapse(panel.id)" aria-label="折叠/展开">
|
|
||||||
<span class="icon-triangle-down"></span>
|
|
||||||
</button>
|
|
||||||
<button class="p-[2px] rounded hover:opacity-100 opacity-80" @click="maximizePanel(panel.id)" aria-label="最大化">
|
|
||||||
<svg class="icon-square-svg" width="11" height="11" viewBox="0 0 11 11" aria-hidden="true">
|
|
||||||
<!-- 外框与底色(仅1px边框) -->
|
|
||||||
<rect x="0.5" y="0.5" width="10" height="10" fill="#cbd6ff" stroke="#8ea3d8" stroke-width="1" />
|
|
||||||
<!-- 两行填充:上行1px浅蓝,下行标题栏色 -->
|
|
||||||
<rect x="1" y="3" width="8.5" height="6.5" fill="#435d9c" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<button class="p-[2px] rounded hover:opacity-100 opacity-80" @click="closePanel(panel.id)" aria-label="关闭">
|
|
||||||
<span class="icon-x"></span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- 工具栏:位于标题栏下方,右侧扩展钮 -->
|
|
||||||
<div class="h-6 bg-[#d5e2f6] text-[#2c3e7a] px-2 flex items-center justify-between border-b border-[#c7d2ea]">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<span class="text-xs">工具栏</span>
|
|
||||||
<button v-if="panel.toolbarExpanded" class="px-2 py-0.5 text-xs bg-white/60 rounded hover:bg-white">示例按钮</button>
|
|
||||||
</div>
|
|
||||||
<button class="px-2 py-0.5 text-xs rounded hover:bg-white/40" @click="toggleToolbarExpand(panel.id)" aria-label="展开工具栏">
|
|
||||||
<i class="fa-solid" :class="panel.toolbarExpanded ? 'fa-angles-left' : 'fa-angles-right'"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<!-- 内容区:可折叠 -->
|
|
||||||
<div class="bg-[#f5f7fb] flex-1" v-show="!panel.collapsed"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
|
</Panel>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
import Panel from '../DockLayout/Panel.js';
|
||||||
|
|
||||||
// 顶部控制栏按钮的引用
|
// 顶部控制栏按钮的引用
|
||||||
const layoutFileInput = ref(null);
|
const layoutFileInput = ref(null);
|
||||||
@@ -211,8 +186,7 @@ function toggleToolbarExpand(id) {
|
|||||||
}
|
}
|
||||||
.icon-square { /* 最大化图标:外层浅蓝方块 + 内层细边框 */
|
.icon-square { /* 最大化图标:外层浅蓝方块 + 内层细边框 */
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 11px;
|
width: 11px; height: 11px;
|
||||||
height: 11px;
|
|
||||||
background: linear-gradient(180deg, #cbd6ff 0%, #b9c8ff 100%);
|
background: linear-gradient(180deg, #cbd6ff 0%, #b9c8ff 100%);
|
||||||
border: 1px solid #b8c6ff;
|
border: 1px solid #b8c6ff;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|||||||
Reference in New Issue
Block a user