CMVR-IOT-UI/src/views/inspection/cockpit/components/ManualTakeover/index.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>