554 lines
17 KiB
Vue
554 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 robotList" class="robot" :class="{ active: activeBot === index }"
|
|
@click="changeBot(index)">
|
|
<div>
|
|
<SvgIcon name="bot" :color="colorList[index]" />
|
|
</div>
|
|
<div>
|
|
<div class="bot-name">{{ item.robotName }}</div>
|
|
<div class="bot-local">{{ robotStatus[item.status] }}</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>
|
|
<DirectionControl v-if="activeController === 1" :robot="activeBotData" />
|
|
<div v-if="activeController === 2" class="robotic-arm-controller-container">
|
|
<el-radio-group v-model="activeRoboticArm" size="small">
|
|
<el-radio :value="1">末端控制</el-radio>
|
|
<el-radio :value="2">关节控制</el-radio>
|
|
<el-radio :value="3">姿态控制</el-radio>
|
|
</el-radio-group>
|
|
|
|
<RoboticArm :type="activeRoboticArm === 1 ? 'end' : 'posture'" :robot="activeBotData"
|
|
v-if="[1, 3].includes(activeRoboticArm)" />
|
|
|
|
<div class="control-panel" v-if="activeRoboticArm === 2">
|
|
<div class="joint-list">
|
|
<div v-for="joint in jointControls" :key="joint.name" class="joint-item">
|
|
<label :for="joint.name">{{ joint.name }}</label>
|
|
<el-input
|
|
v-model="joint.value"
|
|
readonly
|
|
>
|
|
<template #prepend>
|
|
<el-button
|
|
@mousedown="updateJoint(joint.name, '-')"
|
|
@mouseup="stop"
|
|
@touchstart.prevent="updateJoint(joint.name, '-')"
|
|
@touchend.prevent="stop"
|
|
>
|
|
-
|
|
</el-button>
|
|
</template>
|
|
<template #append>
|
|
<el-button
|
|
@mousedown="updateJoint(joint.name, '+')"
|
|
@mouseup="stop"
|
|
@touchstart.prevent="updateJoint(joint.name, '+')"
|
|
@touchend.prevent="stop"
|
|
>
|
|
+
|
|
</el-button>
|
|
</template>
|
|
</el-input>
|
|
</div>
|
|
</div>
|
|
<div class="speed-container">
|
|
<div class="label">速度:</div>
|
|
<el-slider v-model="jointSpeed" :min="0.01" :step="0.01" :max="1" />
|
|
<div class="proportion">{{( jointSpeed / 1 * 100).toFixed(0) }} %</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
<div class="video-container">
|
|
<div v-if="activeMode === 'auto' || activeController === 1" class="map-container">
|
|
<MapCanvas :robotList="robotList" ref="mapCanvasRef" />
|
|
</div>
|
|
<div v-if="activeMode === 'artificial' && activeController === 2" class="model-container">
|
|
<UrdfViewer ref="urdfViewerRef" modelPath="/inspection/elfin10/elfin10.urdf.xacro" />
|
|
</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">
|
|
<IPlayer :isLive="true" :videoUrl="videoUrl" />
|
|
</div>
|
|
<div class="split-line"></div>
|
|
<div class="container-title">语音对话</div>
|
|
<div class="voice-container">
|
|
<VoiceConversation />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<script setup>
|
|
import { onMounted, nextTick } from "vue";
|
|
import SvgIcon from "@/components/SvgIcon";
|
|
import UrdfViewer from '../UrdfView.vue'
|
|
import DirectionControl from './DirectionControl.vue'
|
|
import RoboticArm from "./RoboticArm.vue";
|
|
import { getRobotList } from '@/api/inspection/robot'
|
|
import { getJointStateApi, moveJApi, torqueOnApi, speedJApi, stopMotionApi, clearFault } from '@/api/inspection/cockpit'
|
|
import { Remove, CirclePlus } from '@element-plus/icons-vue'
|
|
import VoiceConversation from "./VoiceConversation.vue";
|
|
import IPlayer from "@/components/IPlayer/index.vue";
|
|
|
|
const robotList = ref([])
|
|
|
|
const fetchRobotList = async () => {
|
|
try {
|
|
const res = await getRobotList({
|
|
robotType: 2,
|
|
})
|
|
if (res.code === 200) {
|
|
robotList.value = res.rows
|
|
activeBotData.value = robotList.value[0] || {}
|
|
} else {
|
|
console.error('获取机器人列表失败:', res.message)
|
|
}
|
|
} catch (error) {
|
|
console.error('获取机器人列表失败:', error)
|
|
}
|
|
}
|
|
|
|
const robotStatus = {
|
|
0: '在线',
|
|
1: '离线',
|
|
2: '充电中',
|
|
3: '巡检中'
|
|
}
|
|
|
|
const activeBot = ref(0)
|
|
const activeBotData = ref({})
|
|
const changeBot = (index) => {
|
|
activeBot.value = index
|
|
activeBotData.value = robotList.value[activeBot.value] || {}
|
|
}
|
|
|
|
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 urdfViewerRef = ref()
|
|
const jointControls = ref()
|
|
|
|
const animationId = ref(null)
|
|
|
|
const handlerController = (value) => {
|
|
if (value === 2) {
|
|
// 开启机械臂使能
|
|
torqueOnApi({
|
|
deviceId: import.meta.env.VITE_INSPECTION_ARM_DEVICE_ID,
|
|
terminalId: activeBotData.value.terminalId
|
|
}).catch(error => {
|
|
console.error('开启机械臂使能失败:', error)
|
|
clearFault({
|
|
deviceId: import.meta.env.VITE_INSPECTION_ARM_DEVICE_ID,
|
|
terminalId: activeBotData.value.terminalId
|
|
})
|
|
})
|
|
jointControls.value = urdfViewerRef.value.jointControls
|
|
if (animationId.value) {
|
|
clearInterval(animationId.value)
|
|
}
|
|
animationId.value = setInterval(() => {
|
|
getJointState(activeBotData.value)
|
|
}, 100)
|
|
} else {
|
|
if (animationId.value) {
|
|
clearInterval(animationId.value)
|
|
}
|
|
}
|
|
}
|
|
|
|
const jointNamsMap = {
|
|
'joint_1' :'elfin_joint1',
|
|
'joint_2': 'elfin_joint2',
|
|
'joint_3': 'elfin_joint3',
|
|
'joint_4': 'elfin_joint4',
|
|
'joint_5': 'elfin_joint5',
|
|
'joint_6': 'elfin_joint6',
|
|
}
|
|
|
|
const errCount = ref(0)
|
|
|
|
/**
|
|
* 获取关节状态
|
|
* @param robot
|
|
*/
|
|
const getJointState = async (robot) => {
|
|
try {
|
|
const res = await getJointStateApi({
|
|
deviceId: import.meta.env.VITE_INSPECTION_ARM_DEVICE_ID,
|
|
terminalId: robot.terminalId
|
|
})
|
|
if (res.code === 200) {
|
|
setTimeout(() => {
|
|
const names = res.data.name
|
|
const positions = res.data.position
|
|
names.forEach((name, index) => {
|
|
urdfViewerRef.value.updateJoint(jointNamsMap[name] || name, positions[index].toFixed(3))
|
|
})
|
|
}, 500)
|
|
} else {
|
|
errCount.value += 1
|
|
if (errCount.value > 2) {
|
|
clearInterval(animationId.value)
|
|
errCount.value = 0
|
|
}
|
|
console.error('获取关节状态失败:', res.message)
|
|
}
|
|
} catch (error) {
|
|
errCount.value += 1
|
|
if (errCount.value > 2) {
|
|
clearInterval(animationId.value)
|
|
errCount.value = 0
|
|
}
|
|
console.error('获取关节状态失败:', error)
|
|
}
|
|
}
|
|
|
|
const jointSpeed = ref(0.8)
|
|
const updateJoint = async (jointName, _dir) => {
|
|
const velocities = jointControls.value.map(joint => {
|
|
return 0
|
|
})
|
|
const index = jointControls.value.findIndex(joint => joint.name === jointName)
|
|
velocities[index] = jointSpeed.value * (_dir === '+' ? 1 : -1)
|
|
const res = await speedJApi({
|
|
deviceId: import.meta.env.VITE_INSPECTION_ARM_DEVICE_ID,
|
|
terminalId: activeBotData.value.terminalId,
|
|
acceleration: jointSpeed.value + 0.1,
|
|
duration: 60,
|
|
velocities: velocities
|
|
})
|
|
}
|
|
|
|
/**
|
|
* 停止机械臂移动
|
|
*/
|
|
const stop = async () => {
|
|
await stopMotionApi({
|
|
deviceId: import.meta.env.VITE_INSPECTION_ARM_DEVICE_ID,
|
|
terminalId: activeBotData.value.terminalId
|
|
})
|
|
}
|
|
|
|
const videoUrl = ref('https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8')
|
|
|
|
onMounted(() => {
|
|
fetchRobotList()
|
|
})
|
|
|
|
onUnmounted(() => {
|
|
if (animationId.value) {
|
|
clearInterval(animationId.value)
|
|
}
|
|
})
|
|
|
|
</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: 12px;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
|
|
.robotic-arm-controller-container {
|
|
:deep(.el-radio-group) {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
}
|
|
}
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
.voice-container {
|
|
width: 100%;
|
|
height: 400px;
|
|
}
|
|
}
|
|
}
|
|
|
|
.control-panel {
|
|
margin-top: 12px;
|
|
background: #0F1E38;
|
|
padding: 15px;
|
|
border-radius: 8px;
|
|
color: #ccc;
|
|
max-height: 80%;
|
|
overflow-y: auto;
|
|
|
|
.joint-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
margin-bottom: 10px;
|
|
padding: 10px;
|
|
border: 1px solid rgba(0, 195, 255, 0.08);
|
|
border-radius: 5px;
|
|
background: #0b182c;
|
|
|
|
&:hover {
|
|
border-color: #2a3f7a;
|
|
background: rgba(59, 130, 246, 0.031372549);
|
|
}
|
|
|
|
label {
|
|
width: 120px;
|
|
font-size: 16px;
|
|
font-weight: bold;
|
|
}
|
|
|
|
input[type=range] {
|
|
flex: 1;
|
|
}
|
|
|
|
.angle {
|
|
width: 50px;
|
|
text-align: right;
|
|
}
|
|
|
|
:deep(.el-input-group__prepend) {
|
|
background: #080b10;
|
|
border-right: 0px;
|
|
box-shadow: 0 0 0 1px #272727 inset;
|
|
}
|
|
|
|
:deep(.el-input-group__append) {
|
|
background: #080b10;
|
|
border-left: 0px;
|
|
box-shadow: 0 0 0 1px #272727 inset;
|
|
}
|
|
|
|
:deep(.el-input__wrapper) {
|
|
background-color: #0e0e0e;
|
|
box-shadow: 0 0 0 1px #272727 inset;
|
|
}
|
|
}
|
|
|
|
.speed-container {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
|
|
.label,
|
|
.proportion {
|
|
width: 100px;
|
|
text-align: center;
|
|
color: #fff;
|
|
}
|
|
}
|
|
}
|
|
</style> |