inspection-host-computer/src/views/arm/MechanicalArm.vue

621 lines
17 KiB
Vue
Raw Normal View History

2026-06-29 16:19:37 +08:00
<template>
<div class="robot-teach-container">
<!-- 主体内容区 -->
<div class="main-wrap">
<!-- 左侧数据面板 -->
<div class="left-panel">
<!-- TCP 笛卡尔坐标 -->
<div class="panel-card">
<div class="row dual-row">
<div class="coord-item">
<span class="key">X</span>
<el-input v-model.number="tcp.x" suffix="mm" />
</div>
<div class="coord-item">
<span class="key">RX</span>
<el-input v-model.number="tcp.rx" suffix="°" />
</div>
</div>
<div class="row dual-row">
<div class="coord-item">
<span class="key">Y</span>
<el-input v-model.number="tcp.y" suffix="mm" />
</div>
<div class="coord-item">
<span class="key">RY</span>
<el-input v-model.number="tcp.ry" suffix="°" />
</div>
</div>
<div class="row dual-row">
<div class="coord-item">
<span class="key">Z</span>
<el-input v-model.number="tcp.z" suffix="mm" />
</div>
<div class="coord-item">
<span class="key">RZ</span>
<el-input v-model.number="tcp.rz" suffix="°" />
</div>
</div>
</div>
<div class="joint-list">
<div class="title">关节控制</div>
<div v-for="joint in jointControls" :key="joint.name" class="joint-item">
<label :for="joint.name">{{ joint.name }}</label>
2026-07-02 16:21:38 +08:00
<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>
2026-06-29 16:19:37 +08:00
</div>
</div>
<div class="speed-container">
<div class="label">速度</div>
2026-07-02 16:21:38 +08:00
<el-slider v-model="jointSpeed" :min="0.1" :step="0.1" :max="9" />
<div class="proportion">{{ (jointSpeed / 9 * 100).toFixed(0) }} %</div>
2026-06-29 16:19:37 +08:00
</div>
</div>
<!-- 中间3D机械臂空白区域 -->
<div class="center-view">
<UrdfView ref="urdfViewerRef" />
<!-- 底部功能按钮 -->
<!-- <div class="center-bottom-btns">
<el-button type="success" size="large">零力示教</el-button>
<el-button size="large">回到原点</el-button>
<el-button size="large">Z轴对齐</el-button>
</div> -->
</div>
<!-- 右侧摇杆操作面板 -->
<div class="right-panel">
<div class="controller">
<div class="title">末端控制</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>
2026-07-02 16:21:38 +08:00
<el-button type="primary" circle @mousedown="move('x', '-', 'end')" @mouseup="stop"
@touchstart.prevent="move('x', '-')" @touchend.prevent="stop">
<el-icon size="24">
<Remove />
</el-icon>
2026-06-29 16:19:37 +08:00
</el-button>
2026-07-02 16:21:38 +08:00
<el-button type="primary" circle @mousedown="move('x', '+', 'end')" @mouseup="stop"
@touchstart.prevent="move('x', '+')" @touchend.prevent="stop">
<el-icon size="24">
<CirclePlus />
</el-icon>
2026-06-29 16:19:37 +08:00
</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>
2026-07-02 16:21:38 +08:00
<el-button type="primary" circle @mousedown="move('y', '-', 'end')" @mouseup="stop"
@touchstart.prevent="move('y', '-')" @touchend.prevent="stop">
<el-icon size="24">
<Remove />
</el-icon>
2026-06-29 16:19:37 +08:00
</el-button>
2026-07-02 16:21:38 +08:00
<el-button type="primary" circle @mousedown="move('y', '+', 'end')" @mouseup="stop"
@touchstart.prevent="move('y', '+')" @touchend.prevent="stop">
<el-icon size="24">
<CirclePlus />
</el-icon>
2026-06-29 16:19:37 +08:00
</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>
2026-07-02 16:21:38 +08:00
<el-button type="primary" circle @mousedown="move('z', '-', 'end')" @mouseup="stop"
@touchstart.prevent="move('z', '-')" @touchend.prevent="stop">
<el-icon size="24">
<Remove />
</el-icon>
2026-06-29 16:19:37 +08:00
</el-button>
2026-07-02 16:21:38 +08:00
<el-button type="primary" circle @mousedown="move('z', '+', 'end')" @mouseup="stop"
@touchstart.prevent="move('z', '+')" @touchend.prevent="stop">
<el-icon size="24">
<CirclePlus />
</el-icon>
2026-06-29 16:19:37 +08:00
</el-button>
</div>
</div>
</div>
</div>
2026-07-02 16:21:38 +08:00
<div class="speed-container">
<div class="label">速度</div>
<el-slider v-model="endSpeed" :min="0.1" :step="0.1" :max="4" />
<div class="proportion">{{ (endSpeed / 4 * 100).toFixed(0) }} %</div>
</div>
2026-06-29 16:19:37 +08:00
</div>
<div class="controller">
<div class="title">姿态控制</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>
2026-07-02 16:21:38 +08:00
<el-button type="primary" circle @mousedown="move('x', '-', 'posture')" @mouseup="stop"
@touchstart.prevent="move('x', '-')" @touchend.prevent="stop">
<el-icon size="24">
<Remove />
</el-icon>
2026-06-29 16:19:37 +08:00
</el-button>
2026-07-02 16:21:38 +08:00
<el-button type="primary" circle @mousedown="move('x', '+', 'posture')" @mouseup="stop"
@touchstart.prevent="move('x', '+')" @touchend.prevent="stop">
<el-icon size="24">
<CirclePlus />
</el-icon>
2026-06-29 16:19:37 +08:00
</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>
2026-07-02 16:21:38 +08:00
<el-button type="primary" circle @mousedown="move('y', '-', 'posture')" @mouseup="stop"
@touchstart.prevent="move('y', '-')" @touchend.prevent="stop">
<el-icon size="24">
<Remove />
</el-icon>
2026-06-29 16:19:37 +08:00
</el-button>
2026-07-02 16:21:38 +08:00
<el-button type="primary" circle @mousedown="move('y', '+', 'posture')" @mouseup="stop"
@touchstart.prevent="move('y', '+')" @touchend.prevent="stop">
<el-icon size="24">
<CirclePlus />
</el-icon>
2026-06-29 16:19:37 +08:00
</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>
2026-07-02 16:21:38 +08:00
<el-button type="primary" circle @mousedown="move('z', '-', 'posture')" @mouseup="stop"
@touchstart.prevent="move('z', '-')" @touchend.prevent="stop">
<el-icon size="24">
<Remove />
</el-icon>
2026-06-29 16:19:37 +08:00
</el-button>
2026-07-02 16:21:38 +08:00
<el-button type="primary" circle @mousedown="move('z', '+', 'posture')" @mouseup="stop"
@touchstart.prevent="move('z', '+')" @touchend.prevent="stop">
<el-icon size="24">
<CirclePlus />
</el-icon>
2026-06-29 16:19:37 +08:00
</el-button>
</div>
</div>
</div>
</div>
2026-07-02 16:21:38 +08:00
<div class="speed-container">
<div class="label">速度</div>
<el-slider v-model="postureSpeed" :min="0.1" :step="0.1" :max="4" />
<div class="proportion">{{ (postureSpeed / 4 * 100).toFixed(0) }} %</div>
</div>
2026-06-29 16:19:37 +08:00
</div>
</div>
</div>
</div>
</template>
<script setup>
2026-07-02 16:21:38 +08:00
import { nextTick, onMounted, onUnmounted, ref } from 'vue'
2026-06-29 16:19:37 +08:00
import UrdfView from './UrdfView.vue'
2026-07-02 16:21:38 +08:00
import { getPoseApi, getJointStateApi, moveJApi, torqueOnApi, speedJApi, stopMotionApi, speedLApi } from '@/api/arm'
2026-06-29 16:19:37 +08:00
import { Remove, CirclePlus } from '@element-plus/icons-vue'
2026-07-02 16:21:38 +08:00
import { useRobotStore } from '@/stores/robot'
const robotStore = useRobotStore()
const deviceId = import.meta.env.VITE_ARM_DEVICE_ID
2026-06-29 16:19:37 +08:00
const urdfViewerRef = ref()
const jointControls = ref()
// TCP坐标数据
const tcp = ref({
x: 46.691,
y: 385.321,
z: 141.877,
rx: -174.595,
ry: 4.212,
rz: -72.937
})
2026-07-02 16:21:38 +08:00
const getPose = async () => {
const res = await getPoseApi({
ip: robotStore.ip,
deviceId: deviceId
})
if (res.code === 200) {
const { x, y, z, rx, ry, rz } = res.data
tcp.value = { x, y, z, rx, ry, rz }
}
}
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: deviceId,
ip: robotStore.ip,
})
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])
})
}, 500)
} else {
console.error('获取关节状态失败:', res.message)
}
} catch (error) {
console.error('获取关节状态失败:', error)
}
}
2026-06-29 16:19:37 +08:00
// 速度
2026-07-02 16:21:38 +08:00
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: deviceId,
ip: robotStore.ip,
acceleration: jointSpeed.value * (_dir === '+' ? 1 : -1) + 0.1,
duration: 60,
velocities: velocities
})
}
2026-06-29 16:19:37 +08:00
2026-07-02 16:21:38 +08:00
const postureSpeed = ref(0.8)
const endSpeed = ref(0.8)
const speedData = reactive({
vx: 0,
vy: 0,
vz: 0,
wx: 0,
wy: 0,
wz: 0
})
/**
* 机械臂移动
* @param axis 坐标轴
* @param direction 方向
*/
const move = async (axis, direction, type) => {
speedData.vx = 0
speedData.vy = 0
speedData.vz = 0
speedData.wx = 0
speedData.wy = 0
speedData.wz = 0
if (type === 'end') {
// 末端移动
const speedValue = endSpeed.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 (type === 'posture') {
// 关节移动
const jointSpeed = postureSpeed.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: deviceId,
ip: robotStore.ip,
duration: 60,
acceleration: type === 'posture' ? postureSpeed.value + 0.1 : endSpeed.value + 0.1,
velocity: {
...speedData
}
})
2026-06-29 16:19:37 +08:00
}
2026-07-02 16:21:38 +08:00
/**
* 停止机械臂移动
*/
const stop = async () => {
await stopMotionApi({
deviceId: deviceId,
ip: robotStore.ip
})
}
const timer = ref(null)
2026-06-29 16:19:37 +08:00
onMounted(() => {
nextTick(() => {
jointControls.value = urdfViewerRef.value.jointControls
2026-07-02 16:21:38 +08:00
stop()
getPose()
if (timer.value) {
clearInterval(timer.value)
}
timer.value = setInterval(() => {
getJointState()
}, 100)
2026-06-29 16:19:37 +08:00
})
})
2026-07-02 16:21:38 +08:00
onUnmounted(() => {
if (timer.value) {
clearInterval(timer.value)
}
})
2026-06-29 16:19:37 +08:00
</script>
<style lang="scss" scoped>
.robot-teach-container {
height: 100%;
background: $bg-deep;
color: $text;
font-family: $font-b;
overflow: hidden;
user-select: none;
// 主体三栏布局
.main-wrap {
width: 100%;
height: 100%;
display: flex;
overflow: hidden;
// 左侧面板
.left-panel {
width: 400px;
padding: 20px;
overflow-y: auto;
// background: #030729;
background: $bg-body;
border-right: 1px solid rgba(0, 195, 255, 0.08);
.panel-card {
padding: 12px;
.row {
display: flex;
gap: 20px;
margin-bottom: 12px;
}
}
.joint-list {
margin-top: 24px;
// background: #161933;
padding: 15px;
border-radius: 8px;
color: #ccc;
.title {
font-size: 20px;
font-weight: bold;
margin-bottom: 10px;
}
.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: #0a0b10;
2026-06-30 17:32:54 +08:00
&:hover {
border-color: #2a3f7a;
background: rgba(59, 130, 246, 0.031372549);
}
2026-06-29 16:19:37 +08:00
label {
width: 120px;
font-size: 16px;
font-weight: bold;
}
input[type=range] {
flex: 1;
}
.angle {
width: 50px;
text-align: right;
}
}
}
}
// 中间3D区域
.center-view {
flex: 1;
display: flex;
}
// 右侧摇杆面板
.right-panel {
width: 400px;
padding: 20px;
// background: #030729;
background: $bg-body;
border-left: 1px solid rgba(0, 195, 255, 0.08);
.controller {
margin-bottom: 60px;
.title {
font-size: 20px;
font-weight: bold;
margin-bottom: 10px;
}
.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: #0a0b10;
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;
}
}
}
}
}
2026-07-02 16:21:38 +08:00
.speed-container {
display: flex;
align-items: center;
justify-content: space-between;
.label,
.proportion {
width: 100px;
text-align: center;
}
}
2026-06-29 16:19:37 +08:00
}
}
// element-plus深色输入框覆盖
:deep(.el-input__wrapper) {
background-color: #2d323e;
box-shadow: $shadow;
.el-input__inner {
color: #fff;
}
}
:deep(.el-slider__runway) {
background: #333842;
}
</style>