feat: 机械臂
This commit is contained in:
parent
d5ce6ee297
commit
0ceb7cc233
@ -7,8 +7,9 @@ VITE_APP_ENV = 'development'
|
||||
# 招商车研物联网平台/开发环境
|
||||
VITE_APP_BASE_API = '/dev-api'
|
||||
|
||||
VITE_API_URL = http://192.168.1.201:13080
|
||||
VITE_WS_URL = ws://192.168.1.201:13080/ws
|
||||
VITE_API_URL = http://192.168.1.101:13080
|
||||
VITE_WS_URL = ws://192.168.1.101:13080/ws
|
||||
|
||||
# explanation
|
||||
VITE_INSPECTION_TYPE = "inspection"
|
||||
VITE_INSPECTION_ALM_DEVICEID = "huayan_arm"
|
||||
@ -92,7 +92,6 @@ export class XacroAdapter {
|
||||
// Check for parsing errors
|
||||
const parseError = cleanUrdfXML.querySelector('parsererror');
|
||||
if (parseError) {
|
||||
console.error('[XacroAdapter] XML parsing error:', parseError.textContent);
|
||||
throw new Error('Generated URDF has invalid XML: ' + parseError.textContent);
|
||||
}
|
||||
|
||||
@ -261,7 +260,6 @@ export class XacroAdapter {
|
||||
} else {
|
||||
let stlPath = path
|
||||
stlPath = stlPath.replace('file:///', 'inspection/elfin10')
|
||||
console.log(stlPath, manager, done)
|
||||
originalLoadMeshCb(path, manager, done);
|
||||
}
|
||||
}).catch(error => {
|
||||
@ -306,9 +304,6 @@ export class XacroAdapter {
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
static async loadFileFromMap(path, fileMap, workingPath) {
|
||||
console.log('path', path)
|
||||
console.log('fileMap', fileMap)
|
||||
console.log('workingPath', workingPath)
|
||||
// Clean path - remove leading slash if present
|
||||
let cleanPath = path;
|
||||
// if (cleanPath.startsWith('/')) {
|
||||
@ -318,7 +313,6 @@ export class XacroAdapter {
|
||||
// // Remove leading ./
|
||||
// cleanPath = cleanPath.replace(/^\.\//, '');
|
||||
cleanPath = cleanPath.split('///')[1];
|
||||
console.log('cleanPath', cleanPath)
|
||||
// Try different path combinations
|
||||
const possiblePaths = [
|
||||
cleanPath,
|
||||
@ -328,13 +322,11 @@ export class XacroAdapter {
|
||||
// Also try without the first directory component
|
||||
cleanPath.includes('/') ? cleanPath.substring(cleanPath.indexOf('/') + 1) : cleanPath,
|
||||
];
|
||||
console.log('possiblePaths', possiblePaths)
|
||||
|
||||
// Try each path
|
||||
for (const tryPath of possiblePaths) {
|
||||
console.log('tryPath', tryPath)
|
||||
const file = fileMap.get(tryPath);
|
||||
if (file) {
|
||||
console.log(12345)
|
||||
const content = await file.text();
|
||||
return content;
|
||||
}
|
||||
|
||||
57
src/api/inspection/cockpit.js
Normal file
57
src/api/inspection/cockpit.js
Normal file
@ -0,0 +1,57 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export function getPoseApi(params) {
|
||||
return request({
|
||||
url: '/api/arm/getPose',
|
||||
method: 'get',
|
||||
params: params
|
||||
})
|
||||
}
|
||||
|
||||
export function getJointStateApi(params) {
|
||||
return request({
|
||||
url: '/api/arm/getJointState',
|
||||
method: 'get',
|
||||
params: params
|
||||
})
|
||||
}
|
||||
|
||||
export function speedLApi(data) {
|
||||
return request({
|
||||
url: '/api/arm/speedL',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
export function moveJApi(data) {
|
||||
return request({
|
||||
url: '/api/arm/moveJ',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
export function stopMotionApi(params) {
|
||||
return request({
|
||||
url: '/api/arm/stopMotion',
|
||||
method: 'get',
|
||||
params: params
|
||||
})
|
||||
}
|
||||
|
||||
export function torqueOnApi(params) {
|
||||
return request({
|
||||
url: '/api/arm/torqueOn',
|
||||
method: 'get',
|
||||
params: params
|
||||
})
|
||||
}
|
||||
|
||||
export function speedJApi(data) {
|
||||
return request({
|
||||
url: '/api/arm/speedJ',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
@ -31,7 +31,7 @@
|
||||
:prop="`nodeParams.${index}.input`">
|
||||
<el-input-number class="param-value" v-if="property.componentType === 'number'"
|
||||
v-model="property.input" :min="0" :max="property.max || Infinity"
|
||||
:controls="false" :step-strictly="true" placeholder="请输入" clearable />
|
||||
:controls="property?.controls || true" :step-strictly="true" :step="property.step || 1" placeholder="请输入" clearable />
|
||||
<el-select v-model="property.input" class="param-value"
|
||||
v-else-if="property.componentType === 'select'">
|
||||
<el-option v-for="item in property.selectOptions" :key="item.value"
|
||||
|
||||
@ -393,8 +393,8 @@ function handler(params) {
|
||||
{ name: "rx", type: "input", input: "", disabled: true },
|
||||
{ name: "ry", type: "input", input: "", disabled: true },
|
||||
{ name: "rz", type: "input", input: "", disabled: true },
|
||||
{ name: "velocity", type: "input", componentType: 'number', input: 0, disabled: true },
|
||||
{ name: "acceleration", type: "input", componentType: 'number', input: 0, disabled: true },
|
||||
{ name: "velocity", type: "input", componentType: 'number', max: 5, step: 0.1, input: 0, disabled: true },
|
||||
{ name: "acceleration", type: "input", componentType: 'number', max: 5, step: 0.1, input: 0, disabled: true },
|
||||
],
|
||||
outputType: 'json'
|
||||
},
|
||||
|
||||
@ -1,674 +0,0 @@
|
||||
<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="mode-tabs">
|
||||
<div class="mode-tab active">左手</div>
|
||||
<div class="mode-tab">右手</div>
|
||||
</div>
|
||||
|
||||
<div v-if="[1, 3].includes(activeRoboticArm)" class="robotic-arm-box">
|
||||
<div class="robotic-arm-status">
|
||||
<div class="coord-display">
|
||||
<div class="coord-item">
|
||||
<div class="coord-key key-x">X</div>
|
||||
<div class="coord-val" id="coordX">+124.50</div>
|
||||
</div>
|
||||
<div class="coord-item">
|
||||
<div class="coord-key key-y">Y</div>
|
||||
<div class="coord-val" id="coordY">−38.20</div>
|
||||
</div>
|
||||
<div class="coord-item">
|
||||
<div class="coord-key key-z">Z</div>
|
||||
<div class="coord-val" id="coordZ">+502.00</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="axis-group">
|
||||
<!-- X Axis -->
|
||||
<div class="axis-row x">
|
||||
<div class="axis-label x">X</div>
|
||||
<div class="axis-slider-wrap">
|
||||
<div class="axis-name">前后平移</div>
|
||||
<div>
|
||||
<el-button type="primary" circle>
|
||||
<el-icon size="24"><Remove /></el-icon>
|
||||
</el-button>
|
||||
<el-button type="primary" circle >
|
||||
<el-icon size="24"><CirclePlus /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Y Axis -->
|
||||
<div class="axis-row y">
|
||||
<div class="axis-label y">Y</div>
|
||||
<div class="axis-slider-wrap">
|
||||
<div class="axis-name">左右平移</div>
|
||||
<div>
|
||||
<el-button type="primary" circle>
|
||||
<el-icon size="24"><Remove /></el-icon>
|
||||
</el-button>
|
||||
<el-button type="primary" circle >
|
||||
<el-icon size="24"><CirclePlus /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Z Axis -->
|
||||
<div class="axis-row z">
|
||||
<div class="axis-label z">Z</div>
|
||||
<div class="axis-slider-wrap">
|
||||
<div class="axis-name">上下平移</div>
|
||||
<div>
|
||||
<el-button type="primary" circle>
|
||||
<el-icon size="24"><Remove /></el-icon>
|
||||
</el-button>
|
||||
<el-button type="primary" circle >
|
||||
<el-icon size="24"><CirclePlus /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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-slider v-model="joint.value" :min="joint.min" :max="joint.max" :step="0.01" @input="updateJoint(joint.name, joint.value)"/>
|
||||
<span class="angle">{{ (joint.value * 180 / Math.PI).toFixed(1) }}°</span>
|
||||
</div>
|
||||
</div>
|
||||
</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">
|
||||
<UrdfViewer ref="urdfViewerRef" base-path="/inspection/elfin10/" />
|
||||
</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";
|
||||
import UrdfViewer from './UrdfView.vue'
|
||||
import { Remove, CirclePlus } from '@element-plus/icons-vue'
|
||||
|
||||
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 urdfViewerRef = ref()
|
||||
const jointControls = ref()
|
||||
const handlerController = (value) => {
|
||||
if (value === 2 && [1, 3].includes(activeRoboticArm.value)) {
|
||||
jointControls.value = urdfViewerRef.value.jointControls
|
||||
}
|
||||
}
|
||||
|
||||
const updateJoint = (jointName, angle) => {
|
||||
urdfViewerRef.value.updateJoint(jointName, angle)
|
||||
}
|
||||
|
||||
const handlerArm = (value) => {
|
||||
if ([1, 3].includes(value)) {
|
||||
}
|
||||
}
|
||||
|
||||
function onLoaded(robot) {
|
||||
console.log('已加载关节:', Object.keys(robot.joints))
|
||||
}
|
||||
function onError(err) {
|
||||
console.error('加载失败:', err)
|
||||
}
|
||||
|
||||
</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;
|
||||
}
|
||||
|
||||
.mode-tabs {
|
||||
display: flex;
|
||||
padding: 12px 20px 0;
|
||||
gap: 6px;
|
||||
|
||||
.mode-tab {
|
||||
flex: 1;
|
||||
padding: 8px 0;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #4a5580;
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
border-bottom: none;
|
||||
border-radius: 8px 8px 0 0;
|
||||
cursor: pointer;
|
||||
transition: all 0.25s ease;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
color: #8b97bf;
|
||||
background: #3b82f60d;
|
||||
}
|
||||
}
|
||||
|
||||
.active {
|
||||
color: #3b82f6;
|
||||
background: #111a36;
|
||||
border-color: #1c2a52;
|
||||
font-weight: 700;
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 20%;
|
||||
right: 20%;
|
||||
height: 2px;
|
||||
background: var(--accent);
|
||||
border-radius: 1px;
|
||||
box-shadow: 0 0 8px var(--accent-glow);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.robotic-arm-box {
|
||||
.robotic-arm-status {
|
||||
.coord-display {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 12px;
|
||||
background: #070b1a;
|
||||
border: 1px solid #1c2a52;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.coord-item {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 6px 0;
|
||||
border-radius: 6px;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
|
||||
.coord-key {
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.key-x {
|
||||
color: #f87171;
|
||||
}
|
||||
|
||||
.key-y {
|
||||
color: #34d399;
|
||||
;
|
||||
}
|
||||
|
||||
.key-z {
|
||||
color: #38bdf8;
|
||||
;
|
||||
}
|
||||
|
||||
.coord-val {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #e8edf8;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.axis-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.axis-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 14px;
|
||||
background: #0c1228;
|
||||
border: 1px solid #1c2a52;
|
||||
border-radius: 10px;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: #2a3f7a;
|
||||
background: #3b82f608;
|
||||
}
|
||||
|
||||
.axis-label {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.x {
|
||||
background: #f8717126;
|
||||
color: #f87171;
|
||||
border: 1px solid #f871714d;
|
||||
}
|
||||
|
||||
.y {
|
||||
background: #34d39926;
|
||||
color: #34d399;
|
||||
border: 1px solid #34d3994d;
|
||||
}
|
||||
|
||||
.z {
|
||||
background: #38bdf826;
|
||||
color: #38bdf8;
|
||||
border: 1px solid #38bdf84d;
|
||||
}
|
||||
|
||||
|
||||
.axis-slider-wrap {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 4px;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.control-panel {
|
||||
margin-top: 12px;
|
||||
background: rgba(20, 20, 40, 0.85);
|
||||
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;
|
||||
|
||||
label {
|
||||
width: 120px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
input[type=range] {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.angle {
|
||||
width: 50px;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,314 @@
|
||||
<template>
|
||||
<div class="panel">
|
||||
<div class="card">
|
||||
<div class="card__head">
|
||||
<h3>方向控制</h3>
|
||||
<el-tag size="small" effect="dark" type="info">长按持续移动</el-tag>
|
||||
</div>
|
||||
|
||||
<div class="dpad">
|
||||
<button class="dpad__btn dpad__btn--up" @mousedown="onPress('forward')" @mouseup="onRelease"
|
||||
@mouseleave="onRelease" @touchstart.prevent="onPress('forward')" @touchend.prevent="onRelease">
|
||||
<svg viewBox="0 0 14 14">
|
||||
<path d="M7 1L13 11H1Z" fill="currentColor" />
|
||||
</svg>
|
||||
<small>前进</small>
|
||||
</button>
|
||||
|
||||
<button class="dpad__btn dpad__btn--lt" @mousedown="onPress('left')" @mouseup="onRelease"
|
||||
@touchstart.prevent="onPress('left')" @touchend.prevent="onRelease">
|
||||
<svg viewBox="0 0 14 14">
|
||||
<path d="M1 7L11 1V13Z" fill="currentColor" />
|
||||
</svg>
|
||||
<small>左转</small>
|
||||
</button>
|
||||
|
||||
<div class="dpad__core"><span /></div>
|
||||
|
||||
<button class="dpad__btn dpad__btn--rt" @mousedown="onPress('right')" @mouseup="onRelease"
|
||||
@touchstart.prevent="onPress('right')" @touchend.prevent="onRelease">
|
||||
<svg viewBox="0 0 14 14">
|
||||
<path d="M13 7L3 1V13Z" fill="currentColor" />
|
||||
</svg>
|
||||
<small>右转</small>
|
||||
</button>
|
||||
|
||||
<button class="dpad__btn dpad__btn--dn" @mousedown="onPress('backward')" @mouseup="onRelease"
|
||||
@touchstart.prevent="onPress('backward')" @touchend.prevent="onRelease">
|
||||
<svg viewBox="0 0 14 14">
|
||||
<path d="M7 13L1 3H13Z" fill="currentColor" />
|
||||
</svg>
|
||||
<small>后退</small>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Parameters -->
|
||||
<div class="card">
|
||||
<div class="card__head">
|
||||
<h3>参数设置</h3>
|
||||
</div>
|
||||
|
||||
<div class="param" v-for="p in paramList" :key="p.key">
|
||||
<div class="param__head">
|
||||
<span>{{ p.label }}</span>
|
||||
<b class="param__val">{{ formatVal(p) }}</b>
|
||||
</div>
|
||||
<el-slider v-model="p.model" :min="p.min" :max="p.max" :step="p.step" :show-tooltip="false" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
<script setup>
|
||||
|
||||
const maxSpeed = ref(0.6)
|
||||
const rotationAngle = ref(0)
|
||||
|
||||
const paramList = reactive([
|
||||
{ key: 'speed', label: '速度', model: maxSpeed, min: 0, max: 1, step: 0.1, unit: 'm/s', digits: 1 },
|
||||
{ key: 'rot', label: '旋转角速度', model: rotationAngle, min: -1, max: 1, step: 0.1, unit: 'm/s', digits: 0 },
|
||||
])
|
||||
|
||||
function formatVal(p) {
|
||||
const v = typeof p.model === 'object' ? p.model.value : p.model
|
||||
return (typeof v === 'number' ? v.toFixed(p.digits) : v) + ' ' + p.unit
|
||||
}
|
||||
|
||||
const moveRobotData = ref({
|
||||
vx: 0,
|
||||
vy: 0,
|
||||
w: 0,
|
||||
duration: -1
|
||||
})
|
||||
|
||||
/* long-press stubs */
|
||||
const onPress = async (_dir) => {
|
||||
if (_dir === 'forward') {
|
||||
moveRobotData.value.vx = maxSpeed.value
|
||||
} else if (_dir === 'backward') {
|
||||
moveRobotData.value.vx = -maxSpeed.value
|
||||
} else if (_dir === 'left') {
|
||||
moveRobotData.value.vy = maxSpeed.value
|
||||
} else {
|
||||
moveRobotData.value.vy = -maxSpeed.value
|
||||
}
|
||||
moveRobotData.value.w = rotationAngle.value
|
||||
// const res = await moveRobot({
|
||||
// ip: robotStore.ip,
|
||||
// deviceId: VITE_DEVICE_ID,
|
||||
// ...moveRobotData.value
|
||||
// })
|
||||
}
|
||||
|
||||
function onRelease() {
|
||||
moveRobotData.value.vx = 0;
|
||||
moveRobotData.value.vy = 0;
|
||||
// stopRobot({
|
||||
// ip: robotStore.ip,
|
||||
// deviceId: VITE_DEVICE_ID
|
||||
// })
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.panel {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
overflow-y: auto;
|
||||
// background: #0F1E38;
|
||||
|
||||
.card {
|
||||
padding: 18px 20px;
|
||||
border-bottom: 1px solid rgba(0, 195, 255, 0.08);
|
||||
|
||||
&__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
|
||||
h3 {
|
||||
font-family: 'Rajdhani', sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 1.2px;
|
||||
color: #475a72;
|
||||
margin: 0;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-tag) {
|
||||
font-size: 10px;
|
||||
height: 20px;
|
||||
background: rgba(0, 195, 255, 0.08);
|
||||
border-color: rgba(0,195,255,0.08);
|
||||
color: #00c3ff;
|
||||
}
|
||||
|
||||
.dpad {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 76px);
|
||||
grid-template-rows: repeat(3, 64px);
|
||||
gap: 6px;
|
||||
justify-content: center;
|
||||
margin-bottom: 18px;
|
||||
|
||||
&__btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
border: 1px solid rgba(0, 195, 255, 0.08);
|
||||
border-radius: 8px;
|
||||
background: #0d1a3b;
|
||||
color: #c0cfdf;
|
||||
cursor: pointer;
|
||||
transition: all .15s ease;
|
||||
|
||||
svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
small {
|
||||
font-size: 10px;
|
||||
letter-spacing: .5px;
|
||||
color: #475a72;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: rgba(0, 195, 255, 0.08);
|
||||
background: #101930;
|
||||
color: #00c3ff;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: #00c3ff;
|
||||
color: #060a12;
|
||||
border-color: #00c3ff;
|
||||
box-shadow: 0 0 24px rgba(0,195,255,0.14);
|
||||
transform: scale(.94);
|
||||
|
||||
small {
|
||||
color: #060a12;
|
||||
}
|
||||
}
|
||||
|
||||
&--up {
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
&--lt {
|
||||
grid-column: 1;
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
&--rt {
|
||||
grid-column: 3;
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
&--dn {
|
||||
grid-column: 2;
|
||||
grid-row: 3;
|
||||
}
|
||||
}
|
||||
|
||||
&__core {
|
||||
grid-column: 2;
|
||||
grid-row: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid rgba(0,195,255,0.08);
|
||||
border-radius: 8px;
|
||||
background: rgba(0, 195, 255, 0.03);
|
||||
|
||||
span {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
background: #00c3ff;
|
||||
opacity: .3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.estop {
|
||||
width: 100%;
|
||||
height: 42px !important;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 1px;
|
||||
border-color: #ff2d55 !important;
|
||||
color: #ff2d55 !important;
|
||||
background: rgba(255, 45, 85, 0.06) !important;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 45, 85, 0.14) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.param {
|
||||
margin-bottom: 18px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&__head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
margin-bottom: 8px;
|
||||
font-size: 13px;
|
||||
color: #475a72;
|
||||
}
|
||||
|
||||
&__val {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #00c3ff;
|
||||
}
|
||||
|
||||
:deep(.el-slider) {
|
||||
.el-slider__runway {
|
||||
height: 4px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.el-slider__bar {
|
||||
height: 4px;
|
||||
background: #00c3ff;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.el-slider__button-wrapper {
|
||||
top: -16px;
|
||||
}
|
||||
|
||||
.el-slider__button {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border: 2px solid #00c3ff;
|
||||
background: #0f1628;
|
||||
box-shadow: 0 0 8px rgba(0, 195, 255, 0.3);
|
||||
transition: box-shadow .2s;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 0 14px rgba(0, 195, 255, 0.35);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,400 @@
|
||||
<template>
|
||||
<div class="robotic-arm-box">
|
||||
<div class="robotic-arm-status">
|
||||
<div class="coord-display">
|
||||
<div class="coord-item">
|
||||
<div class="coord-key key-x">X</div>
|
||||
<div class="coord-val" id="coordX">{{ pose.x?.toFixed(3) }}</div>
|
||||
</div>
|
||||
<div class="coord-item">
|
||||
<div class="coord-key key-y">Y</div>
|
||||
<div class="coord-val" id="coordY">{{ pose.y?.toFixed(3) }}</div>
|
||||
</div>
|
||||
<div class="coord-item">
|
||||
<div class="coord-key key-z">Z</div>
|
||||
<div class="coord-val" id="coordZ">{{ pose.z?.toFixed(3) }}</div>
|
||||
</div>
|
||||
<div class="coord-item">
|
||||
<div class="coord-key key-x">RX</div>
|
||||
<div class="coord-val" id="coordX">{{ pose.rx?.toFixed(3) }}</div>
|
||||
</div>
|
||||
<div class="coord-item">
|
||||
<div class="coord-key key-y">RY</div>
|
||||
<div class="coord-val" id="coordY">{{ pose.ry?.toFixed(3) }}</div>
|
||||
</div>
|
||||
<div class="coord-item">
|
||||
<div class="coord-key key-z">RZ</div>
|
||||
<div class="coord-val" id="coordZ">{{ pose.rz?.toFixed(3) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="axis-group">
|
||||
<!-- X Axis -->
|
||||
<div class="axis-row x">
|
||||
<div class="axis-label x">X</div>
|
||||
<div class="axis-slider-wrap">
|
||||
<div class="axis-name">前后平移</div>
|
||||
<div>
|
||||
<el-button
|
||||
type="primary"
|
||||
circle
|
||||
@mousedown="move('x', '-')"
|
||||
@mouseup="stop"
|
||||
@touchstart.prevent="move('x', '-')"
|
||||
@touchend.prevent="stop"
|
||||
>
|
||||
<el-icon size="24">
|
||||
<Remove />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
circle
|
||||
@mousedown="move('x', '+')"
|
||||
@mouseup="stop"
|
||||
@touchstart.prevent="move('x', '+')"
|
||||
@touchend.prevent="stop"
|
||||
>
|
||||
<el-icon size="24">
|
||||
<CirclePlus />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Y Axis -->
|
||||
<div class="axis-row y">
|
||||
<div class="axis-label y">Y</div>
|
||||
<div class="axis-slider-wrap">
|
||||
<div class="axis-name">左右平移</div>
|
||||
<div>
|
||||
<el-button
|
||||
type="primary"
|
||||
circle
|
||||
@mousedown="move('y', '-')"
|
||||
@mouseup="stop"
|
||||
@touchstart.prevent="move('y', '-')"
|
||||
@touchend.prevent="stop"
|
||||
>
|
||||
<el-icon size="24">
|
||||
<Remove />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
circle
|
||||
@mousedown="move('y', '+')"
|
||||
@mouseup="stop"
|
||||
@touchstart.prevent="move('y', '+')"
|
||||
@touchend.prevent="stop"
|
||||
>
|
||||
<el-icon size="24">
|
||||
<CirclePlus />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Z Axis -->
|
||||
<div class="axis-row z">
|
||||
<div class="axis-label z">Z</div>
|
||||
<div class="axis-slider-wrap">
|
||||
<div class="axis-name">上下平移</div>
|
||||
<div>
|
||||
<el-button
|
||||
type="primary"
|
||||
circle
|
||||
@mousedown="move('z', '-')"
|
||||
@mouseup="stop"
|
||||
@touchstart.prevent="move('z', '-')"
|
||||
@touchend.prevent="stop"
|
||||
>
|
||||
<el-icon size="24">
|
||||
<Remove />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
circle
|
||||
@mousedown="move('z', '+')"
|
||||
@mouseup="stop"
|
||||
@touchstart.prevent="move('z', '+')"
|
||||
@touchend.prevent="stop"
|
||||
>
|
||||
<el-icon size="24">
|
||||
<CirclePlus />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="speed-container">
|
||||
<div class="label">速度:</div>
|
||||
<el-slider v-model="speed" :min="0.1" :step="0.1" :max="4" />
|
||||
<div class="proportion">{{( speed / 4 * 100).toFixed(0) }} %</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted, watch } from 'vue';
|
||||
import { getPoseApi, speedLApi, stopMotionApi } from '@/api/inspection/cockpit';
|
||||
import { reactive } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
robot: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'end'
|
||||
}
|
||||
})
|
||||
|
||||
const pose = ref({
|
||||
x: 0,
|
||||
y: 0,
|
||||
z: 0,
|
||||
rx: 0,
|
||||
ry: 0,
|
||||
rz: 0
|
||||
})
|
||||
|
||||
const speed = ref(1) // 速度百分比,范围 0.1 - 5
|
||||
|
||||
/**
|
||||
* 获取机械臂末端状态信息
|
||||
*/
|
||||
const getPose = async (robot) => {
|
||||
try {
|
||||
const res = await getPoseApi({
|
||||
deviceId: import.meta.env.VITE_INSPECTION_ALM_DEVICEID,
|
||||
terminalId: robot.terminalId
|
||||
})
|
||||
if (res.code === 200) {
|
||||
const { x, y, z, rx, ry, rz } = res.data
|
||||
pose.value = { x, y, z, rx, ry, rz }
|
||||
} else {
|
||||
console.error('获取姿态失败:', res.message)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取姿态失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const speedData = reactive({
|
||||
vx: 0,
|
||||
vy: 0,
|
||||
vz: 0,
|
||||
wx: 0,
|
||||
wy: 0,
|
||||
wz: 0
|
||||
})
|
||||
|
||||
/**
|
||||
* 机械臂移动
|
||||
* @param axis 坐标轴
|
||||
* @param direction 方向
|
||||
*/
|
||||
const move = async (axis, direction) => {
|
||||
speedData.vx = 0
|
||||
speedData.vy = 0
|
||||
speedData.vz = 0
|
||||
speedData.wx = 0
|
||||
speedData.wy = 0
|
||||
speedData.wz = 0
|
||||
if (props.type === 'end') {
|
||||
// 末端移动
|
||||
const speedValue = speed.value
|
||||
switch (axis) {
|
||||
case 'x':
|
||||
speedData.vx = direction === '+' ? speedValue : -speedValue
|
||||
break
|
||||
case 'y':
|
||||
speedData.vy = direction === '+' ? speedValue : -speedValue
|
||||
break
|
||||
case 'z':
|
||||
speedData.vz = direction === '+' ? speedValue : -speedValue
|
||||
break
|
||||
}
|
||||
} else if (props.type === 'posture') {
|
||||
// 关节移动
|
||||
const jointSpeed = speed.value
|
||||
switch (axis) {
|
||||
case 'x':
|
||||
speedData.wx = direction === '+' ? jointSpeed : -jointSpeed
|
||||
break
|
||||
case 'y':
|
||||
speedData.wy = direction === '+' ? jointSpeed : -jointSpeed
|
||||
break
|
||||
case 'z':
|
||||
speedData.wz = direction === '+' ? jointSpeed : -jointSpeed
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
speedLApi({
|
||||
deviceId: import.meta.env.VITE_INSPECTION_ALM_DEVICEID,
|
||||
terminalId: props.robot.terminalId,
|
||||
duration: 60,
|
||||
acceleration: speed.value + 0.1,
|
||||
...speedData
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止机械臂移动
|
||||
*/
|
||||
const stop = async () => {
|
||||
await stopMotionApi({
|
||||
deviceId: import.meta.env.VITE_INSPECTION_ALM_DEVICEID,
|
||||
terminalId: props.robot.terminalId
|
||||
})
|
||||
}
|
||||
|
||||
const timer = ref(null)
|
||||
watch(() => props.robot, (newRobot) => {
|
||||
if (newRobot && newRobot.terminalId) {
|
||||
if (timer.value) {
|
||||
clearInterval(timer.value)
|
||||
}
|
||||
timer.value = setInterval(() => {
|
||||
getPose(newRobot)
|
||||
}, 100)
|
||||
}
|
||||
}, { immediate: true })
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.robotic-arm-box {
|
||||
margin-top: 12px;
|
||||
|
||||
.robotic-arm-status {
|
||||
.coord-display {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
padding: 12px;
|
||||
background: #070b1a;
|
||||
border: 1px solid #1c2a52;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.coord-item {
|
||||
width: calc((100% - 24px) / 3);
|
||||
text-align: center;
|
||||
padding: 6px 0;
|
||||
border-radius: 6px;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
|
||||
.coord-key {
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.key-x {
|
||||
color: #f87171;
|
||||
}
|
||||
|
||||
.key-y {
|
||||
color: #34d399;
|
||||
;
|
||||
}
|
||||
|
||||
.key-z {
|
||||
color: #38bdf8;
|
||||
;
|
||||
}
|
||||
|
||||
.coord-val {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #e8edf8;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.axis-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.axis-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 14px;
|
||||
background: #0c1228;
|
||||
border: 1px solid #1c2a52;
|
||||
border-radius: 10px;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: #2a3f7a;
|
||||
background: #3b82f608;
|
||||
}
|
||||
|
||||
.axis-label {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.x {
|
||||
background: #f8717126;
|
||||
color: #f87171;
|
||||
border: 1px solid #f871714d;
|
||||
}
|
||||
|
||||
.y {
|
||||
background: #34d39926;
|
||||
color: #34d399;
|
||||
border: 1px solid #34d3994d;
|
||||
}
|
||||
|
||||
.z {
|
||||
background: #38bdf826;
|
||||
color: #38bdf8;
|
||||
border: 1px solid #38bdf84d;
|
||||
}
|
||||
|
||||
|
||||
.axis-slider-wrap {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 4px;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.speed-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.label,
|
||||
.proportion {
|
||||
width: 100px;
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -41,46 +41,35 @@ const initScene = () => {
|
||||
const height = container.clientHeight || 600;
|
||||
scene = new THREE.Scene();
|
||||
scene.background = new THREE.Color('#000d2e');
|
||||
camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 1000);
|
||||
camera.position.set(2, 2, 3);
|
||||
|
||||
camera = new THREE.PerspectiveCamera(35, width / height, 0.1, 1000);
|
||||
camera.position.set(0, 0, 4);
|
||||
|
||||
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
|
||||
renderer.setSize(width, height);
|
||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
||||
renderer.shadowMap.enabled = true;
|
||||
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
||||
renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
||||
renderer.toneMappingExposure = 1.2;
|
||||
|
||||
container.appendChild(renderer.domElement);
|
||||
|
||||
controls = new OrbitControls(camera, renderer.domElement);
|
||||
controls.enableDamping = true;
|
||||
controls.dampingFactor = 0.1;
|
||||
controls.target.set(0, 0.5, 0);
|
||||
controls.update();
|
||||
|
||||
const ambient = new THREE.AmbientLight(0x404060, 0.6);
|
||||
const ambient = new THREE.AmbientLight(0xffffff, 0.6)
|
||||
scene.add(ambient);
|
||||
const main = new THREE.DirectionalLight(0xffffff, 1.0);
|
||||
main.position.set(5, 10, 7);
|
||||
main.castShadow = true;
|
||||
scene.add(main);
|
||||
const fill = new THREE.DirectionalLight(0x8888ff, 0.4);
|
||||
fill.position.set(-5, 0, 5);
|
||||
scene.add(fill);
|
||||
const rim = new THREE.DirectionalLight(0xffffff, 0.3);
|
||||
rim.position.set(0, -5, 5);
|
||||
scene.add(rim);
|
||||
// const grid = new THREE.GridHelper(3, 20, 0x444466, 0x333355);
|
||||
// grid.position.y = -0.5;
|
||||
// scene.add(grid);
|
||||
|
||||
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.0);
|
||||
directionalLight.position.set(5, 10, 7);
|
||||
directionalLight.castShadow = true;
|
||||
scene.add(directionalLight);
|
||||
};
|
||||
|
||||
|
||||
// ============ 关节控制(无需修改) ============
|
||||
const updateJoint = (jointName, angle) => {
|
||||
if (!robotInstance) return;
|
||||
|
||||
const joint = robotInstance.joints[jointName];
|
||||
|
||||
if (!joint) {
|
||||
console.warn(`未找到关节: ${jointName}`);
|
||||
return;
|
||||
@ -93,6 +82,7 @@ const updateJoint = (jointName, angle) => {
|
||||
joint.setJointValue(angle);
|
||||
}
|
||||
|
||||
|
||||
// 同步滑块显示值
|
||||
const control = jointControls.find(c => c.name === jointName);
|
||||
if (control) control.value = angle;
|
||||
516
src/views/inspection/cockpit/components/ManualTakeover/index.vue
Normal file
516
src/views/inspection/cockpit/components/ManualTakeover/index.vue
Normal file
@ -0,0 +1,516 @@
|
||||
<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" />
|
||||
<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.1" :step="0.1" :max="9" />
|
||||
<div class="proportion">{{( jointSpeed / 9 * 100).toFixed(0) }} %</div>
|
||||
</div>
|
||||
</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">
|
||||
<UrdfViewer ref="urdfViewerRef" base-path="/inspection/elfin10/" />
|
||||
</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 { 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 } from '@/api/inspection/cockpit'
|
||||
import { Remove, CirclePlus } from '@element-plus/icons-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_ALM_DEVICEID,
|
||||
terminalId: activeBotData.value.terminalId
|
||||
})
|
||||
jointControls.value = urdfViewerRef.value.jointControls
|
||||
if (animationId.value) {
|
||||
clearInterval(animationId.value)
|
||||
}
|
||||
animationId.value = setInterval(() => {
|
||||
getJointState(activeBotData.value)
|
||||
}, 100)
|
||||
}
|
||||
}
|
||||
|
||||
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',
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取关节状态
|
||||
* @param robot
|
||||
*/
|
||||
const getJointState = async (robot) => {
|
||||
try {
|
||||
const res = await getJointStateApi({
|
||||
deviceId: import.meta.env.VITE_INSPECTION_ALM_DEVICEID,
|
||||
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 {
|
||||
console.error('获取关节状态失败:', res.message)
|
||||
}
|
||||
} catch (error) {
|
||||
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_ALM_DEVICEID,
|
||||
terminalId: activeBotData.value.terminalId,
|
||||
acceleration: jointSpeed.value + 0.1,
|
||||
duration: 60,
|
||||
velocities: velocities
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止机械臂移动
|
||||
*/
|
||||
const stop = async () => {
|
||||
await stopMotionApi({
|
||||
deviceId: import.meta.env.VITE_INSPECTION_ALM_DEVICEID,
|
||||
terminalId: activeBotData.value.terminalId
|
||||
})
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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>
|
||||
@ -13,7 +13,7 @@
|
||||
<script setup>
|
||||
import Header from './components/Header.vue';
|
||||
import MapOverview from './components/MapOverview.vue';
|
||||
import ManualTakeover from './components/ManualTakeover.vue';
|
||||
import ManualTakeover from './components/ManualTakeover/index.vue';
|
||||
import Monitor from './components/Monitor.vue';
|
||||
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user