CMVR-IOT-UI/src/views/device/robotManger/index.vue
lixiaolong 45b190ff02 feat(flow): 优化机器人设备绑定功能
- 将terminalId替换为robotId以统一机器人标识
- 实现机器人设备动态加载和选择功能
- 添加设备类型过滤和状态显示
- 重构设备注册页面为机器人设备管理界面
- 优化API调用参数传递方式
- 添加设备自动匹配和锁定机制
2026-08-05 09:27:55 +08:00

875 lines
29 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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-expand-all
@node-click="handleNodeClick"
@node-contextmenu="handleNodeContextMenu"
/>
<!-- 右键菜单组件 -->
<ContextMenu
:visible="contextMenu.visible"
:position="contextMenu.position"
:menu-items="contextMenu.items"
@close="contextMenu.visible = false"
@select="handleMenuItemSelect"
/>
</div>
<div class="robot-panel">
<div id="robot-bg" ref="robotBg" :style="{ backgroundImage: `url(${robotImage})`, width: bgSize.width + 'px', height: bgSize.height + 'px' }">
<TechButtonWithLine
v-for="(area,index) in adjustedAreas"
:key="area.id"
:total-buttons="5"
:button-index="area.index"
:button-position="area.position"
:end-x="area.x"
:end-y="area.y"
:button-name="area.label"
:button-width="120"
:button-height="50"
@click="openPopup(area)"
/>
</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" :initialDeviceId = "popup.deviceId" :initialRobotId = "popup.configId" />
<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>
<!-- 添加你现有的配置编辑对话框 -->
<el-dialog :title="dialogTitle" v-model="dialogFormVisible" width="500px" append-to-body>
<el-form ref="robotConfigFormRef" :model="dialogForm" :rules="dialogRules" label-width="80px">
<el-form-item label="配置类型" prop="configType">
<el-select v-model="dialogForm.configType" placeholder="请选择配置类型,区分是设备还是终端">
<el-option
v-for="dict in de_robot_config_type"
:key="dict.value"
:label="dict.label"
:value="dict.value"
></el-option>
</el-select>
</el-form-item>
<el-form-item label="配置ID" prop="configId">
<el-select v-model="dialogForm.configId" placeholder="请选择配置信息" v-if="dialogForm.configType == 1">
<el-option
v-for="dict in terminalConfigList"
:key="dict.id"
:label="dict.name"
:value="dict.id"
></el-option>
</el-select>
<el-select v-model="dialogForm.configId" placeholder="请选择配置信息" v-else>
<el-option
v-for="dict in registerList"
:key="dict.id"
:label="dict.deviceName || dict.deviceCode"
:value="dict.id"
></el-option>
</el-select>
</el-form-item>
<el-form-item label="节点名称" prop="nodeName">
<el-input v-model="dialogForm.nodeName" placeholder="请输入节点名称,名称可以随意取,不强制唯一" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitDialogForm">确 定</el-button>
<el-button @click="cancelDialog"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, onMounted, computed, onUnmounted, watch, nextTick } 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 MechanicalArm from '../register/components/MechanicalArm/index.vue';
import image from '@/assets/images/robot.png';
import { listRobot, getRobot, updateRobot } from '@/api/device/robot';
import { listTerminal } from '@/api/device/terminal'
import { listRegister } from '@/api/device/register'
import ContextMenu from './ContextMenu.vue'; // 导入右键菜单组件
import TechButtonWithLine from './TechButtonWithLine.vue';
// --- Existing Code ---
// 组件映射
const componentsMap = {
HandControl,
CameraView,
MusicPlayer,
MechanicalArm,
TechButtonWithLine
};
const { proxy } = getCurrentInstance()
const { de_robot_config_type } = proxy.useDict("de_robot_config_type")
const terminalConfigList = ref([])
const registerList = ref([])
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);
const dragIndex = ref(null);
const resizeStart = ref({ index: null, startX: 0, startY: 0, originalWidth: 0, originalHeight: 0, originalTop: 0, originalLeft: 0, edge: '' });
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: 'camera', index: 1, position: 'left', x: 250, y: 134, component: CameraView, label: '摄像头', deviceId: '', configId: '' },
{ id: 'mouth', index: 1, position: 'right', x: 299, y: 220, component: MusicPlayer, label: '扬声器', deviceId: '', configId: '' },
{ id: 'arm-left', index: 2, position: 'left', x: 100, y: 650, component: MechanicalArm, label: '左机械臂', deviceId: '', configId: '' },
{ id: 'arm-right', index: 2, position: 'right', x: 480, y: 650, component: MechanicalArm, label: '右机械臂', deviceId: '', configId: '' },
{ id: 'hand-left', index: 3, position: 'left', x: 75, y: 885, component: HandControl, label: '左手', deviceId: '', configId: '' },
{ id: 'hand-right', index: 3, position: 'right', x: 520, y: 885, component: HandControl, label: '右手', deviceId: '', configId: '' },
]);
// 树数据
const treeData = ref([
]);
/**
* 将扁平化的节点数据转换为树形结构。
* 假设 id === 'root' 的节点是顶级节点,并且保留所有原始数据。
*
* @param {Array<Object>} flatData - 扁平化的节点数据数组。
* @returns {Array<Object>} 转换后的树形结构数据。
*/
function convertFlatDataToTree(flatData) {
const nodeMap = new Map();
const rootNodes = [];
// 1. 将所有节点存入 Map并初始化 children 数组
flatData.forEach(item => {
// 复制 item避免直接修改原始数据并添加 children 属性
// 保留所有原始数据字段
nodeMap.set(item.id, { ...item, children: [] });
});
// 2. 遍历 Map根据 pid 构建树形结构
nodeMap.forEach(node => {
if (node.pid === 'root') {
// 如果 pid 是 'root',则该节点是顶级节点
rootNodes.push(node);
} else {
// 查找父节点
const parent = nodeMap.get(node.pid);
if (parent) {
// 将当前节点添加到父节点的 children 数组中
parent.children.push(node);
} else {
// 如果找不到父节点,可以选择抛出错误或忽略该节点
console.warn(`节点 ${node.id} 的父节点 ${node.pid} 不存在,该节点将被忽略。`);
}
}
});
// 3. 最终返回的就是识别出的顶级节点列表
// 这些顶级节点已经通过上面的逻辑构建了各自的子树
return rootNodes;
}
const treeProps = { children: 'children', label: 'nodeName' };
const getAll = () => {
listRobot().then(res => {
treeData.value = convertFlatDataToTree(res.data)
const configId = treeData.value[0].configId;
for (let item of treeData.value[0].children) {
const areaInfo = originalAreas.value.filter(area => area.label == item.nodeName)[0];
areaInfo.deviceId = item.configId;
areaInfo.configId = configId;
}
});
};
const handleNodeClick = (data) => {
console.log('点击节点', data.label);
};
// --- New Code for Right-Click Menu ---
const contextMenu = ref({
visible: false,
position: { x: 0, y: 0 },
items: [],
currentNode: null // 存储当前右键点击的节点
});
const handleNodeContextMenu = (event, data, node, component) => {
// 阻止默认右键菜单
event.preventDefault();
// 阻止事件冒泡到父元素(比如 el-tree 本身)
event.stopPropagation();
contextMenu.value.position = { x: event.clientX, y: event.clientY };
contextMenu.value.currentNode = data; // 存储当前节点
contextMenu.value.items = [
{ key: 'edit', label: '编辑' },
// 可以添加其他菜单项,例如 '添加子节点', '删除' 等
// { key: 'add', label: '添加子节点' },
// { key: 'delete', label: '删除' }
];
contextMenu.value.visible = true;
};
const handleMenuItemSelect = (item) => {
if (item.key === 'edit') {
openDialogForEdit(contextMenu.value.currentNode);
}
// 根据item.key处理其他菜单项
// else if (item.key === 'add') {
// console.log('添加子节点 to:', contextMenu.value.currentNode);
// // 实现添加子节点的逻辑
// }
// else if (item.key === 'delete') {
// console.log('删除节点:', contextMenu.value.currentNode);
// // 实现删除节点的逻辑
// }
};
// --- Integration with Existing Dialog ---
const dialogFormVisible = ref(false);
const dialogTitle = ref('');
const robotConfigFormRef = ref(null);
const dialogForm = ref({
configId: '',
configType: '',
pid: '',
nodeName: ''
});
const robotOptions = ref([]); // 用于el-tree-select的数据
const openDialogForEdit = (nodeData) => {
console.log('编辑节点:', nodeData.id);
dialogTitle.value = '编辑机器人配置';
getRobot(nodeData.id).then(res => {
dialogForm.value = {
...res.data,
};
dialogFormVisible.value = true;
// 确保 robotOptions 有数据,以便 tree-select 可以渲染
// 这里假设 treeData 结构能直接作为 robotOptions
robotOptions.value = treeData.value[0].children; // 假设第一个子节点是可选择的父节点,或根据实际情况调整
nextTick(() => {
robotConfigFormRef.value?.clearValidate(); // 清除之前的校验
});
})
};
// 提交对话框表单
const submitDialogForm = () => {
robotConfigFormRef.value.validate(async (valid) => {
if (valid) {
// 假设你有一个方法来更新 treeData
await updateRobot(dialogForm.value);
dialogFormVisible.value = false;
getAll();
// Optionally show a success message
ElMessage.success('配置更新成功');
} else {
console.log('表单校验失败');
}
});
};
// 取消对话框
const cancelDialog = () => {
dialogFormVisible.value = false;
};
// --- Existing Code (adjusted for clarity and potential issues) ---
// 计算调整后的区域
const adjustedAreas = computed(() => {
if (!imageDimensions.value.width || !imageDimensions.value.height || !robotBg.value) return [];
const treePanelWidth = isTreeCollapsed.value ? 0 : 240;
// 确保 robot-panel 内部的 content 区域是可用的
const contentContainerWidth = containerSize.value.width - treePanelWidth;
const containerHeight = containerSize.value.height;
// 仅当机器人背景图存在时,才计算缩放比例
if (!robotBg.value || !robotBg.value.parentElement) {
return [];
}
// 获取 robot-panel 的实际可用尺寸
const robotPanel = robotBg.value.parentElement;
const robotPanelRect = robotPanel.getBoundingClientRect();
const robotPanelContentWidth = robotPanelRect.width;
const robotPanelContentHeight = robotPanelRect.height;
const scaleX = robotPanelContentWidth / imageDimensions.value.width;
const scaleY = robotPanelContentHeight / imageDimensions.value.height;
const scale = Math.min(scaleX, scaleY, 1); // 防止放大如果图片比容器小就按100%显示
const scaledWidth = imageDimensions.value.width * scale;
const scaledHeight = imageDimensions.value.height * scale;
// 更新背景图片尺寸
bgSize.value = { width: scaledWidth, height: scaledHeight };
// 计算中心对齐的偏移量
const offsetX = (robotPanelContentWidth - scaledWidth) / 2;
const offsetY = (robotPanelContentHeight - scaledHeight) / 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
}));
});
// 打开弹窗考虑树宽度和padding偏移
const openPopup = (area) => {
const existingPopup = popups.value.find(p => p.id === area.id);
if (existingPopup) {
console.log('已打开ID为', area.id, '的弹窗');
// 如果弹窗已存在,可以考虑将其置顶
existingPopup.zIndex = currentZIndex.value++;
return;
}
const treeWidth = isTreeCollapsed.value ? 0 : 240;
// 弹窗的 left 偏移需要考虑 tree-panel 的宽度,以及 robot-panel 的 padding
const leftOffset = treeWidth + 20; // tree宽度 + robot-panel padding-left
const popup = {
id: area.id,
component: area.component,
top: `${area.y}px`, // area.y 已经是居中后的 Y 坐标
left: `${area.x + leftOffset}px`, // area.x 已经是居中后的 X 坐标,再加上 treePanel宽度和robot-panel的padding
zIndex: currentZIndex.value++,
deviceId: area.deviceId,
configId: area.configId,
width: 640,
height: 480,
originalWidth: 640,
originalHeight: 480,
originalTop: `${area.y}px`,
originalLeft: `${area.x + leftOffset}px`,
isMaximized: false,
props: { robotId: area.robotId || '', cameraId: area.deviceId || '' }
};
popups.value.push(popup);
isResizing.value.push(false);
console.log('打开弹窗ID', area.id, '初始尺寸:', popup.width, 'x', popup.height, '位置:', popup.top, popup.left);
};
// 关闭弹窗
const closePopup = (index) => {
popups.value.splice(index, 1);
isResizing.value.splice(index, 1);
console.log('关闭弹窗索引', 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('最小化弹窗', popup.id, '到尺寸:', popup.width, 'x', popup.height, '位置:', 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;
// 最大化时,需要考虑右侧的区域,而不是整个窗口
const treeWidth = isTreeCollapsed.value ? 0 : 240;
const availableWidth = window.innerWidth - treeWidth - 240;
const availableHeight = window.innerHeight - 120;
popup.width = availableWidth;
popup.height = availableHeight;
popup.top = '0px';
popup.left = `${treeWidth}px`; // 紧贴 tree panel
popup.isMaximized = true;
console.log('最大化弹窗', popup.id, '到尺寸:', popup.width, 'x', popup.height, '位置:', popup.top, popup.left);
}
};
// 拖动相关逻辑
const startDragging = (e, index) => {
e.stopPropagation(); // 阻止事件冒泡到其他可点击区域
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];
// 限制拖动范围,防止移出可视区域
const treeWidth = isTreeCollapsed.value ? 0 : 240;
const maxX = window.innerWidth - popup.width - treeWidth; // 考虑 tree panel
const maxY = window.innerHeight - popup.height;
let newTop = e.pageY - popup.startY;
let newLeft = e.pageX - popup.startX;
// 限制在可视窗口内
newTop = Math.max(0, newTop);
newLeft = Math.max(treeWidth, newLeft); // 限制左侧不能移入 tree panel 区域
newTop = Math.min(maxY, newTop);
newLeft = Math.min(maxX, newLeft);
popup.top = `${newTop}px`;
popup.left = `${newLeft}px`;
});
}
};
const stopDragging = () => {
isDragging.value = false;
dragIndex.value = null;
document.removeEventListener('mousemove', dragHandler);
document.removeEventListener('mouseup', stopDragging);
};
// 调整大小相关逻辑
const startResizing = (e, index, edge) => {
e.stopPropagation(); // 阻止事件冒泡
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;
popup.zIndex = currentZIndex.value++; // 调整大小的时候也提高 z-index
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;
const treeWidth = isTreeCollapsed.value ? 0 : 240;
switch (resizeStart.value.edge) {
case 'top':
const newHeightT = Math.max(minHeight, resizeStart.value.originalHeight - diffY);
popup.height = newHeightT;
popup.top = `${Math.max(0, resizeStart.value.originalTop + (resizeStart.value.originalHeight - newHeightT))}px`;
break;
case 'bottom':
popup.height = Math.max(minHeight, resizeStart.value.originalHeight + diffY);
break;
case 'left':
const newWidthL = Math.max(minWidth, resizeStart.value.originalWidth - diffX);
popup.width = newWidthL;
popup.left = `${Math.max(treeWidth, resizeStart.value.originalLeft + (resizeStart.value.originalWidth - newWidthL))}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 = `${Math.max(treeWidth, resizeStart.value.originalLeft + (resizeStart.value.originalWidth - newWidthTL))}px`;
popup.top = `${Math.max(0, resizeStart.value.originalTop + (resizeStart.value.originalHeight - newHeightTL))}px`;
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`;
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 = `${Math.max(treeWidth, 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 treePanelWidth = isTreeCollapsed.value ? 0 : 240;
const containerRect = robotBg.value.parentElement.getBoundingClientRect();
const maxLeft = containerRect.width - popup.width - treePanelWidth; // 考虑 robot-panel padding
const maxTop = containerRect.height - popup.height;
popup.left = `${Math.max(treePanelWidth, parseInt(popup.left))}px`; // 限制左侧不进入 tree panel
popup.top = `${Math.max(0, parseInt(popup.top))}px`; // 限制顶部
popup.left = `${Math.min(maxLeft + treePanelWidth, parseInt(popup.left))}px`; // 限制右侧
popup.top = `${Math.min(maxTop, parseInt(popup.top))}px`; // 限制底部
}
};
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);
}
};
// 防抖函数
const debounce = (fn, delay) => {
let timeout;
return (...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => fn(...args), delay);
};
};
// 更新容器和图片尺寸
const updateDimensions = debounce(() => {
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);
}
}, 50);
onMounted(() => {
const img = new Image();
img.src = robotImage;
img.onload = () => {
imageDimensions.value = { width: img.width, height: img.height };
console.log('图片尺寸:', imageDimensions.value.width, 'x', imageDimensions.value.height);
if (robotBg.value) {
// 监听 robot-panel 的父元素(通常是 #app 或 main-container的尺寸变化
const containerElement = robotBg.value.parentElement;
if (containerElement) {
const observer = new ResizeObserver(updateDimensions);
observer.observe(containerElement);
watch(() => isTreeCollapsed.value, updateDimensions, { immediate: true });
updateDimensions(); // 初始更新
// 在 unmounted 时断开观察
onUnmounted(() => {
observer.disconnect();
document.removeEventListener('mousemove', handleResize);
document.removeEventListener('mouseup', stopResizingGlobal);
document.removeEventListener('mousemove', dragHandler); // 确保拖动监听也被移除
document.removeEventListener('mouseup', stopDragging);
});
}
}
};
getAll();
listRegister().then((res) => {
registerList.value = res.rows;
});
listTerminal().then((res) => {
terminalConfigList.value = res.rows;
});
// 初始化 robotOptions
// const rootNode = treeData.value.find(item => item.id === 'robot-node-config');
// if (rootNode && rootNode.children) {
// robotOptions.value = rootNode.children.filter(node => node.pid === 'root'); // 假设 'root' 是顶级父节点的 pid
// } else {
// robotOptions.value = [];
// }
});
// onUnmounted 已经包含在 onMounted 的回调中
</script>
<style scoped>
.robot-container {
width: 100%;
position: relative;
display: flex;
overflow: hidden;
padding: 0;
background-color: #fff;
border-radius: 10px;
height: calc(100vh - 120px); /* 示例高度,请根据实际布局调整 */
box-sizing: border-box;
}
.tree-panel {
flex-shrink: 0;
border-right: 1px solid #ddd;
overflow-y: auto;
height: 100%; /* 填充父容器高度 */
transition: width 0.3s ease;
background-color: #f8f8f8; /* 示例背景色 */
}
.tree-panel:deep(.el-tree) {
padding: 10px !important;
background: transparent; /* 确保 treePanel 的背景色生效 */
}
.robot-panel {
flex-grow: 1;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
padding: 0 20px; /* 左右内边距 */
position: relative;
}
#robot-bg {
background-size: contain;
background-repeat: no-repeat;
background-position: center;
min-width: 0;
min-height: 0;
/* position: relative; 添加 relative 定位 */
/* 初始设置一个尺寸,或者由 JS 动态计算 */
width: 100%;
height: 100%;
/* box-sizing: border-box; 包含 padding */
}
.toggle-button {
position: absolute;
left: 20px; /* 紧贴 robot-panel 的左边距 */
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;
box-shadow: 0 2px 4px rgba(0,0,0,.1);
}
.toggle-button:hover {
background-color: #d0d0d0;
}
.clickable-area {
cursor: pointer;
position: absolute;
/* background: rgba(255, 0, 0, 0.2); */ /* 用于调试区域 */
box-sizing: border-box;
}
.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;
font-size: 14px;
font-weight: bold;
pointer-events: none; /* 防止文本本身捕获点击事件 */
}
@keyframes blink {
0% { opacity: 1; }
50% { opacity: 0; }
100% { opacity: 1; }
}
.popup {
display: flex;
flex-direction: column;
background: white;
user-select: none;
overflow: hidden; /* 整体 overflow hidden内容由子组件处理 */
box-sizing: border-box;
transition: width 0.2s, height 0.2s, top 0.2s, left 0.2s;
border: 1px solid #eee;
position: absolute; /* 确保 popup 是绝对定位 */
}
.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;
border-bottom: 1px solid #eee; /* 增加分隔线 */
}
.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;
user-select: none; /* 标题不可选 */
}
.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;
display: flex;
align-items: center;
justify-content: center;
user-select: none;
}
.control-icon:hover {
background: #d0d0d0;
}
.close-icon {
color: #ff4444;
}
.close-icon:hover {
background: #ff6666;
}
.resize-edge, .resize-corner {
position: absolute;
background: transparent;
z-index: 2;
box-sizing: border-box;
}
.top { top: 0; left: 0; right: 0; height: 10px; cursor: ns-resize; }
.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 {
width: 12px;
height: 12px;
background: rgba(0, 0, 0, 0.05); /* 增加一些可见性 */
border-radius: 2px;
}
.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;
}
/* dialog 样式,与你原有的保持一致 */
.dialog-footer {
display: flex;
justify-content: flex-end;
padding-top: 10px;
border-top: 1px solid #eee;
}
.el-dialog {
display: flex;
flex-direction: column;
margin: 0 !important;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.el-dialog .el-dialog__body {
flex: 1;
overflow: auto;
}
</style>