feat: 连续对话

This commit is contained in:
zhanghao 2025-09-09 16:40:02 +08:00
commit e221f8d978
14 changed files with 1638 additions and 1059 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 311 KiB

After

Width:  |  Height:  |  Size: 270 KiB

View File

@ -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 {

View File

@ -47,18 +47,6 @@ export const constantRoutes = [
component: () => import("@/views/register"),
hidden: true,
},
{
path: "/zhtest",
component: import("@/views/system/user/profile/index"),
// children: [
// {
// path: "profilettt",
// component: () => import("@/views/system/user/profile/index"),
// name: "Profile",
// meta: { title: "个人中心", icon: "user", hidden: true },
// },
// ],
},
{
path: "/:pathMatch(.*)*",
component: () => import("@/views/error/404"),
@ -89,6 +77,12 @@ export const constantRoutes = [
meta: { title: "摄像头详情" },
hidden: true,
},
],
},
{
path: "/device",
component: Layout,
children: [
{
path: "hand/:id",
component: () =>
@ -97,16 +91,16 @@ export const constantRoutes = [
meta: { title: "灵巧手详情" },
hidden: true,
},
{
path: "speaker/:id",
component: () =>
import("@/views/device/register/components/Speaker/index.vue"),
name: "扬声器示教",
meta: { title: "扬声器详情" },
hidden: true,
},
],
},
{
path: "speaker/:id",
component: () =>
import("@/views/device/register/components/Speaker/index.vue"),
name: "扬声器示教",
meta: { title: "扬声器详情" },
hidden: true,
},
{
//语音合成
path: "/speech_synthesis",
@ -158,7 +152,6 @@ export const constantRoutes = [
},
],
},
];
// 动态路由,基于用户权限动态去加载

View File

@ -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);
}
});

View File

@ -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>

View File

@ -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>

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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;

View File

@ -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>