2025-08-21 14:11:58 +08:00
|
|
|
|
<script setup>
|
2026-08-18 14:42:33 +08:00
|
|
|
|
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
|
|
|
|
|
2026-08-18 14:42:33 +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: {
|
2025-09-05 14:52:28 +08:00
|
|
|
|
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-09-05 14:52:28 +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-09-05 14:52:28 +08:00
|
|
|
|
});
|
2025-08-21 14:11:58 +08:00
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
|
<div class="mechanical-arm">
|
2025-09-05 14:52:28 +08:00
|
|
|
|
<div class="top card">
|
2025-12-09 10:33:05 +08:00
|
|
|
|
<div class="controls flex align-center" style="justify-content: flex-start">
|
2025-09-05 14:52:28 +08:00
|
|
|
|
<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"
|
|
|
|
|
|
/>
|
2025-09-05 14:52:28 +08:00
|
|
|
|
</el-select>
|
2025-12-09 10:33:05 +08:00
|
|
|
|
<el-button @click="handleResets('zero')" style="margin-right: 10px"
|
|
|
|
|
|
>零位姿态</el-button
|
|
|
|
|
|
>
|
2025-09-05 14:52:28 +08:00
|
|
|
|
<el-button @click="handleResets('init')">初始位置</el-button>
|
2026-08-18 14:42:33 +08:00
|
|
|
|
<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>
|
2025-09-05 14:52:28 +08:00
|
|
|
|
<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>
|
2025-09-05 14:52:28 +08:00
|
|
|
|
</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>
|
2025-09-05 14:52:28 +08:00
|
|
|
|
</div>
|
2025-12-09 10:33:05 +08:00
|
|
|
|
<div class="jointControl-container">
|
|
|
|
|
|
<JointControl
|
|
|
|
|
|
:jointList="leftJointList"
|
|
|
|
|
|
@updateJointAngle="updateJointAngle"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<JointControl
|
|
|
|
|
|
:jointList="rightJointList"
|
|
|
|
|
|
@updateJointAngle="updateJointAngle"
|
|
|
|
|
|
/>
|
2025-09-05 14:52:28 +08:00
|
|
|
|
</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;
|
2025-09-05 14:52:28 +08:00
|
|
|
|
flex: 1;
|
|
|
|
|
|
overflow: hidden;
|
2025-08-21 14:11:58 +08:00
|
|
|
|
flex-direction: column;
|
2025-09-05 14:52:28 +08:00
|
|
|
|
box-sizing: border-box;
|
2025-08-21 14:11:58 +08:00
|
|
|
|
|
2025-09-05 14:52:28 +08:00
|
|
|
|
.card {
|
|
|
|
|
|
border: 1px solid #e4e7ed;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
padding: 10px;
|
|
|
|
|
|
margin-bottom: 10px;
|
2025-08-21 14:11:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-05 14:52:28 +08:00
|
|
|
|
.controls {
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
padding: 10px;
|
2025-08-21 14:11:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-05 14:52:28 +08:00
|
|
|
|
.top {
|
|
|
|
|
|
.pose-info {
|
|
|
|
|
|
justify-content: space-around;
|
|
|
|
|
|
padding: 10px 0;
|
2025-08-21 14:11:58 +08:00
|
|
|
|
|
2025-09-05 14:52:28 +08:00
|
|
|
|
.pose-item {
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
user-select: none;
|
|
|
|
|
|
}
|
2025-08-21 14:11:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-05 14:52:28 +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
|
|
|
|
|
2025-09-05 14:52:28 +08:00
|
|
|
|
.left {
|
2025-12-09 10:33:05 +08:00
|
|
|
|
flex: 2;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.right {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
height: 100%;
|
2025-09-05 14:52:28 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
2025-08-21 14:11:58 +08:00
|
|
|
|
|
2025-12-09 10:33:05 +08:00
|
|
|
|
.top {
|
2025-09-05 14:52:28 +08:00
|
|
|
|
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-09-05 14:52:28 +08:00
|
|
|
|
|
2025-12-09 10:33:05 +08:00
|
|
|
|
.secondary-title {
|
2025-09-05 14:52:28 +08:00
|
|
|
|
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-09-05 14:52:28 +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>
|