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

418 lines
12 KiB
Vue
Raw Normal View History

2025-10-31 21:58:33 +08:00
<template>
2025-11-05 13:06:11 +08:00
<div class="panel bg-white overflow-hidden"
2025-11-20 09:44:51 +08:00
:style="{ width: '100%', height: '100%' }"
:data-panel-id="id">
2025-10-31 21:58:33 +08:00
<div class="flex flex-col h-full">
<!-- 标题栏 -->
<div class="title-bar h-6 bg-[#435d9c] text-white px-2 flex items-center justify-between select-none cursor-move"
@mousedown="onDragStart">
2025-10-31 21:58:33 +08:00
<div class="flex items-center">
<span class="text-xs">{{ title }}</span>
</div>
<div class="title-bar-buttons flex items-center gap-0.5">
<button class="button-icon p-[2px] rounded hover:opacity-100 opacity-80"
@click.stop="onToggleCollapse"
@mousedown.stop
2025-10-31 21:58:33 +08:00
aria-label="折叠/展开">
<!-- 向下小三角使用内联SVG避免样式作用域问题 -->
<svg width="11" height="11" viewBox="0 0 11 11" aria-hidden="true">
<polygon points="5.5,8 2,3.5 9,3.5" fill="#cbd6ff" />
</svg>
</button>
<button class="button-icon p-[2px] rounded hover:opacity-100 opacity-80"
@click.stop="onMaximize"
@mousedown.stop
:aria-label="maximized ? '还原' : '最大化'">
<!-- 最大化图标 -->
<template v-if="!maximized">
<!-- 最大化图标外框 + 内部线条 -->
<svg class="icon-square-svg" width="11" height="11" viewBox="0 0 11 11" aria-hidden="true">
2025-11-04 16:46:24 +08:00
<rect x="0.5" y="0.5" width="10" height="10" fill="#cbd6ff" stroke="#8ea3d8" stroke-width="1" />
<rect x="3" y="3" width="5" height="1" fill="#b8c6ff" />
<rect x="1" y="3" width="8.5" height="6.5" fill="#435d9c" />
</svg>
</template>
<!-- 还原图标 -->
<template v-else>
<svg class="icon-square-svg" width="11" height="11" viewBox="0 0 11 11" aria-hidden="true">
2025-11-04 16:46:24 +08:00
<path
fill="#CED4DD"
fill-rule="evenodd"
clip-rule="evenodd"
d="M1 4 L4 4 L4 1 L11 1 L11 8 L8 8 L8 11 L1 11 Z
M5 4 L5 3 L10 3 L10 7 L8 7 L8 4 Z
M2 6 L12.6 5 L7 6 L7 10 L2 10 Z" />
</svg>
</template>
2025-10-31 21:58:33 +08:00
</button>
<button class="button-icon p-[2px] rounded hover:opacity-100 opacity-80"
@click.stop="onClose"
@mousedown.stop
2025-10-31 21:58:33 +08:00
aria-label="关闭">
<!-- 关闭图标X内联SVG确保1px线条 -->
<svg width="11" height="11" viewBox="0 0 11 11" aria-hidden="true">
<line x1="2" y1="2" x2="9" y2="9" stroke="#e6efff" stroke-width="1" />
<line x1="2" y1="9" x2="9" y2="2" stroke="#e6efff" stroke-width="1" />
</svg>
</button>
</div>
</div>
<!-- 工具栏位于标题栏下方右侧扩展钮 -->
<div class="toolbar h-6 bg-[#d5e2f6] text-[#2c3e7a] px-2 flex items-center justify-between border-b border-[#c7d2ea]">
<div class="toolbar-left flex items-center gap-2">
<span class="text-xs">工具栏</span>
<button v-if="toolbarExpanded" class="toolbar-button px-2 py-0.5 text-xs bg-white/60 rounded hover:bg-white">示例按钮</button>
</div>
<button class="toolbar-toggle px-2 py-0.5 text-xs rounded hover:bg-white/40"
@click="onToggleToolbar"
aria-label="展开工具栏">
<i class="fa-solid" :class="toolbarExpanded ? 'fa-angles-left' : 'fa-angles-right'"></i>
</button>
</div>
2025-11-18 15:39:46 +08:00
<!-- 内容区可折叠添加滚动条 -->
<div class="content-area bg-[#f5f7fb] flex-1 p-4 overflow-auto min-h-0" v-show="!collapsed">
2025-11-17 16:55:03 +08:00
<div v-if="content" class="panel-content">
<div class="mb-4">
<h3 class="text-lg font-bold mb-2" :style="{ color: content.color }">
{{ content.title }}
</h3>
<p class="text-sm text-gray-600 mb-3">
类型{{ content.type }} | 创建时间{{ content.timestamp }}
</p>
</div>
<div class="grid grid-cols-1 gap-3">
<div
v-for="(item, index) in content.data"
:key="item.id"
class="data-item p-3 border rounded-lg shadow-sm"
:style="{
borderLeftColor: content.color,
borderLeftWidth: '4px'
}"
>
<div class="flex justify-between items-center">
<span class="font-medium">{{ item.label }}</span>
<span class="text-sm font-bold" :style="{ color: content.color }">
{{ item.value }}
</span>
</div>
</div>
</div>
</div>
<div v-else class="text-gray-500 text-center">
<p>暂无内容</p>
<div class="mt-2 text-xs">面板ID: {{ id }} - 标题: {{ title }}</div>
</div>
</div>
2025-10-31 21:58:33 +08:00
</div>
</div>
</template>
<script setup>
2025-11-20 09:44:51 +08:00
import { defineProps, onMounted, onUnmounted } from 'vue';
import {
eventBus,
EVENT_TYPES,
emitEvent,
onEvent
} from './eventBus.js';
2025-10-31 21:58:33 +08:00
// 定义组件属性
const props = defineProps({
id: {
type: String,
required: true
},
title: {
type: String,
default: ''
},
x: {
type: Number,
default: 0
},
y: {
type: Number,
default: 0
},
width: {
type: Number,
default: 300
},
height: {
type: Number,
default: 180
},
collapsed: {
type: Boolean,
default: false
},
toolbarExpanded: {
type: Boolean,
default: false
},
maximized: {
type: Boolean,
default: false
2025-11-17 16:55:03 +08:00
},
content: {
type: Object,
default: null
2025-11-20 09:44:51 +08:00
},
// 移除areaId属性因为面板会被拖拽到不同区域
// 改为通过DOM动态获取当前所在区域
2025-10-31 21:58:33 +08:00
});
2025-11-20 09:44:51 +08:00
// 事件订阅管理
const subscriptions = new Map();
2025-10-31 21:58:33 +08:00
2025-11-20 09:44:51 +08:00
// 动态获取当前面板所在的Area ID
const getCurrentAreaId = () => {
// 通过DOM向上查找最近的Area容器
const panelElement = document.querySelector(`[data-panel-id="${props.id}"]`);
if (panelElement) {
const areaElement = panelElement.closest('[data-area-id]');
if (areaElement) {
return areaElement.getAttribute('data-area-id');
}
}
// 备用方法:通过父组件查找
// 向上查找vs-area容器
const parentElement = document.querySelector(`[data-panel-id="${props.id}"]`)?.parentElement;
if (parentElement) {
const areaElement = parentElement.closest('.vs-area');
if (areaElement) {
return areaElement.getAttribute('data-area-id');
}
}
console.warn(`[Panel:${props.id}] 无法找到当前所在的AreaPanel可能未正确挂载`);
return null;
};
// 事件处理函数 - 使用事件总线
2025-10-31 21:58:33 +08:00
const onToggleCollapse = () => {
2025-11-20 09:44:51 +08:00
console.log(`[Panel:${props.id}] 触发折叠/展开事件`)
emitEvent(EVENT_TYPES.PANEL_TOGGLE_COLLAPSE, {
panelId: props.id,
areaId: getCurrentAreaId(),
currentState: props.collapsed
})
2025-10-31 21:58:33 +08:00
};
const onMaximize = () => {
2025-11-20 09:44:51 +08:00
console.log(`[Panel:${props.id}] 触发最大化事件`)
emitEvent(EVENT_TYPES.PANEL_MAXIMIZE, {
panelId: props.id,
areaId: getCurrentAreaId(),
currentState: props.maximized
})
2025-10-31 21:58:33 +08:00
};
const onClose = () => {
2025-11-20 09:44:51 +08:00
// console.log(`[Panel:${props.id}] 触发关闭请求事件`)
// 首先发送关闭请求,让父组件可以处理确认逻辑
emitEvent(EVENT_TYPES.PANEL_CLOSE_REQUEST, {
panelId: props.id,
areaId: getCurrentAreaId(),
panelTitle: props.title,
requestTime: Date.now()
})
2025-10-31 21:58:33 +08:00
};
const onToggleToolbar = () => {
2025-11-20 09:44:51 +08:00
console.log(`[Panel:${props.id}] 触发工具栏切换事件`)
emitEvent(EVENT_TYPES.PANEL_TOGGLE_TOOLBAR, {
panelId: props.id,
areaId: getCurrentAreaId(),
currentState: props.toolbarExpanded
})
2025-10-31 21:58:33 +08:00
};
// 拖拽相关状态
let isDragging = false;
// 拖拽开始
const onDragStart = (e) => {
// 只有当点击的是标题栏区域(不是按钮)时才触发拖拽
if (!e.target.closest('.title-bar-buttons') && !e.target.closest('button')) {
isDragging = true;
2025-11-20 09:44:51 +08:00
console.log(`[Panel:${props.id}] 开始拖拽`)
// 使用事件总线触发拖拽开始事件
emitEvent(EVENT_TYPES.PANEL_DRAG_START, {
panelId: props.id,
areaId: getCurrentAreaId(),
position: { x: e.clientX, y: e.clientY },
timestamp: Date.now()
}, {
source: { component: 'Panel', panelId: props.id }
})
// 防止文本选择和默认行为
e.preventDefault();
e.stopPropagation();
// 将鼠标移动和释放事件绑定到document确保拖拽的连续性
document.addEventListener('mousemove', onDragMove);
document.addEventListener('mouseup', onDragEnd);
document.addEventListener('mouseleave', onDragEnd);
}
};
// 拖拽移动
const onDragMove = (e) => {
if (isDragging) {
// 防止文本选择和默认行为
e.preventDefault();
e.stopPropagation();
2025-11-20 09:44:51 +08:00
// 使用事件总线触发拖拽移动事件
emitEvent(EVENT_TYPES.PANEL_DRAG_MOVE, {
panelId: props.id,
areaId: getCurrentAreaId(),
position: { x: e.clientX, y: e.clientY },
timestamp: Date.now()
}, {
source: { component: 'Panel', panelId: props.id }
})
}
};
// 拖拽结束
const onDragEnd = () => {
if (isDragging) {
isDragging = false;
2025-11-20 09:44:51 +08:00
console.log(`[Panel:${props.id}] 结束拖拽`)
// 使用事件总线触发拖拽结束事件
emitEvent(EVENT_TYPES.PANEL_DRAG_END, {
panelId: props.id,
areaId: getCurrentAreaId(),
timestamp: Date.now()
}, {
source: { component: 'Panel', panelId: props.id }
})
// 拖拽结束后移除事件监听器
document.removeEventListener('mousemove', onDragMove);
document.removeEventListener('mouseup', onDragEnd);
document.removeEventListener('mouseleave', onDragEnd);
}
};
2025-11-20 09:44:51 +08:00
/**
* 监听面板关闭事件更新组件状态可选
*/
const setupEventListeners = () => {
// 监听面板最大化同步事件
const unsubscribeMaximizeSync = onEvent(EVENT_TYPES.PANEL_MAXIMIZE_SYNC, (data) => {
if (data.panelId === props.id) {
// 这里可以添加最大化状态同步的逻辑
console.log(`[Panel:${props.id}] 收到最大化同步事件`)
}
})
subscriptions.set('maximizeSync', unsubscribeMaximizeSync)
}
/**
* 清理所有事件订阅
*/
const cleanupEventListeners = () => {
subscriptions.forEach((unsubscribe) => {
if (typeof unsubscribe === 'function') {
unsubscribe()
}
})
subscriptions.clear()
}
// 生命周期钩子
onMounted(() => {
console.log(`[Panel:${props.id}] 组件已挂载`)
// 启用调试模式(开发环境)
if (import.meta.env.DEV) {
eventBus.setDebugMode(true)
}
// 设置事件监听器
setupEventListeners()
})
onUnmounted(() => {
console.log(`[Panel:${props.id}] 组件即将卸载`)
// 清理事件监听器
cleanupEventListeners()
// 确保拖拽状态已清理
if (isDragging) {
document.removeEventListener('mousemove', onDragMove);
document.removeEventListener('mouseup', onDragEnd);
document.removeEventListener('mouseleave', onDragEnd);
isDragging = false
}
})
2025-10-31 21:58:33 +08:00
</script>
<style scoped>
/* 面板基础样式 */
.panel {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
2025-10-31 21:58:33 +08:00
}
/* 图标样式优化 */
.icon-square-svg {
/* 优化SVG渲染避免1px边框显示过粗的问题 */
shape-rendering: crispEdges;
}
2025-11-18 15:39:46 +08:00
/* 内容区域滚动条样式 */
.content-area {
/* 确保滚动条正确显示 */
scrollbar-width: thin;
scrollbar-color: #c7d2ea #f5f7fb;
}
/* Webkit浏览器滚动条样式 */
.content-area::-webkit-scrollbar {
width: 12px;
height: 12px;
}
.content-area::-webkit-scrollbar-track {
background: #f5f7fb;
border-radius: 6px;
}
.content-area::-webkit-scrollbar-thumb {
background: linear-gradient(to bottom, #d0d6ea, #c0c7e0);
border: 1px solid #b0b6d6;
border-radius: 6px;
}
.content-area::-webkit-scrollbar-thumb:hover {
background: linear-gradient(to bottom, #c1c7e2, #b2b8d9);
}
.content-area::-webkit-scrollbar-corner {
background: #f5f7fb;
}
2025-10-31 21:58:33 +08:00
/* 禁用可能存在的旧伪元素样式 */
:deep(.icon-square::before),
:deep(.icon-square::after) {
content: none !important;
display: none !important;
border: 0 !important;
}
</style>