This commit is contained in:
zhanghao 2025-09-19 14:26:36 +08:00
commit e988f6a183
9 changed files with 1254 additions and 98 deletions

44
src/api/device/robot.js Normal file
View File

@ -0,0 +1,44 @@
import request from '@/utils/request'
// 查询机器人配置列表
export function listRobot(query) {
return request({
url: '/device/robot/list',
method: 'get',
params: query
})
}
// 查询机器人配置详细
export function getRobot(id) {
return request({
url: '/device/robot/' + id,
method: 'get'
})
}
// 新增机器人配置
export function addRobot(data) {
return request({
url: '/device/robot',
method: 'post',
data: data
})
}
// 修改机器人配置
export function updateRobot(data) {
return request({
url: '/device/robot',
method: 'put',
data: data
})
}
// 删除机器人配置
export function delRobot(id) {
return request({
url: '/device/robot/' + id,
method: 'delete'
})
}

View File

@ -17,14 +17,14 @@
<div v-if="form.stereoModule && form.rgbCamera" class="stream-container">
<div class="stream-item">
<div class="stream-wrapper">
<video ref="depthVideo" width="640" height="480" autoplay muted playsinline></video>
<video ref="depthVideoBuffer" width="640" height="480" autoplay muted playsinline style="display: none;"></video>
<video ref="depthVideo" autoplay muted playsinline></video>
<video ref="depthVideoBuffer" autoplay muted playsinline style="display: none;"></video>
</div>
</div>
<div class="stream-item">
<div class="stream-wrapper">
<video ref="colorVideo" width="640" height="480" autoplay muted playsinline></video>
<video ref="colorVideoBuffer" width="640" height="480" autoplay muted playsinline style="display: none;"></video>
<video ref="colorVideo" autoplay muted playsinline></video>
<video ref="colorVideoBuffer" autoplay muted playsinline style="display: none;"></video>
</div>
</div>
</div>
@ -52,6 +52,18 @@ import { getRegister } from "@/api/device/register";
import { useRoute } from "vue-router";
import { debounce } from 'lodash'; // lodash
// 1. props
const props = defineProps({
initialDeviceId: { // propcameraId
type: String,
default: null,
},
initialTerminalId: { // prop terminalId
type: String,
default: null,
},
});
const socket = inject('ws');
const route = useRoute();
const data = reactive({
@ -189,11 +201,36 @@ watch(() => data.form.rgbCamera, async (newVal) => {
subscribeDebounced(newVal, data.terminalId, 'getRGBImageStream');
});
// 2. 使 watchEffect props
watchEffect(() => {
let deviceId = null;
// 使 props
if (props.initialDeviceId) {
deviceId = props.initialDeviceId;
}
// props cameraIdterminalId
//
// route.path.split("/")[3] cameraId
if (!props.initialDeviceId && route.path.split("/")[3]) {
deviceId = route.path.split("/")[3];
// getRegister cameraId
// getRegister
}
getRegister(deviceId).then(res => {
data.cameraId = res.data.deviceCode;
data.terminalId = res.data.idDeDeviceTerminalConfig;
}).catch(error => {
console.error("Error fetching register from route:", error);
});
});
onMounted(() => {
getRegister(route.path.split("/")[3]).then(res => {
data.cameraId = res.data.deviceCode;
data.terminalId = res.data.idDeDeviceTerminalConfig;
});
// getRegister(route.path.split("/")[3]).then(res => {
// data.cameraId = res.data.deviceCode;
// data.terminalId = res.data.idDeDeviceTerminalConfig;
// });
});
onUnmounted(() => {

View File

@ -17,12 +17,30 @@ import { inject } from 'vue';
const emit = defineEmits(['update:bottomSeriesData']);
const socket = inject('ws');
const handCanvas = ref(null);
const terminalId = ref('25449ff3fcc7b27da5f69462c5efcec2'); //
const deviceId = ref('hand1'); //
const terminalId = ref(''); //
const deviceId = ref(''); //
const frameData = ref({ count: 0, lastTime: 0 });
const isHandSeries = ref(false);
const containerRef = ref(null)
// 1. props
const props = defineProps({
initialDeviceId: { // propcameraId
type: String,
default: null,
},
initialTerminalId: { // prop terminalId
type: String,
default: null,
},
});
// 2. 使 watchEffect props
watchEffect(() => {
terminalId.value = props.initialTerminalId;
deviceId.value = props.initialDeviceId;
});
//
const rectangle = [
{ x: 24, y: 305, width: 21, height: 20, xCount: 3, yCount: 3 }, // PINKY.TIP

View File

@ -35,6 +35,26 @@ const handImage = ref(null);
const containerRef = ref(null); //
const defaultHandImageInfo = { width: 891, height: 981 };
const imageAspectRatio = defaultHandImageInfo.width / defaultHandImageInfo.height;
const terminalId = ref('');
const deviceId = ref('');
// 1. props
const props = defineProps({
initialDeviceId: { // propcameraId
type: String,
default: null,
},
initialTerminalId: { // prop terminalId
type: String,
default: null,
},
});
// 2. 使 watchEffect props
watchEffect(() => {
terminalId.value = props.initialTerminalId;
deviceId.value = props.initialDeviceId;
console.log(deviceId.value, terminalId.value,props)
});
const sliders = ref([
{ value: 0, defalutHeight: 466, height: 0, x: 120, y: 140, vertical: true, style: {} }, //
@ -46,8 +66,9 @@ const sliders = ref([
]);
function updateSeriesData(value, i) {
console.log(deviceId.value, terminalId.value)
loading.value = true;
setDexHandAngle({ deviceId: 'hand1', terminalId: '25449ff3fcc7b27da5f69462c5efcec2', value: value / 100, id: i }).then(() => {
setDexHandAngle({ deviceId: deviceId.value, terminalId: terminalId.value, value: value / 100, id: i }).then(() => {
loading.value = false;
updateToSeriesData();
}).catch(() => {
@ -110,7 +131,7 @@ const updateSliderPositions = () => {
};
const updateToSeriesData = () => {
status({ deviceId: 'hand1', terminalId: '25449ff3fcc7b27da5f69462c5efcec2' }).then((res) => {
status({ deviceId: deviceId.value, terminalId: terminalId.value }).then((res) => {
const newTopSeriesData = res.data.handsList.map(item => item.force);
emit('update:topSeriesData', newTopSeriesData);
});

View File

@ -3,10 +3,12 @@
<div class="container">
<div class="left-panel">
<div class="hand-container">
<LeftTopHand @update:topSeriesData="handleUpdateTopSeriesData" />
<!-- 确保这里使用的是 index.vue 中响应式的 deviceId ref -->
<LeftTopHand :initialDeviceId="deviceId" :initialTerminalId="terminalId" @update:topSeriesData="handleUpdateTopSeriesData" />
</div>
<div class="hand-container">
<LeftBottomHand @update:bottomSeriesData="handleUpdateBottomSeriesData" />
<!-- 确保这里使用的是 index.vue 中响应式的 terminalId ref -->
<LeftBottomHand :initialDeviceId="deviceId" :initialTerminalId="terminalId" @update:bottomSeriesData="handleUpdateBottomSeriesData" />
</div>
</div>
<div class="right-panel">
@ -17,12 +19,28 @@
</template>
<script setup>
import { ref } from 'vue';
import { ref, watch } from 'vue'; // watch
// router
import { useRoute } from 'vue-router'; // vue-router
import RightTopCharts from './RightTopCharts.vue';
import LeftTopHand from './LeftTopHand.vue';
import LeftBottomHand from './LeftBottomHand.vue';
import RightBottomCharts from './RightBottomCharts.vue';
import { getRegister } from "@/api/device/register";
// 1. props
const props = defineProps({
initialDeviceId: {
type: String,
default: null,
},
initialTerminalId: {
type: String,
default: null,
},
});
//
const topSeriesData = ref([0, 0, 0, 0, 0, 0]);
const bottomSeriesData = ref([0, 0, 0, 0, 0, 0]);
const maxData = ref({
@ -42,6 +60,10 @@ const avgData = ref({
palm: { touch: 0 }
});
// 使
const deviceId = ref("");
const terminalId = ref("");
const handleUpdateTopSeriesData = (newData) => {
topSeriesData.value = newData;
};
@ -50,6 +72,55 @@ const handleUpdateBottomSeriesData = ({ maxData: newMaxData, avgData: newAvgData
maxData.value = newMaxData;
avgData.value = newAvgData;
};
//
const route = useRoute();
// register
const fetchRegisterData = (currentDeviceId) => {
if (currentDeviceId) {
getRegister(currentDeviceId).then(res => {
deviceId.value = res.data.deviceCode; // API
terminalId.value = res.data.idDeDeviceTerminalConfig;
}).catch(error => {
console.error("Error fetching register:", error);
// deviceId terminalId
deviceId.value = "";
terminalId.value = "";
});
} else {
// deviceId terminalId
terminalId.value = "";
}
};
// 2. 使 watch props
// initialDeviceId prop deviceId ref
watch(() => props.initialDeviceId, (newVal) => {
if (newVal) {
deviceId.value = newVal;
// props.initialDeviceId getRegister
fetchRegisterData(deviceId.value);
}
}, { immediate: true }); // immediate: true watch
// props.initialDeviceId
// route.path.split("/")[3] cameraId
//
watch(() => route.path, (newPath) => {
const routeDeviceId = newPath.split("/")[3];
if (!deviceId.value && routeDeviceId) { // deviceId props
deviceId.value = routeDeviceId;
fetchRegisterData(deviceId.value);
}
}, { immediate: true }); // immediate: true
// initialTerminalId prop terminalId ref watch
watch(() => props.initialTerminalId, (newVal) => {
if (newVal) {
terminalId.value = newVal;
}
}, { immediate: true });
</script>
<style scoped>

View File

@ -0,0 +1,89 @@
<template>
<div
v-if="visible"
class="context-menu"
:style="{ top: position.y + 'px', left: position.x + 'px' }"
>
<div
v-for="item in menuItems"
:key="item.key"
class="menu-item"
@click="handleClick(item)"
>
{{ item.label }}
</div>
</div>
</template>
<script setup>
import { ref, watch, onMounted, onUnmounted } from 'vue';
const props = defineProps({
visible: {
type: Boolean,
default: false
},
position: {
type: Object,
default: () => ({ x: 0, y: 0 })
},
menuItems: {
type: Array,
default: () => []
}
});
const emit = defineEmits(['close', 'select']);
watch(() => props.visible, (newVal) => {
if (newVal) {
document.addEventListener('click', closeMenu);
} else {
document.removeEventListener('click', closeMenu);
}
});
const handleClick = (item) => {
emit('select', item);
closeMenu();
};
const closeMenu = () => {
emit('close');
};
onMounted(() => {
// visibletrue
if (props.visible) {
document.addEventListener('click', closeMenu);
}
});
onUnmounted(() => {
document.removeEventListener('click', closeMenu);
});
</script>
<style scoped>
.context-menu {
position: fixed; /* 使用 fixed 定位,不受父容器 scroll 影响 */
z-index: 9999;
background-color: #fff;
border: 1px solid #eee;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
border-radius: 4px;
padding: 5px 0;
}
.menu-item {
padding: 8px 15px;
cursor: pointer;
font-size: 14px;
color: #606266;
white-space: nowrap;
}
.menu-item:hover {
background-color: #f5f7fa;
}
</style>

View File

@ -0,0 +1,547 @@
<template>
<div
class="tech-button-with-line"
:style="buttonStyle"
ref="techButtonContainer"
@mouseover="isHovering = true"
@mouseleave="isHovering = false"
>
<!-- 新增一个包裹层用于应用 clip-path -->
<div class="button-shape-clipper" :style="clipperStyle">
<div class="button-content-wrapper">
<slot name="button-content">
<span class="button-text">{{ buttonName }}</span>
</slot>
</div>
</div>
<!-- SVG 部分保持原有结构但内部样式和滤镜会改变 -->
<svg
v-if="svgReady"
class="line-svg"
:width="svgWidth"
:height="svgHeight"
:style="{
top: `0px`,
left: `0px`,
}"
>
<defs>
<!-- 霓虹光效滤镜 -->
<!-- stdDeviation 控制模糊程度值越大越模糊光效越柔和 -->
<filter id="neon-filter" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur in="SourceGraphic" :stdDeviation="props.neonGlowIntensity" result="blur"/>
<feMerge>
<feMergeNode in="blur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
<!-- 扫描线效果渐变 -->
<!-- 动态调整扫描线颜色 -->
<linearGradient id="scan-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color:rgba(255,255,255,0);stop-opacity:1" />
<stop offset="50%" :style="`stop-color:${props.lineHoverColor};stop-opacity:0.8`" />
<stop offset="100%" style="stop-color:rgba(255,255,255,0);stop-opacity:1" />
</linearGradient>
</defs>
<!-- 主线条应用霓虹光效和扫描线 -->
<polyline
:points="linePoints"
stroke="url(#scan-gradient)"
:stroke-width="props.lineStrokeWidth"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
filter="url(#neon-filter)"
class="main-line"
/>
<!-- 可选一条细微的基线用于对比或增加层次 -->
<polyline
:points="baseLinePoints"
:stroke="props.lineColor"
stroke-width="1"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
class="base-line"
/>
</svg>
</div>
</template>
<script setup>
import { ref, computed, onMounted, nextTick, watch, defineProps, onUnmounted } from 'vue';
// --- Props ---
// props props
const props = defineProps({
totalButtons: { type: Number, required: true, default: 1 },
buttonIndex: { type: Number, required: true },
buttonPosition: { type: String, required: true, validator: (value) => ['left', 'right'].includes(value) },
buttonOffsetX: { type: Number, default: 350 },
endX: { type: Number, required: true },
endY: { type: Number, required: true },
buttonName: { type: String, default: '科技按钮' },
buttonWidth: { type: [Number, String], default: 150 },
buttonHeight: { type: [Number, String], default: 40 },
linePadding: { type: Number, default: 20 },
// --- Props ---
buttonGradient: { type: Array, default: () => ['#00f0ff', '#00d0ff'] }, //
buttonHoverGradient: { type: Array, default: () => ['#00d0ff', '#00f0ff'] }, //
buttonBorderColor: { type: String, default: 'rgba(0, 255, 255, 0.6)' },
lineColor: { type: String, default: 'rgba(0, 150, 200, 0.4)' }, // 线
lineHoverColor: { type: String, default: 'rgba(0, 255, 255, 1)' }, // 线
lineStrokeWidth: { type: Number, default: 3 },
neonGlowIntensity: { type: Number, default: 3 }, //
scanLineSpeed: { type: Number, default: 8 }, // 线
animationDuration: { type: Number, default: 0.4 }, //
hoverScale: { type: Number, default: 1.03 }, //
// --- New Props for Flowing Light Effect ---
flowLineColor: { type: String, default: 'rgba(0, 255, 255, 0.8)' }, //
flowLineAnimationSpeed: { type: Number, default: 7 }, //
});
// --- Refs ---
const techButtonContainer = ref(null);
const svgReady = ref(false);
const parentContainerRef = ref(null);
const parentHeight = ref(0);
const parentWidth = ref(0);
const svgWidth = ref(0);
const svgHeight = ref(0);
const svgLeftOffset = ref(0);
const svgTopOffset = ref(0);
const isHovering = ref(false); //
// --- Computed Properties ---
// 线 ()
const autoCenterX = computed(() => parentWidth.value / 2);
// X ()
const calculatedButtonX = computed(() => {
if (!parentWidth.value) return 0;
if (props.buttonPosition === 'left') {
return autoCenterX.value - props.buttonOffsetX;
} else if (props.buttonPosition === 'right') {
return autoCenterX.value + props.buttonOffsetX;
}
return 0;
});
// Y ()
const calculatedButtonY = computed(() => {
if (!parentHeight.value || props.totalButtons <= 0) return 0;
const buttonSlotHeight = parentHeight.value / props.totalButtons;
const centerY = (props.buttonIndex - 1) * buttonSlotHeight + buttonSlotHeight / 2;
return centerY;
});
// ()
const actualButtonWidth = computed(() => {
if (techButtonContainer.value) {
return techButtonContainer.value.offsetWidth;
}
return typeof props.buttonWidth === 'number' ? props.buttonWidth : 150;
});
const actualButtonHeight = computed(() => {
if (techButtonContainer.value) {
return techButtonContainer.value.offsetHeight;
}
return typeof props.buttonHeight === 'number' ? props.buttonHeight : 40;
});
// 线 (SVG ) ()
const relativeEndX = computed(() => props.endX - calculatedButtonX.value + actualButtonWidth.value / 2);
const relativeEndY = computed(() => props.endY - calculatedButtonY.value + actualButtonHeight.value / 2);
//
const buttonStyle = computed(() => {
const buttonWidthPx = typeof props.buttonWidth === 'number' ? `${props.buttonWidth}px` : props.buttonWidth;
const buttonHeightPx = typeof props.buttonHeight === 'number' ? `${props.buttonHeight}px` : props.buttonHeight;
const currentGradient = isHovering.value ? props.buttonHoverGradient : props.buttonGradient;
const style = {
position: 'absolute',
left: `${calculatedButtonX.value}px`,
top: `${calculatedButtonY.value}px`,
transform: 'translate(-50%, -50%)', //
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
boxSizing: 'border-box',
width: buttonWidthPx,
height: buttonHeightPx,
borderRadius: '15px', //
//
background: `linear-gradient(135deg, ${currentGradient[0]}, ${currentGradient[1]})`,
// /
boxShadow: `
inset 0 0 0 2px ${props.buttonBorderColor}, /* 内部边框 */
0 0 5px rgba(0, 150, 200, 0.5), /* 较弱的主光晕 */
0 0 12px rgba(0, 200, 255, 0.6), /* 第二层光晕 */
0 3px 10px rgba(0, 0, 0, 0.4) /* 底部阴影 */
`,
color: '#ffffff',
fontSize: '14px',
fontWeight: 'bold', //
letterSpacing: '0.7px',
textShadow: '0 0 5px rgba(0, 150, 200, 0.9)', //
transition: `all ${props.animationDuration}s ease-in-out`,
overflow: 'visible', //
zIndex: 1,
};
if (isHovering.value) {
style.transform = `translate(-50%, -50%) scale(${props.hoverScale})`;
style.boxShadow = `
inset 0 0 0 3px ${props.buttonBorderColor}, /* 悬停时内部边框变宽 */
0 0 8px rgba(0, 200, 255, 0.7),
0 0 18px rgba(0, 230, 255, 0.8),
0 5px 15px rgba(0, 0, 0, 0.5) /* 底部阴影增强 */
`;
}
return style;
});
//
const clipperStyle = computed(() => {
const currentGradient = isHovering.value ? props.buttonHoverGradient : props.buttonGradient;
const style = {
position: 'relative', // techButtonContainer
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
boxSizing: 'border-box',
borderRadius: '0px', // **clipper clip-path **
background: `linear-gradient(135deg, ${currentGradient[0]}, ${currentGradient[1]})`,
boxShadow: `
inset 0 0 0 2px ${props.buttonBorderColor}, /* 内部边框 */
0 0 5px rgba(0, 150, 200, 0.5), /* 较弱的主光晕 */
0 0 12px rgba(0, 200, 255, 0.6), /* 第二层光晕 */
0 3px 10px rgba(0, 0, 0, 0.4) /* 底部阴影 */
`,
color: '#ffffff',
fontSize: '14px',
fontWeight: 'bold',
letterSpacing: '0.7px',
textShadow: '0 0 5px rgba(0, 150, 200, 0.9)', //
transition: `all ${props.animationDuration}s ease-in-out`,
overflow: 'hidden', // **clipper clip-path **
clipPath: `polygon(
6px 0, /* 左上角内缩进5px */
calc(100% - 6px) 0, /* 右上角内缩进5px */
100% 6px, /* 右上角外延5px */
100% calc(100% - 6px), /* 右下角内缩进5px */
calc(100% - 6px) 100%,
6px 100%, /* 左下角内缩进5px */
0 calc(100% - 6px),
0 6px, /* 左下角外延5px */
6px 0px /* 左上角内缩进5px重复起点闭合多边形 */
)`,
zIndex: 1, // clipper
};
if (isHovering.value) {
style.boxShadow = `
inset 0 0 0 3px ${props.buttonBorderColor}, /* 悬停时内部边框变宽 */
0 0 8px rgba(0, 200, 255, 0.7),
0 0 18px rgba(0, 230, 255, 0.8),
0 5px 15px rgba(0, 0, 0, 0.5) /* 底部阴影增强 */
`;
}
return style;
});
// 线 ( SVG (0,0) )
// --- ---
const linePoints = computed(() => {
if (!svgReady.value) return '';
const startPointX = (props.buttonPosition === 'left' ? props.buttonWidth : 0);
const startPointY = props.buttonHeight/2;
const endPointX = relativeEndX.value;
const endPointY = relativeEndY.value;
// L
const middlePointY = endPointY;
// X 使 Y
// +10 -10
const adjustedMiddlePointX = startPointX + (props.buttonPosition === 'left' ? 100 : -100);
return `${startPointX},${startPointY} ${adjustedMiddlePointX},${middlePointY} ${endPointX},${endPointY}`;
});
// 线 ()
const baseLinePoints = computed(() => {
if (!svgReady.value) return '';
const startPointX = (props.buttonPosition === 'left' ? props.buttonWidth : 0);
const startPointY = props.buttonHeight/2;
const endPointX = relativeEndX.value;
const endPointY = relativeEndY.value;
const adjustedMiddlePointX = startPointX + (props.buttonPosition === 'left' ? 100 : -100);
return `${startPointX},${startPointY} ${adjustedMiddlePointX},${endPointY} ${endPointX},${endPointY}`;
});
// --- Methods --- ()
const updateSvgDimensions = async () => {
if (!techButtonContainer.value) {
svgReady.value = false;
return;
}
await nextTick();
if (!parentContainerRef.value || parentContainerRef.value.offsetHeight === 0 || parentContainerRef.value.offsetWidth === 0) {
svgReady.value = false;
return;
}
parentHeight.value = parentContainerRef.value.offsetHeight;
parentWidth.value = parentContainerRef.value.offsetWidth;
if (parentHeight.value <= 0 || parentWidth.value <= 0 || props.totalButtons <= 0) {
svgReady.value = false;
return;
}
let minX = Math.min(0, 0, relativeEndX.value);
let maxX = Math.max(0, 0, relativeEndX.value);
let minY = Math.min(0, relativeEndY.value, relativeEndY.value);
let maxY = Math.max(0, relativeEndY.value, relativeEndY.value);
minX -= props.linePadding;
maxX += props.linePadding;
minY -= props.linePadding;
maxY += props.linePadding;
svgWidth.value = maxX - minX;
svgHeight.value = maxY - minY;
svgLeftOffset.value = minX;
svgTopOffset.value = minY;
svgReady.value = true;
};
// --- Resize Observer --- ()
let resizeObserver = null;
const setupResizeObserver = () => {
if (resizeObserver) {
resizeObserver.disconnect();
}
if (parentContainerRef.value) {
resizeObserver = new ResizeObserver(() => {
const newHeight = parentContainerRef.value.offsetHeight;
const newWidth = parentContainerRef.value.offsetWidth;
if (newHeight !== parentHeight.value || newWidth !== parentWidth.value) {
parentHeight.value = newHeight;
parentWidth.value = newWidth;
debouncedUpdate(updateSvgDimensions)();
}
});
resizeObserver.observe(parentContainerRef.value);
} else {
console.error("TechButtonWithLine: Could not find parent element to observe.");
}
};
// --- Lifecycle Hooks --- ()
onMounted(() => {
if (techButtonContainer.value && techButtonContainer.value.parentElement) {
parentContainerRef.value = techButtonContainer.value.parentElement.parentElement;
setupResizeObserver();
} else {
console.error("TechButtonWithLine: Could not find techButtonContainer or its parent.");
}
updateSvgDimensions();
});
onUnmounted(() => {
if (resizeObserver) {
resizeObserver.disconnect();
}
});
// --- Watchers --- ()
const debouncedUpdate = (fn, delay = 10) => {
let timer = null;
return () => {
clearTimeout(timer);
timer = setTimeout(fn, delay);
};
};
watch(
[
() => parentHeight.value,
() => parentWidth.value,
() => props.totalButtons,
() => props.buttonIndex,
() => props.buttonPosition,
() => props.endX,
() => props.endY,
() => props.buttonOffsetX,
() => props.linePadding,
() => props.buttonWidth,
() => props.buttonHeight,
// props
() => props.buttonGradient,
() => props.buttonHoverGradient,
() => props.lineColor,
() => props.lineHoverColor,
() => props.lineStrokeWidth,
() => props.neonGlowIntensity,
() => props.scanLineSpeed,
() => props.animationDuration,
() => props.hoverScale,
() => props.flowLineColor,
() => props.flowLineAnimationSpeed,
],
debouncedUpdate(updateSvgDimensions),
{ immediate: true, deep: true }
);
</script>
<style scoped>
/* --- Base Styles for the Button Container --- */
.tech-button-with-line {
position: absolute;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
overflow: visible; /* 允许 SVG 滤镜和动画效果溢出 */
z-index: 1;
border-radius: 8px; /* 科技感圆角 */
/* 基础过渡效果,用于缩放和阴影 */
/* transition: transform v-bind('`${props.animationDuration}s`') ease-in-out, box-shadow v-bind('`${props.animationDuration}s`') ease-in-out, background v-bind('`${props.animationDuration}s`') ease-in-out; */
}
.button-shape-clipper {
position: relative; /* 相对于 techButtonContainer以填充其空间 */
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
border-radius: 0px; /* **clipper 的形状由 clip-path 定义** */
overflow: hidden; /* **clipper 内部的内容会被 clip-path 裁剪** */
transition: all v-bind('`${props.animationDuration}s`') ease-in-out; /* 过渡背景和阴影 */
z-index: 1; /* 确保 clipper 在流光之上 */
}
.button-content-wrapper {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
pointer-events: none; /* 让鼠标事件穿透到按钮本身 */
}
.button-text {
font-family: 'Orbitron', sans-serif; /* 科技感字体 */
font-weight: bold;
text-transform: uppercase;
letter-spacing: 1px;
/* color: v-bind('props.buttonBorderColor'); 文本颜色与边框颜色呼应 */
color: #fff;
}
/* --- SVG Line Styles --- */
.line-svg {
position: absolute;
pointer-events: none; /* SVG 不捕获鼠标事件 */
overflow: visible; /* 允许滤镜和渐变效果溢出 SVG bounds */
transition: all v-bind('`${props.animationDuration}s`') ease-in-out;
z-index: 0; /* 放在按钮后面 */
}
/* 主线条的动画 */
.main-line {
animation: scanline v-bind('`${props.scanLineSpeed}s`') linear infinite;
/* 调整 stroke-dasharray 来控制扫描线的长度和间隔 */
stroke-dasharray: 15px, 80px; /* 15px 显示, 100px 隐藏 */
stroke-dashoffset: 0; /* 初始位置 */
}
/* 鼠标悬停时,主线条颜色变亮,动画速度可能加快 */
.tech-button-with-line:hover .main-line {
stroke: v-bind('props.lineHoverColor'); /* 悬停时线条颜色 */
animation-duration: v-bind('`${props.scanLineSpeed / 1.5}s`'); /* 悬停时速度加快 */
stroke-dasharray: 20px, 120px; /* 悬停时线条显示部分变长 */
}
/* 细微基线样式 */
.base-line {
opacity: 0.6;
transition: all v-bind('`${props.animationDuration}s`') ease-in-out;
}
.tech-button-with-line:hover .base-line {
opacity: 0.9; /* 悬停时基线也变亮 */
}
/* --- Flowing Light Border Effect --- */
/* 使用 ::before 伪元素实现四周的流光 */
.tech-button-with-line::before {
content: '';
position: absolute;
top: -4px; /* 稍向外延伸 */
left: -4px;
right: -4px;
bottom: -4px;
/* 关键:创建动态的流光渐变 */
/* 渐变方向是从左到右,颜色从透明到高亮再到透明 */
background: linear-gradient(
90deg,
transparent,
v-bind('props.flowLineColor'),
transparent
);
border-radius: 0px; /* **与按钮一致的平直四角** */
opacity: 0.8; /* 初始可见度 */
filter: blur(5px); /* 轻微模糊,模拟光晕 */
z-index: -1; /* 放在按钮内容后面 */
animation: flowLight v-bind('`${props.flowLineAnimationSpeed}s`') linear infinite;
}
/* 悬停时,流光效果可以增强 */
.tech-button-with-line:hover::before {
opacity: 1;
filter: blur(8px); /* 悬停时模糊度增加 */
}
/* --- Keyframes for Animations --- */
@keyframes scanline {
0% {
stroke-dashoffset: 0;
}
100% {
stroke-dashoffset: -1000; /* 确保足够长以扫过整个线段 */
}
}
/* 流光动画 */
@keyframes flowLight {
0% { background-position: -100% 0; } /* 从左侧完全移出 */
50% { background-position: 100% 0; } /* 移动到右侧完全移出 */
100% { background-position: -100% 0; } /* 回到初始状态,循环 */
}
</style>

View File

@ -1,29 +1,43 @@
<template>
<div id="robot-interface" class="robot-container">
<div class="tree-panel" :style="{ width: isTreeCollapsed ? '0px' : '240px' }">
<el-tree v-if="!isTreeCollapsed" :data="treeData" :props="treeProps" :default-expanded-keys="['robot-node-config']" @node-click="handleNodeClick" />
<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' }">
<div
v-for="area in adjustedAreas"
<TechButtonWithLine
v-for="(area,index) in adjustedAreas"
:key="area.id"
class="clickable-area"
:style="{
position: 'absolute',
top: area.y + 'px',
left: area.x + 'px',
width: area.width + 'px',
height: area.height + 'px'
}"
: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)"
>
<span class="clickable-text">{{ area.label }}</span>
</div>
/>
</div>
<div class="toggle-button" @click="isTreeCollapsed = !isTreeCollapsed">
{{ isTreeCollapsed ? '>' : '<' }}
</div>
</div>
<div
v-for="(popup, index) in popups"
@ -47,7 +61,7 @@
<span class="control-icon close-icon" @click="closePopup(index)">×</span>
</div>
</div>
<component :is="popup.component" v-bind="popup.props" />
<component :is="popup.component" v-bind="popup.props" :initialDeviceId = "popup.deviceId" :initialTerminalId = "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>
@ -57,25 +71,82 @@
<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 } from 'vue';
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
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);
@ -91,53 +162,209 @@ const bgSize = ref({ width: 0, height: 0 });
//
const originalAreas = ref([
{ id: 'hand-left', x: 15, y: 505, width: 110, height: 50, component: HandControl, label: '左手' },
{ id: 'hand-right', x: 300, y: 505, width: 110, height: 50, component: HandControl, label: '右手' },
{ id: 'arm-left', x: 0, y: 150, width: 220, height: 50, component: MechanicalArm, label: '左机械臂' },
{ id: 'arm-right', x: 200, y: 150, width: 220, height: 50, component: MechanicalArm, label: '右机械臂' },
{ id: 'camera', x: 140, y: 50, width: 160, height: 50, component: CameraView, label: '摄像头' },
{ id: 'mouth', x: 140, y: 100, width: 160, height: 50, component: MusicPlayer, label: '扬声器' }
{ 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: 'robot-node-config',
label: '机器人节点配置',
children: [
{ id: 'node-1', label: '节点1', children: [{ id: 'node-1-1', label: '子节点1-1' }, { id: 'node-1-2', label: '子节点1-2' }] },
{ id: 'node-2', label: '节点2', children: [{ id: 'node-2-1', label: '子节点2-1' }, { id: 'node-2-2', label: '子节点2-2' }] },
]
}
]);
const treeProps = { children: 'children', label: 'label' };
/**
* 将扁平化的节点数据转换为树形结构
* 假设 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 containerWidth = containerSize.value.width - (isTreeCollapsed.value ? 0 : 240); //
const containerHeight = containerSize.value.height; // padding
const scaleX = containerWidth / imageDimensions.value.width;
const scaleY = containerHeight / imageDimensions.value.height;
const scale = Math.min(scaleX, scaleY, 1); //
const scaledWidth = imageDimensions.value.width * scale; //
const scaledHeight = imageDimensions.value.height * scale;
const treePanelWidth = isTreeCollapsed.value ? 0 : 240;
// robot-panel content
const contentContainerWidth = containerSize.value.width - treePanelWidth;
const containerHeight = containerSize.value.height;
// const offsetX = (containerWidth - scaledWidth) / 2;
// const offsetY = (containerHeight - scaledHeight) / 2;
//
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,
y: area.y * scale,
x: area.x * scale + offsetX, //
y: area.y * scale + offsetY, //
width: area.width * scale,
height: area.height * scale
}));
@ -148,16 +375,22 @@ 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`,
left: `${area.x + leftOffset}px`,
top: `${area.y}px`, // area.y Y
left: `${area.x + leftOffset}px`, // area.x X treePanelrobot-panelpadding
zIndex: currentZIndex.value++,
deviceId: area.deviceId,
configId: area.configId,
width: 640,
height: 480,
originalWidth: 640,
@ -165,7 +398,7 @@ const openPopup = (area) => {
originalTop: `${area.y}px`,
originalLeft: `${area.x + leftOffset}px`,
isMaximized: false,
props: { terminalId: 'your-terminal-id', cameraId: 'your-camera-id' }
props: { terminalId: 'your-terminal-id', cameraId: 'your-camera-id' } // props
};
popups.value.push(popup);
isResizing.value.push(false);
@ -200,10 +433,14 @@ const maximizePopup = (index) => {
popup.originalHeight = popup.height;
popup.originalTop = popup.top;
popup.originalLeft = popup.left;
popup.width = window.innerWidth;
popup.height = window.innerHeight;
//
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 = '0px';
popup.left = `${treeWidth}px`; // tree panel
popup.isMaximized = true;
console.log('最大化弹窗', popup.id, '到尺寸:', popup.width, 'x', popup.height, '位置:', popup.top, popup.left);
}
@ -211,6 +448,7 @@ const maximizePopup = (index) => {
//
const startDragging = (e, index) => {
e.stopPropagation(); //
isDragging.value = true;
dragIndex.value = index;
const popup = popups.value[index];
@ -225,8 +463,21 @@ const dragHandler = (e) => {
if (isDragging.value && dragIndex.value !== null) {
requestAnimationFrame(() => {
const popup = popups.value[dragIndex.value];
const newTop = Math.max(0, e.pageY - popup.startY);
const newLeft = e.pageX - popup.startX;
//
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`;
});
@ -242,6 +493,7 @@ const stopDragging = () => {
//
const startResizing = (e, index, edge) => {
e.stopPropagation(); //
const popup = popups.value[index];
resizeStart.value = {
index,
@ -254,6 +506,7 @@ const startResizing = (e, index, edge) => {
edge
};
isResizing.value[index] = true;
popup.zIndex = currentZIndex.value++; // z-index
document.addEventListener('mousemove', handleResize);
document.addEventListener('mouseup', stopResizingGlobal);
};
@ -267,19 +520,21 @@ const handleResize = (e) => {
const minWidth = 200;
const minHeight = 150;
const treeWidth = isTreeCollapsed.value ? 0 : 240;
switch (resizeStart.value.edge) {
case 'top':
const newHeight = Math.max(minHeight, resizeStart.value.originalHeight - diffY);
popup.height = newHeight;
popup.top = `${Math.max(0, resizeStart.value.originalTop + (resizeStart.value.originalHeight - newHeight))}px`;
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 newWidth = Math.max(minWidth, resizeStart.value.originalWidth - diffX);
popup.width = newWidth;
popup.left = `${resizeStart.value.originalLeft + (resizeStart.value.originalWidth - newWidth)}px`;
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);
@ -289,7 +544,7 @@ const handleResize = (e) => {
const newHeightTL = Math.max(minHeight, resizeStart.value.originalHeight - diffY);
popup.width = newWidthTL;
popup.height = newHeightTL;
popup.left = `${resizeStart.value.originalLeft + (resizeStart.value.originalWidth - newWidthTL)}px`;
popup.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':
@ -304,13 +559,23 @@ const handleResize = (e) => {
const newHeightBL = Math.max(minHeight, resizeStart.value.originalHeight + diffY);
popup.width = newWidthBL;
popup.height = newHeightBL;
popup.left = `${resizeStart.value.originalLeft + (resizeStart.value.originalWidth - newWidthBL)}px`;
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`; //
}
};
@ -334,15 +599,14 @@ const debounce = (fn, delay) => {
//
const updateDimensions = debounce(() => {
console.log('图片',robotBg.value.width,robotBg.value.offsetWidth, robotBg.value.offsetHeight)
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);
// console.log(':', containerSize.value.width, 'x', containerSize.value.height);
}
}, 100);
}, 50);
onMounted(() => {
const img = new Image();
@ -351,19 +615,41 @@ onMounted(() => {
imageDimensions.value = { width: img.width, height: img.height };
console.log('图片尺寸:', imageDimensions.value.width, 'x', imageDimensions.value.height);
if (robotBg.value) {
const observer = new ResizeObserver(updateDimensions);
observer.observe(robotBg.value.parentElement);
watch(() => isTreeCollapsed.value, updateDimensions, { immediate: true });
updateDimensions(); //
onUnmounted(() => observer.disconnect());
// 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(() => {
document.removeEventListener('mousemove', handleResize);
document.removeEventListener('mouseup', stopResizingGlobal);
});
// onUnmounted onMounted
</script>
<style scoped>
@ -375,18 +661,22 @@ onUnmounted(() => {
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: auto;
height: 100%; /* 填充父容器高度 */
transition: width 0.3s ease;
background-color: #f8f8f8; /* 示例背景色 */
}
.tree-panel:deep(.el-tree) {
padding: 10px !important;
background: transparent; /* 确保 treePanel 的背景色生效 */
}
.robot-panel {
@ -395,7 +685,7 @@ onUnmounted(() => {
justify-content: center;
align-items: center;
overflow: hidden;
padding: 0 20px;
padding: 0 20px; /* 左右内边距 */
position: relative;
}
@ -405,12 +695,16 @@ onUnmounted(() => {
background-position: center;
min-width: 0;
min-height: 0;
position: relative; /* 添加 relative 定位 */
/* position: relative; 添加 relative 定位 */
/* 初始设置一个尺寸,或者由 JS 动态计算 */
width: 100%;
height: 100%;
/* box-sizing: border-box; 包含 padding */
}
.toggle-button {
position: absolute;
left: 20px;
left: 20px; /* 紧贴 robot-panel 的左边距 */
top: 10px;
width: 40px;
height: 40px;
@ -422,6 +716,7 @@ onUnmounted(() => {
background-color: #e0e0e0;
border-radius: 4px;
z-index: 2001;
box-shadow: 0 2px 4px rgba(0,0,0,.1);
}
.toggle-button:hover {
@ -431,6 +726,8 @@ onUnmounted(() => {
.clickable-area {
cursor: pointer;
position: absolute;
/* background: rgba(255, 0, 0, 0.2); */ /* 用于调试区域 */
box-sizing: border-box;
}
.clickable-text {
@ -441,6 +738,9 @@ onUnmounted(() => {
color: #fff;
text-shadow: 0 0 5px #000;
animation: blink 1.5s infinite;
font-size: 14px;
font-weight: bold;
pointer-events: none; /* 防止文本本身捕获点击事件 */
}
@keyframes blink {
@ -454,10 +754,11 @@ onUnmounted(() => {
flex-direction: column;
background: white;
user-select: none;
overflow: auto;
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 {
@ -474,6 +775,7 @@ onUnmounted(() => {
display: flex;
align-items: center;
justify-content: center;
border-bottom: 1px solid #eee; /* 增加分隔线 */
}
.popup-title {
@ -485,8 +787,9 @@ onUnmounted(() => {
text-overflow: ellipsis;
position: absolute;
left: 10px;
right: 60px;
right: 60px; /* 为右侧按钮留出空间 */
text-align: center;
user-select: none; /* 标题不可选 */
}
.popup-controls {
@ -502,6 +805,10 @@ onUnmounted(() => {
padding: 4px 8px;
background: #f0f0f0;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
user-select: none;
}
.control-icon:hover {
@ -516,10 +823,11 @@ onUnmounted(() => {
background: #ff6666;
}
.resize-edge {
.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; }
@ -528,10 +836,10 @@ onUnmounted(() => {
.right { top: 0; bottom: 0; right: 0; width: 5px; cursor: ew-resize; }
.resize-corner {
position: absolute;
width: 10px;
height: 10px;
background: rgba(0, 0, 0, 0.1);
width: 12px;
height: 12px;
background: rgba(0, 0, 0, 0.05); /* 增加一些可见性 */
border-radius: 2px;
}
.top-left { top: 0; left: 0; cursor: nwse-resize; }
@ -542,4 +850,25 @@ onUnmounted(() => {
.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>

View File

@ -36,7 +36,7 @@ export default defineConfig(({mode, command}) => {
//赵 http://10.148.108.58:13080
// dev http://10.148.20.34:13080
// target: command === 'build' ? VITE_API_URL : 'http://10.148.20.34:13080',
target: command === 'build' ? VITE_API_URL : 'http://192.168.0.10:13080',
target: command === 'build' ? VITE_API_URL : 'http://127.0.0.1:13080',
changeOrigin: true,
rewrite: (p) => p.replace(/^\/dev-api/, '')
}