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

385 lines
11 KiB
Vue
Raw Normal View History

2025-08-21 14:11:58 +08:00
<script setup>
import { computed, reactive, toRefs, onUnmounted, ref, onMounted } from "vue";
import { useRoute } from "vue-router";
import { CircleClose } from "@element-plus/icons-vue";
import { ElMessage } from "element-plus";
import { clearFault as clearArmFault } from "@/api/device/arm";
2025-12-09 10:33:05 +08:00
import UrdfViewer from "./components/UrdfViewer.vue";
import JointControl from "./components/JointControl.vue";
2025-08-21 14:11:58 +08:00
const route = useRoute();
const robotId = computed(() => String(route.query.robotId || ""));
const deviceId = computed(() => String(route.query.deviceId || ""));
const clearingFault = ref(false);
const handleClearFault = async () => {
if (!robotId.value || !deviceId.value) {
ElMessage.warning("缺少机器人或机械臂设备信息");
return;
}
clearingFault.value = true;
try {
await clearArmFault({ robotId: robotId.value, deviceId: deviceId.value });
ElMessage.success("机械臂故障状态已清除");
} catch (error) {
console.error("清除机械臂故障失败:", error);
} finally {
clearingFault.value = false;
}
};
2025-08-21 14:11:58 +08:00
const data = reactive({
form: {
x: 11,
y: 11,
z: 11,
rx: 11,
ry: 11,
rz: 11,
arm: 1,
},
2025-08-21 14:11:58 +08:00
armList: [
2025-12-09 10:33:05 +08:00
{ label: "目标1", value: 1 },
{ label: "目标2", value: 2 },
{ label: "目标3", value: 3 },
2025-08-21 14:11:58 +08:00
],
});
2025-12-09 10:33:05 +08:00
const { form, armList } = toRefs(data);
const leftJointList = ref(null);
const rightJointList = ref(null);
const computedChange = (val) => {
leftJointList.value = val.filter((item) => {
return item.name[0] === "L";
});
rightJointList.value = val.filter((item) => {
return item.name[0] === "R";
});
};
const urdfViewerRef = ref(null);
const updateJointAngle = ({ name, value }) => {
if (urdfViewerRef.value) {
urdfViewerRef.value.updateJointAngle(name, value);
}
};
2025-11-20 16:04:46 +08:00
const coordinateSystem = ref(null); // 用于获取容器元素
const buttonPositions = ref([]); // 存储按钮的动态位置
2025-12-09 10:33:05 +08:00
const buttonDirections = [
{ target: "z_plus", direction: "top", label: "Z+" }, // left
{ target: "rz_plus", direction: "top-right", label: "RZ+" },
{ target: "x_plus", direction: "right", label: "X+" }, // bottom
{ target: "ry_plus", direction: "bottom-right", label: "RY+" },
{ target: "y_minus", direction: "bottom", label: "Y-" },
{ target: "ry_minus", direction: "bottom-left", label: "RY-" },
{ target: "z_minus", direction: "bottom", label: "Z-" },
{ target: "rx_minus", direction: "bottom-left", label: "RX-" },
{ target: "x_minus", direction: "left", label: "X-" },
{ target: "rx_plus", direction: "top-left", label: "RX+" },
{ target: "y_plus", direction: "left", label: "Y+" }, // right
{ target: "rz_minus", direction: "top-left", label: "RZ-" },
2025-11-20 16:04:46 +08:00
];
// 默认图片尺寸
const ORIGINAL_IMAGE_WIDTH = 1025;
const ORIGINAL_IMAGE_HEIGHT = 817;
const updateButtonPositions = () => {
if (coordinateSystem.value) {
2025-12-09 10:33:05 +08:00
const containerRect = coordinateSystem.value.getBoundingClientRect();
const containerWidth = containerRect.width;
const containerHeight = containerRect.height;
2025-11-20 16:04:46 +08:00
// 计算图片的实际显示尺寸(考虑 background-size: contain)
2025-12-09 10:33:05 +08:00
const imageAspectRatio = ORIGINAL_IMAGE_WIDTH / ORIGINAL_IMAGE_HEIGHT;
const containerAspectRatio = containerWidth / containerHeight;
2025-11-20 16:04:46 +08:00
2025-12-09 10:33:05 +08:00
let imageWidth, imageHeight, imageLeft, imageTop;
2025-11-20 16:04:46 +08:00
if (containerAspectRatio > imageAspectRatio) {
2025-12-09 10:33:05 +08:00
imageHeight = containerHeight;
imageWidth = imageHeight * imageAspectRatio;
imageLeft = (containerWidth - imageWidth) / 2;
imageTop = 0;
2025-11-20 16:04:46 +08:00
} else {
2025-12-09 10:33:05 +08:00
imageWidth = containerWidth;
imageHeight = imageWidth / imageAspectRatio;
imageLeft = 0;
imageTop = (containerHeight - imageHeight) / 2;
2025-11-20 16:04:46 +08:00
}
2025-12-09 10:33:05 +08:00
// 计算图片中心坐标
const centerX = imageLeft + imageWidth / 2;
const centerY = imageTop + imageHeight / 2;
// 计算圆的半径(根据图片尺寸的较小值比例确定,可调整比例系数)
const radiusScale = 0.85; // 半径系数,0-1之间,越大半径越大
const radius = Math.min(imageWidth, imageHeight) * radiusScale / 2;
// 定义方位对应的角度(弧度制),top为0度,顺时针增加
const directionAngles = {
"top": 0,
"top-right": Math.PI / 6, // 30度
"right": Math.PI / 3, // 60度
"bottom-right": Math.PI / 2, // 90度
"bottom": 2 * Math.PI / 3, // 120度
"bottom-left": 5 * Math.PI / 6, // 150度
"left": Math.PI, // 180度
"top-left": 7 * Math.PI / 6, // 210度
};
// 调整角度以均匀分布12个按钮(360/12=30度,即Math.PI/6弧度)
const totalButtons = buttonDirections.length;
const angleStep = 2 * Math.PI / totalButtons;
2025-11-20 16:04:46 +08:00
// 更新按钮位置
2025-12-09 10:33:05 +08:00
buttonPositions.value = buttonDirections.map((btn, index) => {
// 计算每个按钮的角度(从top开始顺时针排列)
const angle = index * angleStep;
// 计算按钮的坐标
const x = centerX + radius * Math.sin(angle);
const y = centerY - radius * Math.cos(angle);
return {
...btn,
left: x,
top: y,
};
});
2025-11-20 16:04:46 +08:00
}
};
2025-12-09 10:33:05 +08:00
2025-08-21 14:11:58 +08:00
onMounted(() => {
2025-11-20 16:04:46 +08:00
updateButtonPositions();
2025-12-09 10:33:05 +08:00
2025-11-20 16:04:46 +08:00
// 监听窗口大小变化,更新按钮位置
2025-12-09 10:33:05 +08:00
window.addEventListener("resize", updateButtonPositions);
2025-11-20 16:04:46 +08:00
});
onUnmounted(() => {
2025-12-09 10:33:05 +08:00
window.removeEventListener("resize", updateButtonPositions);
});
2025-08-21 14:11:58 +08:00
</script>
<template>
<div class="mechanical-arm">
<div class="top card">
2025-12-09 10:33:05 +08:00
<div class="controls flex align-center" style="justify-content: flex-start">
<div class="control-label">目标</div>
2025-12-09 10:33:05 +08:00
<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>
2025-12-09 10:33:05 +08:00
<el-button @click="handleResets('zero')" style="margin-right: 10px"
>零位姿态</el-button
>
<el-button @click="handleResets('init')">初始位置</el-button>
<el-button
type="danger"
plain
:loading="clearingFault"
:disabled="!robotId || !deviceId"
@click="handleClearFault"
>
<el-icon><CircleClose /></el-icon>清除故障
</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>
2025-12-09 10:33:05 +08:00
<div class="bottom">
<div class="left">
<UrdfViewer
ref="urdfViewerRef"
:base-path="'/public/models/'"
:urdf-path="'dual_arm.urdf'"
:model-color="'#8a8ae9'"
@computedChange="computedChange"
/>
2025-08-21 14:11:58 +08:00
</div>
2025-12-09 10:33:05 +08:00
<div class="right">
<div class="top">
<div class="secondary-title">
<div class="label">左手</div>
<div class="label">右手</div>
</div>
2025-12-09 10:33:05 +08:00
<div class="jointControl-container">
<JointControl
:jointList="leftJointList"
@updateJointAngle="updateJointAngle"
/>
<JointControl
:jointList="rightJointList"
@updateJointAngle="updateJointAngle"
/>
</div>
2025-12-09 10:33:05 +08:00
</div>
<div class="bottom">
<div class="left card" style="position: relative">
<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>
</div>
</div>
2025-08-21 14:11:58 +08:00
</div>
</div>
</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 {
2025-12-09 10:33:05 +08:00
height: calc(100% - 120px);
display: flex;
gap: 12px;
2025-08-21 14:11:58 +08:00
.left {
2025-12-09 10:33:05 +08:00
flex: 2;
height: 100%;
}
.right {
flex: 1;
height: 100%;
display: flex;
flex-direction: column;
2025-08-21 14:11:58 +08:00
2025-12-09 10:33:05 +08:00
.top {
flex: 1;
2025-12-09 10:33:05 +08:00
border: 1px solid #e4e7ed;
padding: 0 8px;
overflow: hidden;
2025-08-21 14:11:58 +08:00
2025-12-09 10:33:05 +08:00
.container-title {
font-size: 16px;
text-align: center;
}
2025-12-09 10:33:05 +08:00
.secondary-title {
display: flex;
2025-12-09 10:33:05 +08:00
justify-content: space-around;
padding: 10px;
margin-bottom: 10px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
2025-08-21 14:11:58 +08:00
2025-12-09 10:33:05 +08:00
.jointControl-container {
display: flex;
gap: 10px;
height: calc(100% - 75px);
overflow: auto;
2025-08-21 14:11:58 +08:00
}
}
2025-08-21 14:11:58 +08:00
2025-12-09 10:33:05 +08:00
.bottom {
flex: 1;
2025-08-21 14:11:58 +08:00
2025-12-09 10:33:05 +08:00
.card {
border: 1px solid #e4e7ed;
border-radius: 8px;
padding: 10px;
margin-bottom: 10px;
height: 100%;
display: flex;
flex-direction: column;
2025-08-21 14:11:58 +08:00
2025-12-09 10:33:05 +08:00
.coordinate-system {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
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
}
}
}
}
2025-12-09 10:33:05 +08:00
</style>