2025-08-21 16:24:54 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<div id="robot-interface" class="robot-container">
|
|
|
|
|
|
<div class="tree-panel" :style="{ width: isTreeCollapsed ? '0px' : '240px' }">
|
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"
|
|
|
|
|
|
/>
|
2025-08-21 16:24:54 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="robot-panel">
|
2025-09-05 14:52:28 +08:00
|
|
|
|
<div id="robot-bg" ref="robotBg" :style="{ backgroundImage: `url(${robotImage})`, width: bgSize.width + 'px', height: bgSize.height + 'px' }">
|
2025-09-12 09:12:58 +08:00
|
|
|
|
<TechButtonWithLine
|
|
|
|
|
|
v-for="(area,index) in adjustedAreas"
|
2025-08-21 16:24:54 +08:00
|
|
|
|
: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"
|
2025-08-21 16:24:54 +08:00
|
|
|
|
@click="openPopup(area)"
|
2025-09-12 09:12:58 +08:00
|
|
|
|
/>
|
2025-08-21 16:24:54 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="toggle-button" @click="isTreeCollapsed = !isTreeCollapsed">
|
|
|
|
|
|
{{ isTreeCollapsed ? '>' : '<' }}
|
|
|
|
|
|
</div>
|
2025-09-12 09:12:58 +08:00
|
|
|
|
|
2025-08-21 16:24:54 +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>
|
2026-08-05 09:27:55 +08:00
|
|
|
|
<component :is="popup.component" v-bind="popup.props" :initialDeviceId = "popup.deviceId" :initialRobotId = "popup.configId" />
|
2025-08-21 16:24:54 +08:00
|
|
|
|
<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>
|
2025-08-21 16:24:54 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
2025-09-12 09:12:58 +08:00
|
|
|
|
import { ref, onMounted, computed, onUnmounted, watch, nextTick } from 'vue';
|
2025-08-21 16:24:54 +08:00
|
|
|
|
import HandControl from '../register/components/DexHand/index.vue';
|
|
|
|
|
|
import CameraView from '../register/components/Camera/index.vue';
|
|
|
|
|
|
import MusicPlayer from '../register/components/Speaker/index.vue';
|
2025-09-05 14:52:28 +08:00
|
|
|
|
import MechanicalArm from '../register/components/MechanicalArm/index.vue';
|
2025-08-21 16:24:54 +08:00
|
|
|
|
import image from '@/assets/images/robot.png';
|
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-08-21 16:24:54 +08:00
|
|
|
|
|
2025-09-12 09:12:58 +08:00
|
|
|
|
|
|
|
|
|
|
// --- Existing Code ---
|
2025-08-21 16:24:54 +08:00
|
|
|
|
// 组件映射
|
|
|
|
|
|
const componentsMap = {
|
|
|
|
|
|
HandControl,
|
|
|
|
|
|
CameraView,
|
2025-09-05 14:52:28 +08:00
|
|
|
|
MusicPlayer,
|
2025-09-12 09:12:58 +08:00
|
|
|
|
MechanicalArm,
|
|
|
|
|
|
TechButtonWithLine
|
2025-08-21 16:24:54 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
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([])
|
|
|
|
|
|
|
2025-08-21 16:24:54 +08:00
|
|
|
|
const robotImage = image;
|
|
|
|
|
|
const robotBg = ref(null);
|
2025-09-05 14:52:28 +08:00
|
|
|
|
const isTreeCollapsed = ref(false);
|
2025-08-21 16:24:54 +08:00
|
|
|
|
const popups = ref([]);
|
|
|
|
|
|
const isDragging = ref(false);
|
|
|
|
|
|
const isResizing = ref([]);
|
2025-09-05 14:52:28 +08:00
|
|
|
|
const currentZIndex = ref(2000);
|
|
|
|
|
|
const dragIndex = ref(null);
|
2025-08-21 16:24:54 +08:00
|
|
|
|
const resizeStart = ref({ index: null, startX: 0, startY: 0, originalWidth: 0, originalHeight: 0, originalTop: 0, originalLeft: 0, edge: '' });
|
2025-09-05 14:52:28 +08:00
|
|
|
|
const imageDimensions = ref({ width: 0, height: 0 });
|
|
|
|
|
|
const containerSize = ref({ width: 0, height: 0 });
|
|
|
|
|
|
const bgSize = ref({ width: 0, height: 0 });
|
|
|
|
|
|
|
|
|
|
|
|
// 原始坐标和尺寸(基于图片原始大小)
|
|
|
|
|
|
const originalAreas = ref([
|
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: '' },
|
2025-09-05 14:52:28 +08:00
|
|
|
|
]);
|
2025-08-21 16:24:54 +08:00
|
|
|
|
|
|
|
|
|
|
// 树数据
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
2025-08-21 16:24:54 +08:00
|
|
|
|
|
|
|
|
|
|
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) ---
|
|
|
|
|
|
|
2025-09-05 14:52:28 +08:00
|
|
|
|
// 计算调整后的区域
|
2025-08-21 16:24:54 +08:00
|
|
|
|
const adjustedAreas = computed(() => {
|
|
|
|
|
|
if (!imageDimensions.value.width || !imageDimensions.value.height || !robotBg.value) return [];
|
2025-09-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-05 14:52:28 +08:00
|
|
|
|
|
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;
|
2025-09-05 14:52:28 +08:00
|
|
|
|
|
|
|
|
|
|
// 更新背景图片尺寸
|
|
|
|
|
|
bgSize.value = { width: scaledWidth, height: scaledHeight };
|
2025-08-21 16:24:54 +08:00
|
|
|
|
|
2025-09-12 09:12:58 +08:00
|
|
|
|
// 计算中心对齐的偏移量
|
|
|
|
|
|
const offsetX = (robotPanelContentWidth - scaledWidth) / 2;
|
|
|
|
|
|
const offsetY = (robotPanelContentHeight - scaledHeight) / 2;
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-08-21 16:24:54 +08:00
|
|
|
|
return originalAreas.value.map(area => ({
|
|
|
|
|
|
...area,
|
2025-09-12 09:12:58 +08:00
|
|
|
|
x: area.x * scale + offsetX, // 应用缩放和中心偏移
|
|
|
|
|
|
y: area.y * scale + offsetY, // 应用缩放和中心偏移
|
2025-08-21 16:24:54 +08:00
|
|
|
|
width: area.width * scale,
|
|
|
|
|
|
height: area.height * scale
|
|
|
|
|
|
}));
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-09-05 14:52:28 +08:00
|
|
|
|
// 打开弹窗,考虑树宽度和padding偏移
|
2025-08-21 16:24:54 +08:00
|
|
|
|
const openPopup = (area) => {
|
|
|
|
|
|
const existingPopup = popups.value.find(p => p.id === area.id);
|
|
|
|
|
|
if (existingPopup) {
|
2025-09-05 14:52:28 +08:00
|
|
|
|
console.log('已打开ID为', area.id, '的弹窗');
|
2025-09-12 09:12:58 +08:00
|
|
|
|
// 如果弹窗已存在,可以考虑将其置顶
|
|
|
|
|
|
existingPopup.zIndex = currentZIndex.value++;
|
2025-09-05 14:52:28 +08:00
|
|
|
|
return;
|
2025-08-21 16:24:54 +08:00
|
|
|
|
}
|
2025-09-05 14:52:28 +08:00
|
|
|
|
const treeWidth = isTreeCollapsed.value ? 0 : 240;
|
2025-09-12 09:12:58 +08:00
|
|
|
|
// 弹窗的 left 偏移需要考虑 tree-panel 的宽度,以及 robot-panel 的 padding
|
2025-09-05 14:52:28 +08:00
|
|
|
|
const leftOffset = treeWidth + 20; // tree宽度 + robot-panel padding-left
|
2025-09-12 09:12:58 +08:00
|
|
|
|
|
2025-09-05 14:52:28 +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
|
2025-08-21 16:24:54 +08:00
|
|
|
|
zIndex: currentZIndex.value++,
|
2025-09-12 09:12:58 +08:00
|
|
|
|
deviceId: area.deviceId,
|
|
|
|
|
|
configId: area.configId,
|
2025-09-05 14:52:28 +08:00
|
|
|
|
width: 640,
|
|
|
|
|
|
height: 480,
|
|
|
|
|
|
originalWidth: 640,
|
|
|
|
|
|
originalHeight: 480,
|
|
|
|
|
|
originalTop: `${area.y}px`,
|
|
|
|
|
|
originalLeft: `${area.x + leftOffset}px`,
|
2025-08-21 16:24:54 +08:00
|
|
|
|
isMaximized: false,
|
2026-08-05 09:27:55 +08:00
|
|
|
|
props: { robotId: area.robotId || '', cameraId: area.deviceId || '' }
|
2025-08-21 16:24:54 +08:00
|
|
|
|
};
|
|
|
|
|
|
popups.value.push(popup);
|
|
|
|
|
|
isResizing.value.push(false);
|
2025-09-05 14:52:28 +08:00
|
|
|
|
console.log('打开弹窗ID', area.id, '初始尺寸:', popup.width, 'x', popup.height, '位置:', popup.top, popup.left);
|
2025-08-21 16:24:54 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2025-09-05 14:52:28 +08:00
|
|
|
|
// 关闭弹窗
|
2025-08-21 16:24:54 +08:00
|
|
|
|
const closePopup = (index) => {
|
|
|
|
|
|
popups.value.splice(index, 1);
|
|
|
|
|
|
isResizing.value.splice(index, 1);
|
2025-09-05 14:52:28 +08:00
|
|
|
|
console.log('关闭弹窗索引', index);
|
2025-08-21 16:24:54 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2025-09-05 14:52:28 +08:00
|
|
|
|
// 最小化弹窗
|
2025-08-21 16:24:54 +08:00
|
|
|
|
const minimizePopup = (index) => {
|
|
|
|
|
|
const popup = popups.value[index];
|
|
|
|
|
|
if (popup.isMaximized) {
|
2025-09-05 14:52:28 +08:00
|
|
|
|
popup.width = popup.originalWidth;
|
|
|
|
|
|
popup.height = popup.originalHeight;
|
|
|
|
|
|
popup.top = popup.originalTop;
|
|
|
|
|
|
popup.left = popup.originalLeft;
|
2025-08-21 16:24:54 +08:00
|
|
|
|
popup.isMaximized = false;
|
2025-09-05 14:52:28 +08:00
|
|
|
|
console.log('最小化弹窗', popup.id, '到尺寸:', popup.width, 'x', popup.height, '位置:', popup.top, popup.left);
|
2025-08-21 16:24:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-09-05 14:52:28 +08:00
|
|
|
|
// 最大化弹窗
|
2025-08-21 16:24:54 +08:00
|
|
|
|
const maximizePopup = (index) => {
|
|
|
|
|
|
const popup = popups.value[index];
|
|
|
|
|
|
if (!popup.isMaximized) {
|
2025-09-05 14:52:28 +08:00
|
|
|
|
popup.originalWidth = popup.width;
|
|
|
|
|
|
popup.originalHeight = popup.height;
|
|
|
|
|
|
popup.originalTop = popup.top;
|
|
|
|
|
|
popup.originalLeft = popup.left;
|
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;
|
2025-09-05 14:52:28 +08:00
|
|
|
|
popup.top = '0px';
|
2025-09-12 09:12:58 +08:00
|
|
|
|
popup.left = `${treeWidth}px`; // 紧贴 tree panel
|
2025-08-21 16:24:54 +08:00
|
|
|
|
popup.isMaximized = true;
|
2025-09-05 14:52:28 +08:00
|
|
|
|
console.log('最大化弹窗', popup.id, '到尺寸:', popup.width, 'x', popup.height, '位置:', popup.top, popup.left);
|
2025-08-21 16:24:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-09-05 14:52:28 +08:00
|
|
|
|
// 拖动相关逻辑
|
2025-08-21 16:24:54 +08:00
|
|
|
|
const startDragging = (e, index) => {
|
2025-09-12 09:12:58 +08:00
|
|
|
|
e.stopPropagation(); // 阻止事件冒泡到其他可点击区域
|
2025-08-21 16:24:54 +08:00
|
|
|
|
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);
|
|
|
|
|
|
|
2025-08-21 16:24:54 +08:00
|
|
|
|
popup.top = `${newTop}px`;
|
|
|
|
|
|
popup.left = `${newLeft}px`;
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const stopDragging = () => {
|
|
|
|
|
|
isDragging.value = false;
|
|
|
|
|
|
dragIndex.value = null;
|
|
|
|
|
|
document.removeEventListener('mousemove', dragHandler);
|
|
|
|
|
|
document.removeEventListener('mouseup', stopDragging);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-09-05 14:52:28 +08:00
|
|
|
|
// 调整大小相关逻辑
|
2025-08-21 16:24:54 +08:00
|
|
|
|
const startResizing = (e, index, edge) => {
|
2025-09-12 09:12:58 +08:00
|
|
|
|
e.stopPropagation(); // 阻止事件冒泡
|
2025-08-21 16:24:54 +08:00
|
|
|
|
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
|
2025-08-21 16:24:54 +08:00
|
|
|
|
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;
|
|
|
|
|
|
|
2025-08-21 16:24:54 +08:00
|
|
|
|
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`;
|
2025-08-21 16:24:54 +08:00
|
|
|
|
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`;
|
2025-08-21 16:24:54 +08:00
|
|
|
|
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`;
|
2025-09-05 14:52:28 +08:00
|
|
|
|
popup.top = `${Math.max(0, resizeStart.value.originalTop + (resizeStart.value.originalHeight - newHeightTL))}px`;
|
2025-08-21 16:24:54 +08:00
|
|
|
|
break;
|
|
|
|
|
|
case 'top-right':
|
|
|
|
|
|
const newWidthTR = Math.max(minWidth, resizeStart.value.originalWidth + diffX);
|
|
|
|
|
|
const newHeightTR = Math.max(minHeight, resizeStart.value.originalHeight - diffY);
|
|
|
|
|
|
popup.width = newWidthTR;
|
|
|
|
|
|
popup.height = newHeightTR;
|
2025-09-05 14:52:28 +08:00
|
|
|
|
popup.top = `${Math.max(0, resizeStart.value.originalTop + (resizeStart.value.originalHeight - newHeightTR))}px`;
|
2025-08-21 16:24:54 +08:00
|
|
|
|
break;
|
|
|
|
|
|
case 'bottom-left':
|
|
|
|
|
|
const newWidthBL = Math.max(minWidth, resizeStart.value.originalWidth - diffX);
|
|
|
|
|
|
const newHeightBL = Math.max(minHeight, resizeStart.value.originalHeight + diffY);
|
|
|
|
|
|
popup.width = newWidthBL;
|
|
|
|
|
|
popup.height = newHeightBL;
|
2025-09-12 09:12:58 +08:00
|
|
|
|
popup.left = `${Math.max(treeWidth, resizeStart.value.originalLeft + (resizeStart.value.originalWidth - newWidthBL))}px`;
|
2025-08-21 16:24:54 +08:00
|
|
|
|
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`; // 限制底部
|
2025-08-21 16:24:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const stopResizingGlobal = () => {
|
|
|
|
|
|
if (resizeStart.value.index !== null) {
|
|
|
|
|
|
isResizing.value[resizeStart.value.index] = false;
|
|
|
|
|
|
resizeStart.value = { index: null, startX: 0, startY: 0, originalWidth: 0, originalHeight: 0, originalTop: 0, originalLeft: 0, edge: '' };
|
|
|
|
|
|
document.removeEventListener('mousemove', handleResize);
|
|
|
|
|
|
document.removeEventListener('mouseup', stopResizingGlobal);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-09-05 14:52:28 +08:00
|
|
|
|
// 防抖函数
|
|
|
|
|
|
const debounce = (fn, delay) => {
|
|
|
|
|
|
let timeout;
|
|
|
|
|
|
return (...args) => {
|
|
|
|
|
|
clearTimeout(timeout);
|
|
|
|
|
|
timeout = setTimeout(() => fn(...args), delay);
|
|
|
|
|
|
};
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 更新容器和图片尺寸
|
|
|
|
|
|
const updateDimensions = debounce(() => {
|
|
|
|
|
|
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-05 14:52:28 +08:00
|
|
|
|
}
|
2025-09-12 09:12:58 +08:00
|
|
|
|
}, 50);
|
2025-09-05 14:52:28 +08:00
|
|
|
|
|
2025-08-21 16:24:54 +08:00
|
|
|
|
onMounted(() => {
|
|
|
|
|
|
const img = new Image();
|
|
|
|
|
|
img.src = robotImage;
|
|
|
|
|
|
img.onload = () => {
|
|
|
|
|
|
imageDimensions.value = { width: img.width, height: img.height };
|
2025-09-05 14:52:28 +08:00
|
|
|
|
console.log('图片尺寸:', imageDimensions.value.width, 'x', imageDimensions.value.height);
|
2025-08-21 16:24:54 +08:00
|
|
|
|
if (robotBg.value) {
|
2025-09-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-08-21 16:24:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
};
|
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-08-21 16:24:54 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2025-09-12 09:12:58 +08:00
|
|
|
|
// onUnmounted 已经包含在 onMounted 的回调中
|
2025-08-21 16:24:54 +08:00
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
.robot-container {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
position: relative;
|
2025-09-05 14:52:28 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
padding: 0;
|
2025-08-21 16:24:54 +08:00
|
|
|
|
background-color: #fff;
|
|
|
|
|
|
border-radius: 10px;
|
2025-09-12 09:12:58 +08:00
|
|
|
|
height: calc(100vh - 120px); /* 示例高度,请根据实际布局调整 */
|
|
|
|
|
|
box-sizing: border-box;
|
2025-08-21 16:24:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tree-panel {
|
2025-09-05 14:52:28 +08:00
|
|
|
|
flex-shrink: 0;
|
2025-08-21 16:24:54 +08:00
|
|
|
|
border-right: 1px solid #ddd;
|
2025-09-05 14:52:28 +08:00
|
|
|
|
overflow-y: auto;
|
2025-09-12 09:12:58 +08:00
|
|
|
|
height: 100%; /* 填充父容器高度 */
|
2025-09-05 14:52:28 +08:00
|
|
|
|
transition: width 0.3s ease;
|
2025-09-12 09:12:58 +08:00
|
|
|
|
background-color: #f8f8f8; /* 示例背景色 */
|
2025-08-21 16:24:54 +08:00
|
|
|
|
}
|
2025-09-05 14:52:28 +08:00
|
|
|
|
|
2025-08-21 16:24:54 +08:00
|
|
|
|
.tree-panel:deep(.el-tree) {
|
2025-09-05 14:52:28 +08:00
|
|
|
|
padding: 10px !important;
|
2025-09-12 09:12:58 +08:00
|
|
|
|
background: transparent; /* 确保 treePanel 的背景色生效 */
|
2025-09-05 14:52:28 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-21 16:24:54 +08:00
|
|
|
|
.robot-panel {
|
2025-09-05 14:52:28 +08:00
|
|
|
|
flex-grow: 1;
|
2025-08-21 16:24:54 +08:00
|
|
|
|
display: flex;
|
2025-09-05 14:52:28 +08:00
|
|
|
|
justify-content: center;
|
|
|
|
|
|
align-items: center;
|
2025-08-21 16:24:54 +08:00
|
|
|
|
overflow: hidden;
|
2025-09-12 09:12:58 +08:00
|
|
|
|
padding: 0 20px; /* 左右内边距 */
|
2025-09-05 14:52:28 +08:00
|
|
|
|
position: relative;
|
2025-08-21 16:24:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#robot-bg {
|
|
|
|
|
|
background-size: contain;
|
|
|
|
|
|
background-repeat: no-repeat;
|
|
|
|
|
|
background-position: center;
|
2025-09-05 14:52:28 +08:00
|
|
|
|
min-width: 0;
|
|
|
|
|
|
min-height: 0;
|
2025-09-12 09:12:58 +08:00
|
|
|
|
/* position: relative; 添加 relative 定位 */
|
|
|
|
|
|
/* 初始设置一个尺寸,或者由 JS 动态计算 */
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
/* box-sizing: border-box; 包含 padding */
|
2025-08-21 16:24:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.toggle-button {
|
|
|
|
|
|
position: absolute;
|
2025-09-12 09:12:58 +08:00
|
|
|
|
left: 20px; /* 紧贴 robot-panel 的左边距 */
|
2025-09-05 14:52:28 +08:00
|
|
|
|
top: 10px;
|
2025-08-21 16:24:54 +08:00
|
|
|
|
width: 40px;
|
|
|
|
|
|
height: 40px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
font-size: 24px;
|
|
|
|
|
|
background-color: #e0e0e0;
|
|
|
|
|
|
border-radius: 4px;
|
2025-09-05 14:52:28 +08:00
|
|
|
|
z-index: 2001;
|
2025-09-12 09:12:58 +08:00
|
|
|
|
box-shadow: 0 2px 4px rgba(0,0,0,.1);
|
2025-08-21 16:24:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.toggle-button:hover {
|
|
|
|
|
|
background-color: #d0d0d0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.clickable-area {
|
|
|
|
|
|
cursor: pointer;
|
2025-09-05 14:52:28 +08:00
|
|
|
|
position: absolute;
|
2025-09-12 09:12:58 +08:00
|
|
|
|
/* background: rgba(255, 0, 0, 0.2); */ /* 用于调试区域 */
|
|
|
|
|
|
box-sizing: border-box;
|
2025-08-21 16:24:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.clickable-text {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 50%;
|
|
|
|
|
|
left: 50%;
|
|
|
|
|
|
transform: translate(-50%, -50%);
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
text-shadow: 0 0 5px #000;
|
|
|
|
|
|
animation: blink 1.5s infinite;
|
2025-09-12 09:12:58 +08:00
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
pointer-events: none; /* 防止文本本身捕获点击事件 */
|
2025-08-21 16:24:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@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,内容由子组件处理 */
|
2025-08-21 16:24:54 +08:00
|
|
|
|
box-sizing: border-box;
|
2025-09-05 14:52:28 +08:00
|
|
|
|
transition: width 0.2s, height 0.2s, top 0.2s, left 0.2s;
|
|
|
|
|
|
border: 1px solid #eee;
|
2025-09-12 09:12:58 +08:00
|
|
|
|
position: absolute; /* 确保 popup 是绝对定位 */
|
2025-08-21 16:24:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.popup.dragging {
|
2025-09-05 14:52:28 +08:00
|
|
|
|
transition: none;
|
2025-08-21 16:24:54 +08:00
|
|
|
|
opacity: 0.8;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.drag-handle {
|
2025-09-05 14:52:28 +08:00
|
|
|
|
height: 30px;
|
|
|
|
|
|
background: #e0e0e0;
|
|
|
|
|
|
cursor: move;
|
2025-08-21 16:24:54 +08:00
|
|
|
|
position: relative;
|
|
|
|
|
|
z-index: 1;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
2025-09-05 14:52:28 +08:00
|
|
|
|
justify-content: center;
|
2025-09-12 09:12:58 +08:00
|
|
|
|
border-bottom: 1px solid #eee; /* 增加分隔线 */
|
2025-08-21 16:24:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.popup-title {
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
color: #333;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
left: 10px;
|
2025-09-12 09:12:58 +08:00
|
|
|
|
right: 60px; /* 为右侧按钮留出空间 */
|
2025-09-05 14:52:28 +08:00
|
|
|
|
text-align: center;
|
2025-09-12 09:12:58 +08:00
|
|
|
|
user-select: none; /* 标题不可选 */
|
2025-08-21 16:24:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.popup-controls {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
right: 5px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 5px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.control-icon {
|
|
|
|
|
|
cursor: pointer;
|
2025-09-05 14:52:28 +08:00
|
|
|
|
font-size: 18px;
|
|
|
|
|
|
padding: 4px 8px;
|
2025-08-21 16:24:54 +08:00
|
|
|
|
background: #f0f0f0;
|
|
|
|
|
|
border-radius: 4px;
|
2025-09-12 09:12:58 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
user-select: none;
|
2025-08-21 16:24:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.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 {
|
2025-08-21 16:24:54 +08:00
|
|
|
|
position: absolute;
|
|
|
|
|
|
background: transparent;
|
2025-09-05 14:52:28 +08:00
|
|
|
|
z-index: 2;
|
2025-09-12 09:12:58 +08:00
|
|
|
|
box-sizing: border-box;
|
2025-08-21 16:24:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-05 14:52:28 +08:00
|
|
|
|
.top { top: 0; left: 0; right: 0; height: 10px; cursor: ns-resize; }
|
2025-08-21 16:24:54 +08:00
|
|
|
|
.bottom { bottom: 0; left: 0; right: 0; height: 5px; cursor: ns-resize; }
|
|
|
|
|
|
.left { top: 0; bottom: 0; left: 0; width: 5px; cursor: ew-resize; }
|
|
|
|
|
|
.right { top: 0; bottom: 0; right: 0; width: 5px; cursor: ew-resize; }
|
|
|
|
|
|
|
|
|
|
|
|
.resize-corner {
|
2025-09-12 09:12:58 +08:00
|
|
|
|
width: 12px;
|
|
|
|
|
|
height: 12px;
|
|
|
|
|
|
background: rgba(0, 0, 0, 0.05); /* 增加一些可见性 */
|
|
|
|
|
|
border-radius: 2px;
|
2025-08-21 16:24:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.top-left { top: 0; left: 0; cursor: nwse-resize; }
|
|
|
|
|
|
.top-right { top: 0; right: 0; cursor: nesw-resize; }
|
|
|
|
|
|
.bottom-left { bottom: 0; left: 0; cursor: nesw-resize; }
|
|
|
|
|
|
.bottom-right { bottom: 0; right: 0; cursor: nwse-resize; }
|
|
|
|
|
|
|
|
|
|
|
|
.popup.resizing {
|
2025-09-05 14:52:28 +08:00
|
|
|
|
transition: none;
|
2025-08-21 16:24:54 +08:00
|
|
|
|
}
|
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;
|
|
|
|
|
|
}
|
2026-08-05 09:27:55 +08:00
|
|
|
|
</style>
|