521 lines
17 KiB
Vue
521 lines
17 KiB
Vue
|
|
<template>
|
|||
|
|
<div class="monitor-container">
|
|||
|
|
<div class="left-container">
|
|||
|
|
<div class="robot-container">
|
|||
|
|
<div class="title">选择机器人</div>
|
|||
|
|
<div class="robot-box">
|
|||
|
|
<div v-for="item, index in 4" class="robot" :class="{ active: activeBot === index }" @click="changeBot(index)">
|
|||
|
|
<div>
|
|||
|
|
<SvgIcon name="bot" :color="colorList[index]" />
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<div class="bot-name">机器人#01</div>
|
|||
|
|
<div class="bot-local">厂区A-在线</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="split-line"></div>
|
|||
|
|
<div class="mode-container">
|
|||
|
|
<div class="title">控制模式</div>
|
|||
|
|
<div class="mode-box">
|
|||
|
|
<div class="mode-item" :class="{ activeMode: activeMode === 'auto' }" @click="changeMode('auto')">
|
|||
|
|
<SvgIcon name="cpu" :color="activeMode === 'auto' ? '#00FF88' : '#fff'" :size="16" />
|
|||
|
|
<span class="label">自动巡检</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="mode-item" :class="{ activeMode: activeMode === 'artificial' }"
|
|||
|
|
@click="changeMode('artificial')">
|
|||
|
|
<SvgIcon name="gamepad" :color="activeMode === 'artificial' ? '#00FF88' : '#fff'" :size="16" />
|
|||
|
|
<span class="label">人工接管</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div v-if="activeMode === 'artificial'" class="artificial-panel">
|
|||
|
|
<el-radio-group v-model="activeController" size="small" @change="handlerController">
|
|||
|
|
<el-radio :value="1">底盘</el-radio>
|
|||
|
|
<el-radio :value="2">机械臂</el-radio>
|
|||
|
|
<el-radio :value="3">相机</el-radio>
|
|||
|
|
</el-radio-group>
|
|||
|
|
<div class="controller-panel">人工控制面板</div>
|
|||
|
|
<div v-if="activeController === 1" class="direction-controller-container">
|
|||
|
|
<div class="title">
|
|||
|
|
<SvgIcon name="navigation" color="00D4FF" :size="16" />
|
|||
|
|
<span class="label">方向控制器</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="direction-box">
|
|||
|
|
<div class="direction-container">
|
|||
|
|
<div class="item"></div>
|
|||
|
|
<div class="item">
|
|||
|
|
<SvgIcon name="chevron-up" :size="24" />
|
|||
|
|
</div>
|
|||
|
|
<div class="item"></div>
|
|||
|
|
<div class="item">
|
|||
|
|
<SvgIcon name="chevron-left" :size="24" />
|
|||
|
|
</div>
|
|||
|
|
<div class="item">
|
|||
|
|
<SvgIcon name="chevron-stop" :size="52" />
|
|||
|
|
</div>
|
|||
|
|
<div class="item">
|
|||
|
|
<SvgIcon name="chevron-right" :size="24" />
|
|||
|
|
</div>
|
|||
|
|
<div class="item"></div>
|
|||
|
|
<div class="item">
|
|||
|
|
<SvgIcon name="chevron-down" :size="24" />
|
|||
|
|
</div>
|
|||
|
|
<div class="item"></div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="speed-box">
|
|||
|
|
<div class="label">移动速度</div>
|
|||
|
|
<div class="speed">{{ speed }}m/s</div>
|
|||
|
|
</div>
|
|||
|
|
<el-slider v-model="speed" :max="2" :step="0.1" size="small" />
|
|||
|
|
</div>
|
|||
|
|
<div v-if="activeController === 2" class="robotic-arm-controller-container">
|
|||
|
|
<el-radio-group v-model="activeRoboticArm" @change="handlerArm" size="small">
|
|||
|
|
<el-radio :value="1">末端控制</el-radio>
|
|||
|
|
<el-radio :value="2">关节控制</el-radio>
|
|||
|
|
<el-radio :value="3">姿态控制</el-radio>
|
|||
|
|
</el-radio-group>
|
|||
|
|
|
|||
|
|
<div class="leftHand">左手</div>
|
|||
|
|
<div v-if="[1,3].includes(activeRoboticArm)" class="coordinate-system" ref="coordinateSystem">
|
|||
|
|
<div class="control-buttons">
|
|||
|
|
<button v-for="(pos, index) in buttonPositions" :key="index" class="pos-btn"
|
|||
|
|
:data-target="pos.target" :style="{ left: `${pos.left}px`, top: `${pos.top}px` }">
|
|||
|
|
{{ pos.label }}
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="rightHand">右手</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
</div>
|
|||
|
|
<div class="video-container">
|
|||
|
|
<div v-if="activeMode === 'auto' || activeController === 1" class="map-container">地图</div>
|
|||
|
|
<div v-if="activeMode === 'artificial' && activeController === 2" class="model-container">机械臂模型</div>
|
|||
|
|
<div v-if="activeMode === 'artificial' && activeController === 3" class="monitor-container">监控</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="right-container">
|
|||
|
|
<div class="container-title">车载监控</div>
|
|||
|
|
<div class="monitor-container"></div>
|
|||
|
|
<div class="split-line"></div>
|
|||
|
|
<div class="container-title">语音对话</div>
|
|||
|
|
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
<script setup>
|
|||
|
|
import SvgIcon from "@/components/SvgIcon";
|
|||
|
|
|
|||
|
|
const activeBot = ref(0)
|
|||
|
|
const changeBot = (index) => {
|
|||
|
|
activeBot.value = index
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const colorList = ['#00D4FF', '#FFB300', '#00FF88', '#fff']
|
|||
|
|
|
|||
|
|
const activeMode = ref('auto')
|
|||
|
|
const changeMode = (mode) => {
|
|||
|
|
activeMode.value = mode
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const activeController = ref(1)
|
|||
|
|
|
|||
|
|
const speed = ref(0.8)
|
|||
|
|
|
|||
|
|
const activeRoboticArm = ref(1)
|
|||
|
|
|
|||
|
|
const coordinateSystem = ref(null); // 用于获取容器元素
|
|||
|
|
const buttonPositions = ref([]); // 存储按钮的动态位置
|
|||
|
|
const buttonDirections = [
|
|||
|
|
{ target: "z_plus", direction: "top", label: "Z+" }, // left
|
|||
|
|
{ target: "x_plus", direction: "right", label: "X+" }, // bottom
|
|||
|
|
{ target: "y_minus", direction: "bottom", label: "Y-" },
|
|||
|
|
{ target: "z_minus", direction: "bottom", label: "Z-" },
|
|||
|
|
{ target: "x_minus", direction: "left", label: "X-" },
|
|||
|
|
{ target: "y_plus", direction: "left", label: "Y+" }, // right
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
// 默认图片尺寸
|
|||
|
|
const ORIGINAL_IMAGE_WIDTH = 1025;
|
|||
|
|
const ORIGINAL_IMAGE_HEIGHT = 817;
|
|||
|
|
|
|||
|
|
const handlerController = (value) => {
|
|||
|
|
if (value === 2 && [1, 3].includes(activeRoboticArm.value)) {
|
|||
|
|
updateButtonPositions()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const handlerArm = (value) => {
|
|||
|
|
if ([1, 3].includes(value)) {
|
|||
|
|
updateButtonPositions()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const updateButtonPositions = () => {
|
|||
|
|
if (coordinateSystem.value) {
|
|||
|
|
const containerRect = coordinateSystem.value.getBoundingClientRect();
|
|||
|
|
const containerWidth = containerRect.width;
|
|||
|
|
const containerHeight = containerRect.height;
|
|||
|
|
|
|||
|
|
// 计算图片的实际显示尺寸(考虑 background-size: contain)
|
|||
|
|
const imageAspectRatio = ORIGINAL_IMAGE_WIDTH / ORIGINAL_IMAGE_HEIGHT;
|
|||
|
|
const containerAspectRatio = containerWidth / containerHeight;
|
|||
|
|
|
|||
|
|
let imageWidth, imageHeight, imageLeft, imageTop;
|
|||
|
|
if (containerAspectRatio > imageAspectRatio) {
|
|||
|
|
imageHeight = containerHeight;
|
|||
|
|
imageWidth = imageHeight * imageAspectRatio;
|
|||
|
|
imageLeft = (containerWidth - imageWidth) / 2;
|
|||
|
|
imageTop = 0;
|
|||
|
|
} else {
|
|||
|
|
imageWidth = containerWidth;
|
|||
|
|
imageHeight = imageWidth / imageAspectRatio;
|
|||
|
|
imageLeft = 0;
|
|||
|
|
imageTop = (containerHeight - imageHeight) / 2;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 计算图片中心坐标
|
|||
|
|
const centerX = imageLeft + imageWidth / 2;
|
|||
|
|
const centerY = imageTop + imageHeight / 2;
|
|||
|
|
|
|||
|
|
// 计算圆的半径(根据图片尺寸的较小值比例确定,可调整比例系数)
|
|||
|
|
const radiusScale = 0.85; // 半径系数,0-1之间,越大半径越大
|
|||
|
|
const radius = Math.min(imageWidth, imageHeight) * radiusScale / 2;
|
|||
|
|
|
|||
|
|
// 定义方位对应的角度(弧度制),top为0度,顺时针增加
|
|||
|
|
const directionAngles = {
|
|||
|
|
"top": 0,
|
|||
|
|
"top-right": Math.PI / 6, // 30度
|
|||
|
|
"right": Math.PI / 3, // 60度
|
|||
|
|
"bottom-right": Math.PI / 2, // 90度
|
|||
|
|
"bottom": 2 * Math.PI / 3, // 120度
|
|||
|
|
"bottom-left": 5 * Math.PI / 6, // 150度
|
|||
|
|
"left": Math.PI, // 180度
|
|||
|
|
"top-left": 7 * Math.PI / 6, // 210度
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 调整角度以均匀分布12个按钮(360/12=30度,即Math.PI/6弧度)
|
|||
|
|
const totalButtons = buttonDirections.length;
|
|||
|
|
const angleStep = 2 * Math.PI / totalButtons;
|
|||
|
|
|
|||
|
|
// 更新按钮位置
|
|||
|
|
buttonPositions.value = buttonDirections.map((btn, index) => {
|
|||
|
|
// 计算每个按钮的角度(从top开始顺时针排列)
|
|||
|
|
const angle = index * angleStep;
|
|||
|
|
// 计算按钮的坐标
|
|||
|
|
const x = centerX + radius * Math.sin(angle);
|
|||
|
|
const y = centerY - radius * Math.cos(angle);
|
|||
|
|
return {
|
|||
|
|
...btn,
|
|||
|
|
left: x,
|
|||
|
|
top: y,
|
|||
|
|
};
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
console.log('buttonPositions', buttonPositions.value)
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
|
|||
|
|
onMounted(() => {
|
|||
|
|
// 监听窗口大小变化,更新按钮位置
|
|||
|
|
window.addEventListener("resize", updateButtonPositions);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
onUnmounted(() => {
|
|||
|
|
window.removeEventListener("resize", updateButtonPositions);
|
|||
|
|
});
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style lang="scss" scoped>
|
|||
|
|
.monitor-container {
|
|||
|
|
width: 100%;
|
|||
|
|
height: 100%;
|
|||
|
|
display: flex;
|
|||
|
|
|
|||
|
|
.left-container {
|
|||
|
|
width: 360px;
|
|||
|
|
height: 100%;
|
|||
|
|
padding: 16px;
|
|||
|
|
|
|||
|
|
.robot-container {
|
|||
|
|
|
|||
|
|
.title {
|
|||
|
|
color: #8899BB;
|
|||
|
|
font-size: 14px;
|
|||
|
|
margin-bottom: 16px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.robot-box {
|
|||
|
|
display: flex;
|
|||
|
|
flex-wrap: wrap;
|
|||
|
|
gap: 10px;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
|
|||
|
|
.robot {
|
|||
|
|
flex: 0 0 calc(50% - 10px);
|
|||
|
|
background: #0F1E38;
|
|||
|
|
height: 56px;
|
|||
|
|
border-radius: 8px;
|
|||
|
|
padding: 16px;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 20px;
|
|||
|
|
margin-bottom: 16px;
|
|||
|
|
cursor: pointer;
|
|||
|
|
border: 1px solid transparent;
|
|||
|
|
|
|||
|
|
.bot-name {
|
|||
|
|
color: #fff;
|
|||
|
|
font-size: 13px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.bot-local {
|
|||
|
|
color: #00d4ff;
|
|||
|
|
font-size: 11px;
|
|||
|
|
margin-top: 4px;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.active {
|
|||
|
|
border-color: #00c2ff;
|
|||
|
|
color: #fff;
|
|||
|
|
box-shadow: 0 0 10px rgba(0, 194, 255, 0.5);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.split-line {
|
|||
|
|
width: 100%;
|
|||
|
|
height: 1px;
|
|||
|
|
margin: 16px 0;
|
|||
|
|
background: #1E3A5F;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.mode-container {
|
|||
|
|
.title {
|
|||
|
|
color: #8899BB;
|
|||
|
|
font-size: 14px;
|
|||
|
|
margin-bottom: 16px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.mode-box {
|
|||
|
|
display: flex;
|
|||
|
|
flex-wrap: wrap;
|
|||
|
|
gap: 10px;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
|
|||
|
|
.mode-item {
|
|||
|
|
height: 36px;
|
|||
|
|
flex: 0 0 calc(50% - 10px);
|
|||
|
|
background: #0F1E38;
|
|||
|
|
border: 1px solid transparent;
|
|||
|
|
border-radius: 8px;
|
|||
|
|
padding: 16px;
|
|||
|
|
margin-bottom: 12px;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 16px;
|
|||
|
|
cursor: pointer;
|
|||
|
|
|
|||
|
|
.label {
|
|||
|
|
color: #8899BB;
|
|||
|
|
font-size: 13px;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.activeMode {
|
|||
|
|
border-color: #00FF88;
|
|||
|
|
|
|||
|
|
.label {
|
|||
|
|
color: #00FF88;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.controller-panel {
|
|||
|
|
padding: 16px;
|
|||
|
|
color: #fff;
|
|||
|
|
font-size: 16px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.direction-controller-container {
|
|||
|
|
padding: 16px;
|
|||
|
|
height: 272px;
|
|||
|
|
background: #0F1E38;
|
|||
|
|
border-radius: 10px;
|
|||
|
|
|
|||
|
|
.title {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 16px;
|
|||
|
|
margin-bottom: 16px;
|
|||
|
|
|
|||
|
|
.label {
|
|||
|
|
color: #fff;
|
|||
|
|
font-size: 14px;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.direction-box {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
|
|||
|
|
.direction-container {
|
|||
|
|
display: flex;
|
|||
|
|
flex-wrap: wrap;
|
|||
|
|
gap: 4px;
|
|||
|
|
width: 164px;
|
|||
|
|
height: 164px;
|
|||
|
|
|
|||
|
|
.item {
|
|||
|
|
// flex: 0 0 calc(33.333% - 4px);
|
|||
|
|
width: 52px;
|
|||
|
|
height: 52px;
|
|||
|
|
background: #1E3A5F;
|
|||
|
|
border-radius: 8px;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.speed-box {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
margin: 8px 0;
|
|||
|
|
|
|||
|
|
.label {
|
|||
|
|
color: #8899BB;
|
|||
|
|
font-size: 12px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.speed {
|
|||
|
|
color: #00D4FF;
|
|||
|
|
font-size: 12px;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
:deep(.el-slider__button) {
|
|||
|
|
scale: 0.5;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.robotic-arm-controller-container {
|
|||
|
|
:deep(.el-radio-group) {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.coordinate-system {
|
|||
|
|
height: 200px;
|
|||
|
|
display: flex;
|
|||
|
|
justify-content: center;
|
|||
|
|
align-items: center;
|
|||
|
|
position: relative;
|
|||
|
|
background-image: url("@/assets/images/arm.png");
|
|||
|
|
background-size: contain;
|
|||
|
|
background-position: center;
|
|||
|
|
background-repeat: no-repeat;
|
|||
|
|
|
|||
|
|
.control-buttons {
|
|||
|
|
position: absolute;
|
|||
|
|
width: 100%;
|
|||
|
|
height: 100%;
|
|||
|
|
top: 0;
|
|||
|
|
left: 0;
|
|||
|
|
|
|||
|
|
.pos-btn {
|
|||
|
|
position: absolute;
|
|||
|
|
width: 28px;
|
|||
|
|
height: 28px;
|
|||
|
|
background: #4076ff;
|
|||
|
|
color: white;
|
|||
|
|
border: none;
|
|||
|
|
cursor: pointer;
|
|||
|
|
font-size: 14px;
|
|||
|
|
border-radius: 4px;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
transform: translate(-50%, -50%);
|
|||
|
|
/* 按钮中心对齐坐标 */
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.video-container {
|
|||
|
|
flex: 1;
|
|||
|
|
border: 1px solid #1E3A5F;
|
|||
|
|
border-top: 0;
|
|||
|
|
|
|||
|
|
.map-container {
|
|||
|
|
width: 100%;
|
|||
|
|
height: 100%;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
color: #00c2ff;
|
|||
|
|
font-size: 20px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.model-container {
|
|||
|
|
width: 100%;
|
|||
|
|
height: 100%;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
color: #00c2ff;
|
|||
|
|
font-size: 20px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.monitor-container {
|
|||
|
|
width: 100%;
|
|||
|
|
height: 100%;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
color: #00c2ff;
|
|||
|
|
font-size: 20px;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.right-container {
|
|||
|
|
width: 360px;
|
|||
|
|
height: 100%;
|
|||
|
|
padding: 16px;
|
|||
|
|
|
|||
|
|
.container-title {
|
|||
|
|
color: #fff;
|
|||
|
|
font-size: 16px;
|
|||
|
|
font-weight: bold;
|
|||
|
|
margin-bottom: 16px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.split-line {
|
|||
|
|
width: 100%;
|
|||
|
|
height: 1px;
|
|||
|
|
margin: 16px 0;
|
|||
|
|
background: #1E3A5F;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.monitor-container {
|
|||
|
|
width: 100%;
|
|||
|
|
height: 360px;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</style>
|