- 将terminalId替换为robotId以统一机器人标识 - 实现机器人设备动态加载和选择功能 - 添加设备类型过滤和状态显示 - 重构设备注册页面为机器人设备管理界面 - 优化API调用参数传递方式 - 添加设备自动匹配和锁定机制
875 lines
29 KiB
Vue
875 lines
29 KiB
Vue
<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>
|