2025-08-21 16:24:54 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<div id="robot-interface" class="robot-container">
|
|
|
|
|
|
<div class="tree-panel" :style="{ width: isTreeCollapsed ? '0px' : '240px' }">
|
|
|
|
|
|
<el-tree v-if="!isTreeCollapsed" :data="treeData" :props="treeProps" :default-expanded-keys="['robot-node-config']" @node-click="handleNodeClick" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="robot-panel">
|
2025-09-05 14:52:28 +08:00
|
|
|
|
<div id="robot-bg" ref="robotBg" :style="{ backgroundImage: `url(${robotImage})`, width: bgSize.width + 'px', height: bgSize.height + 'px' }">
|
2025-08-21 16:24:54 +08:00
|
|
|
|
<div
|
|
|
|
|
|
v-for="area in adjustedAreas"
|
|
|
|
|
|
:key="area.id"
|
|
|
|
|
|
class="clickable-area"
|
|
|
|
|
|
:style="{
|
|
|
|
|
|
position: 'absolute',
|
|
|
|
|
|
top: area.y + 'px',
|
|
|
|
|
|
left: area.x + 'px',
|
|
|
|
|
|
width: area.width + 'px',
|
|
|
|
|
|
height: area.height + 'px'
|
|
|
|
|
|
}"
|
|
|
|
|
|
@click="openPopup(area)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<span class="clickable-text">{{ area.label }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="toggle-button" @click="isTreeCollapsed = !isTreeCollapsed">
|
|
|
|
|
|
{{ isTreeCollapsed ? '>' : '<' }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-for="(popup, index) in popups"
|
|
|
|
|
|
:key="popup.id"
|
|
|
|
|
|
class="popup"
|
|
|
|
|
|
:style="{
|
|
|
|
|
|
position: 'absolute',
|
|
|
|
|
|
top: popup.top,
|
|
|
|
|
|
left: popup.left,
|
|
|
|
|
|
zIndex: popup.zIndex,
|
|
|
|
|
|
width: popup.width + 'px',
|
|
|
|
|
|
height: popup.height + 'px'
|
|
|
|
|
|
}"
|
|
|
|
|
|
:class="{ resizing: isResizing[index], dragging: isDragging && dragIndex === index }"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="drag-handle" @mousedown="startDragging($event, index)">
|
|
|
|
|
|
<span class="popup-title">{{ popup.id.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase()) }}</span>
|
|
|
|
|
|
<div class="popup-controls">
|
|
|
|
|
|
<span class="control-icon" @click="minimizePopup(index)">⧓</span>
|
|
|
|
|
|
<span class="control-icon" @click="maximizePopup(index)">□</span>
|
|
|
|
|
|
<span class="control-icon close-icon" @click="closePopup(index)">×</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<component :is="popup.component" v-bind="popup.props" />
|
|
|
|
|
|
<div class="resize-edge top" @mousedown="startResizing($event, index, 'top')"></div>
|
|
|
|
|
|
<div class="resize-edge bottom" @mousedown="startResizing($event, index, 'bottom')"></div>
|
|
|
|
|
|
<div class="resize-edge left" @mousedown="startResizing($event, index, 'left')"></div>
|
|
|
|
|
|
<div class="resize-edge right" @mousedown="startResizing($event, index, 'right')"></div>
|
|
|
|
|
|
<div class="resize-corner top-left" @mousedown="startResizing($event, index, 'top-left')"></div>
|
|
|
|
|
|
<div class="resize-corner top-right" @mousedown="startResizing($event, index, 'top-right')"></div>
|
|
|
|
|
|
<div class="resize-corner bottom-left" @mousedown="startResizing($event, index, 'bottom-left')"></div>
|
|
|
|
|
|
<div class="resize-corner bottom-right" @mousedown="startResizing($event, index, 'bottom-right')"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
|
|
|
|
|
import { ref, onMounted, computed, onUnmounted, watch } from 'vue';
|
|
|
|
|
|
import HandControl from '../register/components/DexHand/index.vue';
|
|
|
|
|
|
import CameraView from '../register/components/Camera/index.vue';
|
|
|
|
|
|
import MusicPlayer from '../register/components/Speaker/index.vue';
|
2025-09-05 14:52:28 +08:00
|
|
|
|
import MechanicalArm from '../register/components/MechanicalArm/index.vue';
|
2025-08-21 16:24:54 +08:00
|
|
|
|
import image from '@/assets/images/robot.png';
|
|
|
|
|
|
|
|
|
|
|
|
// 组件映射
|
|
|
|
|
|
const componentsMap = {
|
|
|
|
|
|
HandControl,
|
|
|
|
|
|
CameraView,
|
2025-09-05 14:52:28 +08:00
|
|
|
|
MusicPlayer,
|
|
|
|
|
|
MechanicalArm
|
2025-08-21 16:24:54 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const robotImage = image;
|
|
|
|
|
|
const robotBg = ref(null);
|
2025-09-05 14:52:28 +08:00
|
|
|
|
const isTreeCollapsed = ref(false);
|
2025-08-21 16:24:54 +08:00
|
|
|
|
const popups = ref([]);
|
|
|
|
|
|
const isDragging = ref(false);
|
|
|
|
|
|
const isResizing = ref([]);
|
2025-09-05 14:52:28 +08:00
|
|
|
|
const currentZIndex = ref(2000);
|
|
|
|
|
|
const dragIndex = ref(null);
|
2025-08-21 16:24:54 +08:00
|
|
|
|
const resizeStart = ref({ index: null, startX: 0, startY: 0, originalWidth: 0, originalHeight: 0, originalTop: 0, originalLeft: 0, edge: '' });
|
2025-09-05 14:52:28 +08:00
|
|
|
|
const imageDimensions = ref({ width: 0, height: 0 });
|
|
|
|
|
|
const containerSize = ref({ width: 0, height: 0 });
|
|
|
|
|
|
const bgSize = ref({ width: 0, height: 0 });
|
|
|
|
|
|
|
|
|
|
|
|
// 原始坐标和尺寸(基于图片原始大小)
|
|
|
|
|
|
const originalAreas = ref([
|
|
|
|
|
|
{ id: 'hand-left', x: 15, y: 505, width: 110, height: 50, component: HandControl, label: '左手' },
|
|
|
|
|
|
{ id: 'hand-right', x: 300, y: 505, width: 110, height: 50, component: HandControl, label: '右手' },
|
|
|
|
|
|
{ id: 'arm-left', x: 0, y: 150, width: 220, height: 50, component: MechanicalArm, label: '左机械臂' },
|
|
|
|
|
|
{ id: 'arm-right', x: 200, y: 150, width: 220, height: 50, component: MechanicalArm, label: '右机械臂' },
|
|
|
|
|
|
{ id: 'camera', x: 140, y: 50, width: 160, height: 50, component: CameraView, label: '摄像头' },
|
|
|
|
|
|
{ id: 'mouth', x: 140, y: 100, width: 160, height: 50, component: MusicPlayer, label: '扬声器' }
|
|
|
|
|
|
]);
|
2025-08-21 16:24:54 +08:00
|
|
|
|
|
|
|
|
|
|
// 树数据
|
|
|
|
|
|
const treeData = ref([
|
|
|
|
|
|
{
|
2025-09-05 14:52:28 +08:00
|
|
|
|
id: 'robot-node-config',
|
2025-08-21 16:24:54 +08:00
|
|
|
|
label: '机器人节点配置',
|
|
|
|
|
|
children: [
|
2025-09-05 14:52:28 +08:00
|
|
|
|
{ id: 'node-1', label: '节点1', children: [{ id: 'node-1-1', label: '子节点1-1' }, { id: 'node-1-2', label: '子节点1-2' }] },
|
|
|
|
|
|
{ id: 'node-2', label: '节点2', children: [{ id: 'node-2-1', label: '子节点2-1' }, { id: 'node-2-2', label: '子节点2-2' }] },
|
2025-08-21 16:24:54 +08:00
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
2025-09-05 14:52:28 +08:00
|
|
|
|
const treeProps = { children: 'children', label: 'label' };
|
2025-08-21 16:24:54 +08:00
|
|
|
|
|
|
|
|
|
|
const handleNodeClick = (data) => {
|
|
|
|
|
|
console.log('点击节点', data.label);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-09-05 14:52:28 +08:00
|
|
|
|
// 计算调整后的区域
|
2025-08-21 16:24:54 +08:00
|
|
|
|
const adjustedAreas = computed(() => {
|
|
|
|
|
|
if (!imageDimensions.value.width || !imageDimensions.value.height || !robotBg.value) return [];
|
2025-09-05 14:52:28 +08:00
|
|
|
|
const containerWidth = containerSize.value.width - (isTreeCollapsed.value ? 0 : 240); // 仅减去树宽度
|
|
|
|
|
|
const containerHeight = containerSize.value.height; // 无垂直padding
|
2025-08-21 16:24:54 +08:00
|
|
|
|
const scaleX = containerWidth / imageDimensions.value.width;
|
|
|
|
|
|
const scaleY = containerHeight / imageDimensions.value.height;
|
2025-09-05 14:52:28 +08:00
|
|
|
|
const scale = Math.min(scaleX, scaleY, 1); // 防止放大
|
|
|
|
|
|
const scaledWidth = imageDimensions.value.width * scale; // 图片宽
|
|
|
|
|
|
const scaledHeight = imageDimensions.value.height * scale;
|
|
|
|
|
|
|
|
|
|
|
|
// const offsetX = (containerWidth - scaledWidth) / 2;
|
|
|
|
|
|
// const offsetY = (containerHeight - scaledHeight) / 2;
|
|
|
|
|
|
|
|
|
|
|
|
// 更新背景图片尺寸
|
|
|
|
|
|
bgSize.value = { width: scaledWidth, height: scaledHeight };
|
2025-08-21 16:24:54 +08:00
|
|
|
|
|
|
|
|
|
|
return originalAreas.value.map(area => ({
|
|
|
|
|
|
...area,
|
2025-09-05 14:52:28 +08:00
|
|
|
|
x: area.x * scale,
|
|
|
|
|
|
y: area.y * scale,
|
2025-08-21 16:24:54 +08:00
|
|
|
|
width: area.width * scale,
|
|
|
|
|
|
height: area.height * scale
|
|
|
|
|
|
}));
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-09-05 14:52:28 +08:00
|
|
|
|
// 打开弹窗,考虑树宽度和padding偏移
|
2025-08-21 16:24:54 +08:00
|
|
|
|
const openPopup = (area) => {
|
|
|
|
|
|
const existingPopup = popups.value.find(p => p.id === area.id);
|
|
|
|
|
|
if (existingPopup) {
|
2025-09-05 14:52:28 +08:00
|
|
|
|
console.log('已打开ID为', area.id, '的弹窗');
|
|
|
|
|
|
return;
|
2025-08-21 16:24:54 +08:00
|
|
|
|
}
|
2025-09-05 14:52:28 +08:00
|
|
|
|
const treeWidth = isTreeCollapsed.value ? 0 : 240;
|
|
|
|
|
|
const leftOffset = treeWidth + 20; // tree宽度 + robot-panel padding-left
|
|
|
|
|
|
const popup = {
|
|
|
|
|
|
id: area.id,
|
|
|
|
|
|
component: area.component,
|
|
|
|
|
|
top: `${area.y}px`,
|
|
|
|
|
|
left: `${area.x + leftOffset}px`,
|
2025-08-21 16:24:54 +08:00
|
|
|
|
zIndex: currentZIndex.value++,
|
2025-09-05 14:52:28 +08:00
|
|
|
|
width: 640,
|
|
|
|
|
|
height: 480,
|
|
|
|
|
|
originalWidth: 640,
|
|
|
|
|
|
originalHeight: 480,
|
|
|
|
|
|
originalTop: `${area.y}px`,
|
|
|
|
|
|
originalLeft: `${area.x + leftOffset}px`,
|
2025-08-21 16:24:54 +08:00
|
|
|
|
isMaximized: false,
|
2025-09-05 14:52:28 +08:00
|
|
|
|
props: { terminalId: 'your-terminal-id', cameraId: 'your-camera-id' }
|
2025-08-21 16:24:54 +08:00
|
|
|
|
};
|
|
|
|
|
|
popups.value.push(popup);
|
|
|
|
|
|
isResizing.value.push(false);
|
2025-09-05 14:52:28 +08:00
|
|
|
|
console.log('打开弹窗ID', area.id, '初始尺寸:', popup.width, 'x', popup.height, '位置:', popup.top, popup.left);
|
2025-08-21 16:24:54 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2025-09-05 14:52:28 +08:00
|
|
|
|
// 关闭弹窗
|
2025-08-21 16:24:54 +08:00
|
|
|
|
const closePopup = (index) => {
|
|
|
|
|
|
popups.value.splice(index, 1);
|
|
|
|
|
|
isResizing.value.splice(index, 1);
|
2025-09-05 14:52:28 +08:00
|
|
|
|
console.log('关闭弹窗索引', index);
|
2025-08-21 16:24:54 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2025-09-05 14:52:28 +08:00
|
|
|
|
// 最小化弹窗
|
2025-08-21 16:24:54 +08:00
|
|
|
|
const minimizePopup = (index) => {
|
|
|
|
|
|
const popup = popups.value[index];
|
|
|
|
|
|
if (popup.isMaximized) {
|
2025-09-05 14:52:28 +08:00
|
|
|
|
popup.width = popup.originalWidth;
|
|
|
|
|
|
popup.height = popup.originalHeight;
|
|
|
|
|
|
popup.top = popup.originalTop;
|
|
|
|
|
|
popup.left = popup.originalLeft;
|
2025-08-21 16:24:54 +08:00
|
|
|
|
popup.isMaximized = false;
|
2025-09-05 14:52:28 +08:00
|
|
|
|
console.log('最小化弹窗', popup.id, '到尺寸:', popup.width, 'x', popup.height, '位置:', popup.top, popup.left);
|
2025-08-21 16:24:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-09-05 14:52:28 +08:00
|
|
|
|
// 最大化弹窗
|
2025-08-21 16:24:54 +08:00
|
|
|
|
const maximizePopup = (index) => {
|
|
|
|
|
|
const popup = popups.value[index];
|
|
|
|
|
|
if (!popup.isMaximized) {
|
2025-09-05 14:52:28 +08:00
|
|
|
|
popup.originalWidth = popup.width;
|
|
|
|
|
|
popup.originalHeight = popup.height;
|
|
|
|
|
|
popup.originalTop = popup.top;
|
|
|
|
|
|
popup.originalLeft = popup.left;
|
|
|
|
|
|
popup.width = window.innerWidth;
|
|
|
|
|
|
popup.height = window.innerHeight;
|
|
|
|
|
|
popup.top = '0px';
|
|
|
|
|
|
popup.left = '0px';
|
2025-08-21 16:24:54 +08:00
|
|
|
|
popup.isMaximized = true;
|
2025-09-05 14:52:28 +08:00
|
|
|
|
console.log('最大化弹窗', popup.id, '到尺寸:', popup.width, 'x', popup.height, '位置:', popup.top, popup.left);
|
2025-08-21 16:24:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-09-05 14:52:28 +08:00
|
|
|
|
// 拖动相关逻辑
|
2025-08-21 16:24:54 +08:00
|
|
|
|
const startDragging = (e, index) => {
|
|
|
|
|
|
isDragging.value = true;
|
|
|
|
|
|
dragIndex.value = index;
|
|
|
|
|
|
const popup = popups.value[index];
|
|
|
|
|
|
popup.startX = e.pageX - parseInt(popup.left);
|
|
|
|
|
|
popup.startY = e.pageY - parseInt(popup.top);
|
|
|
|
|
|
popup.zIndex = currentZIndex.value++;
|
|
|
|
|
|
document.addEventListener('mousemove', dragHandler);
|
|
|
|
|
|
document.addEventListener('mouseup', stopDragging);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const dragHandler = (e) => {
|
|
|
|
|
|
if (isDragging.value && dragIndex.value !== null) {
|
|
|
|
|
|
requestAnimationFrame(() => {
|
|
|
|
|
|
const popup = popups.value[dragIndex.value];
|
2025-09-05 14:52:28 +08:00
|
|
|
|
const newTop = Math.max(0, e.pageY - popup.startY);
|
|
|
|
|
|
const newLeft = e.pageX - popup.startX;
|
2025-08-21 16:24:54 +08:00
|
|
|
|
popup.top = `${newTop}px`;
|
|
|
|
|
|
popup.left = `${newLeft}px`;
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const stopDragging = () => {
|
|
|
|
|
|
isDragging.value = false;
|
|
|
|
|
|
dragIndex.value = null;
|
|
|
|
|
|
document.removeEventListener('mousemove', dragHandler);
|
|
|
|
|
|
document.removeEventListener('mouseup', stopDragging);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-09-05 14:52:28 +08:00
|
|
|
|
// 调整大小相关逻辑
|
2025-08-21 16:24:54 +08:00
|
|
|
|
const startResizing = (e, index, edge) => {
|
|
|
|
|
|
const popup = popups.value[index];
|
|
|
|
|
|
resizeStart.value = {
|
|
|
|
|
|
index,
|
|
|
|
|
|
startX: e.pageX,
|
|
|
|
|
|
startY: e.pageY,
|
|
|
|
|
|
originalWidth: popup.width,
|
|
|
|
|
|
originalHeight: popup.height,
|
|
|
|
|
|
originalTop: parseInt(popup.top) || 0,
|
|
|
|
|
|
originalLeft: parseInt(popup.left) || 0,
|
|
|
|
|
|
edge
|
|
|
|
|
|
};
|
|
|
|
|
|
isResizing.value[index] = true;
|
|
|
|
|
|
document.addEventListener('mousemove', handleResize);
|
|
|
|
|
|
document.addEventListener('mouseup', stopResizingGlobal);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleResize = (e) => {
|
|
|
|
|
|
if (resizeStart.value.index !== null) {
|
|
|
|
|
|
const index = resizeStart.value.index;
|
|
|
|
|
|
const popup = popups.value[index];
|
|
|
|
|
|
const diffX = e.pageX - resizeStart.value.startX;
|
|
|
|
|
|
const diffY = e.pageY - resizeStart.value.startY;
|
|
|
|
|
|
const minWidth = 200;
|
|
|
|
|
|
const minHeight = 150;
|
|
|
|
|
|
|
|
|
|
|
|
switch (resizeStart.value.edge) {
|
|
|
|
|
|
case 'top':
|
|
|
|
|
|
const newHeight = Math.max(minHeight, resizeStart.value.originalHeight - diffY);
|
|
|
|
|
|
popup.height = newHeight;
|
2025-09-05 14:52:28 +08:00
|
|
|
|
popup.top = `${Math.max(0, resizeStart.value.originalTop + (resizeStart.value.originalHeight - newHeight))}px`;
|
2025-08-21 16:24:54 +08:00
|
|
|
|
break;
|
|
|
|
|
|
case 'bottom':
|
|
|
|
|
|
popup.height = Math.max(minHeight, resizeStart.value.originalHeight + diffY);
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'left':
|
|
|
|
|
|
const newWidth = Math.max(minWidth, resizeStart.value.originalWidth - diffX);
|
|
|
|
|
|
popup.width = newWidth;
|
|
|
|
|
|
popup.left = `${resizeStart.value.originalLeft + (resizeStart.value.originalWidth - newWidth)}px`;
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'right':
|
|
|
|
|
|
popup.width = Math.max(minWidth, resizeStart.value.originalWidth + diffX);
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'top-left':
|
|
|
|
|
|
const newWidthTL = Math.max(minWidth, resizeStart.value.originalWidth - diffX);
|
|
|
|
|
|
const newHeightTL = Math.max(minHeight, resizeStart.value.originalHeight - diffY);
|
|
|
|
|
|
popup.width = newWidthTL;
|
|
|
|
|
|
popup.height = newHeightTL;
|
|
|
|
|
|
popup.left = `${resizeStart.value.originalLeft + (resizeStart.value.originalWidth - newWidthTL)}px`;
|
2025-09-05 14:52:28 +08:00
|
|
|
|
popup.top = `${Math.max(0, resizeStart.value.originalTop + (resizeStart.value.originalHeight - newHeightTL))}px`;
|
2025-08-21 16:24:54 +08:00
|
|
|
|
break;
|
|
|
|
|
|
case 'top-right':
|
|
|
|
|
|
const newWidthTR = Math.max(minWidth, resizeStart.value.originalWidth + diffX);
|
|
|
|
|
|
const newHeightTR = Math.max(minHeight, resizeStart.value.originalHeight - diffY);
|
|
|
|
|
|
popup.width = newWidthTR;
|
|
|
|
|
|
popup.height = newHeightTR;
|
2025-09-05 14:52:28 +08:00
|
|
|
|
popup.top = `${Math.max(0, resizeStart.value.originalTop + (resizeStart.value.originalHeight - newHeightTR))}px`;
|
2025-08-21 16:24:54 +08:00
|
|
|
|
break;
|
|
|
|
|
|
case 'bottom-left':
|
|
|
|
|
|
const newWidthBL = Math.max(minWidth, resizeStart.value.originalWidth - diffX);
|
|
|
|
|
|
const newHeightBL = Math.max(minHeight, resizeStart.value.originalHeight + diffY);
|
|
|
|
|
|
popup.width = newWidthBL;
|
|
|
|
|
|
popup.height = newHeightBL;
|
|
|
|
|
|
popup.left = `${resizeStart.value.originalLeft + (resizeStart.value.originalWidth - newWidthBL)}px`;
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'bottom-right':
|
|
|
|
|
|
popup.width = Math.max(minWidth, resizeStart.value.originalWidth + diffX);
|
|
|
|
|
|
popup.height = Math.max(minHeight, resizeStart.value.originalHeight + diffY);
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const stopResizingGlobal = () => {
|
|
|
|
|
|
if (resizeStart.value.index !== null) {
|
|
|
|
|
|
isResizing.value[resizeStart.value.index] = false;
|
|
|
|
|
|
resizeStart.value = { index: null, startX: 0, startY: 0, originalWidth: 0, originalHeight: 0, originalTop: 0, originalLeft: 0, edge: '' };
|
|
|
|
|
|
document.removeEventListener('mousemove', handleResize);
|
|
|
|
|
|
document.removeEventListener('mouseup', stopResizingGlobal);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-09-05 14:52:28 +08:00
|
|
|
|
// 防抖函数
|
|
|
|
|
|
const debounce = (fn, delay) => {
|
|
|
|
|
|
let timeout;
|
|
|
|
|
|
return (...args) => {
|
|
|
|
|
|
clearTimeout(timeout);
|
|
|
|
|
|
timeout = setTimeout(() => fn(...args), delay);
|
|
|
|
|
|
};
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 更新容器和图片尺寸
|
|
|
|
|
|
const updateDimensions = debounce(() => {
|
|
|
|
|
|
console.log('图片',robotBg.value.width,robotBg.value.offsetWidth, robotBg.value.offsetHeight)
|
|
|
|
|
|
if (robotBg.value && robotBg.value.parentElement) {
|
|
|
|
|
|
containerSize.value = {
|
|
|
|
|
|
width: robotBg.value.parentElement.offsetWidth - 40, // 左右padding 20*2
|
|
|
|
|
|
height: robotBg.value.parentElement.offsetHeight // 无垂直padding
|
|
|
|
|
|
};
|
|
|
|
|
|
console.log('更新容器尺寸:', containerSize.value.width, 'x', containerSize.value.height);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 100);
|
|
|
|
|
|
|
2025-08-21 16:24:54 +08:00
|
|
|
|
onMounted(() => {
|
|
|
|
|
|
const img = new Image();
|
|
|
|
|
|
img.src = robotImage;
|
|
|
|
|
|
img.onload = () => {
|
|
|
|
|
|
imageDimensions.value = { width: img.width, height: img.height };
|
2025-09-05 14:52:28 +08:00
|
|
|
|
console.log('图片尺寸:', imageDimensions.value.width, 'x', imageDimensions.value.height);
|
2025-08-21 16:24:54 +08:00
|
|
|
|
if (robotBg.value) {
|
2025-09-05 14:52:28 +08:00
|
|
|
|
const observer = new ResizeObserver(updateDimensions);
|
|
|
|
|
|
observer.observe(robotBg.value.parentElement);
|
|
|
|
|
|
watch(() => isTreeCollapsed.value, updateDimensions, { immediate: true });
|
|
|
|
|
|
updateDimensions(); // 初始更新
|
|
|
|
|
|
onUnmounted(() => observer.disconnect());
|
2025-08-21 16:24:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
|
|
|
document.removeEventListener('mousemove', handleResize);
|
|
|
|
|
|
document.removeEventListener('mouseup', stopResizingGlobal);
|
|
|
|
|
|
});
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
.robot-container {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
position: relative;
|
2025-09-05 14:52:28 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
padding: 0;
|
2025-08-21 16:24:54 +08:00
|
|
|
|
background-color: #fff;
|
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tree-panel {
|
2025-09-05 14:52:28 +08:00
|
|
|
|
flex-shrink: 0;
|
2025-08-21 16:24:54 +08:00
|
|
|
|
border-right: 1px solid #ddd;
|
2025-09-05 14:52:28 +08:00
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
height: auto;
|
|
|
|
|
|
transition: width 0.3s ease;
|
2025-08-21 16:24:54 +08:00
|
|
|
|
}
|
2025-09-05 14:52:28 +08:00
|
|
|
|
|
2025-08-21 16:24:54 +08:00
|
|
|
|
.tree-panel:deep(.el-tree) {
|
2025-09-05 14:52:28 +08:00
|
|
|
|
padding: 10px !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-21 16:24:54 +08:00
|
|
|
|
.robot-panel {
|
2025-09-05 14:52:28 +08:00
|
|
|
|
flex-grow: 1;
|
2025-08-21 16:24:54 +08:00
|
|
|
|
display: flex;
|
2025-09-05 14:52:28 +08:00
|
|
|
|
justify-content: center;
|
|
|
|
|
|
align-items: center;
|
2025-08-21 16:24:54 +08:00
|
|
|
|
overflow: hidden;
|
2025-09-05 14:52:28 +08:00
|
|
|
|
padding: 0 20px;
|
|
|
|
|
|
position: relative;
|
2025-08-21 16:24:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#robot-bg {
|
|
|
|
|
|
background-size: contain;
|
|
|
|
|
|
background-repeat: no-repeat;
|
|
|
|
|
|
background-position: center;
|
2025-09-05 14:52:28 +08:00
|
|
|
|
min-width: 0;
|
|
|
|
|
|
min-height: 0;
|
|
|
|
|
|
position: relative; /* 添加 relative 定位 */
|
2025-08-21 16:24:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.toggle-button {
|
|
|
|
|
|
position: absolute;
|
2025-09-05 14:52:28 +08:00
|
|
|
|
left: 20px;
|
|
|
|
|
|
top: 10px;
|
2025-08-21 16:24:54 +08:00
|
|
|
|
width: 40px;
|
|
|
|
|
|
height: 40px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
font-size: 24px;
|
|
|
|
|
|
background-color: #e0e0e0;
|
|
|
|
|
|
border-radius: 4px;
|
2025-09-05 14:52:28 +08:00
|
|
|
|
z-index: 2001;
|
2025-08-21 16:24:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.toggle-button:hover {
|
|
|
|
|
|
background-color: #d0d0d0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.clickable-area {
|
|
|
|
|
|
cursor: pointer;
|
2025-09-05 14:52:28 +08:00
|
|
|
|
position: absolute;
|
2025-08-21 16:24:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.clickable-text {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 50%;
|
|
|
|
|
|
left: 50%;
|
|
|
|
|
|
transform: translate(-50%, -50%);
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
text-shadow: 0 0 5px #000;
|
|
|
|
|
|
animation: blink 1.5s infinite;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@keyframes blink {
|
|
|
|
|
|
0% { opacity: 1; }
|
|
|
|
|
|
50% { opacity: 0; }
|
|
|
|
|
|
100% { opacity: 1; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.popup {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
background: white;
|
|
|
|
|
|
user-select: none;
|
|
|
|
|
|
overflow: auto;
|
|
|
|
|
|
box-sizing: border-box;
|
2025-09-05 14:52:28 +08:00
|
|
|
|
transition: width 0.2s, height 0.2s, top 0.2s, left 0.2s;
|
|
|
|
|
|
border: 1px solid #eee;
|
2025-08-21 16:24:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.popup.dragging {
|
2025-09-05 14:52:28 +08:00
|
|
|
|
transition: none;
|
2025-08-21 16:24:54 +08:00
|
|
|
|
opacity: 0.8;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.drag-handle {
|
2025-09-05 14:52:28 +08:00
|
|
|
|
height: 30px;
|
|
|
|
|
|
background: #e0e0e0;
|
|
|
|
|
|
cursor: move;
|
2025-08-21 16:24:54 +08:00
|
|
|
|
position: relative;
|
|
|
|
|
|
z-index: 1;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
2025-09-05 14:52:28 +08:00
|
|
|
|
justify-content: center;
|
2025-08-21 16:24:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.popup-title {
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
color: #333;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
left: 10px;
|
2025-09-05 14:52:28 +08:00
|
|
|
|
right: 60px;
|
|
|
|
|
|
text-align: center;
|
2025-08-21 16:24:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.popup-controls {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
right: 5px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 5px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.control-icon {
|
|
|
|
|
|
cursor: pointer;
|
2025-09-05 14:52:28 +08:00
|
|
|
|
font-size: 18px;
|
|
|
|
|
|
padding: 4px 8px;
|
2025-08-21 16:24:54 +08:00
|
|
|
|
background: #f0f0f0;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.control-icon:hover {
|
|
|
|
|
|
background: #d0d0d0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.close-icon {
|
|
|
|
|
|
color: #ff4444;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.close-icon:hover {
|
|
|
|
|
|
background: #ff6666;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.resize-edge {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
background: transparent;
|
2025-09-05 14:52:28 +08:00
|
|
|
|
z-index: 2;
|
2025-08-21 16:24:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-05 14:52:28 +08:00
|
|
|
|
.top { top: 0; left: 0; right: 0; height: 10px; cursor: ns-resize; }
|
2025-08-21 16:24:54 +08:00
|
|
|
|
.bottom { bottom: 0; left: 0; right: 0; height: 5px; cursor: ns-resize; }
|
|
|
|
|
|
.left { top: 0; bottom: 0; left: 0; width: 5px; cursor: ew-resize; }
|
|
|
|
|
|
.right { top: 0; bottom: 0; right: 0; width: 5px; cursor: ew-resize; }
|
|
|
|
|
|
|
|
|
|
|
|
.resize-corner {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
width: 10px;
|
|
|
|
|
|
height: 10px;
|
|
|
|
|
|
background: rgba(0, 0, 0, 0.1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.top-left { top: 0; left: 0; cursor: nwse-resize; }
|
|
|
|
|
|
.top-right { top: 0; right: 0; cursor: nesw-resize; }
|
|
|
|
|
|
.bottom-left { bottom: 0; left: 0; cursor: nesw-resize; }
|
|
|
|
|
|
.bottom-right { bottom: 0; right: 0; cursor: nwse-resize; }
|
|
|
|
|
|
|
|
|
|
|
|
.popup.resizing {
|
2025-09-05 14:52:28 +08:00
|
|
|
|
transition: none;
|
2025-08-21 16:24:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
</style>
|