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

875 lines
29 KiB
Vue
Raw Normal View History

<template>
<div id="robot-interface" class="robot-container">
<div class="tree-panel" :style="{ width: isTreeCollapsed ? '0px' : '240px' }">
2025-09-12 09:12:58 +08:00
<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' }">
2025-09-12 09:12:58 +08:00
<TechButtonWithLine
v-for="(area,index) in adjustedAreas"
:key="area.id"
2025-09-12 09:12:58 +08:00
: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)"
2025-09-12 09:12:58 +08:00
/>
</div>
<div class="toggle-button" @click="isTreeCollapsed = !isTreeCollapsed">
{{ isTreeCollapsed ? '>' : '<' }}
</div>
2025-09-12 09:12:58 +08:00
</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>
2025-09-12 09:12:58 +08:00
<!-- 添加你现有的配置编辑对话框 -->
<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>
2025-09-12 09:12:58 +08:00
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';
2025-09-12 09:12:58 +08:00
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';
2025-09-12 09:12:58 +08:00
// --- Existing Code ---
// 组件映射
const componentsMap = {
HandControl,
CameraView,
MusicPlayer,
2025-09-12 09:12:58 +08:00
MechanicalArm,
TechButtonWithLine
};
2025-09-12 09:12:58 +08:00
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([
2025-09-12 09:12:58 +08:00
{ 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([
]);
2025-09-12 09:12:58 +08:00
/**
* 将扁平化的节点数据转换为树形结构
* 假设 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);
};
2025-09-12 09:12:58 +08:00
// --- 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 [];
2025-09-12 09:12:58 +08:00
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;
2025-09-12 09:12:58 +08:00
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 };
2025-09-12 09:12:58 +08:00
// 计算中心对齐的偏移量
const offsetX = (robotPanelContentWidth - scaledWidth) / 2;
const offsetY = (robotPanelContentHeight - scaledHeight) / 2;
return originalAreas.value.map(area => ({
...area,
2025-09-12 09:12:58 +08:00
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, '的弹窗');
2025-09-12 09:12:58 +08:00
// 如果弹窗已存在,可以考虑将其置顶
existingPopup.zIndex = currentZIndex.value++;
return;
}
const treeWidth = isTreeCollapsed.value ? 0 : 240;
2025-09-12 09:12:58 +08:00
// 弹窗的 left 偏移需要考虑 tree-panel 的宽度,以及 robot-panel 的 padding
const leftOffset = treeWidth + 20; // tree宽度 + robot-panel padding-left
2025-09-12 09:12:58 +08:00
const popup = {
id: area.id,
component: area.component,
2025-09-12 09:12:58 +08:00
top: `${area.y}px`, // area.y 已经是居中后的 Y 坐标
left: `${area.x + leftOffset}px`, // area.x 已经是居中后的 X 坐标,再加上 treePanel宽度和robot-panel的padding
zIndex: currentZIndex.value++,
2025-09-12 09:12:58 +08:00
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;
2025-09-12 09:12:58 +08:00
// 最大化时,需要考虑右侧的区域,而不是整个窗口
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';
2025-09-12 09:12:58 +08:00
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) => {
2025-09-12 09:12:58 +08:00
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];
2025-09-12 09:12:58 +08:00
// 限制拖动范围,防止移出可视区域
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) => {
2025-09-12 09:12:58 +08:00
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;
2025-09-12 09:12:58 +08:00
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;
2025-09-12 09:12:58 +08:00
const treeWidth = isTreeCollapsed.value ? 0 : 240;
switch (resizeStart.value.edge) {
case 'top':
2025-09-12 09:12:58 +08:00
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':
2025-09-12 09:12:58 +08:00
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;
2025-09-12 09:12:58 +08:00
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;
2025-09-12 09:12:58 +08:00
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;
}
2025-09-12 09:12:58 +08:00
// 限制弹窗不超出容器
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
};
2025-09-12 09:12:58 +08:00
// console.log('更新容器尺寸:', containerSize.value.width, 'x', containerSize.value.height);
}
2025-09-12 09:12:58 +08:00
}, 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) {
2025-09-12 09:12:58 +08:00
// 监听 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);
});
}
}
};
2025-09-12 09:12:58 +08:00
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 = [];
// }
});
2025-09-12 09:12:58 +08:00
// onUnmounted 已经包含在 onMounted 的回调中
</script>
<style scoped>
.robot-container {
width: 100%;
position: relative;
display: flex;
overflow: hidden;
padding: 0;
background-color: #fff;
border-radius: 10px;
2025-09-12 09:12:58 +08:00
height: calc(100vh - 120px); /* 示例高度,请根据实际布局调整 */
box-sizing: border-box;
}
.tree-panel {
flex-shrink: 0;
border-right: 1px solid #ddd;
overflow-y: auto;
2025-09-12 09:12:58 +08:00
height: 100%; /* 填充父容器高度 */
transition: width 0.3s ease;
2025-09-12 09:12:58 +08:00
background-color: #f8f8f8; /* 示例背景色 */
}
.tree-panel:deep(.el-tree) {
padding: 10px !important;
2025-09-12 09:12:58 +08:00
background: transparent; /* 确保 treePanel 的背景色生效 */
}
.robot-panel {
flex-grow: 1;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
2025-09-12 09:12:58 +08:00
padding: 0 20px; /* 左右内边距 */
position: relative;
}
#robot-bg {
background-size: contain;
background-repeat: no-repeat;
background-position: center;
min-width: 0;
min-height: 0;
2025-09-12 09:12:58 +08:00
/* position: relative; 添加 relative 定位 */
/* 初始设置一个尺寸,或者由 JS 动态计算 */
width: 100%;
height: 100%;
/* box-sizing: border-box; 包含 padding */
}
.toggle-button {
position: absolute;
2025-09-12 09:12:58 +08:00
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;
2025-09-12 09:12:58 +08:00
box-shadow: 0 2px 4px rgba(0,0,0,.1);
}
.toggle-button:hover {
background-color: #d0d0d0;
}
.clickable-area {
cursor: pointer;
position: absolute;
2025-09-12 09:12:58 +08:00
/* 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;
2025-09-12 09:12:58 +08:00
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;
2025-09-12 09:12:58 +08:00
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;
2025-09-12 09:12:58 +08:00
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;
2025-09-12 09:12:58 +08:00
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;
2025-09-12 09:12:58 +08:00
right: 60px; /* 为右侧按钮留出空间 */
text-align: center;
2025-09-12 09:12:58 +08:00
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;
2025-09-12 09:12:58 +08:00
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;
}
2025-09-12 09:12:58 +08:00
.resize-edge, .resize-corner {
position: absolute;
background: transparent;
z-index: 2;
2025-09-12 09:12:58 +08:00
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 {
2025-09-12 09:12:58 +08:00
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;
}
2025-09-12 09:12:58 +08:00
/* 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>