CMVR-IOT-UI/src/views/device/register/components/MechanicalArm/index.vue
lixiaolong eb056f097c feat(flow): 更新声音类型检测功能并添加系统错误状态清除
- 修改声音事件识别为声音类型检测,调整相关参数配置
- 添加目标声音类型选择功能和预期标签匹配逻辑
- 新增机械臂故障清除API接口和操作按钮
- 实现工作流中清除机器人错误状态的功能和对话框
- 更新任务执行日志中声音分析结果显示逻辑
- 优化声音类型检测的输出参数和验证规则
2026-08-18 14:42:33 +08:00

385 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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";
import UrdfViewer from "./components/UrdfViewer.vue";
import JointControl from "./components/JointControl.vue";
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;
}
};
const data = reactive({
form: {
x: 11,
y: 11,
z: 11,
rx: 11,
ry: 11,
rz: 11,
arm: 1,
},
armList: [
{ label: "目标1", value: 1 },
{ label: "目标2", value: 2 },
{ label: "目标3", value: 3 },
],
});
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);
}
};
const coordinateSystem = ref(null); // 用于获取容器元素
const buttonPositions = ref([]); // 存储按钮的动态位置
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-" },
];
// 默认图片尺寸
const ORIGINAL_IMAGE_WIDTH = 1025;
const ORIGINAL_IMAGE_HEIGHT = 817;
const updateButtonPositions = () => {
if (coordinateSystem.value) {
const containerRect = coordinateSystem.value.getBoundingClientRect();
const containerWidth = containerRect.width;
const containerHeight = containerRect.height;
// 计算图片的实际显示尺寸(考虑 background-size: contain)
const imageAspectRatio = ORIGINAL_IMAGE_WIDTH / ORIGINAL_IMAGE_HEIGHT;
const containerAspectRatio = containerWidth / containerHeight;
let imageWidth, imageHeight, imageLeft, imageTop;
if (containerAspectRatio > imageAspectRatio) {
imageHeight = containerHeight;
imageWidth = imageHeight * imageAspectRatio;
imageLeft = (containerWidth - imageWidth) / 2;
imageTop = 0;
} else {
imageWidth = containerWidth;
imageHeight = imageWidth / imageAspectRatio;
imageLeft = 0;
imageTop = (containerHeight - imageHeight) / 2;
}
// 计算图片中心坐标
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;
// 更新按钮位置
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,
};
});
}
};
onMounted(() => {
updateButtonPositions();
// 监听窗口大小变化,更新按钮位置
window.addEventListener("resize", updateButtonPositions);
});
onUnmounted(() => {
window.removeEventListener("resize", updateButtonPositions);
});
</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>
<el-button
type="danger"
plain
:loading="clearingFault"
:disabled="!robotId || !deviceId"
@click="handleClearFault"
>
<el-icon><CircleClose /></el-icon>清除故障
</el-button>
</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>
</div>
</div>
<div class="bottom">
<div class="left">
<UrdfViewer
ref="urdfViewerRef"
:base-path="'/public/models/'"
:urdf-path="'dual_arm.urdf'"
:model-color="'#8a8ae9'"
@computedChange="computedChange"
/>
</div>
<div class="right">
<div class="top">
<div class="secondary-title">
<div class="label">左手</div>
<div class="label">右手</div>
</div>
<div class="jointControl-container">
<JointControl
:jointList="leftJointList"
@updateJointAngle="updateJointAngle"
/>
<JointControl
:jointList="rightJointList"
@updateJointAngle="updateJointAngle"
/>
</div>
</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>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
.mechanical-arm {
display: flex;
flex: 1;
overflow: hidden;
flex-direction: column;
box-sizing: border-box;
.card {
border: 1px solid #e4e7ed;
border-radius: 8px;
padding: 10px;
margin-bottom: 10px;
}
.controls {
gap: 10px;
padding: 10px;
}
.top {
.pose-info {
justify-content: space-around;
padding: 10px 0;
.pose-item {
font-size: 16px;
user-select: none;
}
}
}
.bottom {
height: calc(100% - 120px);
display: flex;
gap: 12px;
.left {
flex: 2;
height: 100%;
}
.right {
flex: 1;
height: 100%;
display: flex;
flex-direction: column;
.top {
flex: 1;
border: 1px solid #e4e7ed;
padding: 0 8px;
overflow: hidden;
.container-title {
font-size: 16px;
text-align: center;
}
.secondary-title {
display: flex;
justify-content: space-around;
padding: 10px;
margin-bottom: 10px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.jointControl-container {
display: flex;
gap: 10px;
height: calc(100% - 75px);
overflow: auto;
}
}
.bottom {
flex: 1;
.card {
border: 1px solid #e4e7ed;
border-radius: 8px;
padding: 10px;
margin-bottom: 10px;
height: 100%;
display: flex;
flex-direction: column;
.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%); /* 按钮中心对齐坐标 */
}
}
}
}
}
}
}
}
</style>