refactor(device): 优化 DexHand 组件布局和响应式处理
- 使用 ResizeObserver 替代 window resize 事件监听,提高性能 - 优化组件布局,确保在不同屏幕尺寸下正常显示 - 修复了一些样式问题,如重复 padding、min-height 等 - 优化了图表组件的初始化和销毁逻辑,提高资源利用率
This commit is contained in:
parent
aed84e31b4
commit
84373c8292
Binary file not shown.
|
Before Width: | Height: | Size: 311 KiB After Width: | Height: | Size: 270 KiB |
@ -37,7 +37,7 @@ function addIframe() {
|
||||
@import "@/assets/styles/variables.module";
|
||||
.app-main {
|
||||
/* 50= navbar 50 */
|
||||
min-height: calc(100vh - 50px);
|
||||
height: calc(100vh - 84px);
|
||||
padding: 20px;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
@ -54,7 +54,7 @@ function addIframe() {
|
||||
.hasTagsView {
|
||||
.app-main {
|
||||
/* 84 = navbar + tags-view = 50 + 34 */
|
||||
min-height: calc(100vh - 84px);
|
||||
height: calc(100vh - 84px);
|
||||
}
|
||||
|
||||
.fixed-header + .app-main {
|
||||
|
||||
@ -57,12 +57,20 @@ export const constantRoutes = [
|
||||
component: () => import('@/views/error/401'),
|
||||
hidden: true
|
||||
},
|
||||
{
|
||||
//机械臂路由
|
||||
path: '/mechanical_arm/:id',
|
||||
component: () => import('@/views/device/register/components/MechanicalArm/index.vue'),
|
||||
hidden: true
|
||||
{
|
||||
path: '/device',
|
||||
component: Layout,
|
||||
children: [
|
||||
{
|
||||
path: 'mechanical_arm/:id',
|
||||
component: () => import('@/views/device/register/components/MechanicalArm/index.vue'),
|
||||
name: '机械臂示教',
|
||||
meta: { title: '机械臂详情' },
|
||||
hidden: true
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
// router/index.js
|
||||
{
|
||||
path: '/device',
|
||||
|
||||
@ -21,6 +21,7 @@ const terminalId = ref('25449ff3fcc7b27da5f69462c5efcec2'); // 待您修改
|
||||
const deviceId = ref('hand1'); // 待您修改
|
||||
const frameData = ref({ count: 0, lastTime: 0 });
|
||||
const isHandSeries = ref(false);
|
||||
const containerRef = ref(null)
|
||||
|
||||
// 矩形定义
|
||||
const rectangle = [
|
||||
@ -362,15 +363,39 @@ onMounted(async () => {
|
||||
await nextTick();
|
||||
initializeRectangleData(); // 初始化数据
|
||||
initCanvas(); // 初始化 Canvas
|
||||
|
||||
// 使用 ResizeObserver 监听父容器尺寸变化
|
||||
const container = containerRef.value || handCanvas.value?.parentElement;
|
||||
if (container) {
|
||||
const resizeObserver = new ResizeObserver(debounce(updateCanvas, 10));
|
||||
resizeObserver.observe(container);
|
||||
// 保存 ResizeObserver 实例以便清理
|
||||
data.resizeObserver = resizeObserver;
|
||||
} else {
|
||||
console.error('Failed to find container for ResizeObserver');
|
||||
}
|
||||
|
||||
const debouncedResize = debounce(updateCanvas, 10);
|
||||
window.addEventListener('resize', debouncedResize);
|
||||
data.debouncedResize = debouncedResize; // 保存以便清理
|
||||
|
||||
|
||||
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', debounce(updateCanvas, 10));
|
||||
// 如果流正在启动
|
||||
// 清理 ResizeObserver
|
||||
if (data.resizeObserver) {
|
||||
data.resizeObserver.disconnect();
|
||||
delete data.resizeObserver;
|
||||
}
|
||||
// 清理 window 的 resize 事件
|
||||
if (data.debouncedResize) {
|
||||
window.removeEventListener('resize', data.debouncedResize);
|
||||
delete data.debouncedResize;
|
||||
}
|
||||
// 清理 WebSocket 订阅
|
||||
if (isHandSeries.value) {
|
||||
// 关闭 WebSocket 通道
|
||||
subscribeSensorData(false);
|
||||
}
|
||||
});
|
||||
|
||||
@ -32,6 +32,7 @@ const emit = defineEmits(['update:topSeriesData']);
|
||||
|
||||
const loading = ref(false);
|
||||
const handImage = ref(null);
|
||||
const containerRef = ref(null); // 新增:引用父容器
|
||||
const defaultHandImageInfo = { width: 891, height: 981 };
|
||||
const imageAspectRatio = defaultHandImageInfo.width / defaultHandImageInfo.height;
|
||||
|
||||
@ -114,16 +115,39 @@ const updateToSeriesData = () => {
|
||||
emit('update:topSeriesData', newTopSeriesData);
|
||||
});
|
||||
};
|
||||
|
||||
const data = ref({
|
||||
debouncedResize: null,
|
||||
resizeObserver: null,
|
||||
});
|
||||
onMounted(() => {
|
||||
updateSliderPositions();
|
||||
// 使用 ResizeObserver 监听父容器尺寸变化
|
||||
const container = containerRef.value || handImage.value?.parentElement;
|
||||
if (container) {
|
||||
const resizeObserver = new ResizeObserver(debounce(updateSliderPositions, 10));
|
||||
resizeObserver.observe(container);
|
||||
data.value.resizeObserver = resizeObserver;
|
||||
} else {
|
||||
console.error('Failed to find container for ResizeObserver');
|
||||
}
|
||||
|
||||
const debouncedResize = debounce(updateSliderPositions, 0);
|
||||
window.addEventListener('resize', debouncedResize);
|
||||
data.value.debouncedResize = debouncedResize;
|
||||
updateToSeriesData();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', debounce(updateSliderPositions, 0));
|
||||
// 清理 ResizeObserver
|
||||
if (data.value.resizeObserver) {
|
||||
data.value.resizeObserver.disconnect();
|
||||
data.value.resizeObserver = null;
|
||||
}
|
||||
// 清理 window 的 resize 事件
|
||||
if (data.value.debouncedResize) {
|
||||
window.removeEventListener('resize', data.value.debouncedResize);
|
||||
data.value.debouncedResize = null;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div style="width: 100%;height: 100%; margin: 0 auto;">
|
||||
<div ref="containerRef" style="width: 100%;height: 100%; margin: 0 auto;">
|
||||
<div class="tabs">
|
||||
<div
|
||||
v-for="(tab, index) in tabs"
|
||||
@ -48,6 +48,7 @@ const props = defineProps({
|
||||
});
|
||||
|
||||
const chart = ref(null);
|
||||
const containerRef = ref(null); // 新增:引用父容器
|
||||
const activeTab = ref('avg'); // 默认选中平均值
|
||||
|
||||
// Tab 配置
|
||||
@ -58,6 +59,15 @@ const tabs = [
|
||||
|
||||
let myChart;
|
||||
|
||||
// 防抖函数
|
||||
const debounce = (func, delay) => {
|
||||
let timeoutId;
|
||||
return (...args) => {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = setTimeout(() => func.apply(null, args), delay);
|
||||
};
|
||||
};
|
||||
|
||||
const initChart = () => {
|
||||
if (!chart.value) {
|
||||
console.error('Chart element is null');
|
||||
@ -127,22 +137,55 @@ watch([() => props.maxData, () => props.avgData, activeTab], () => {
|
||||
updateChart(activeTab.value === 'max' ? props.maxData : props.avgData);
|
||||
});
|
||||
|
||||
const data = ref({
|
||||
resizeObserver: null,
|
||||
debouncedResize: null,
|
||||
});
|
||||
onMounted(() => {
|
||||
initChart();
|
||||
updateChart(activeTab.value === 'max' ? props.maxData : props.avgData);
|
||||
// 处理窗口大小变化
|
||||
const handleResize = () => {
|
||||
myChart?.resize();
|
||||
};
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
// 使用 ResizeObserver 监听父容器尺寸变化
|
||||
const container = containerRef.value;
|
||||
if (container) {
|
||||
const resizeObserver = new ResizeObserver(debounce(() => {
|
||||
if (myChart) {
|
||||
console.log('Container resized:', container.offsetWidth, container.offsetHeight);
|
||||
myChart.resize();
|
||||
}
|
||||
}, 10));
|
||||
resizeObserver.observe(container);
|
||||
data.value.resizeObserver = resizeObserver;
|
||||
} else {
|
||||
console.error('Failed to find container for ResizeObserver');
|
||||
}
|
||||
|
||||
// 保留 window 的 resize 事件监听
|
||||
const debouncedResize = debounce(() => {
|
||||
if (myChart) {
|
||||
myChart.resize();
|
||||
}
|
||||
}, 10);
|
||||
window.addEventListener('resize', debouncedResize);
|
||||
data.value.debouncedResize = debouncedResize;
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
// 清理图表实例和事件监听器
|
||||
myChart?.dispose();
|
||||
window.removeEventListener('resize', () => {
|
||||
myChart?.resize();
|
||||
});
|
||||
// 清理图表实例
|
||||
if (myChart) {
|
||||
myChart.dispose();
|
||||
myChart = null;
|
||||
}
|
||||
// 清理 ResizeObserver
|
||||
if (data.value.resizeObserver) {
|
||||
data.value.resizeObserver.disconnect();
|
||||
data.value.resizeObserver = null;
|
||||
}
|
||||
// 清理 window 的 resize 事件
|
||||
if (data.value.debouncedResize) {
|
||||
window.removeEventListener('resize', data.value.debouncedResize);
|
||||
data.value.debouncedResize = null;
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="right-chart-panel">
|
||||
<div class="right-chart-panel" ref="containerRef">
|
||||
<div ref="topChart" class="chart"></div>
|
||||
</div>
|
||||
</template>
|
||||
@ -42,8 +42,18 @@ const props = defineProps({
|
||||
});
|
||||
|
||||
const topChart = ref(null);
|
||||
const containerRef = ref(null); // 新增:引用父容器
|
||||
let chart1Instance = null;
|
||||
|
||||
// 防抖函数
|
||||
const debounce = (func, delay) => {
|
||||
let timeoutId;
|
||||
return (...args) => {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = setTimeout(() => func.apply(null, args), delay);
|
||||
};
|
||||
};
|
||||
|
||||
const topChartData = ref({
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
@ -62,27 +72,64 @@ const topChartData = ref({
|
||||
series: [{ data: props.topSeriesData, type: 'bar' }],
|
||||
});
|
||||
|
||||
const data = ref({
|
||||
resizeObserver: null,
|
||||
debouncedResize: null,
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
try {
|
||||
if (!topChart.value) {
|
||||
console.error('Chart element is null');
|
||||
return;
|
||||
}
|
||||
chart1Instance = echarts.init(topChart.value);
|
||||
chart1Instance.setOption(topChartData.value);
|
||||
|
||||
// 处理窗口大小变化
|
||||
const handleResize = () => {
|
||||
chart1Instance?.resize();
|
||||
};
|
||||
window.addEventListener('resize', handleResize);
|
||||
// 使用 ResizeObserver 监听父容器尺寸变化
|
||||
const container = containerRef.value;
|
||||
if (container) {
|
||||
const resizeObserver = new ResizeObserver(debounce(() => {
|
||||
if (chart1Instance) {
|
||||
console.log('Container resized:', container.offsetWidth, container.offsetHeight);
|
||||
chart1Instance.resize();
|
||||
}
|
||||
}, 10));
|
||||
resizeObserver.observe(container);
|
||||
data.value.resizeObserver = resizeObserver;
|
||||
} else {
|
||||
console.error('Failed to find container for ResizeObserver');
|
||||
}
|
||||
|
||||
// 保留 window 的 resize 事件监听
|
||||
const debouncedResize = debounce(() => {
|
||||
if (chart1Instance) {
|
||||
chart1Instance.resize();
|
||||
}
|
||||
}, 10);
|
||||
window.addEventListener('resize', debouncedResize);
|
||||
data.value.debouncedResize = debouncedResize;
|
||||
} catch (error) {
|
||||
console.error('图表初始化失败:', error);
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
// 清理图表实例和事件监听器
|
||||
chart1Instance?.dispose();
|
||||
window.removeEventListener('resize', () => {
|
||||
chart1Instance?.resize();
|
||||
});
|
||||
// 清理图表实例
|
||||
if (chart1Instance) {
|
||||
chart1Instance.dispose();
|
||||
chart1Instance = null;
|
||||
}
|
||||
// 清理 ResizeObserver
|
||||
if (data.value.resizeObserver) {
|
||||
data.value.resizeObserver.disconnect();
|
||||
data.value.resizeObserver = null;
|
||||
}
|
||||
// 清理 window 的 resize 事件
|
||||
if (data.value.debouncedResize) {
|
||||
window.removeEventListener('resize', data.value.debouncedResize);
|
||||
data.value.debouncedResize = null;
|
||||
}
|
||||
});
|
||||
|
||||
// 监听 props.topSeriesData
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
<!-- index.vue -->
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="left-panel">
|
||||
@ -9,23 +10,21 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="right-panel">
|
||||
<RightCharts :bottom-series-data="bottomSeriesData" />
|
||||
<RightBottomCharts :max-data="maxData" :avg-data="avgData" style="height: 50%" />
|
||||
<RightTopCharts :top-series-data="topSeriesData" style="height: 50%;" />
|
||||
<RightBottomCharts :max-data="maxData" :avg-data="avgData" style="height: 50%;" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import RightCharts from './RightTopCharts.vue';
|
||||
import RightTopCharts from './RightTopCharts.vue';
|
||||
import LeftTopHand from './LeftTopHand.vue';
|
||||
import LeftBottomHand from './LeftBottomHand.vue'
|
||||
import LeftBottomHand from './LeftBottomHand.vue';
|
||||
import RightBottomCharts from './RightBottomCharts.vue';
|
||||
|
||||
|
||||
const topSeriesData = ref([0, 0, 0, 0, 0, 0]);
|
||||
const bottomSeriesData = ref([0, 0, 0, 0, 0, 0]);
|
||||
|
||||
const maxData = ref({
|
||||
thumb: { end: 0, tip: 0, middle: 0, palm: 0 },
|
||||
indexFinger: { end: 0, tip: 0, middle: 0 },
|
||||
@ -43,27 +42,28 @@ const avgData = ref({
|
||||
palm: { touch: 0 }
|
||||
});
|
||||
|
||||
|
||||
// 处理更新事件
|
||||
const handleUpdateTopSeriesData = (newData) => {
|
||||
topSeriesData.value = newData;
|
||||
};
|
||||
|
||||
// 处理更新事件
|
||||
const handleUpdateBottomSeriesData = ({ maxData, avgData }) => {
|
||||
maxData.value = newData;
|
||||
avgData.value = avgData;
|
||||
const handleUpdateBottomSeriesData = ({ maxData: newMaxData, avgData: newAvgData }) => {
|
||||
maxData.value = newMaxData;
|
||||
avgData.value = newAvgData;
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex: 1; /* 占据父容器所有可用空间 */
|
||||
width: 100%;
|
||||
height: 100%; /* 占满父容器高度 */
|
||||
min-height: 0; /* 防止 flex 子项最小高度问题 */
|
||||
background-color: #fff;
|
||||
border-radius: 20px;
|
||||
overflow: hidden; /* 防止内容溢出 */
|
||||
box-sizing: border-box; /* 包含 padding 和 border */
|
||||
padding: 20px; /* 与父容器一致,避免双重 padding */
|
||||
}
|
||||
|
||||
.left-panel,
|
||||
@ -72,12 +72,16 @@ const handleUpdateBottomSeriesData = ({ maxData, avgData }) => {
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
width: 50%;
|
||||
padding: 10px;
|
||||
height: 100%; /* 占满 container 高度 */
|
||||
padding: 0; /* 移除子面板内边距,交给 container 处理 */
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.hand-container {
|
||||
position: relative;
|
||||
height: 50%;
|
||||
height: 50%; /* 每个 hand-container 占左面板的 50% 高度 */
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
</style>
|
||||
@ -0,0 +1,565 @@
|
||||
<script setup>
|
||||
import {useRoute} from "vue-router"
|
||||
import dayjs from "dayjs";
|
||||
import {deviceTeach, deviceUpdateProfile} from "@/api/device/register.js";
|
||||
import {formatNumber} from "@/views/device/register/components/index.js";
|
||||
import {convertNumbersToStringAndRemoveNulls} from "@/utils/fn.js";
|
||||
|
||||
const data = reactive({
|
||||
form: {
|
||||
workModule: 1,//工作模式
|
||||
posStep: 1,//位置步进
|
||||
postureStep: 1,//姿态步进
|
||||
jointStep: 1,//关节步进
|
||||
joint1: 0.000012,//关节1
|
||||
joint2: 0.000012,//
|
||||
joint3: 0.000012,//
|
||||
joint4: 0.000012,//
|
||||
joint5: 0.000012,//
|
||||
joint6: 0.000012,//关节6
|
||||
x: 11,//
|
||||
y: 11,//
|
||||
z: 11,//
|
||||
rx: 11,//
|
||||
ry: 11,//
|
||||
rz: 11,//
|
||||
arm: 1,//目标
|
||||
coordinate: 1,//坐标系
|
||||
},
|
||||
speed: 20,//速度
|
||||
zeroPos: {//零位姿态
|
||||
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,
|
||||
},//初始姿态
|
||||
armList: [
|
||||
{
|
||||
label: '机械臂1',
|
||||
value: 1
|
||||
},
|
||||
{
|
||||
label: '机械臂2',
|
||||
value: 2
|
||||
}
|
||||
],
|
||||
coordinateList: [
|
||||
{
|
||||
label: 'x轴',
|
||||
value: 1
|
||||
},
|
||||
{
|
||||
label: 'y轴',
|
||||
value: 2
|
||||
},
|
||||
{
|
||||
label: 'z轴',
|
||||
value: 3
|
||||
}
|
||||
],
|
||||
time: dayjs().format("YYYY-MM-DD HH:mm:ss"),
|
||||
})
|
||||
|
||||
const {
|
||||
form, time,
|
||||
zeroPos,
|
||||
initPos,
|
||||
armList,
|
||||
coordinateList
|
||||
} = toRefs(data)
|
||||
|
||||
const route = useRoute()
|
||||
let timer = null
|
||||
|
||||
onUnmounted(() => {
|
||||
clearInterval(timer)
|
||||
})
|
||||
|
||||
//获取机械臂参数
|
||||
const getDeviceInfo = () => {
|
||||
deviceTeach(devId.value).then((res) => {
|
||||
let data = formatNumber(res.data)//字符类型转数字类型
|
||||
Object.assign(form.value, data)
|
||||
//设置初始位置
|
||||
initPos.value.x = data.x
|
||||
initPos.value.y = data.y
|
||||
initPos.value.z = data.z
|
||||
initPos.value.rx = data.rx
|
||||
initPos.value.ry = data.ry
|
||||
initPos.value.rz = data.rz
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getDeviceInfo()
|
||||
const posControls = document.querySelectorAll(".btn")
|
||||
//监听按钮点击事件
|
||||
posControls.forEach((item) => {
|
||||
item.addEventListener('click', (e) => {
|
||||
const target = e.currentTarget.classList[1]
|
||||
const vals = target.split("_")//x_0 rx_0
|
||||
const pos = vals[0]
|
||||
const calc = !!Number(vals[1])
|
||||
if (calc) {
|
||||
form.value[pos] += 0.01 * data.speed
|
||||
} else {
|
||||
form.value[pos] -= 0.01 * data.speed
|
||||
}
|
||||
})
|
||||
})
|
||||
//监听失焦事件
|
||||
// const warp = document.querySelector(".mechanical-arm")
|
||||
// warp.addEventListener('focusout', handleFocus, false)
|
||||
|
||||
timer = setInterval(() => {
|
||||
time.value = dayjs().format("YYYY-MM-DD HH:mm:ss")
|
||||
getDeviceInfo()
|
||||
}, 1000)
|
||||
})
|
||||
watch(() => JSON.parse(JSON.stringify(form.value)), (newData, oldData) => {
|
||||
// const change = findDifferences(oldData, newData)
|
||||
const data = convertNumbersToStringAndRemoveNulls(newData)
|
||||
deviceUpdateProfile({idDeDeviceRegistration: devId.value, ...data}).then((res)=>{
|
||||
console.log(res)
|
||||
})
|
||||
})
|
||||
|
||||
const handleFocus = (e) => {
|
||||
const type = e.target.type
|
||||
if (type === "number") {
|
||||
// console.log("数字框失焦")
|
||||
}
|
||||
}
|
||||
|
||||
const devId = computed(() => {
|
||||
return route.path.split("/")[2]
|
||||
})
|
||||
const deviceModel = computed(() => {
|
||||
return route.path.split("/")[1]
|
||||
})
|
||||
|
||||
const handleResets = (type) => {
|
||||
if (type === "zero") {
|
||||
Object.assign(form.value, zeroPos.value)
|
||||
return
|
||||
}
|
||||
if (type === "init") {
|
||||
Object.assign(form.value, initPos.value)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mechanical-arm">
|
||||
<div class="top flex between">
|
||||
<div class="left " style="width: 38%">
|
||||
<div class="card" style="height: 100%">
|
||||
<div class="title">机械臂</div>
|
||||
<div class="body">机械臂图片</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="center" style="width: 22%">
|
||||
<div class="card">
|
||||
<div class="title">工作模式</div>
|
||||
<div class="body">
|
||||
<el-radio-group v-model="form.workModule">
|
||||
<el-radio :value="1">真实机械臂</el-radio>
|
||||
<el-radio :value="2">仿真机械臂</el-radio>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card" style="margin-top: 10px">
|
||||
<div class="title">步进模式</div>
|
||||
<div class="mb10">
|
||||
<div class="mb5 action-label">位置步进( mm )</div>
|
||||
<el-input-number style="width: 100%;" v-model="form.posStep" :step="2"/>
|
||||
</div>
|
||||
|
||||
<div class="mb10">
|
||||
<div class="mb5 action-label">姿态步进( deg )</div>
|
||||
<el-input-number style="width: 100%;" v-model="form.postureStep" :step="2"/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="mb5 action-label">关节步进( deg )</div>
|
||||
<el-input-number style="width: 100%;" v-model="form.jointStep" :step="2"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right " style="width: 38%">
|
||||
<div class="card" style="height: 100%">
|
||||
<div class="title">位置控制</div>
|
||||
<div class="body">
|
||||
<div class="btn-wrap pos-control" style="transform: rotateX(40deg);height: calc(100% - 70px)">
|
||||
<div class="btn z_0">
|
||||
<span>Z-</span>
|
||||
<svg t="1747126605366" class="turn-bottom" viewBox="0 0 1024 1024" version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg" p-id="1461" width="32" height="32">
|
||||
<path
|
||||
d="M311.432533 448.170667H163.84L510.293333 3.413333 856.746667 448.170667h-147.592534L760.832 1006.933333H259.754667z"
|
||||
fill="#4076ff" p-id="1462"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="btn x_0">
|
||||
<span> X-</span>
|
||||
<svg t="1747126605366" class="turn-up" viewBox="0 0 1024 1024" version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg" p-id="1461" width="32" height="32">
|
||||
<path
|
||||
d="M311.432533 448.170667H163.84L510.293333 3.413333 856.746667 448.170667h-147.592534L760.832 1006.933333H259.754667z"
|
||||
fill="#4076ff" p-id="1462"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="btn z_1">
|
||||
<span>Z+</span>
|
||||
<svg t="1747126605366" class="turn-up" viewBox="0 0 1024 1024" version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg" p-id="1461" width="32" height="32">
|
||||
<path
|
||||
d="M311.432533 448.170667H163.84L510.293333 3.413333 856.746667 448.170667h-147.592534L760.832 1006.933333H259.754667z"
|
||||
fill="#4076ff" p-id="1462"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="btn y_0">
|
||||
<span>Y-</span>
|
||||
<svg t="1747126605366" class="turn-left" viewBox="0 0 1024 1024" version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg" p-id="1461" width="32" height="32">
|
||||
<path
|
||||
d="M311.432533 448.170667H163.84L510.293333 3.413333 856.746667 448.170667h-147.592534L760.832 1006.933333H259.754667z"
|
||||
fill="#4076ff" p-id="1462"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="btn"></div>
|
||||
<div class="btn y_1">
|
||||
<span>Y+</span>
|
||||
<svg t="1747126605366" class="turn-right" viewBox="0 0 1024 1024" version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg" p-id="1461" width="32" height="32">
|
||||
<path
|
||||
d="M311.432533 448.170667H163.84L510.293333 3.413333 856.746667 448.170667h-147.592534L760.832 1006.933333H259.754667z"
|
||||
fill="#4076ff" p-id="1462"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="btn"></div>
|
||||
<div class="btn x_1">
|
||||
<span>X+</span>
|
||||
<svg t="1747126605366" class="turn-bottom" viewBox="0 0 1024 1024" version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg" p-id="1461" width="32" height="32">
|
||||
<path
|
||||
d="M311.432533 448.170667H163.84L510.293333 3.413333 856.746667 448.170667h-147.592534L760.832 1006.933333H259.754667z"
|
||||
fill="#4076ff" p-id="1462"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="btn"></div>
|
||||
|
||||
</div>
|
||||
<div>
|
||||
<div class="mb10 action-label">参考坐标系</div>
|
||||
<el-select
|
||||
|
||||
v-model="form.coordinate"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in coordinateList"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bottom flex between">
|
||||
<div class="left " style="width: 30%">
|
||||
<div class="card" style="height: 100%">
|
||||
<div class="title">机械臂位置姿态</div>
|
||||
<div class="body">
|
||||
<div class="flex mb10">
|
||||
<div class="left flex1">
|
||||
<div class="pos-title mb10">位置 ( m )</div>
|
||||
<div class="mb10"> X: {{ form.x.toFixed(6) }}</div>
|
||||
<div class="mb10"> Y: {{ form.y.toFixed(6) }}</div>
|
||||
<div class="mb10"> Z: {{ form.z.toFixed(6) }}</div>
|
||||
</div>
|
||||
<div class="right flex1">
|
||||
<div class="pos-title mb10">姿态 ( deg )</div>
|
||||
<div class="mb10"> RX: {{ form.rx.toFixed(6) }}</div>
|
||||
<div class="mb10"> RY: {{ form.ry.toFixed(6) }}</div>
|
||||
<div class="mb10"> RZ: {{ form.rz.toFixed(6) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="mb10 action-label">目标</div>
|
||||
<el-select
|
||||
v-model="form.arm"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in armList"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="center " style="width: 30%">
|
||||
<div class="card" style="height: 100%">
|
||||
<div class="title">关节控制单位( deg )</div>
|
||||
<div class="body">
|
||||
<div class="mb10 action-label-pos">
|
||||
<div class="action-label">关节1</div>
|
||||
<el-input-number v-model="form.joint1" :step="0.000001" :precision="6"/>
|
||||
</div>
|
||||
<div class="mb10 action-label-pos">
|
||||
<div class="action-label">关节2</div>
|
||||
<el-input-number v-model="form.joint2" :step="0.000001" :precision="6"/>
|
||||
</div>
|
||||
<div class="mb10 action-label-pos">
|
||||
<div class="action-label">关节3</div>
|
||||
<el-input-number v-model="form.joint3" :step="0.000001" :precision="6"/>
|
||||
</div>
|
||||
<div class="mb10 action-label-pos">
|
||||
<div class="action-label">关节4</div>
|
||||
<el-input-number v-model="form.joint4" :step="0.000001" :precision="6"/>
|
||||
</div>
|
||||
<div class="mb10 action-label-pos">
|
||||
<div class="action-label">关节5</div>
|
||||
<el-input-number v-model="form.joint5" :step="0.000001" :precision="6"/>
|
||||
</div>
|
||||
<div class="mb10 action-label-pos">
|
||||
<div class="action-label">关节6</div>
|
||||
<el-input-number v-model="form.joint6" :step="0.000001" :precision="6"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right " style="width: 38%">
|
||||
<div class="card" style="height: 100%">
|
||||
<div class="title">姿态控制</div>
|
||||
<div class="body">
|
||||
<div class="btn-wrap">
|
||||
<div class="btn rz_0 c">
|
||||
<span>Z-</span>
|
||||
<svg t="1747126605366" class="turn-lb" viewBox="0 0 1024 1024" version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg" p-id="1461" width="32" height="32">
|
||||
<path
|
||||
d="M311.432533 448.170667H163.84L510.293333 3.413333 856.746667 448.170667h-147.592534L760.832 1006.933333H259.754667z"
|
||||
fill="#4076ff" p-id="1462"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="btn rx_0 c">
|
||||
<span>X-</span>
|
||||
<svg t="1747126605366" class="turn-up" viewBox="0 0 1024 1024" version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg" p-id="1461" width="32" height="32">
|
||||
<path
|
||||
d="M311.432533 448.170667H163.84L510.293333 3.413333 856.746667 448.170667h-147.592534L760.832 1006.933333H259.754667z"
|
||||
fill="#4076ff" p-id="1462"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="btn rz_1 c">
|
||||
<span>Z+</span>
|
||||
<svg t="1747126605366" class="turn-rb" viewBox="0 0 1024 1024" version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg" p-id="1461" width="32" height="32">
|
||||
<path
|
||||
d="M311.432533 448.170667H163.84L510.293333 3.413333 856.746667 448.170667h-147.592534L760.832 1006.933333H259.754667z"
|
||||
fill="#4076ff" p-id="1462"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="btn ry_0 c">
|
||||
<span> Y-</span>
|
||||
<svg t="1747126605366" class="turn-left" viewBox="0 0 1024 1024" version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg" p-id="1461" width="32" height="32">
|
||||
<path
|
||||
d="M311.432533 448.170667H163.84L510.293333 3.413333 856.746667 448.170667h-147.592534L760.832 1006.933333H259.754667z"
|
||||
fill="#4076ff" p-id="1462"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="btn"></div>
|
||||
<div class="btn ry_1 c">
|
||||
<span>Y+</span>
|
||||
<svg t="1747126605366" class="turn-right" viewBox="0 0 1024 1024" version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg" p-id="1461" width="32" height="32">
|
||||
<path
|
||||
d="M311.432533 448.170667H163.84L510.293333 3.413333 856.746667 448.170667h-147.592534L760.832 1006.933333H259.754667z"
|
||||
fill="#4076ff" p-id="1462"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="btn"></div>
|
||||
<div class="btn rx_1 c">
|
||||
<span> X+</span>
|
||||
<svg t="1747126605366" class="turn-bottom" viewBox="0 0 1024 1024" version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg" p-id="1461" width="32" height="32">
|
||||
<path
|
||||
d="M311.432533 448.170667H163.84L510.293333 3.413333 856.746667 448.170667h-147.592534L760.832 1006.933333H259.754667z"
|
||||
fill="#4076ff" p-id="1462"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="btn"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions flex align-center between">
|
||||
<div class="left">
|
||||
<el-button @click="handleResets('zero')">零位姿态</el-button>
|
||||
<el-button @click="handleResets('init')">初始位姿</el-button>
|
||||
</div>
|
||||
<div class="center">
|
||||
{{ time }}
|
||||
</div>
|
||||
<div class="right flex align-center">
|
||||
<div style="width: 100px;user-select: none">速度</div>
|
||||
<el-slider size="small" :show-tooltip="false" v-model="data.speed"/>
|
||||
<div style="margin-left: 20px"> {{ data.speed }}%</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.mechanical-arm {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
|
||||
.top {
|
||||
padding: 10px 10px 0;
|
||||
height: 48%;
|
||||
|
||||
.center {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.card:last-child {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.bottom {
|
||||
padding: 0 10px;
|
||||
height: 44%;
|
||||
|
||||
|
||||
}
|
||||
|
||||
.actions {
|
||||
height: 6%;
|
||||
box-shadow: 0 0 8px rgba(0, 0, 0, 0.2);
|
||||
padding: 0 10px;
|
||||
|
||||
.right {
|
||||
width: 300px;
|
||||
|
||||
}
|
||||
|
||||
.center {
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
|
||||
.action-label {
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.action-label-pos {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.el-input-number {
|
||||
width: calc(100% - 50px);
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e4e7ed;
|
||||
padding: 10px;
|
||||
|
||||
.title {
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
margin-bottom: 10px;
|
||||
font-weight: 900;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.body {
|
||||
height: calc(100% - 29px);
|
||||
overflow-y: auto;
|
||||
|
||||
.pos-title {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.btn-wrap {
|
||||
height: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
grid-template-rows: repeat(3, 1fr);
|
||||
justify-items: center;
|
||||
align-items: center;
|
||||
|
||||
.btn {
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
span {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
transform-origin: center center;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.turn-lb {
|
||||
transform: rotate(-135deg);
|
||||
}
|
||||
|
||||
.turn-rb {
|
||||
transform: rotate(135deg);
|
||||
}
|
||||
|
||||
.turn-left {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.turn-right {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.turn-bottom {
|
||||
transform: rotate(-180deg);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
@ -1,33 +1,34 @@
|
||||
<script setup>
|
||||
import {useRoute} from "vue-router"
|
||||
import { reactive, toRefs, onMounted, onUnmounted, watch, computed } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import dayjs from "dayjs";
|
||||
import {deviceTeach, deviceUpdateProfile} from "@/api/device/register.js";
|
||||
import {formatNumber} from "@/views/device/register/components/index.js";
|
||||
import {convertNumbersToStringAndRemoveNulls} from "@/utils/fn.js";
|
||||
import { deviceTeach, deviceUpdateProfile } from "@/api/device/register.js";
|
||||
import { formatNumber } from "@/views/device/register/components/index.js";
|
||||
import { convertNumbersToStringAndRemoveNulls } from "@/utils/fn.js";
|
||||
|
||||
const data = reactive({
|
||||
form: {
|
||||
workModule: 1,//工作模式
|
||||
posStep: 1,//位置步进
|
||||
postureStep: 1,//姿态步进
|
||||
jointStep: 1,//关节步进
|
||||
joint1: 0.000012,//关节1
|
||||
joint2: 0.000012,//
|
||||
joint3: 0.000012,//
|
||||
joint4: 0.000012,//
|
||||
joint5: 0.000012,//
|
||||
joint6: 0.000012,//关节6
|
||||
x: 11,//
|
||||
y: 11,//
|
||||
z: 11,//
|
||||
rx: 11,//
|
||||
ry: 11,//
|
||||
rz: 11,//
|
||||
arm: 1,//目标
|
||||
coordinate: 1,//坐标系
|
||||
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,
|
||||
x: 11,
|
||||
y: 11,
|
||||
z: 11,
|
||||
rx: 11,
|
||||
ry: 11,
|
||||
rz: 11,
|
||||
arm: 1,
|
||||
coordinate: 1,
|
||||
},
|
||||
speed: 20,//速度
|
||||
zeroPos: {//零位姿态
|
||||
speed: 20,
|
||||
zeroPos: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
z: 0,
|
||||
@ -42,388 +43,217 @@ const data = reactive({
|
||||
rx: 1,
|
||||
ry: 1,
|
||||
rz: 1,
|
||||
},//初始姿态
|
||||
},
|
||||
armList: [
|
||||
{
|
||||
label: '机械臂1',
|
||||
value: 1
|
||||
},
|
||||
{
|
||||
label: '机械臂2',
|
||||
value: 2
|
||||
}
|
||||
],
|
||||
coordinateList: [
|
||||
{
|
||||
label: 'x轴',
|
||||
value: 1
|
||||
},
|
||||
{
|
||||
label: 'y轴',
|
||||
value: 2
|
||||
},
|
||||
{
|
||||
label: 'z轴',
|
||||
value: 3
|
||||
}
|
||||
{ label: '目标1', value: 1 },
|
||||
{ label: '目标2', value: 2 },
|
||||
{ label: '目标3', value: 3 },
|
||||
],
|
||||
time: dayjs().format("YYYY-MM-DD HH:mm:ss"),
|
||||
})
|
||||
});
|
||||
|
||||
const {
|
||||
form, time,
|
||||
zeroPos,
|
||||
initPos,
|
||||
armList,
|
||||
coordinateList
|
||||
} = toRefs(data)
|
||||
const { form, time, zeroPos, initPos, armList } = toRefs(data);
|
||||
|
||||
const route = useRoute()
|
||||
let timer = null
|
||||
const route = useRoute();
|
||||
let timer = null;
|
||||
|
||||
onUnmounted(() => {
|
||||
clearInterval(timer)
|
||||
})
|
||||
clearInterval(timer);
|
||||
});
|
||||
|
||||
//获取机械臂参数
|
||||
const getDeviceInfo = () => {
|
||||
deviceTeach(devId.value).then((res) => {
|
||||
let data = formatNumber(res.data)//字符类型转数字类型
|
||||
Object.assign(form.value, data)
|
||||
//设置初始位置
|
||||
initPos.value.x = data.x
|
||||
initPos.value.y = data.y
|
||||
initPos.value.z = data.z
|
||||
initPos.value.rx = data.rx
|
||||
initPos.value.ry = data.ry
|
||||
initPos.value.rz = data.rz
|
||||
})
|
||||
}
|
||||
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;
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getDeviceInfo()
|
||||
const posControls = document.querySelectorAll(".btn")
|
||||
//监听按钮点击事件
|
||||
getDeviceInfo();
|
||||
const posControls = document.querySelectorAll(".pos-btn");
|
||||
posControls.forEach((item) => {
|
||||
item.addEventListener('click', (e) => {
|
||||
const target = e.currentTarget.classList[1]
|
||||
const vals = target.split("_")//x_0 rx_0
|
||||
const pos = vals[0]
|
||||
const calc = !!Number(vals[1])
|
||||
if (calc) {
|
||||
form.value[pos] += 0.01 * data.speed
|
||||
} else {
|
||||
form.value[pos] -= 0.01 * data.speed
|
||||
}
|
||||
})
|
||||
})
|
||||
//监听失焦事件
|
||||
// const warp = document.querySelector(".mechanical-arm")
|
||||
// warp.addEventListener('focusout', handleFocus, false)
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
timer = setInterval(() => {
|
||||
time.value = dayjs().format("YYYY-MM-DD HH:mm:ss")
|
||||
getDeviceInfo()
|
||||
}, 1000)
|
||||
})
|
||||
watch(() => JSON.parse(JSON.stringify(form.value)), (newData, oldData) => {
|
||||
// const change = findDifferences(oldData, newData)
|
||||
const data = convertNumbersToStringAndRemoveNulls(newData)
|
||||
deviceUpdateProfile({idDeDeviceRegistration: devId.value, ...data}).then((res)=>{
|
||||
console.log(res)
|
||||
})
|
||||
})
|
||||
|
||||
const handleFocus = (e) => {
|
||||
const type = e.target.type
|
||||
if (type === "number") {
|
||||
// console.log("数字框失焦")
|
||||
}
|
||||
}
|
||||
watch(() => JSON.parse(JSON.stringify(form.value)), (newData) => {
|
||||
const dataToSend = convertNumbersToStringAndRemoveNulls(newData);
|
||||
deviceUpdateProfile({ idDeDeviceRegistration: devId.value, ...dataToSend }).then((res) => {
|
||||
console.log(res);
|
||||
});
|
||||
});
|
||||
|
||||
const devId = computed(() => {
|
||||
return route.path.split("/")[2]
|
||||
})
|
||||
const deviceModel = computed(() => {
|
||||
return route.path.split("/")[1]
|
||||
})
|
||||
return route.path.split("/")[2];
|
||||
});
|
||||
|
||||
const handleResets = (type) => {
|
||||
if (type === "zero") {
|
||||
Object.assign(form.value, zeroPos.value)
|
||||
return
|
||||
Object.assign(form.value, zeroPos.value);
|
||||
return;
|
||||
}
|
||||
if (type === "init") {
|
||||
Object.assign(form.value, initPos.value)
|
||||
Object.assign(form.value, initPos.value);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mechanical-arm">
|
||||
<div class="top flex between">
|
||||
<div class="left " style="width: 38%">
|
||||
<div class="card" style="height: 100%">
|
||||
<div class="title">机械臂</div>
|
||||
<div class="body">机械臂图片</div>
|
||||
<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>
|
||||
</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 flex">
|
||||
<div class="left card" style="flex: 1;">
|
||||
<div class="title">姿态与位置控制</div>
|
||||
<div class="control-buttons">
|
||||
<div class="axis-controls flex">
|
||||
<div class="axis-group">
|
||||
<span>X: {{ form.x.toFixed(6) }} m</span>
|
||||
<button class="pos-btn" data-target="x_minus">-</button>
|
||||
<button class="pos-btn" data-target="x_plus">+</button>
|
||||
</div>
|
||||
<div class="axis-group">
|
||||
<span>Y: {{ form.y.toFixed(6) }} m</span>
|
||||
<button class="pos-btn" data-target="y_minus">-</button>
|
||||
<button class="pos-btn" data-target="y_plus">+</button>
|
||||
</div>
|
||||
<div class="axis-group">
|
||||
<span>Z: {{ form.z.toFixed(6) }} m</span>
|
||||
<button class="pos-btn" data-target="z_minus">-</button>
|
||||
<button class="pos-btn" data-target="z_plus">+</button>
|
||||
</div>
|
||||
<div class="axis-group">
|
||||
<span>RX: {{ form.rx.toFixed(6) }} deg</span>
|
||||
<button class="pos-btn rotation" data-target="rx_minus">
|
||||
<svg t="1747126605366" class="turn-up" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1461" width="20" height="20">
|
||||
<path d="M311.432533 448.170667H163.84L510.293333 3.413333 856.746667 448.170667h-147.592534L760.832 1006.933333H259.754667z" fill="#4076ff" p-id="1462"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="pos-btn rotation" data-target="rx_plus">
|
||||
<svg t="1747126605366" class="turn-bottom" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1461" width="20" height="20">
|
||||
<path d="M311.432533 448.170667H163.84L510.293333 3.413333 856.746667 448.170667h-147.592534L760.832 1006.933333H259.754667z" fill="#4076ff" p-id="1462"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="axis-group">
|
||||
<span>RY: {{ form.ry.toFixed(6) }} deg</span>
|
||||
<button class="pos-btn rotation" data-target="ry_minus">
|
||||
<svg t="1747126605366" class="turn-left" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1461" width="20" height="20">
|
||||
<path d="M311.432533 448.170667H163.84L510.293333 3.413333 856.746667 448.170667h-147.592534L760.832 1006.933333H259.754667z" fill="#4076ff" p-id="1462"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="pos-btn rotation" data-target="ry_plus">
|
||||
<svg t="1747126605366" class="turn-right" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1461" width="20" height="20">
|
||||
<path d="M311.432533 448.170667H163.84L510.293333 3.413333 856.746667 448.170667h-147.592534L760.832 1006.933333H259.754667z" fill="#4076ff" p-id="1462"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="axis-group">
|
||||
<span>RZ: {{ form.rz.toFixed(6) }} deg</span>
|
||||
<button class="pos-btn rotation" data-target="rz_minus">
|
||||
<svg t="1747126605366" class="turn-lb" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1461" width="20" height="20">
|
||||
<path d="M311.432533 448.170667H163.84L510.293333 3.413333 856.746667 448.170667h-147.592534L760.832 1006.933333H259.754667z" fill="#4076ff" p-id="1462"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="pos-btn rotation" data-target="rz_plus">
|
||||
<svg t="1747126605366" class="turn-rb" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1461" width="20" height="20">
|
||||
<path d="M311.432533 448.170667H163.84L510.293333 3.413333 856.746667 448.170667h-147.592534L760.832 1006.933333H259.754667z" fill="#4076ff" p-id="1462"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="coordinate-system">
|
||||
<svg width="400" height="400" viewBox="0 0 400 400" class="coordinate-svg">
|
||||
<!-- X axis (positive) -->
|
||||
<line x1="200" y1="200" x2="350" y2="200" stroke="#f00" stroke-width="4" />
|
||||
<text x="360" y="205" fill="#f00">X</text>
|
||||
<!-- X axis (negative) -->
|
||||
<line x1="200" y1="200" x2="50" y2="200" stroke="#f00" stroke-width="4" />
|
||||
<text x="40" y="205" fill="#f00">-X</text>
|
||||
<!-- Y axis (positive) -->
|
||||
<line x1="200" y1="200" x2="200" y2="50" stroke="#0f0" stroke-width="4" />
|
||||
<text x="195" y="40" fill="#0f0">Y</text>
|
||||
<!-- Y axis (negative) -->
|
||||
<line x1="200" y1="200" x2="200" y2="350" stroke="#0f0" stroke-width="4" />
|
||||
<text x="195" y="360" fill="#0f0">-Y</text>
|
||||
<!-- Z axis (positive) -->
|
||||
<line x1="200" y1="200" x2="300" y2="100" stroke="#00f" stroke-width="4" />
|
||||
<text x="305" y="95" fill="#00f">Z</text>
|
||||
<!-- Z axis (negative) -->
|
||||
<line x1="200" y1="200" x2="100" y2="300" stroke="#00f" stroke-width="4" />
|
||||
<text x="95" y="305" fill="#00f">-Z</text>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="center" style="width: 22%">
|
||||
<div class="card">
|
||||
<div class="title">工作模式</div>
|
||||
<div class="body">
|
||||
<el-radio-group v-model="form.workModule">
|
||||
<el-radio :value="1">真实机械臂</el-radio>
|
||||
<el-radio :value="2">仿真机械臂</el-radio>
|
||||
</el-radio-group>
|
||||
<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" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="card" style="margin-top: 10px">
|
||||
<div class="title">步进模式</div>
|
||||
<div class="mb10">
|
||||
<div class="mb5 action-label">位置步进( mm )</div>
|
||||
<el-input-number style="width: 100%;" v-model="form.posStep" :step="2"/>
|
||||
<div class="input-item">
|
||||
<span>关节2</span>
|
||||
<el-input-number v-model="form.joint2" :step="form.jointStep" :precision="6" />
|
||||
</div>
|
||||
|
||||
<div class="mb10">
|
||||
<div class="mb5 action-label">姿态步进( deg )</div>
|
||||
<el-input-number style="width: 100%;" v-model="form.postureStep" :step="2"/>
|
||||
<div class="input-item">
|
||||
<span>关节3</span>
|
||||
<el-input-number v-model="form.joint3" :step="form.jointStep" :precision="6" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="mb5 action-label">关节步进( deg )</div>
|
||||
<el-input-number style="width: 100%;" v-model="form.jointStep" :step="2"/>
|
||||
<div class="input-item">
|
||||
<span>关节4</span>
|
||||
<el-input-number v-model="form.joint4" :step="form.jointStep" :precision="6" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right " style="width: 38%">
|
||||
<div class="card" style="height: 100%">
|
||||
<div class="title">位置控制</div>
|
||||
<div class="body">
|
||||
<div class="btn-wrap pos-control" style="transform: rotateX(40deg);height: calc(100% - 70px)">
|
||||
<div class="btn z_0">
|
||||
<span>Z-</span>
|
||||
<svg t="1747126605366" class="turn-bottom" viewBox="0 0 1024 1024" version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg" p-id="1461" width="32" height="32">
|
||||
<path
|
||||
d="M311.432533 448.170667H163.84L510.293333 3.413333 856.746667 448.170667h-147.592534L760.832 1006.933333H259.754667z"
|
||||
fill="#4076ff" p-id="1462"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="btn x_0">
|
||||
<span> X-</span>
|
||||
<svg t="1747126605366" class="turn-up" viewBox="0 0 1024 1024" version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg" p-id="1461" width="32" height="32">
|
||||
<path
|
||||
d="M311.432533 448.170667H163.84L510.293333 3.413333 856.746667 448.170667h-147.592534L760.832 1006.933333H259.754667z"
|
||||
fill="#4076ff" p-id="1462"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="btn z_1">
|
||||
<span>Z+</span>
|
||||
<svg t="1747126605366" class="turn-up" viewBox="0 0 1024 1024" version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg" p-id="1461" width="32" height="32">
|
||||
<path
|
||||
d="M311.432533 448.170667H163.84L510.293333 3.413333 856.746667 448.170667h-147.592534L760.832 1006.933333H259.754667z"
|
||||
fill="#4076ff" p-id="1462"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="btn y_0">
|
||||
<span>Y-</span>
|
||||
<svg t="1747126605366" class="turn-left" viewBox="0 0 1024 1024" version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg" p-id="1461" width="32" height="32">
|
||||
<path
|
||||
d="M311.432533 448.170667H163.84L510.293333 3.413333 856.746667 448.170667h-147.592534L760.832 1006.933333H259.754667z"
|
||||
fill="#4076ff" p-id="1462"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="btn"></div>
|
||||
<div class="btn y_1">
|
||||
<span>Y+</span>
|
||||
<svg t="1747126605366" class="turn-right" viewBox="0 0 1024 1024" version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg" p-id="1461" width="32" height="32">
|
||||
<path
|
||||
d="M311.432533 448.170667H163.84L510.293333 3.413333 856.746667 448.170667h-147.592534L760.832 1006.933333H259.754667z"
|
||||
fill="#4076ff" p-id="1462"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="btn"></div>
|
||||
<div class="btn x_1">
|
||||
<span>X+</span>
|
||||
<svg t="1747126605366" class="turn-bottom" viewBox="0 0 1024 1024" version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg" p-id="1461" width="32" height="32">
|
||||
<path
|
||||
d="M311.432533 448.170667H163.84L510.293333 3.413333 856.746667 448.170667h-147.592534L760.832 1006.933333H259.754667z"
|
||||
fill="#4076ff" p-id="1462"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="btn"></div>
|
||||
|
||||
</div>
|
||||
<div>
|
||||
<div class="mb10 action-label">参考坐标系</div>
|
||||
<el-select
|
||||
|
||||
v-model="form.coordinate"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in coordinateList"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</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>
|
||||
<div class="input-item">
|
||||
<span>关节步进</span>
|
||||
<el-input-number v-model="form.jointStep" :step="0.1" :precision="1" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bottom flex between">
|
||||
<div class="left " style="width: 30%">
|
||||
<div class="card" style="height: 100%">
|
||||
<div class="title">机械臂位置姿态</div>
|
||||
<div class="body">
|
||||
<div class="flex mb10">
|
||||
<div class="left flex1">
|
||||
<div class="pos-title mb10">位置 ( m )</div>
|
||||
<div class="mb10"> X: {{ form.x.toFixed(6) }}</div>
|
||||
<div class="mb10"> Y: {{ form.y.toFixed(6) }}</div>
|
||||
<div class="mb10"> Z: {{ form.z.toFixed(6) }}</div>
|
||||
</div>
|
||||
<div class="right flex1">
|
||||
<div class="pos-title mb10">姿态 ( deg )</div>
|
||||
<div class="mb10"> RX: {{ form.rx.toFixed(6) }}</div>
|
||||
<div class="mb10"> RY: {{ form.ry.toFixed(6) }}</div>
|
||||
<div class="mb10"> RZ: {{ form.rz.toFixed(6) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="mb10 action-label">目标</div>
|
||||
<el-select
|
||||
v-model="form.arm"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in armList"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="center " style="width: 30%">
|
||||
<div class="card" style="height: 100%">
|
||||
<div class="title">关节控制单位( deg )</div>
|
||||
<div class="body">
|
||||
<div class="mb10 action-label-pos">
|
||||
<div class="action-label">关节1</div>
|
||||
<el-input-number v-model="form.joint1" :step="0.000001" :precision="6"/>
|
||||
</div>
|
||||
<div class="mb10 action-label-pos">
|
||||
<div class="action-label">关节2</div>
|
||||
<el-input-number v-model="form.joint2" :step="0.000001" :precision="6"/>
|
||||
</div>
|
||||
<div class="mb10 action-label-pos">
|
||||
<div class="action-label">关节3</div>
|
||||
<el-input-number v-model="form.joint3" :step="0.000001" :precision="6"/>
|
||||
</div>
|
||||
<div class="mb10 action-label-pos">
|
||||
<div class="action-label">关节4</div>
|
||||
<el-input-number v-model="form.joint4" :step="0.000001" :precision="6"/>
|
||||
</div>
|
||||
<div class="mb10 action-label-pos">
|
||||
<div class="action-label">关节5</div>
|
||||
<el-input-number v-model="form.joint5" :step="0.000001" :precision="6"/>
|
||||
</div>
|
||||
<div class="mb10 action-label-pos">
|
||||
<div class="action-label">关节6</div>
|
||||
<el-input-number v-model="form.joint6" :step="0.000001" :precision="6"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right " style="width: 38%">
|
||||
<div class="card" style="height: 100%">
|
||||
<div class="title">姿态控制</div>
|
||||
<div class="body">
|
||||
<div class="btn-wrap">
|
||||
<div class="btn rz_0 c">
|
||||
<span>Z-</span>
|
||||
<svg t="1747126605366" class="turn-lb" viewBox="0 0 1024 1024" version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg" p-id="1461" width="32" height="32">
|
||||
<path
|
||||
d="M311.432533 448.170667H163.84L510.293333 3.413333 856.746667 448.170667h-147.592534L760.832 1006.933333H259.754667z"
|
||||
fill="#4076ff" p-id="1462"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="btn rx_0 c">
|
||||
<span>X-</span>
|
||||
<svg t="1747126605366" class="turn-up" viewBox="0 0 1024 1024" version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg" p-id="1461" width="32" height="32">
|
||||
<path
|
||||
d="M311.432533 448.170667H163.84L510.293333 3.413333 856.746667 448.170667h-147.592534L760.832 1006.933333H259.754667z"
|
||||
fill="#4076ff" p-id="1462"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="btn rz_1 c">
|
||||
<span>Z+</span>
|
||||
<svg t="1747126605366" class="turn-rb" viewBox="0 0 1024 1024" version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg" p-id="1461" width="32" height="32">
|
||||
<path
|
||||
d="M311.432533 448.170667H163.84L510.293333 3.413333 856.746667 448.170667h-147.592534L760.832 1006.933333H259.754667z"
|
||||
fill="#4076ff" p-id="1462"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="btn ry_0 c">
|
||||
<span> Y-</span>
|
||||
<svg t="1747126605366" class="turn-left" viewBox="0 0 1024 1024" version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg" p-id="1461" width="32" height="32">
|
||||
<path
|
||||
d="M311.432533 448.170667H163.84L510.293333 3.413333 856.746667 448.170667h-147.592534L760.832 1006.933333H259.754667z"
|
||||
fill="#4076ff" p-id="1462"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="btn"></div>
|
||||
<div class="btn ry_1 c">
|
||||
<span>Y+</span>
|
||||
<svg t="1747126605366" class="turn-right" viewBox="0 0 1024 1024" version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg" p-id="1461" width="32" height="32">
|
||||
<path
|
||||
d="M311.432533 448.170667H163.84L510.293333 3.413333 856.746667 448.170667h-147.592534L760.832 1006.933333H259.754667z"
|
||||
fill="#4076ff" p-id="1462"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="btn"></div>
|
||||
<div class="btn rx_1 c">
|
||||
<span> X+</span>
|
||||
<svg t="1747126605366" class="turn-bottom" viewBox="0 0 1024 1024" version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg" p-id="1461" width="32" height="32">
|
||||
<path
|
||||
d="M311.432533 448.170667H163.84L510.293333 3.413333 856.746667 448.170667h-147.592534L760.832 1006.933333H259.754667z"
|
||||
fill="#4076ff" p-id="1462"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="btn"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions flex align-center between">
|
||||
<div class="left">
|
||||
<el-button @click="handleResets('zero')">零位姿态</el-button>
|
||||
<el-button @click="handleResets('init')">初始位姿</el-button>
|
||||
</div>
|
||||
<div class="center">
|
||||
{{ time }}
|
||||
</div>
|
||||
<div class="right flex align-center">
|
||||
<div style="width: 100px;user-select: none">速度</div>
|
||||
<el-slider size="small" :show-tooltip="false" v-model="data.speed"/>
|
||||
<div style="margin-left: 20px"> {{ data.speed }}%</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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -431,135 +261,167 @@ const handleResets = (type) => {
|
||||
|
||||
<style scoped lang="scss">
|
||||
.mechanical-arm {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
box-sizing: border-box;
|
||||
|
||||
.card {
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.controls {
|
||||
gap: 10px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.top {
|
||||
padding: 10px 10px 0;
|
||||
height: 48%;
|
||||
.pose-info {
|
||||
justify-content: space-around;
|
||||
padding: 10px 0;
|
||||
|
||||
.center {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.card:last-child {
|
||||
flex: 1;
|
||||
.pose-item {
|
||||
font-size: 16px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.bottom {
|
||||
padding: 0 10px;
|
||||
height: 44%;
|
||||
|
||||
|
||||
}
|
||||
|
||||
.actions {
|
||||
height: 6%;
|
||||
box-shadow: 0 0 8px rgba(0, 0, 0, 0.2);
|
||||
padding: 0 10px;
|
||||
|
||||
.right {
|
||||
width: 300px;
|
||||
|
||||
}
|
||||
|
||||
.center {
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
|
||||
.action-label {
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.action-label-pos {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.el-input-number {
|
||||
width: calc(100% - 50px);
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e4e7ed;
|
||||
padding: 10px;
|
||||
flex: 1;
|
||||
gap: 20px;
|
||||
|
||||
.title {
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
font-weight: 900;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.body {
|
||||
height: calc(100% - 29px);
|
||||
overflow-y: auto;
|
||||
.left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.pos-title {
|
||||
user-select: none;
|
||||
.control-buttons {
|
||||
margin-bottom: 20px;
|
||||
|
||||
.axis-controls {
|
||||
justify-content: space-around;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
|
||||
.axis-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
min-width: 150px;
|
||||
|
||||
span {
|
||||
width: 80px;
|
||||
text-align: center;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
button {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
background: #4076ff;
|
||||
color: white;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.rotation svg {
|
||||
transform-origin: center;
|
||||
}
|
||||
|
||||
.turn-up {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
.turn-bottom {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.turn-left {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.turn-right {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.turn-lb {
|
||||
transform: rotate(-135deg);
|
||||
}
|
||||
|
||||
.turn-rb {
|
||||
transform: rotate(135deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn-wrap {
|
||||
height: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
grid-template-rows: repeat(3, 1fr);
|
||||
justify-items: center;
|
||||
.coordinate-system {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.btn {
|
||||
.coordinate-svg {
|
||||
background: #f5f7fa;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cursor: pointer;
|
||||
.right {
|
||||
.input-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
|
||||
.input-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
span {
|
||||
width: 80px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
transform-origin: center center;
|
||||
|
||||
.el-input-number {
|
||||
width: calc(100% - 90px);
|
||||
}
|
||||
}
|
||||
|
||||
.turn-lb {
|
||||
transform: rotate(-135deg);
|
||||
}
|
||||
|
||||
.turn-rb {
|
||||
transform: rotate(135deg);
|
||||
}
|
||||
|
||||
.turn-left {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.turn-right {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.turn-bottom {
|
||||
transform: rotate(-180deg);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
height: 50px;
|
||||
|
||||
.time {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.speed {
|
||||
gap: 10px;
|
||||
|
||||
span {
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -194,7 +194,11 @@ const debouncedSetVolume = debounce(setVolume, 500);
|
||||
|
||||
<style scoped>
|
||||
.audio-player {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
max-width: 800px;
|
||||
width: 100%;
|
||||
height: calc(100vh - 125px);
|
||||
background-color: #fff;
|
||||
border-radius: 20px;
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
<el-tree v-if="!isTreeCollapsed" :data="treeData" :props="treeProps" :default-expanded-keys="['robot-node-config']" @node-click="handleNodeClick" />
|
||||
</div>
|
||||
<div class="robot-panel">
|
||||
<div id="robot-bg" ref="robotBg" :style="{ backgroundImage: `url(${robotImage})` }">
|
||||
<div id="robot-bg" ref="robotBg" :style="{ backgroundImage: `url(${robotImage})`, width: bgSize.width + 'px', height: bgSize.height + 'px' }">
|
||||
<div
|
||||
v-for="area in adjustedAreas"
|
||||
:key="area.id"
|
||||
@ -48,12 +48,10 @@
|
||||
</div>
|
||||
</div>
|
||||
<component :is="popup.component" v-bind="popup.props" />
|
||||
<!-- 边缘拖动区域 -->
|
||||
<div class="resize-edge top" @mousedown="startResizing($event, index, 'top')"></div>
|
||||
<div class="resize-edge bottom" @mousedown="startResizing($event, index, 'bottom')"></div>
|
||||
<div class="resize-edge left" @mousedown="startResizing($event, index, 'left')"></div>
|
||||
<div class="resize-edge right" @mousedown="startResizing($event, index, 'right')"></div>
|
||||
<!-- 角落拖动区域 -->
|
||||
<div class="resize-corner top-left" @mousedown="startResizing($event, index, 'top-left')"></div>
|
||||
<div class="resize-corner top-right" @mousedown="startResizing($event, index, 'top-right')"></div>
|
||||
<div class="resize-corner bottom-left" @mousedown="startResizing($event, index, 'bottom-left')"></div>
|
||||
@ -67,147 +65,151 @@ import { ref, onMounted, computed, onUnmounted, watch } from 'vue';
|
||||
import HandControl from '../register/components/DexHand/index.vue';
|
||||
import CameraView from '../register/components/Camera/index.vue';
|
||||
import MusicPlayer from '../register/components/Speaker/index.vue';
|
||||
import MechanicalArm from '../register/components/MechanicalArm/index.vue';
|
||||
import image from '@/assets/images/robot.png';
|
||||
|
||||
// 组件映射
|
||||
const componentsMap = {
|
||||
HandControl,
|
||||
CameraView,
|
||||
MusicPlayer
|
||||
MusicPlayer,
|
||||
MechanicalArm
|
||||
};
|
||||
|
||||
const robotImage = image;
|
||||
const robotBg = ref(null);
|
||||
const isTreeCollapsed = ref(false); // 控制树形控件收起/展开
|
||||
|
||||
const isTreeCollapsed = ref(false);
|
||||
const popups = ref([]);
|
||||
const isDragging = ref(false);
|
||||
const isResizing = ref([]);
|
||||
const currentZIndex = ref(2000); // 提高初始 z-index
|
||||
const dragIndex = ref(null); // 跟踪当前拖动的索引
|
||||
|
||||
const currentZIndex = ref(2000);
|
||||
const dragIndex = ref(null);
|
||||
const resizeStart = ref({ index: null, startX: 0, startY: 0, originalWidth: 0, originalHeight: 0, originalTop: 0, originalLeft: 0, edge: '' });
|
||||
const imageDimensions = ref({ width: 0, height: 0 });
|
||||
const containerSize = ref({ width: 0, height: 0 });
|
||||
const bgSize = ref({ width: 0, height: 0 });
|
||||
|
||||
// 原始坐标和尺寸(基于图片原始大小)
|
||||
const originalAreas = ref([
|
||||
{ id: 'hand-left', x: 15, y: 505, width: 110, height: 50, component: HandControl, label: '左手' },
|
||||
{ id: 'hand-right', x: 300, y: 505, width: 110, height: 50, component: HandControl, label: '右手' },
|
||||
{ id: 'arm-left', x: 0, y: 150, width: 220, height: 50, component: MechanicalArm, label: '左机械臂' },
|
||||
{ id: 'arm-right', x: 200, y: 150, width: 220, height: 50, component: MechanicalArm, label: '右机械臂' },
|
||||
{ id: 'camera', x: 140, y: 50, width: 160, height: 50, component: CameraView, label: '摄像头' },
|
||||
{ id: 'mouth', x: 140, y: 100, width: 160, height: 50, component: MusicPlayer, label: '扬声器' }
|
||||
]);
|
||||
|
||||
// 树数据
|
||||
const treeData = ref([
|
||||
{
|
||||
id: 'robot-node-config', // 添加唯一 ID 用于默认展开
|
||||
id: 'robot-node-config',
|
||||
label: '机器人节点配置',
|
||||
children: [
|
||||
{
|
||||
id: 'node-1',
|
||||
label: '节点1',
|
||||
children: [{ id: 'node-1-1', label: '子节点1-1' }, { id: 'node-1-2', label: '子节点1-2' }]
|
||||
},
|
||||
{
|
||||
id: 'node-2',
|
||||
label: '节点2',
|
||||
children: [{ id: 'node-2-1', label: '子节点2-1' }, { id: 'node-2-2', label: '子节点2-2' }]
|
||||
},
|
||||
{ id: 'node-1', label: '节点1', children: [{ id: 'node-1-1', label: '子节点1-1' }, { id: 'node-1-2', label: '子节点1-2' }] },
|
||||
{ id: 'node-2', label: '节点2', children: [{ id: 'node-2-1', label: '子节点2-1' }, { id: 'node-2-2', label: '子节点2-2' }] },
|
||||
]
|
||||
}
|
||||
]);
|
||||
|
||||
const treeProps = {
|
||||
children: 'children',
|
||||
label: 'label'
|
||||
};
|
||||
const treeProps = { children: 'children', label: 'label' };
|
||||
|
||||
const handleNodeClick = (data) => {
|
||||
console.log('点击节点', data.label);
|
||||
};
|
||||
|
||||
// 原始坐标和尺寸(基于图片真实尺寸),添加 label 用于显示文字
|
||||
const originalAreas = ref([
|
||||
{ id: 'hand-left', x: 50, y: 200, width: 50, height: 100, component: HandControl, label: '左手' },
|
||||
{ id: 'hand-right', x: 400, y: 200, width: 50, height: 100, component: HandControl, label: '右手' },
|
||||
{ id: 'camera', x: 225, y: 100, width: 50, height: 50, component: CameraView, label: '摄像头' },
|
||||
{ id: 'mouth', x: 225, y: 300, width: 50, height: 50, component: MusicPlayer, label: '扬声器' }
|
||||
]);
|
||||
|
||||
const imageDimensions = ref({ width: 0, height: 0 });
|
||||
|
||||
// 计算调整后的区域
|
||||
const adjustedAreas = computed(() => {
|
||||
if (!imageDimensions.value.width || !imageDimensions.value.height || !robotBg.value) return [];
|
||||
const containerWidth = robotBg.value.parentElement.offsetWidth - 40 - (isTreeCollapsed.value ? 0 : 240); // 减去外层 padding 和 tree-panel 宽度
|
||||
console.log(containerWidth)
|
||||
const containerHeight = robotBg.value.parentElement.offsetHeight - 40; // 减去外层 padding
|
||||
const containerWidth = containerSize.value.width - (isTreeCollapsed.value ? 0 : 240); // 仅减去树宽度
|
||||
const containerHeight = containerSize.value.height; // 无垂直padding
|
||||
const scaleX = containerWidth / imageDimensions.value.width;
|
||||
const scaleY = containerHeight / imageDimensions.value.height;
|
||||
const scale = Math.min(scaleX, scaleY); // 取最小缩放比例以适配
|
||||
const offsetX = (containerWidth - (imageDimensions.value.width * scale)) / 2;
|
||||
const offsetY = (containerHeight - (imageDimensions.value.height * scale)) / 2;
|
||||
const scale = Math.min(scaleX, scaleY, 1); // 防止放大
|
||||
const scaledWidth = imageDimensions.value.width * scale; // 图片宽
|
||||
const scaledHeight = imageDimensions.value.height * scale;
|
||||
|
||||
// const offsetX = (containerWidth - scaledWidth) / 2;
|
||||
// const offsetY = (containerHeight - scaledHeight) / 2;
|
||||
|
||||
// 更新背景图片尺寸
|
||||
bgSize.value = { width: scaledWidth, height: scaledHeight };
|
||||
|
||||
return originalAreas.value.map(area => ({
|
||||
...area,
|
||||
x: (area.x * scale) + offsetX,
|
||||
y: (area.y * scale) + offsetY,
|
||||
x: area.x * scale,
|
||||
y: area.y * scale,
|
||||
width: area.width * scale,
|
||||
height: area.height * scale
|
||||
}));
|
||||
});
|
||||
|
||||
// 打开弹窗,考虑树宽度和padding偏移
|
||||
const openPopup = (area) => {
|
||||
// 检查是否已存在相同 id 的窗口
|
||||
const existingPopup = popups.value.find(p => p.id === area.id);
|
||||
if (existingPopup) {
|
||||
console.log('Popup with id', area.id, 'is already open');
|
||||
return; // 如果已打开,阻止重复打开
|
||||
console.log('已打开ID为', area.id, '的弹窗');
|
||||
return;
|
||||
}
|
||||
const treeWidth = isTreeCollapsed.value ? 0 : 240;
|
||||
const leftOffset = treeWidth + 20; // tree宽度 + robot-panel padding-left
|
||||
const popup = {
|
||||
id: area.id,
|
||||
component: area.component, // 使用组件引用
|
||||
top: area.y + 'px',
|
||||
left: area.x + 'px',
|
||||
component: area.component,
|
||||
top: `${area.y}px`,
|
||||
left: `${area.x + leftOffset}px`,
|
||||
zIndex: currentZIndex.value++,
|
||||
width: 640, // 默认宽度
|
||||
height: 480, // 默认高度
|
||||
originalWidth: 640, // 初始化原始宽度
|
||||
originalHeight: 480, // 初始化原始高度
|
||||
originalTop: area.y + 'px', // 初始化原始顶部位置
|
||||
originalLeft: area.x + 'px', // 初始化原始左侧位置
|
||||
width: 640,
|
||||
height: 480,
|
||||
originalWidth: 640,
|
||||
originalHeight: 480,
|
||||
originalTop: `${area.y}px`,
|
||||
originalLeft: `${area.x + leftOffset}px`,
|
||||
isMaximized: false,
|
||||
props: { terminalId: 'your-terminal-id', cameraId: 'your-camera-id' } // 替换为实际值
|
||||
props: { terminalId: 'your-terminal-id', cameraId: 'your-camera-id' }
|
||||
};
|
||||
popups.value.push(popup);
|
||||
isResizing.value.push(false);
|
||||
console.log('Opened popup with id', area.id, 'initial size:', popup.width, 'x', popup.height, 'position:', popup.top, popup.left);
|
||||
console.log('打开弹窗ID', area.id, '初始尺寸:', popup.width, 'x', popup.height, '位置:', popup.top, popup.left);
|
||||
};
|
||||
|
||||
// 关闭弹窗
|
||||
const closePopup = (index) => {
|
||||
popups.value.splice(index, 1);
|
||||
isResizing.value.splice(index, 1);
|
||||
console.log('Closed popup at index', index);
|
||||
console.log('关闭弹窗索引', index);
|
||||
};
|
||||
|
||||
// 最小化弹窗
|
||||
const minimizePopup = (index) => {
|
||||
const popup = popups.value[index];
|
||||
if (popup.isMaximized) {
|
||||
popup.width = popup.originalWidth; // 恢复到原始宽度
|
||||
popup.height = popup.originalHeight; // 恢复到原始高度
|
||||
popup.top = popup.originalTop; // 恢复到原始顶部位置
|
||||
popup.left = popup.originalLeft; // 恢复到原始左侧位置
|
||||
popup.width = popup.originalWidth;
|
||||
popup.height = popup.originalHeight;
|
||||
popup.top = popup.originalTop;
|
||||
popup.left = popup.originalLeft;
|
||||
popup.isMaximized = false;
|
||||
console.log('Minimized popup', popup.id, 'to size:', popup.width, 'x', popup.height, 'position:', popup.top, popup.left);
|
||||
console.log('最小化弹窗', popup.id, '到尺寸:', popup.width, 'x', popup.height, '位置:', popup.top, popup.left);
|
||||
}
|
||||
};
|
||||
|
||||
// 最大化弹窗
|
||||
const maximizePopup = (index) => {
|
||||
const popup = popups.value[index];
|
||||
if (!popup.isMaximized) {
|
||||
popup.originalWidth = popup.width; // 保存当前宽度作为原始宽度
|
||||
popup.originalHeight = popup.height; // 保存当前高度作为原始高度
|
||||
popup.originalTop = popup.top; // 保存当前顶部位置
|
||||
popup.originalLeft = popup.left; // 保存当前左侧位置
|
||||
popup.width = window.innerWidth; // 全屏宽度
|
||||
popup.height = window.innerHeight; // 全屏高度
|
||||
popup.top = '0px'; // 从顶部开始
|
||||
popup.left = '0px'; // 从左侧开始
|
||||
popup.originalWidth = popup.width;
|
||||
popup.originalHeight = popup.height;
|
||||
popup.originalTop = popup.top;
|
||||
popup.originalLeft = popup.left;
|
||||
popup.width = window.innerWidth;
|
||||
popup.height = window.innerHeight;
|
||||
popup.top = '0px';
|
||||
popup.left = '0px';
|
||||
popup.isMaximized = true;
|
||||
console.log('Maximized popup', popup.id, 'to size:', popup.width, 'x', popup.height, 'position:', popup.top, popup.left);
|
||||
console.log('最大化弹窗', popup.id, '到尺寸:', popup.width, 'x', popup.height, '位置:', popup.top, popup.left);
|
||||
}
|
||||
};
|
||||
|
||||
// 拖动相关逻辑
|
||||
const startDragging = (e, index) => {
|
||||
isDragging.value = true;
|
||||
dragIndex.value = index;
|
||||
@ -217,18 +219,16 @@ const startDragging = (e, index) => {
|
||||
popup.zIndex = currentZIndex.value++;
|
||||
document.addEventListener('mousemove', dragHandler);
|
||||
document.addEventListener('mouseup', stopDragging);
|
||||
console.log('Start dragging', index, 'ClientY:', e.clientY, 'Handle height:', e.currentTarget.offsetHeight);
|
||||
};
|
||||
|
||||
const dragHandler = (e) => {
|
||||
if (isDragging.value && dragIndex.value !== null) {
|
||||
requestAnimationFrame(() => {
|
||||
const popup = popups.value[dragIndex.value];
|
||||
const newTop = Math.max(0, e.pageY - popup.startY); // 限制顶部不小于 0
|
||||
const newLeft = e.pageX - popup.startX; // 左侧无限制
|
||||
const newTop = Math.max(0, e.pageY - popup.startY);
|
||||
const newLeft = e.pageX - popup.startX;
|
||||
popup.top = `${newTop}px`;
|
||||
popup.left = `${newLeft}px`;
|
||||
console.log('Dragging', dragIndex.value, 'New position:', popup.left, popup.top);
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -240,6 +240,7 @@ const stopDragging = () => {
|
||||
document.removeEventListener('mouseup', stopDragging);
|
||||
};
|
||||
|
||||
// 调整大小相关逻辑
|
||||
const startResizing = (e, index, edge) => {
|
||||
const popup = popups.value[index];
|
||||
resizeStart.value = {
|
||||
@ -255,7 +256,6 @@ const startResizing = (e, index, edge) => {
|
||||
isResizing.value[index] = true;
|
||||
document.addEventListener('mousemove', handleResize);
|
||||
document.addEventListener('mouseup', stopResizingGlobal);
|
||||
console.log('Start resizing', edge);
|
||||
};
|
||||
|
||||
const handleResize = (e) => {
|
||||
@ -271,22 +271,18 @@ const handleResize = (e) => {
|
||||
case 'top':
|
||||
const newHeight = Math.max(minHeight, resizeStart.value.originalHeight - diffY);
|
||||
popup.height = newHeight;
|
||||
popup.top = `${Math.max(0, resizeStart.value.originalTop + (resizeStart.value.originalHeight - newHeight))}px`; // 限制顶部
|
||||
console.log('Resizing top', 'newHeight:', newHeight, 'newTop:', popup.top);
|
||||
popup.top = `${Math.max(0, resizeStart.value.originalTop + (resizeStart.value.originalHeight - newHeight))}px`;
|
||||
break;
|
||||
case 'bottom':
|
||||
popup.height = Math.max(minHeight, resizeStart.value.originalHeight + diffY);
|
||||
console.log('Resizing bottom', 'newHeight:', popup.height);
|
||||
break;
|
||||
case 'left':
|
||||
const newWidth = Math.max(minWidth, resizeStart.value.originalWidth - diffX);
|
||||
popup.width = newWidth;
|
||||
popup.left = `${resizeStart.value.originalLeft + (resizeStart.value.originalWidth - newWidth)}px`;
|
||||
console.log('Resizing left', 'newWidth:', newWidth, 'newLeft:', popup.left);
|
||||
break;
|
||||
case 'right':
|
||||
popup.width = Math.max(minWidth, resizeStart.value.originalWidth + diffX);
|
||||
console.log('Resizing right', 'newWidth:', popup.width);
|
||||
break;
|
||||
case 'top-left':
|
||||
const newWidthTL = Math.max(minWidth, resizeStart.value.originalWidth - diffX);
|
||||
@ -294,16 +290,14 @@ const handleResize = (e) => {
|
||||
popup.width = newWidthTL;
|
||||
popup.height = newHeightTL;
|
||||
popup.left = `${resizeStart.value.originalLeft + (resizeStart.value.originalWidth - newWidthTL)}px`;
|
||||
popup.top = `${Math.max(0, resizeStart.value.originalTop + (resizeStart.value.originalHeight - newHeightTL))}px`; // 限制顶部
|
||||
console.log('Resizing top-left', 'newWidth:', newWidthTL, 'newHeight:', newHeightTL, 'newTop:', popup.top);
|
||||
popup.top = `${Math.max(0, resizeStart.value.originalTop + (resizeStart.value.originalHeight - newHeightTL))}px`;
|
||||
break;
|
||||
case 'top-right':
|
||||
const newWidthTR = Math.max(minWidth, resizeStart.value.originalWidth + diffX);
|
||||
const newHeightTR = Math.max(minHeight, resizeStart.value.originalHeight - diffY);
|
||||
popup.width = newWidthTR;
|
||||
popup.height = newHeightTR;
|
||||
popup.top = `${Math.max(0, resizeStart.value.originalTop + (resizeStart.value.originalHeight - newHeightTR))}px`; // 限制顶部
|
||||
console.log('Resizing top-right', 'newWidth:', newWidthTR, 'newHeight:', newHeightTR, 'newTop:', popup.top);
|
||||
popup.top = `${Math.max(0, resizeStart.value.originalTop + (resizeStart.value.originalHeight - newHeightTR))}px`;
|
||||
break;
|
||||
case 'bottom-left':
|
||||
const newWidthBL = Math.max(minWidth, resizeStart.value.originalWidth - diffX);
|
||||
@ -311,12 +305,10 @@ const handleResize = (e) => {
|
||||
popup.width = newWidthBL;
|
||||
popup.height = newHeightBL;
|
||||
popup.left = `${resizeStart.value.originalLeft + (resizeStart.value.originalWidth - newWidthBL)}px`;
|
||||
console.log('Resizing bottom-left', 'newWidth:', newWidthBL, 'newHeight:', newHeightBL, 'newLeft:', popup.left);
|
||||
break;
|
||||
case 'bottom-right':
|
||||
popup.width = Math.max(minWidth, resizeStart.value.originalWidth + diffX);
|
||||
popup.height = Math.max(minHeight, resizeStart.value.originalHeight + diffY);
|
||||
console.log('Resizing bottom-right', 'newWidth:', popup.width, 'newHeight:', popup.height);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -331,19 +323,39 @@ const stopResizingGlobal = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 防抖函数
|
||||
const debounce = (fn, delay) => {
|
||||
let timeout;
|
||||
return (...args) => {
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => fn(...args), delay);
|
||||
};
|
||||
};
|
||||
|
||||
// 更新容器和图片尺寸
|
||||
const updateDimensions = debounce(() => {
|
||||
console.log('图片',robotBg.value.width,robotBg.value.offsetWidth, robotBg.value.offsetHeight)
|
||||
if (robotBg.value && robotBg.value.parentElement) {
|
||||
containerSize.value = {
|
||||
width: robotBg.value.parentElement.offsetWidth - 40, // 左右padding 20*2
|
||||
height: robotBg.value.parentElement.offsetHeight // 无垂直padding
|
||||
};
|
||||
console.log('更新容器尺寸:', containerSize.value.width, 'x', containerSize.value.height);
|
||||
}
|
||||
}, 100);
|
||||
|
||||
onMounted(() => {
|
||||
const img = new Image();
|
||||
img.src = robotImage;
|
||||
img.onload = () => {
|
||||
imageDimensions.value = { width: img.width, height: img.height };
|
||||
console.log('图片尺寸:', imageDimensions.value.width, 'x', imageDimensions.value.height);
|
||||
if (robotBg.value) {
|
||||
watch(() => [robotBg.value.parentElement.offsetWidth, robotBg.value.parentElement.offsetHeight, isTreeCollapsed.value], ([newWidth, newHeight]) => {
|
||||
const containerWidth = newWidth - 40 - (isTreeCollapsed.value ? 0 : 240); // 减去外层 padding 和 tree-panel 宽度
|
||||
const containerHeight = newHeight - 40; // 减去外层 padding
|
||||
const scale = Math.min(containerWidth / img.width, containerHeight / img.height);
|
||||
robotBg.value.style.minWidth = `${img.width * scale}px`;
|
||||
robotBg.value.style.minHeight = `${img.height * scale}px`;
|
||||
}, { immediate: true });
|
||||
const observer = new ResizeObserver(updateDimensions);
|
||||
observer.observe(robotBg.value.parentElement);
|
||||
watch(() => isTreeCollapsed.value, updateDimensions, { immediate: true });
|
||||
updateDimensions(); // 初始更新
|
||||
onUnmounted(() => observer.disconnect());
|
||||
}
|
||||
};
|
||||
});
|
||||
@ -358,47 +370,48 @@ onUnmounted(() => {
|
||||
.robot-container {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
display: flex; /* 保持与外层一致 */
|
||||
overflow: hidden; /* 防止遮挡弹窗 */
|
||||
padding: 0; /* 外层已处理 padding */
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
background-color: #fff;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.tree-panel {
|
||||
flex-shrink: 0; /* 防止收缩 */
|
||||
/* padding: 10px; */
|
||||
flex-shrink: 0;
|
||||
border-right: 1px solid #ddd;
|
||||
overflow-y: auto; /* 垂直滚动 */
|
||||
height: auto; /* 动态高度 */
|
||||
transition: width 0.3s ease; /* 平滑过渡 */
|
||||
|
||||
overflow-y: auto;
|
||||
height: auto;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.tree-panel:deep(.el-tree) {
|
||||
padding: 10px !important;
|
||||
}
|
||||
padding: 10px !important;
|
||||
}
|
||||
|
||||
.robot-panel {
|
||||
flex-grow: 1; /* 填满剩余空间 */
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
justify-content: center; /* 水平居中 */
|
||||
align-items: center; /* 垂直居中 */
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
padding: 0 20px; /* 补偿外层 padding */
|
||||
position: relative; /* 确保按钮定位 */
|
||||
padding: 0 20px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#robot-bg {
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
min-width: 0; /* 防止收缩 */
|
||||
min-height: 0; /* 防止收缩 */
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
position: relative; /* 添加 relative 定位 */
|
||||
}
|
||||
|
||||
.toggle-button {
|
||||
position: absolute;
|
||||
left: 20px; /* 贴近左边边 */
|
||||
top: 10px; /* 顶部留点间距 */
|
||||
left: 20px;
|
||||
top: 10px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
@ -408,7 +421,7 @@ onUnmounted(() => {
|
||||
font-size: 24px;
|
||||
background-color: #e0e0e0;
|
||||
border-radius: 4px;
|
||||
z-index: 2001; /* 确保高于其他元素 */
|
||||
z-index: 2001;
|
||||
}
|
||||
|
||||
.toggle-button:hover {
|
||||
@ -417,7 +430,7 @@ onUnmounted(() => {
|
||||
|
||||
.clickable-area {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.clickable-text {
|
||||
@ -443,23 +456,24 @@ onUnmounted(() => {
|
||||
user-select: none;
|
||||
overflow: auto;
|
||||
box-sizing: border-box;
|
||||
transition: width 0.2s, height 0.2s, top 0.2s, left 0.2s; /* 平滑过渡 */
|
||||
transition: width 0.2s, height 0.2s, top 0.2s, left 0.2s;
|
||||
border: 1px solid #eee;
|
||||
}
|
||||
|
||||
.popup.dragging {
|
||||
transition: none; /* 拖动时禁用过渡 */
|
||||
transition: none;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
height: 30px; /* 增加高度以容纳按钮 */
|
||||
background: #e0e0e0; /* 背景色 */
|
||||
cursor: move; /* 拖动光标 */
|
||||
height: 30px;
|
||||
background: #e0e0e0;
|
||||
cursor: move;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center; /* 标题居中 */
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.popup-title {
|
||||
@ -471,8 +485,8 @@ onUnmounted(() => {
|
||||
text-overflow: ellipsis;
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
right: 60px; /* 留出按钮空间 */
|
||||
text-align: center; /* 居中文本 */
|
||||
right: 60px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.popup-controls {
|
||||
@ -484,8 +498,8 @@ onUnmounted(() => {
|
||||
|
||||
.control-icon {
|
||||
cursor: pointer;
|
||||
font-size: 18px; /* 放大按钮 */
|
||||
padding: 4px 8px; /* 增加内边距 */
|
||||
font-size: 18px;
|
||||
padding: 4px 8px;
|
||||
background: #f0f0f0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
@ -502,19 +516,17 @@ onUnmounted(() => {
|
||||
background: #ff6666;
|
||||
}
|
||||
|
||||
/* 边缘拖动区域 */
|
||||
.resize-edge {
|
||||
position: absolute;
|
||||
background: transparent;
|
||||
z-index: 2; /* 提高拖动区域优先级 */
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.top { top: 0; left: 0; right: 0; height: 10px; cursor: ns-resize; } /* 增加高度以覆盖 drag-handle */
|
||||
.top { top: 0; left: 0; right: 0; height: 10px; cursor: ns-resize; }
|
||||
.bottom { bottom: 0; left: 0; right: 0; height: 5px; cursor: ns-resize; }
|
||||
.left { top: 0; bottom: 0; left: 0; width: 5px; cursor: ew-resize; }
|
||||
.right { top: 0; bottom: 0; right: 0; width: 5px; cursor: ew-resize; }
|
||||
|
||||
/* 角落拖动区域 */
|
||||
.resize-corner {
|
||||
position: absolute;
|
||||
width: 10px;
|
||||
@ -528,6 +540,6 @@ onUnmounted(() => {
|
||||
.bottom-right { bottom: 0; right: 0; cursor: nwse-resize; }
|
||||
|
||||
.popup.resizing {
|
||||
transition: none; /* 缩放时禁用过渡 */
|
||||
transition: none;
|
||||
}
|
||||
</style>
|
||||
Loading…
Reference in New Issue
Block a user