CMVR-IOT-UI/src/views/device/robot/index.vue

533 lines
18 KiB
Vue
Raw Normal View History

<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">
<div id="robot-bg" ref="robotBg" :style="{ backgroundImage: `url(${robotImage})` }">
<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';
import image from '@/assets/images/robot.png';
// 组件映射
const componentsMap = {
HandControl,
CameraView,
MusicPlayer
};
const robotImage = image;
const robotBg = ref(null);
const isTreeCollapsed = ref(false); // 控制树形控件收起/展开
const popups = ref([]);
const isDragging = ref(false);
const isResizing = ref([]);
const currentZIndex = ref(2000); // 提高初始 z-index
const dragIndex = ref(null); // 跟踪当前拖动的索引
const resizeStart = ref({ index: null, startX: 0, startY: 0, originalWidth: 0, originalHeight: 0, originalTop: 0, originalLeft: 0, edge: '' });
// 树数据
const treeData = ref([
{
id: 'robot-node-config', // 添加唯一 ID 用于默认展开
label: '机器人节点配置',
children: [
{
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' }]
},
]
}
]);
const treeProps = {
children: 'children',
label: 'label'
};
const handleNodeClick = (data) => {
console.log('点击节点', data.label);
};
// 原始坐标和尺寸(基于图片真实尺寸),添加 label 用于显示文字
const originalAreas = ref([
{ id: 'hand-left', x: 50, y: 200, width: 50, height: 100, component: HandControl, label: '左手' },
{ id: 'hand-right', x: 400, y: 200, width: 50, height: 100, component: HandControl, label: '右手' },
{ id: 'camera', x: 225, y: 100, width: 50, height: 50, component: CameraView, label: '摄像头' },
{ id: 'mouth', x: 225, y: 300, width: 50, height: 50, component: MusicPlayer, label: '扬声器' }
]);
const imageDimensions = ref({ width: 0, height: 0 });
const adjustedAreas = computed(() => {
if (!imageDimensions.value.width || !imageDimensions.value.height || !robotBg.value) return [];
const containerWidth = robotBg.value.parentElement.offsetWidth - 40 - (isTreeCollapsed.value ? 0 : 240); // 减去外层 padding 和 tree-panel 宽度
console.log(containerWidth)
const containerHeight = robotBg.value.parentElement.offsetHeight - 40; // 减去外层 padding
const scaleX = containerWidth / imageDimensions.value.width;
const scaleY = containerHeight / imageDimensions.value.height;
const scale = Math.min(scaleX, scaleY); // 取最小缩放比例以适配
const offsetX = (containerWidth - (imageDimensions.value.width * scale)) / 2;
const offsetY = (containerHeight - (imageDimensions.value.height * scale)) / 2;
return originalAreas.value.map(area => ({
...area,
x: (area.x * scale) + offsetX,
y: (area.y * scale) + offsetY,
width: area.width * scale,
height: area.height * scale
}));
});
const openPopup = (area) => {
// 检查是否已存在相同 id 的窗口
const existingPopup = popups.value.find(p => p.id === area.id);
if (existingPopup) {
console.log('Popup with id', area.id, 'is already open');
return; // 如果已打开,阻止重复打开
}
const popup = {
id: area.id,
component: area.component, // 使用组件引用
top: area.y + 'px',
left: area.x + 'px',
zIndex: currentZIndex.value++,
width: 640, // 默认宽度
height: 480, // 默认高度
originalWidth: 640, // 初始化原始宽度
originalHeight: 480, // 初始化原始高度
originalTop: area.y + 'px', // 初始化原始顶部位置
originalLeft: area.x + 'px', // 初始化原始左侧位置
isMaximized: false,
props: { terminalId: 'your-terminal-id', cameraId: 'your-camera-id' } // 替换为实际值
};
popups.value.push(popup);
isResizing.value.push(false);
console.log('Opened popup with id', area.id, 'initial size:', popup.width, 'x', popup.height, 'position:', popup.top, popup.left);
};
const closePopup = (index) => {
popups.value.splice(index, 1);
isResizing.value.splice(index, 1);
console.log('Closed popup at index', index);
};
const minimizePopup = (index) => {
const popup = popups.value[index];
if (popup.isMaximized) {
popup.width = popup.originalWidth; // 恢复到原始宽度
popup.height = popup.originalHeight; // 恢复到原始高度
popup.top = popup.originalTop; // 恢复到原始顶部位置
popup.left = popup.originalLeft; // 恢复到原始左侧位置
popup.isMaximized = false;
console.log('Minimized popup', popup.id, 'to size:', popup.width, 'x', popup.height, 'position:', popup.top, popup.left);
}
};
const maximizePopup = (index) => {
const popup = popups.value[index];
if (!popup.isMaximized) {
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'; // 从左侧开始
popup.isMaximized = true;
console.log('Maximized popup', popup.id, 'to size:', popup.width, 'x', popup.height, 'position:', popup.top, popup.left);
}
};
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);
console.log('Start dragging', index, 'ClientY:', e.clientY, 'Handle height:', e.currentTarget.offsetHeight);
};
const dragHandler = (e) => {
if (isDragging.value && dragIndex.value !== null) {
requestAnimationFrame(() => {
const popup = popups.value[dragIndex.value];
const newTop = Math.max(0, e.pageY - popup.startY); // 限制顶部不小于 0
const newLeft = e.pageX - popup.startX; // 左侧无限制
popup.top = `${newTop}px`;
popup.left = `${newLeft}px`;
console.log('Dragging', dragIndex.value, 'New position:', popup.left, popup.top);
});
}
};
const stopDragging = () => {
isDragging.value = false;
dragIndex.value = null;
document.removeEventListener('mousemove', dragHandler);
document.removeEventListener('mouseup', stopDragging);
};
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);
console.log('Start resizing', edge);
};
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;
popup.top = `${Math.max(0, resizeStart.value.originalTop + (resizeStart.value.originalHeight - newHeight))}px`; // 限制顶部
console.log('Resizing top', 'newHeight:', newHeight, 'newTop:', popup.top);
break;
case 'bottom':
popup.height = Math.max(minHeight, resizeStart.value.originalHeight + diffY);
console.log('Resizing bottom', 'newHeight:', popup.height);
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`;
console.log('Resizing left', 'newWidth:', newWidth, 'newLeft:', popup.left);
break;
case 'right':
popup.width = Math.max(minWidth, resizeStart.value.originalWidth + diffX);
console.log('Resizing right', 'newWidth:', popup.width);
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`;
popup.top = `${Math.max(0, resizeStart.value.originalTop + (resizeStart.value.originalHeight - newHeightTL))}px`; // 限制顶部
console.log('Resizing top-left', 'newWidth:', newWidthTL, 'newHeight:', newHeightTL, 'newTop:', popup.top);
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;
popup.top = `${Math.max(0, resizeStart.value.originalTop + (resizeStart.value.originalHeight - newHeightTR))}px`; // 限制顶部
console.log('Resizing top-right', 'newWidth:', newWidthTR, 'newHeight:', newHeightTR, 'newTop:', popup.top);
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`;
console.log('Resizing bottom-left', 'newWidth:', newWidthBL, 'newHeight:', newHeightBL, 'newLeft:', popup.left);
break;
case 'bottom-right':
popup.width = Math.max(minWidth, resizeStart.value.originalWidth + diffX);
popup.height = Math.max(minHeight, resizeStart.value.originalHeight + diffY);
console.log('Resizing bottom-right', 'newWidth:', popup.width, 'newHeight:', popup.height);
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);
}
};
onMounted(() => {
const img = new Image();
img.src = robotImage;
img.onload = () => {
imageDimensions.value = { width: img.width, height: img.height };
if (robotBg.value) {
watch(() => [robotBg.value.parentElement.offsetWidth, robotBg.value.parentElement.offsetHeight, isTreeCollapsed.value], ([newWidth, newHeight]) => {
const containerWidth = newWidth - 40 - (isTreeCollapsed.value ? 0 : 240); // 减去外层 padding 和 tree-panel 宽度
const containerHeight = newHeight - 40; // 减去外层 padding
const scale = Math.min(containerWidth / img.width, containerHeight / img.height);
robotBg.value.style.minWidth = `${img.width * scale}px`;
robotBg.value.style.minHeight = `${img.height * scale}px`;
}, { immediate: true });
}
};
});
onUnmounted(() => {
document.removeEventListener('mousemove', handleResize);
document.removeEventListener('mouseup', stopResizingGlobal);
});
</script>
<style scoped>
.robot-container {
width: 100%;
position: relative;
display: flex; /* 保持与外层一致 */
overflow: hidden; /* 防止遮挡弹窗 */
padding: 0; /* 外层已处理 padding */
background-color: #fff;
border-radius: 10px;
}
.tree-panel {
flex-shrink: 0; /* 防止收缩 */
/* padding: 10px; */
border-right: 1px solid #ddd;
overflow-y: auto; /* 垂直滚动 */
height: auto; /* 动态高度 */
transition: width 0.3s ease; /* 平滑过渡 */
}
.tree-panel:deep(.el-tree) {
padding: 10px !important;
}
.robot-panel {
flex-grow: 1; /* 填满剩余空间 */
display: flex;
justify-content: center; /* 水平居中 */
align-items: center; /* 垂直居中 */
overflow: hidden;
padding: 0 20px; /* 补偿外层 padding */
position: relative; /* 确保按钮定位 */
}
#robot-bg {
background-size: contain;
background-repeat: no-repeat;
background-position: center;
min-width: 0; /* 防止收缩 */
min-height: 0; /* 防止收缩 */
}
.toggle-button {
position: absolute;
left: 20px; /* 贴近左边边 */
top: 10px; /* 顶部留点间距 */
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 24px;
background-color: #e0e0e0;
border-radius: 4px;
z-index: 2001; /* 确保高于其他元素 */
}
.toggle-button:hover {
background-color: #d0d0d0;
}
.clickable-area {
cursor: pointer;
position: relative;
}
.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;
transition: width 0.2s, height 0.2s, top 0.2s, left 0.2s; /* 平滑过渡 */
}
.popup.dragging {
transition: none; /* 拖动时禁用过渡 */
opacity: 0.8;
}
.drag-handle {
height: 30px; /* 增加高度以容纳按钮 */
background: #e0e0e0; /* 背景色 */
cursor: move; /* 拖动光标 */
position: relative;
z-index: 1;
display: flex;
align-items: center;
justify-content: center; /* 标题居中 */
}
.popup-title {
font-size: 16px;
font-weight: bold;
color: #333;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
position: absolute;
left: 10px;
right: 60px; /* 留出按钮空间 */
text-align: center; /* 居中文本 */
}
.popup-controls {
position: absolute;
right: 5px;
display: flex;
gap: 5px;
}
.control-icon {
cursor: pointer;
font-size: 18px; /* 放大按钮 */
padding: 4px 8px; /* 增加内边距 */
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;
z-index: 2; /* 提高拖动区域优先级 */
}
.top { top: 0; left: 0; right: 0; height: 10px; cursor: ns-resize; } /* 增加高度以覆盖 drag-handle */
.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 {
transition: none; /* 缩放时禁用过渡 */
}
</style>