CMVR-IOT-UI/src/views/device/register/components/MechanicalArm/index.vue

437 lines
12 KiB
Vue
Raw Normal View History

2025-08-21 14:11:58 +08:00
<script setup>
2025-11-20 16:04:46 +08:00
import { reactive, toRefs, onMounted, onUnmounted, watch, computed, ref } from "vue";
import { useRoute } from "vue-router";
2025-08-21 14:11:58 +08:00
import dayjs from "dayjs";
2025-11-20 16:04:46 +08:00
import { deviceTeach } from "@/api/device/register.js";
import { formatNumber } from "@/views/device/register/components/index.js";
import { convertNumbersToStringAndRemoveNulls } from "@/utils/fn.js";
2025-11-20 16:04:46 +08:00
import {
moveJ,
status
} from '@/api/device/arm';
2025-08-21 14:11:58 +08:00
const data = reactive({
form: {
workModule: 1,
posStep: 1,
postureStep: 1,
jointStep: 1,
joint1: 0.000012,
joint2: 0.000012,
joint3: 0.000012,
joint4: 0.000012,
joint5: 0.000012,
joint6: 0.000012,
2025-11-20 16:04:46 +08:00
joint7: 0.000012,
x: 11,
y: 11,
z: 11,
rx: 11,
ry: 11,
rz: 11,
arm: 1,
coordinate: 1,
2025-08-21 14:11:58 +08:00
},
speed: 20,
zeroPos: {
2025-08-21 14:11:58 +08:00
x: 0,
y: 0,
z: 0,
rx: 0,
ry: 0,
rz: 0,
},
initPos: {
x: 1,
y: 1,
z: 1,
rx: 1,
ry: 1,
rz: 1,
},
2025-08-21 14:11:58 +08:00
armList: [
{ label: '目标1', value: 1 },
{ label: '目标2', value: 2 },
{ label: '目标3', value: 3 },
2025-08-21 14:11:58 +08:00
],
time: dayjs().format("YYYY-MM-DD HH:mm:ss"),
});
2025-11-20 16:04:46 +08:00
const terminalId = ref('25449ff3fcc7b27da5f69462c5efcec2');
const deviceId = ref('spk1');
const { form, time, zeroPos, initPos, armList } = toRefs(data);
const route = useRoute();
2025-11-20 16:04:46 +08:00
const coordinateSystem = ref(null); // 用于获取容器元素
const buttonPositions = ref([]); // 存储按钮的动态位置
// 调整后的原始坐标(基于默认图片尺寸 1025x817px)
const initialPositions = [
{ target: "z_plus", x: 500, y: 80, label: "Z+" },
{ target: "rz_plus", x: 675, y: 124, label: "RZ+" },
{ target: "x_plus", x: 796, y: 245, label: "X+" },
{ target: "ry_plus", x: 840, y: 410, label: "RY+" },
{ target: "y_minus", x: 796, y: 570, label: "Y-" },
{ target: "ry_minus", x: 675, y: 696, label: "RY-" },
{ target: "z_minus", x: 500, y: 745, label: "Z-" },
{ target: "rx_minus", x: 345, y: 696, label: "RX-" },
{ target: "x_minus", x: 206, y: 570, label: "X-" },
{ target: "rx_plus", x: 180, y: 410, label: "RX+" },
{ target: "y_plus", x: 204, y: 245, label: "Y+" },
{ target: "rz_minus", x: 345, y: 124, label: "RZ-" },
];
// 默认图片尺寸
const ORIGINAL_IMAGE_WIDTH = 1025;
const ORIGINAL_IMAGE_HEIGHT = 817;
let timer = null;
2025-08-21 14:11:58 +08:00
onUnmounted(() => {
clearInterval(timer);
});
2025-08-21 14:11:58 +08:00
const getDeviceInfo = () => {
deviceTeach(devId.value).then((res) => {
let dataRes = formatNumber(res.data);
Object.assign(form.value, dataRes);
initPos.value.x = dataRes.x;
initPos.value.y = dataRes.y;
initPos.value.z = dataRes.z;
initPos.value.rx = dataRes.rx;
initPos.value.ry = dataRes.ry;
initPos.value.rz = dataRes.rz;
});
};
2025-08-21 14:11:58 +08:00
2025-11-20 16:04:46 +08:00
const updateButtonPositions = () => {
if (coordinateSystem.value) {
const { width, height } = coordinateSystem.value.getBoundingClientRect();
// 计算图片的实际显示尺寸(考虑 background-size: contain)
const imageAspectRatio = ORIGINAL_IMAGE_WIDTH / ORIGINAL_IMAGE_HEIGHT; // 1025 / 817 ≈ 1.255
const containerAspectRatio = width / height;
let imageWidth, imageHeight, offsetX, offsetY;
if (containerAspectRatio > imageAspectRatio) {
// 容器较宽,高度限制图片大小,上下有留白
imageHeight = height;
imageWidth = height * imageAspectRatio;
offsetX = (width - imageWidth) / 2; // 水平居中
offsetY = 0; // 顶部对齐
} else {
// 容器较窄,宽度限制图片大小,左右有留白
imageWidth = width;
imageHeight = width / imageAspectRatio;
offsetX = 0; // 左对齐
offsetY = (height - imageHeight) / 2; // 垂直居中
}
// 计算缩放比例(基于图片实际显示尺寸)
const scaleX = imageWidth / ORIGINAL_IMAGE_WIDTH;
const scaleY = imageHeight / ORIGINAL_IMAGE_HEIGHT;
// 更新按钮位置
buttonPositions.value = initialPositions.map((pos) => ({
...pos,
left: pos.x * scaleX + offsetX, // 按比例缩放 x 坐标并加上水平偏移
top: pos.y * scaleY + offsetY, // 按比例缩放 y 坐标并加上垂直偏移
}));
}
};
2025-08-21 14:11:58 +08:00
onMounted(() => {
getDeviceInfo();
2025-11-20 16:04:46 +08:00
// 初始计算按钮位置
updateButtonPositions();
// 添加按钮事件监听
const posControls = document.querySelectorAll(".pos-btn");
2025-08-21 14:11:58 +08:00
posControls.forEach((item) => {
item.addEventListener('click', (e) => {
const target = e.currentTarget.dataset.target;
const [pos, direction] = target.split("_");
const step = (pos === 'x' || pos === 'y' || pos === 'z') ? form.value.posStep : form.value.postureStep;
const delta = direction === 'plus' ? step : -step;
form.value[pos] += delta * (data.speed / 100);
});
});
2025-11-20 16:04:46 +08:00
// 监听窗口大小变化,更新按钮位置
window.addEventListener('resize', updateButtonPositions);
});
onUnmounted(() => {
window.removeEventListener('resize', updateButtonPositions);
});
watch(() => JSON.parse(JSON.stringify(form.value)), (newData) => {
const dataToSend = convertNumbersToStringAndRemoveNulls(newData);
2025-11-20 16:04:46 +08:00
moveJ({
"deviceId": deviceId.value,
"terminalId": terminalId.value,
"acc": 0.8,
"vel": 0.8,
"cmds": [
{
"jointName": "R_SHOULDER_P",
"rad": dataToSend.joint1,
"vel": 1.0
},
{
"jointName": "R_SHOULDER_R",
"rad": dataToSend.joint2,
"vel": 1.0
},
{
"jointName": "R_SHOULDER_Y",
"rad": dataToSend.joint3,
"vel": 1.0
},
{
"jointName": "R_ELBOW_R",
"rad": dataToSend.joint4,
"vel": 1.0
},
{
"jointName": "R_WRIST_P",
"rad": dataToSend.joint5,
"vel": 1.0
},
{
"jointName": "R_WRIST_Y",
"rad": dataToSend.joint6,
"vel": 1.0
},
{
"jointName": "R_WRIST_R",
"rad": dataToSend.joint7,
"vel": 1.0
}
]
})
});
2025-08-21 14:11:58 +08:00
const devId = computed(() => {
return route.path.split("/")[2];
});
2025-08-21 14:11:58 +08:00
const handleResets = (type) => {
if (type === "zero") {
Object.assign(form.value, zeroPos.value);
return;
2025-08-21 14:11:58 +08:00
}
if (type === "init") {
Object.assign(form.value, initPos.value);
2025-08-21 14:11:58 +08:00
}
};
2025-08-21 14:11:58 +08:00
</script>
<template>
<div class="mechanical-arm">
<div class="top card">
<div class="controls flex align-center" style="justify-content: flex-start;">
<div class="control-label">目标</div>
<el-select v-model="form.arm" placeholder="选择目标" style="width: 100px; margin-right: 20px;">
<el-option v-for="item in armList" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
<el-button @click="handleResets('zero')" style="margin-right: 10px;">零位姿态</el-button>
<el-button @click="handleResets('init')">初始位置</el-button>
2025-08-21 14:11:58 +08:00
</div>
<div class="pose-info flex align-center">
<div class="pose-item">X: {{ form.x.toFixed(6) }} m</div>
<div class="pose-item">Y: {{ form.y.toFixed(6) }} m</div>
<div class="pose-item">Z: {{ form.z.toFixed(6) }} m</div>
<div class="pose-item">RX: {{ form.rx.toFixed(6) }} deg</div>
<div class="pose-item">RY: {{ form.ry.toFixed(6) }} deg</div>
<div class="pose-item">RZ: {{ form.rz.toFixed(6) }} deg</div>
2025-08-21 14:11:58 +08:00
</div>
</div>
<div class="bottom flex">
2025-11-20 16:04:46 +08:00
<div class="left card" style="flex: 1; position: relative;">
<div class="title">姿态与位置控制</div>
2025-11-20 16:04:46 +08:00
<div class="coordinate-system" ref="coordinateSystem">
<div class="control-buttons">
<button
v-for="(pos, index) in buttonPositions"
:key="index"
class="pos-btn"
:data-target="pos.target"
:style="{ left: `${pos.left}px`, top: `${pos.top}px` }"
>
{{ pos.label }}
</button>
2025-08-21 14:11:58 +08:00
</div>
</div>
</div>
<div class="right card" style="flex: 1;">
<div class="title">关节控制 (deg)</div>
<div class="input-list">
<div class="input-item">
<span>关节1</span>
<el-input-number v-model="form.joint1" :step="form.jointStep" :precision="6" />
2025-08-21 14:11:58 +08:00
</div>
<div class="input-item">
<span>关节2</span>
<el-input-number v-model="form.joint2" :step="form.jointStep" :precision="6" />
</div>
<div class="input-item">
<span>关节3</span>
<el-input-number v-model="form.joint3" :step="form.jointStep" :precision="6" />
</div>
<div class="input-item">
<span>关节4</span>
<el-input-number v-model="form.joint4" :step="form.jointStep" :precision="6" />
</div>
<div class="input-item">
<span>关节5</span>
<el-input-number v-model="form.joint5" :step="form.jointStep" :precision="6" />
</div>
<div class="input-item">
<span>关节6</span>
<el-input-number v-model="form.joint6" :step="form.jointStep" :precision="6" />
</div>
2025-11-20 16:04:46 +08:00
<div class="input-item">
<span>关节7</span>
<el-input-number v-model="form.joint7" :step="form.jointStep" :precision="6" />
</div>
<div class="input-item">
<span>关节步进</span>
<el-input-number v-model="form.jointStep" :step="0.1" :precision="1" />
2025-08-21 14:11:58 +08:00
</div>
</div>
</div>
</div>
<div class="footer card flex align-center between">
<div class="time">{{ time }}</div>
<div class="speed flex align-center">
<span>速度</span>
<el-slider v-model="data.speed" :min="0" :max="100" style="width: 200px; margin: 0 10px;" />
<span>{{ data.speed }}%</span>
2025-08-21 14:11:58 +08:00
</div>
</div>
</div>
</template>
<style scoped lang="scss">
.mechanical-arm {
display: flex;
flex: 1;
overflow: hidden;
2025-08-21 14:11:58 +08:00
flex-direction: column;
box-sizing: border-box;
2025-08-21 14:11:58 +08:00
.card {
border: 1px solid #e4e7ed;
border-radius: 8px;
padding: 10px;
margin-bottom: 10px;
2025-08-21 14:11:58 +08:00
}
.controls {
gap: 10px;
padding: 10px;
2025-08-21 14:11:58 +08:00
}
.top {
.pose-info {
justify-content: space-around;
padding: 10px 0;
2025-08-21 14:11:58 +08:00
.pose-item {
font-size: 16px;
user-select: none;
}
2025-08-21 14:11:58 +08:00
}
}
.bottom {
flex: 1;
gap: 20px;
2025-08-21 14:11:58 +08:00
.title {
font-size: 16px;
font-weight: bold;
2025-08-21 14:11:58 +08:00
margin-bottom: 10px;
user-select: none;
}
.left {
display: flex;
flex-direction: column;
2025-11-20 16:04:46 +08:00
position: relative;
2025-08-21 14:11:58 +08:00
.coordinate-system {
flex: 1;
display: flex;
justify-content: center;
2025-08-21 14:11:58 +08:00
align-items: center;
2025-11-20 16:04:46 +08:00
position: relative;
background-color: #FFF;
background-image: url('@/assets/images/arm.png');
background-size: contain;
background-position: center;
background-repeat: no-repeat;
.control-buttons {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
.pos-btn {
position: absolute;
width: 40px;
height: 40px;
background: #4076ff;
color: white;
border: none;
cursor: pointer;
font-size: 16px;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
transform: translate(-50%, -50%); /* 按钮中心对齐坐标 */
}
}
}
}
2025-08-21 14:11:58 +08:00
.right {
.input-list {
display: flex;
flex-direction: column;
gap: 10px;
.input-item {
display: flex;
align-items: center;
justify-content: space-between;
2025-08-21 14:11:58 +08:00
span {
width: 80px;
2025-08-21 14:11:58 +08:00
user-select: none;
}
.el-input-number {
width: calc(100% - 90px);
2025-08-21 14:11:58 +08:00
}
}
}
}
}
2025-08-21 14:11:58 +08:00
.footer {
height: 50px;
2025-08-21 14:11:58 +08:00
.time {
font-size: 14px;
}
2025-08-21 14:11:58 +08:00
.speed {
gap: 10px;
2025-08-21 14:11:58 +08:00
span {
user-select: none;
2025-08-21 14:11:58 +08:00
}
}
}
}
</style>