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"; @import "@/assets/styles/variables.module";
.app-main { .app-main {
/* 50= navbar 50 */ /* 50= navbar 50 */
min-height: calc(100vh - 50px); height: calc(100vh - 84px);
padding: 20px; padding: 20px;
width: 100%; width: 100%;
position: relative; position: relative;
@ -54,7 +54,7 @@ function addIframe() {
.hasTagsView { .hasTagsView {
.app-main { .app-main {
/* 84 = navbar + tags-view = 50 + 34 */ /* 84 = navbar + tags-view = 50 + 34 */
min-height: calc(100vh - 84px); height: calc(100vh - 84px);
} }
.fixed-header + .app-main { .fixed-header + .app-main {

View File

@ -1,96 +1,96 @@
import { createApp } from 'vue' import { createApp } from 'vue'
import Cookies from 'js-cookie' import Cookies from 'js-cookie'
import ElementPlus from 'element-plus' import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css' import 'element-plus/dist/index.css'
import 'element-plus/theme-chalk/dark/css-vars.css' import 'element-plus/theme-chalk/dark/css-vars.css'
import locale from 'element-plus/es/locale/lang/zh-cn' import locale from 'element-plus/es/locale/lang/zh-cn'
import '@/assets/styles/index.scss' // global css import '@/assets/styles/index.scss' // global css
import App from './App' import App from './App'
import store from './store' import store from './store'
import router from './router' import router from './router'
import directive from './directive' // directive import directive from './directive' // directive
import WebSocketPlugin from './plugins/websocket'; import WebSocketPlugin from './plugins/websocket';
// 注册指令 // 注册指令
import plugins from './plugins' // plugins import plugins from './plugins' // plugins
import { download } from '@/utils/request' import { download } from '@/utils/request'
// svg图标 // svg图标
import 'virtual:svg-icons-register' import 'virtual:svg-icons-register'
import SvgIcon from '@/components/SvgIcon' import SvgIcon from '@/components/SvgIcon'
import elementIcons from '@/components/SvgIcon/svgicon' import elementIcons from '@/components/SvgIcon/svgicon'
import './permission' // permission control import './permission' // permission control
import { useDict } from '@/utils/dict' import { useDict } from '@/utils/dict'
import { parseTime, resetForm, addDateRange, handleTree, selectDictLabel, selectDictLabels } from '@/utils/ruoyi' import { parseTime, resetForm, addDateRange, handleTree, selectDictLabel, selectDictLabels } from '@/utils/ruoyi'
// 分页组件 // 分页组件
import Pagination from '@/components/Pagination' import Pagination from '@/components/Pagination'
// 自定义表格工具组件 // 自定义表格工具组件
import RightToolbar from '@/components/RightToolbar' import RightToolbar from '@/components/RightToolbar'
// 富文本组件 // 富文本组件
import Editor from "@/components/Editor" import Editor from "@/components/Editor"
// 文件上传组件 // 文件上传组件
import FileUpload from "@/components/FileUpload" import FileUpload from "@/components/FileUpload"
// 图片上传组件 // 图片上传组件
import ImageUpload from "@/components/ImageUpload" import ImageUpload from "@/components/ImageUpload"
// 图片预览组件 // 图片预览组件
import ImagePreview from "@/components/ImagePreview" import ImagePreview from "@/components/ImagePreview"
// 字典标签组件 // 字典标签组件
import DictTag from '@/components/DictTag' import DictTag from '@/components/DictTag'
// ace.config.js(独立配置文件) // ace.config.js(独立配置文件)
import ace from 'ace-builds'; import ace from 'ace-builds';
import modeJsonUrl from 'ace-builds/src-noconflict/mode-json?url'; import modeJsonUrl from 'ace-builds/src-noconflict/mode-json?url';
ace.config.setModuleUrl('ace/mode/json', modeJsonUrl); // 动态绑定语法模块路径 ace.config.setModuleUrl('ace/mode/json', modeJsonUrl); // 动态绑定语法模块路径
const app = createApp(App) const app = createApp(App)
// 全局方法挂载 // 全局方法挂载
app.config.globalProperties.useDict = useDict app.config.globalProperties.useDict = useDict
app.config.globalProperties.download = download app.config.globalProperties.download = download
app.config.globalProperties.parseTime = parseTime app.config.globalProperties.parseTime = parseTime
app.config.globalProperties.resetForm = resetForm app.config.globalProperties.resetForm = resetForm
app.config.globalProperties.handleTree = handleTree app.config.globalProperties.handleTree = handleTree
app.config.globalProperties.addDateRange = addDateRange app.config.globalProperties.addDateRange = addDateRange
app.config.globalProperties.selectDictLabel = selectDictLabel app.config.globalProperties.selectDictLabel = selectDictLabel
app.config.globalProperties.selectDictLabels = selectDictLabels app.config.globalProperties.selectDictLabels = selectDictLabels
// 全局组件挂载 // 全局组件挂载
app.component('DictTag', DictTag) app.component('DictTag', DictTag)
app.component('Pagination', Pagination) app.component('Pagination', Pagination)
app.component('FileUpload', FileUpload) app.component('FileUpload', FileUpload)
app.component('ImageUpload', ImageUpload) app.component('ImageUpload', ImageUpload)
app.component('ImagePreview', ImagePreview) app.component('ImagePreview', ImagePreview)
app.component('RightToolbar', RightToolbar) app.component('RightToolbar', RightToolbar)
app.component('Editor', Editor) app.component('Editor', Editor)
app.use(router) app.use(router)
app.use(store) app.use(store)
app.use(plugins) app.use(plugins)
app.use(elementIcons) app.use(elementIcons)
app.component('svg-icon', SvgIcon) app.component('svg-icon', SvgIcon)
directive(app) directive(app)
// 使用element-plus 并且设置全局的大小 // 使用element-plus 并且设置全局的大小
app.use(ElementPlus, { app.use(ElementPlus, {
locale: locale, locale: locale,
// 支持 large、default、small // 支持 large、default、small
size: Cookies.get('size') || 'default' size: Cookies.get('size') || 'default'
}) })
app.use(WebSocketPlugin, { app.use(WebSocketPlugin, {
// url: import.meta.env.VITE_WS_URL, // url: import.meta.env.VITE_WS_URL,
url: 'ws://192.168.0.100:13080/ws', url: 'ws://192.168.0.100:13080/ws',
// url: 'ws://10.148.209.5:13080/ws', // url: 'ws://10.148.209.5:13080/ws',
userId: 'your_user_id' userId: 'your_user_id'
}); });
app.mount('#app') app.mount('#app')

View File

@ -1,249 +1,242 @@
import { createWebHistory, createRouter } from "vue-router"; import { createWebHistory, createRouter } from "vue-router";
/* Layout */ /* Layout */
import Layout from "@/layout"; import Layout from "@/layout";
/** /**
* Note: 路由配置项 * Note: 路由配置项
* *
* hidden: true // 当设置 true 的时候该路由不会再侧边栏出现 如401,login等页面,或者如一些编辑页面/edit/1 * hidden: true // 当设置 true 的时候该路由不会再侧边栏出现 如401,login等页面,或者如一些编辑页面/edit/1
* alwaysShow: true // 当你一个路由下面的 children 声明的路由大于1个时,自动会变成嵌套的模式--如组件页面 * alwaysShow: true // 当你一个路由下面的 children 声明的路由大于1个时,自动会变成嵌套的模式--如组件页面
* // 只有一个时,会将那个子路由当做根路由显示在侧边栏--如引导页面 * // 只有一个时,会将那个子路由当做根路由显示在侧边栏--如引导页面
* // 若你想不管路由下面的 children 声明的个数都显示你的根路由 * // 若你想不管路由下面的 children 声明的个数都显示你的根路由
* // 你可以设置 alwaysShow: true,这样它就会忽略之前定义的规则,一直显示根路由 * // 你可以设置 alwaysShow: true,这样它就会忽略之前定义的规则,一直显示根路由
* redirect: noRedirect // 当设置 noRedirect 的时候该路由在面包屑导航中不可被点击 * redirect: noRedirect // 当设置 noRedirect 的时候该路由在面包屑导航中不可被点击
* name:'router-name' // 设定路由的名字,一定要填写不然使用<keep-alive>时会出现各种问题 * name:'router-name' // 设定路由的名字,一定要填写不然使用<keep-alive>时会出现各种问题
* query: '{"id": 1, "name": "ry"}' // 访问路由的默认传递参数 * query: '{"id": 1, "name": "ry"}' // 访问路由的默认传递参数
* roles: ['admin', 'common'] // 访问路由的角色权限 * roles: ['admin', 'common'] // 访问路由的角色权限
* permissions: ['a:a:a', 'b:b:b'] // 访问路由的菜单权限 * permissions: ['a:a:a', 'b:b:b'] // 访问路由的菜单权限
* meta : { * meta : {
noCache: true // 如果设置为true,则不会被 <keep-alive> 缓存(默认 false) noCache: true // 如果设置为true,则不会被 <keep-alive> 缓存(默认 false)
title: 'title' // 设置该路由在侧边栏和面包屑中展示的名字 title: 'title' // 设置该路由在侧边栏和面包屑中展示的名字
icon: 'svg-name' // 设置该路由的图标,对应路径src/assets/icons/svg icon: 'svg-name' // 设置该路由的图标,对应路径src/assets/icons/svg
breadcrumb: false // 如果设置为false,则不会在breadcrumb面包屑中显示 breadcrumb: false // 如果设置为false,则不会在breadcrumb面包屑中显示
activeMenu: '/system/user' // 当路由设置了该属性,则会高亮相对应的侧边栏。 activeMenu: '/system/user' // 当路由设置了该属性,则会高亮相对应的侧边栏。
} }
*/ */
// 公共路由 // 公共路由
export const constantRoutes = [ export const constantRoutes = [
{ {
path: "/redirect", path: "/redirect",
component: Layout, component: Layout,
hidden: true, hidden: true,
children: [ children: [
{ {
path: "/redirect/:path(.*)", path: "/redirect/:path(.*)",
component: () => import("@/views/redirect/index.vue"), component: () => import("@/views/redirect/index.vue"),
}, },
], ],
}, },
{ {
path: "/login", path: "/login",
component: () => import("@/views/login"), component: () => import("@/views/login"),
hidden: true, hidden: true,
}, },
{ {
path: "/register", path: "/register",
component: () => import("@/views/register"), component: () => import("@/views/register"),
hidden: true, hidden: true,
}, },
{ {
path: "/zhtest", path: "/:pathMatch(.*)*",
component: import("@/views/system/user/profile/index"), component: () => import("@/views/error/404"),
// children: [ hidden: true,
// { },
// path: "profilettt", {
// component: () => import("@/views/system/user/profile/index"), path: "/401",
// name: "Profile", component: () => import("@/views/error/401"),
// meta: { title: "个人中心", icon: "user", hidden: true }, hidden: true,
// }, },
// ], {
}, //机械臂路由
{ path: "/mechanical_arm/:id",
path: "/:pathMatch(.*)*", component: () =>
component: () => import("@/views/error/404"), import("@/views/device/register/components/MechanicalArm/index.vue"),
hidden: true, hidden: true,
}, },
{ // router/index.js
path: "/401", {
component: () => import("@/views/error/401"), path: "/device",
hidden: true, component: Layout,
}, children: [
{ {
//机械臂路由 path: "camera/:id",
path: "/mechanical_arm/:id", component: () =>
component: () => import("@/views/device/register/components/Camera/index.vue"),
import("@/views/device/register/components/MechanicalArm/index.vue"), name: "摄像头示教",
hidden: true, meta: { title: "摄像头详情" },
}, hidden: true,
// router/index.js },
{ ],
path: "/device", },
component: Layout, {
children: [ path: "/device",
{ component: Layout,
path: "camera/:id", children: [
component: () => {
import("@/views/device/register/components/Camera/index.vue"), path: "hand/:id",
name: "摄像头示教", component: () =>
meta: { title: "摄像头详情" }, import("@/views/device/register/components/DexHand/index.vue"),
hidden: true, name: "灵巧手示教",
}, meta: { title: "灵巧手详情" },
{ hidden: true,
path: "hand/:id", },
component: () => ],
import("@/views/device/register/components/DexHand/index.vue"), },
name: "灵巧手示教", {
meta: { title: "灵巧手详情" }, path: "speaker/:id",
hidden: true, component: () =>
}, import("@/views/device/register/components/Speaker/index.vue"),
{ name: "扬声器示教",
path: "speaker/:id", meta: { title: "扬声器详情" },
component: () => hidden: true,
import("@/views/device/register/components/Speaker/index.vue"), },
name: "扬声器示教", {
meta: { title: "扬声器详情" }, //语音合成
hidden: true, path: "/speech_synthesis",
}, component: () =>
], import("@/views/device/register/components/SpeechSynthesis/index.vue"),
}, meta: { title: "语音合成", noLogin: true },
{ hidden: true,
//语音合成 },
path: "/speech_synthesis", {
component: () => //触摸交互
import("@/views/device/register/components/SpeechSynthesis/index.vue"), path: "/touch_interaction",
meta: { title: "语音合成", noLogin: true }, component: () =>
hidden: true, import("@/views/device/register/components/TouchInteraction/index.vue"),
}, meta: { title: "触摸交互", noLogin: true },
{ hidden: true,
//触摸交互 },
path: "/touch_interaction", {
component: () => //logicFlow
import("@/views/device/register/components/TouchInteraction/index.vue"), path: "/logic_flow",
meta: { title: "触摸交互", noLogin: true }, component: () =>
hidden: true, import("@/views/device/register/components/LogicFlow/index.vue"),
}, meta: { title: "logicFlow", noLogin: true },
{ hidden: true,
//logicFlow },
path: "/logic_flow", {
component: () => //logicFlow
import("@/views/device/register/components/LogicFlow/index.vue"), path: "/flow",
meta: { title: "logicFlow", noLogin: true }, component: () => import("@/views/flow/index.vue"),
hidden: true, meta: { title: "flow", noLogin: true },
}, hidden: true,
{ },
//logicFlow {
path: "/flow", //电机路由
component: () => import("@/views/flow/index.vue"), path: "/Motor/:id",
meta: { title: "flow", noLogin: true }, component: () =>
hidden: true, import("@/views/device/register/components/Motor/index.vue"),
}, hidden: true,
{ },
//电机路由 {
path: "/Motor/:id", path: "",
component: () => component: Layout,
import("@/views/device/register/components/Motor/index.vue"), redirect: "/index",
hidden: true, children: [
}, {
{ path: "/index",
path: "", component: () => import("@/views/index"),
component: Layout, name: "Index",
redirect: "/index", meta: { title: "首页", icon: "dashboard", affix: true },
children: [ },
{ ],
path: "/index", },
component: () => import("@/views/index"), ];
name: "Index",
meta: { title: "首页", icon: "dashboard", affix: true }, // 动态路由,基于用户权限动态去加载
}, export const dynamicRoutes = [
], {
}, path: "/system/user-auth",
component: Layout,
]; hidden: true,
permissions: ["system:user:edit"],
// 动态路由,基于用户权限动态去加载 children: [
export const dynamicRoutes = [ {
{ path: "role/:userId(\\d+)",
path: "/system/user-auth", component: () => import("@/views/system/user/authRole"),
component: Layout, name: "AuthRole",
hidden: true, meta: { title: "分配角色", activeMenu: "/system/user" },
permissions: ["system:user:edit"], },
children: [ ],
{ },
path: "role/:userId(\\d+)", {
component: () => import("@/views/system/user/authRole"), path: "/system/role-auth",
name: "AuthRole", component: Layout,
meta: { title: "分配角色", activeMenu: "/system/user" }, hidden: true,
}, permissions: ["system:role:edit"],
], children: [
}, {
{ path: "user/:roleId(\\d+)",
path: "/system/role-auth", component: () => import("@/views/system/role/authUser"),
component: Layout, name: "AuthUser",
hidden: true, meta: { title: "分配用户", activeMenu: "/system/role" },
permissions: ["system:role:edit"], },
children: [ ],
{ },
path: "user/:roleId(\\d+)", {
component: () => import("@/views/system/role/authUser"), path: "/system/dict-data",
name: "AuthUser", component: Layout,
meta: { title: "分配用户", activeMenu: "/system/role" }, hidden: true,
}, permissions: ["system:dict:list"],
], children: [
}, {
{ path: "index/:dictId(\\d+)",
path: "/system/dict-data", component: () => import("@/views/system/dict/data"),
component: Layout, name: "Data",
hidden: true, meta: { title: "字典数据", activeMenu: "/system/dict" },
permissions: ["system:dict:list"], },
children: [ ],
{ },
path: "index/:dictId(\\d+)", {
component: () => import("@/views/system/dict/data"), path: "/monitor/job-log",
name: "Data", component: Layout,
meta: { title: "字典数据", activeMenu: "/system/dict" }, hidden: true,
}, permissions: ["monitor:job:list"],
], children: [
}, {
{ path: "index/:jobId(\\d+)",
path: "/monitor/job-log", component: () => import("@/views/monitor/job/log"),
component: Layout, name: "JobLog",
hidden: true, meta: { title: "调度日志", activeMenu: "/monitor/job" },
permissions: ["monitor:job:list"], },
children: [ ],
{ },
path: "index/:jobId(\\d+)", {
component: () => import("@/views/monitor/job/log"), path: "/tool/gen-edit",
name: "JobLog", component: Layout,
meta: { title: "调度日志", activeMenu: "/monitor/job" }, hidden: true,
}, permissions: ["tool:gen:edit"],
], children: [
}, {
{ path: "index/:tableId(\\d+)",
path: "/tool/gen-edit", component: () => import("@/views/tool/gen/editTable"),
component: Layout, name: "GenEdit",
hidden: true, meta: { title: "修改生成配置", activeMenu: "/tool/gen" },
permissions: ["tool:gen:edit"], },
children: [ ],
{ },
path: "index/:tableId(\\d+)", ];
component: () => import("@/views/tool/gen/editTable"), console.log("constantRoutes", constantRoutes);
name: "GenEdit", const router = createRouter({
meta: { title: "修改生成配置", activeMenu: "/tool/gen" }, history: createWebHistory(),
}, routes: constantRoutes,
], scrollBehavior(to, from, savedPosition) {
}, if (savedPosition) {
]; return savedPosition;
console.log("constantRoutes", constantRoutes); }
const router = createRouter({ return { top: 0 };
history: createWebHistory(), },
routes: constantRoutes, });
scrollBehavior(to, from, savedPosition) {
if (savedPosition) { export default router;
return savedPosition;
}
return { top: 0 };
},
});
export default router;

View File

@ -21,6 +21,7 @@ const terminalId = ref('25449ff3fcc7b27da5f69462c5efcec2'); // 待您修改
const deviceId = ref('hand1'); // 待您修改 const deviceId = ref('hand1'); // 待您修改
const frameData = ref({ count: 0, lastTime: 0 }); const frameData = ref({ count: 0, lastTime: 0 });
const isHandSeries = ref(false); const isHandSeries = ref(false);
const containerRef = ref(null)
// 矩形定义 // 矩形定义
const rectangle = [ const rectangle = [
@ -362,15 +363,39 @@ onMounted(async () => {
await nextTick(); await nextTick();
initializeRectangleData(); // 初始化数据 initializeRectangleData(); // 初始化数据
initCanvas(); // 初始化 Canvas 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); const debouncedResize = debounce(updateCanvas, 10);
window.addEventListener('resize', debouncedResize); window.addEventListener('resize', debouncedResize);
data.debouncedResize = debouncedResize; // 保存以便清理
}); });
onUnmounted(() => { 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) { if (isHandSeries.value) {
// 关闭 WebSocket 通道
subscribeSensorData(false); subscribeSensorData(false);
} }
}); });

View File

@ -32,6 +32,7 @@ const emit = defineEmits(['update:topSeriesData']);
const loading = ref(false); const loading = ref(false);
const handImage = ref(null); const handImage = ref(null);
const containerRef = ref(null); // 新增:引用父容器
const defaultHandImageInfo = { width: 891, height: 981 }; const defaultHandImageInfo = { width: 891, height: 981 };
const imageAspectRatio = defaultHandImageInfo.width / defaultHandImageInfo.height; const imageAspectRatio = defaultHandImageInfo.width / defaultHandImageInfo.height;
@ -114,16 +115,39 @@ const updateToSeriesData = () => {
emit('update:topSeriesData', newTopSeriesData); emit('update:topSeriesData', newTopSeriesData);
}); });
}; };
const data = ref({
debouncedResize: null,
resizeObserver: null,
});
onMounted(() => { onMounted(() => {
updateSliderPositions(); 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); const debouncedResize = debounce(updateSliderPositions, 0);
window.addEventListener('resize', debouncedResize); window.addEventListener('resize', debouncedResize);
data.value.debouncedResize = debouncedResize;
updateToSeriesData(); updateToSeriesData();
}); });
onUnmounted(() => { 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> </script>

View File

@ -1,5 +1,5 @@
<template> <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 class="tabs">
<div <div
v-for="(tab, index) in tabs" v-for="(tab, index) in tabs"
@ -48,6 +48,7 @@ const props = defineProps({
}); });
const chart = ref(null); const chart = ref(null);
const containerRef = ref(null); // 新增:引用父容器
const activeTab = ref('avg'); // 默认选中平均值 const activeTab = ref('avg'); // 默认选中平均值
// Tab 配置 // Tab 配置
@ -58,6 +59,15 @@ const tabs = [
let myChart; let myChart;
// 防抖函数
const debounce = (func, delay) => {
let timeoutId;
return (...args) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => func.apply(null, args), delay);
};
};
const initChart = () => { const initChart = () => {
if (!chart.value) { if (!chart.value) {
console.error('Chart element is null'); console.error('Chart element is null');
@ -127,22 +137,55 @@ watch([() => props.maxData, () => props.avgData, activeTab], () => {
updateChart(activeTab.value === 'max' ? props.maxData : props.avgData); updateChart(activeTab.value === 'max' ? props.maxData : props.avgData);
}); });
const data = ref({
resizeObserver: null,
debouncedResize: null,
});
onMounted(() => { onMounted(() => {
initChart(); initChart();
updateChart(activeTab.value === 'max' ? props.maxData : props.avgData); updateChart(activeTab.value === 'max' ? props.maxData : props.avgData);
// 处理窗口大小变化
const handleResize = () => { // 使用 ResizeObserver 监听父容器尺寸变化
myChart?.resize(); const container = containerRef.value;
}; if (container) {
window.addEventListener('resize', handleResize); 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(() => { onUnmounted(() => {
// 清理图表实例和事件监听器 // 清理图表实例
myChart?.dispose(); if (myChart) {
window.removeEventListener('resize', () => { myChart.dispose();
myChart?.resize(); 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> </script>

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="right-chart-panel"> <div class="right-chart-panel" ref="containerRef">
<div ref="topChart" class="chart"></div> <div ref="topChart" class="chart"></div>
</div> </div>
</template> </template>
@ -42,8 +42,18 @@ const props = defineProps({
}); });
const topChart = ref(null); const topChart = ref(null);
const containerRef = ref(null); // 新增:引用父容器
let chart1Instance = null; let chart1Instance = null;
// 防抖函数
const debounce = (func, delay) => {
let timeoutId;
return (...args) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => func.apply(null, args), delay);
};
};
const topChartData = ref({ const topChartData = ref({
xAxis: { xAxis: {
type: 'category', type: 'category',
@ -62,27 +72,64 @@ const topChartData = ref({
series: [{ data: props.topSeriesData, type: 'bar' }], series: [{ data: props.topSeriesData, type: 'bar' }],
}); });
const data = ref({
resizeObserver: null,
debouncedResize: null,
});
onMounted(() => { onMounted(() => {
try { try {
if (!topChart.value) {
console.error('Chart element is null');
return;
}
chart1Instance = echarts.init(topChart.value); chart1Instance = echarts.init(topChart.value);
chart1Instance.setOption(topChartData.value); chart1Instance.setOption(topChartData.value);
// 处理窗口大小变化 // 使用 ResizeObserver 监听父容器尺寸变化
const handleResize = () => { const container = containerRef.value;
chart1Instance?.resize(); if (container) {
}; const resizeObserver = new ResizeObserver(debounce(() => {
window.addEventListener('resize', handleResize); 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) { } catch (error) {
console.error('图表初始化失败:', error); console.error('图表初始化失败:', error);
} }
}); });
onUnmounted(() => { onUnmounted(() => {
// 清理图表实例和事件监听器 // 清理图表实例
chart1Instance?.dispose(); if (chart1Instance) {
window.removeEventListener('resize', () => { chart1Instance.dispose();
chart1Instance?.resize(); 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 // 监听 props.topSeriesData

View File

@ -1,3 +1,4 @@
<!-- index.vue -->
<template> <template>
<div class="container"> <div class="container">
<div class="left-panel"> <div class="left-panel">
@ -9,23 +10,21 @@
</div> </div>
</div> </div>
<div class="right-panel"> <div class="right-panel">
<RightCharts :bottom-series-data="bottomSeriesData" /> <RightTopCharts :top-series-data="topSeriesData" style="height: 50%;" />
<RightBottomCharts :max-data="maxData" :avg-data="avgData" style="height: 50%" /> <RightBottomCharts :max-data="maxData" :avg-data="avgData" style="height: 50%;" />
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref } from 'vue'; import { ref } from 'vue';
import RightCharts from './RightTopCharts.vue'; import RightTopCharts from './RightTopCharts.vue';
import LeftTopHand from './LeftTopHand.vue'; import LeftTopHand from './LeftTopHand.vue';
import LeftBottomHand from './LeftBottomHand.vue' import LeftBottomHand from './LeftBottomHand.vue';
import RightBottomCharts from './RightBottomCharts.vue'; import RightBottomCharts from './RightBottomCharts.vue';
const topSeriesData = ref([0, 0, 0, 0, 0, 0]); const topSeriesData = ref([0, 0, 0, 0, 0, 0]);
const bottomSeriesData = ref([0, 0, 0, 0, 0, 0]); const bottomSeriesData = ref([0, 0, 0, 0, 0, 0]);
const maxData = ref({ const maxData = ref({
thumb: { end: 0, tip: 0, middle: 0, palm: 0 }, thumb: { end: 0, tip: 0, middle: 0, palm: 0 },
indexFinger: { end: 0, tip: 0, middle: 0 }, indexFinger: { end: 0, tip: 0, middle: 0 },
@ -43,27 +42,28 @@ const avgData = ref({
palm: { touch: 0 } palm: { touch: 0 }
}); });
// 处理更新事件
const handleUpdateTopSeriesData = (newData) => { const handleUpdateTopSeriesData = (newData) => {
topSeriesData.value = newData; topSeriesData.value = newData;
}; };
// 处理更新事件 const handleUpdateBottomSeriesData = ({ maxData: newMaxData, avgData: newAvgData }) => {
const handleUpdateBottomSeriesData = ({ maxData, avgData }) => { maxData.value = newMaxData;
maxData.value = newData; avgData.value = newAvgData;
avgData.value = avgData;
}; };
</script> </script>
<style scoped> <style scoped>
.container { .container {
display: flex; display: flex;
flex: 1; flex: 1; /* 占据父容器所有可用空间 */
width: 100%; width: 100%;
height: 100%; /* 占满父容器高度 */
min-height: 0; /* 防止 flex 子项最小高度问题 */
background-color: #fff; background-color: #fff;
border-radius: 20px; border-radius: 20px;
overflow: hidden; /* 防止内容溢出 */
box-sizing: border-box; /* 包含 padding 和 border */
padding: 20px; /* 与父容器一致,避免双重 padding */
} }
.left-panel, .left-panel,
@ -72,12 +72,16 @@ const handleUpdateBottomSeriesData = ({ maxData, avgData }) => {
flex-direction: column; flex-direction: column;
justify-content: space-between; justify-content: space-between;
width: 50%; width: 50%;
padding: 10px; height: 100%; /* 占满 container 高度 */
padding: 0; /* 移除子面板内边距,交给 container 处理 */
box-sizing: border-box;
overflow: hidden;
} }
.hand-container { .hand-container {
position: relative; position: relative;
height: 50%; height: 50%; /* 每个 hand-container 占左面板的 50% 高度 */
width: 100%;
overflow: hidden;
} }
</style> </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> <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 dayjs from "dayjs";
import {deviceTeach, deviceUpdateProfile} from "@/api/device/register.js"; import { deviceTeach, deviceUpdateProfile } from "@/api/device/register.js";
import {formatNumber} from "@/views/device/register/components/index.js"; import { formatNumber } from "@/views/device/register/components/index.js";
import {convertNumbersToStringAndRemoveNulls} from "@/utils/fn.js"; import { convertNumbersToStringAndRemoveNulls } from "@/utils/fn.js";
const data = reactive({ const data = reactive({
form: { form: {
workModule: 1,//工作模式 workModule: 1,
posStep: 1,//位置步进 posStep: 1,
postureStep: 1,//姿态步进 postureStep: 1,
jointStep: 1,//关节步进 jointStep: 1,
joint1: 0.000012,//关节1 joint1: 0.000012,
joint2: 0.000012,// joint2: 0.000012,
joint3: 0.000012,// joint3: 0.000012,
joint4: 0.000012,// joint4: 0.000012,
joint5: 0.000012,// joint5: 0.000012,
joint6: 0.000012,//关节6 joint6: 0.000012,
x: 11,// x: 11,
y: 11,// y: 11,
z: 11,// z: 11,
rx: 11,// rx: 11,
ry: 11,// ry: 11,
rz: 11,// rz: 11,
arm: 1,//目标 arm: 1,
coordinate: 1,//坐标系 coordinate: 1,
}, },
speed: 20,//速度 speed: 20,
zeroPos: {//零位姿态 zeroPos: {
x: 0, x: 0,
y: 0, y: 0,
z: 0, z: 0,
@ -42,388 +43,217 @@ const data = reactive({
rx: 1, rx: 1,
ry: 1, ry: 1,
rz: 1, rz: 1,
},//初始姿态 },
armList: [ armList: [
{ { label: '目标1', value: 1 },
label: '机械臂1', { label: '目标2', value: 2 },
value: 1 { label: '目标3', value: 3 },
},
{
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"), time: dayjs().format("YYYY-MM-DD HH:mm:ss"),
}) });
const { const { form, time, zeroPos, initPos, armList } = toRefs(data);
form, time,
zeroPos,
initPos,
armList,
coordinateList
} = toRefs(data)
const route = useRoute() const route = useRoute();
let timer = null let timer = null;
onUnmounted(() => { onUnmounted(() => {
clearInterval(timer) clearInterval(timer);
}) });
//获取机械臂参数
const getDeviceInfo = () => { const getDeviceInfo = () => {
deviceTeach(devId.value).then((res) => { deviceTeach(devId.value).then((res) => {
let data = formatNumber(res.data)//字符类型转数字类型 let dataRes = formatNumber(res.data);
Object.assign(form.value, data) Object.assign(form.value, dataRes);
//设置初始位置 initPos.value.x = dataRes.x;
initPos.value.x = data.x initPos.value.y = dataRes.y;
initPos.value.y = data.y initPos.value.z = dataRes.z;
initPos.value.z = data.z initPos.value.rx = dataRes.rx;
initPos.value.rx = data.rx initPos.value.ry = dataRes.ry;
initPos.value.ry = data.ry initPos.value.rz = dataRes.rz;
initPos.value.rz = data.rz });
}) };
}
onMounted(() => { onMounted(() => {
getDeviceInfo() getDeviceInfo();
const posControls = document.querySelectorAll(".btn") const posControls = document.querySelectorAll(".pos-btn");
//监听按钮点击事件
posControls.forEach((item) => { posControls.forEach((item) => {
item.addEventListener('click', (e) => { item.addEventListener('click', (e) => {
const target = e.currentTarget.classList[1] const target = e.currentTarget.dataset.target;
const vals = target.split("_")//x_0 rx_0 const [pos, direction] = target.split("_");
const pos = vals[0] const step = (pos === 'x' || pos === 'y' || pos === 'z') ? form.value.posStep : form.value.postureStep;
const calc = !!Number(vals[1]) const delta = direction === 'plus' ? step : -step;
if (calc) { form.value[pos] += delta * (data.speed / 100);
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(() => { watch(() => JSON.parse(JSON.stringify(form.value)), (newData) => {
time.value = dayjs().format("YYYY-MM-DD HH:mm:ss") const dataToSend = convertNumbersToStringAndRemoveNulls(newData);
getDeviceInfo() deviceUpdateProfile({ idDeDeviceRegistration: devId.value, ...dataToSend }).then((res) => {
}, 1000) console.log(res);
}) });
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(() => { const devId = computed(() => {
return route.path.split("/")[2] return route.path.split("/")[2];
}) });
const deviceModel = computed(() => {
return route.path.split("/")[1]
})
const handleResets = (type) => { const handleResets = (type) => {
if (type === "zero") { if (type === "zero") {
Object.assign(form.value, zeroPos.value) Object.assign(form.value, zeroPos.value);
return return;
} }
if (type === "init") { if (type === "init") {
Object.assign(form.value, initPos.value) Object.assign(form.value, initPos.value);
} }
} };
</script> </script>
<template> <template>
<div class="mechanical-arm"> <div class="mechanical-arm">
<div class="top flex between"> <div class="top card">
<div class="left " style="width: 38%"> <div class="controls flex align-center" style="justify-content: flex-start;">
<div class="card" style="height: 100%"> <div class="control-label">目标</div>
<div class="title">机械臂</div> <el-select v-model="form.arm" placeholder="选择目标" style="width: 100px; margin-right: 20px;">
<div class="body">机械臂图片</div> <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> </div>
<div class="center" style="width: 22%"> <div class="right card" style="flex: 1;">
<div class="card"> <div class="title">关节控制 (deg)</div>
<div class="title">工作模式</div> <div class="input-list">
<div class="body"> <div class="input-item">
<el-radio-group v-model="form.workModule"> <span>关节1</span>
<el-radio :value="1">真实机械臂</el-radio> <el-input-number v-model="form.joint1" :step="form.jointStep" :precision="6" />
<el-radio :value="2">仿真机械臂</el-radio>
</el-radio-group>
</div> </div>
</div> <div class="input-item">
<div class="card" style="margin-top: 10px"> <span>关节2</span>
<div class="title">步进模式</div> <el-input-number v-model="form.joint2" :step="form.jointStep" :precision="6" />
<div class="mb10">
<div class="mb5 action-label">位置步进( mm )</div>
<el-input-number style="width: 100%;" v-model="form.posStep" :step="2"/>
</div> </div>
<div class="input-item">
<div class="mb10"> <span>关节3</span>
<div class="mb5 action-label">姿态步进( deg )</div> <el-input-number v-model="form.joint3" :step="form.jointStep" :precision="6" />
<el-input-number style="width: 100%;" v-model="form.postureStep" :step="2"/>
</div> </div>
<div class="input-item">
<div> <span>关节4</span>
<div class="mb5 action-label">关节步进( deg )</div> <el-input-number v-model="form.joint4" :step="form.jointStep" :precision="6" />
<el-input-number style="width: 100%;" v-model="form.jointStep" :step="2"/>
</div> </div>
</div> <div class="input-item">
</div> <span>关节5</span>
<div class="right " style="width: 38%"> <el-input-number v-model="form.joint5" :step="form.jointStep" :precision="6" />
<div class="card" style="height: 100%"> </div>
<div class="title">位置控制</div> <div class="input-item">
<div class="body"> <span>关节6</span>
<div class="btn-wrap pos-control" style="transform: rotateX(40deg);height: calc(100% - 70px)"> <el-input-number v-model="form.joint6" :step="form.jointStep" :precision="6" />
<div class="btn z_0"> </div>
<span>Z-</span> <div class="input-item">
<svg t="1747126605366" class="turn-bottom" viewBox="0 0 1024 1024" version="1.1" <span>关节步进</span>
xmlns="http://www.w3.org/2000/svg" p-id="1461" width="32" height="32"> <el-input-number v-model="form.jointStep" :step="0.1" :precision="1" />
<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> </div>
</div> </div>
<div class="bottom flex between"> <div class="footer card flex align-center between">
<div class="left " style="width: 30%"> <div class="time">{{ time }}</div>
<div class="card" style="height: 100%"> <div class="speed flex align-center">
<div class="title">机械臂位置姿态</div> <span>速度</span>
<div class="body"> <el-slider v-model="data.speed" :min="0" :max="100" style="width: 200px; margin: 0 10px;" />
<div class="flex mb10"> <span>{{ data.speed }}%</span>
<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> </div>
</div> </div>
@ -431,135 +261,167 @@ const handleResets = (type) => {
<style scoped lang="scss"> <style scoped lang="scss">
.mechanical-arm { .mechanical-arm {
width: 100vw;
height: 100vh;
overflow: hidden;
display: flex; display: flex;
flex: 1;
overflow: hidden;
flex-direction: column; 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 { .top {
padding: 10px 10px 0; .pose-info {
height: 48%; justify-content: space-around;
padding: 10px 0;
.center { .pose-item {
display: flex; font-size: 16px;
flex-direction: column; user-select: none;
.card:last-child {
flex: 1;
} }
} }
} }
.bottom { .bottom {
padding: 0 10px; flex: 1;
height: 44%; gap: 20px;
}
.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 { .title {
color: #333; font-size: 16px;
font-size: 14px; font-weight: bold;
margin-bottom: 10px; margin-bottom: 10px;
font-weight: 900;
user-select: none; user-select: none;
} }
.body { .left {
height: calc(100% - 29px); display: flex;
overflow-y: auto; flex-direction: column;
.pos-title { .control-buttons {
user-select: none; 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 { .coordinate-system {
height: 100%; flex: 1;
display: grid; display: flex;
grid-template-columns: repeat(3, 1fr); justify-content: center;
grid-template-rows: repeat(3, 1fr);
justify-items: center;
align-items: 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 { span {
width: 80px;
user-select: none; user-select: none;
} }
svg { .el-input-number {
width: 60px; width: calc(100% - 90px);
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);
}
} }
} }
} }
.footer {
height: 50px;
.time {
font-size: 14px;
}
.speed {
gap: 10px;
span {
user-select: none;
}
}
}
} }
</style> </style>

View File

@ -194,7 +194,11 @@ const debouncedSetVolume = debounce(setVolume, 500);
<style scoped> <style scoped>
.audio-player { .audio-player {
display: flex;
flex: 1;
flex-direction: column;
max-width: 800px; max-width: 800px;
width: 100%;
height: calc(100vh - 125px); height: calc(100vh - 125px);
background-color: #fff; background-color: #fff;
border-radius: 20px; 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" /> <el-tree v-if="!isTreeCollapsed" :data="treeData" :props="treeProps" :default-expanded-keys="['robot-node-config']" @node-click="handleNodeClick" />
</div> </div>
<div class="robot-panel"> <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 <div
v-for="area in adjustedAreas" v-for="area in adjustedAreas"
:key="area.id" :key="area.id"
@ -48,12 +48,10 @@
</div> </div>
</div> </div>
<component :is="popup.component" v-bind="popup.props" /> <component :is="popup.component" v-bind="popup.props" />
<!-- 边缘拖动区域 -->
<div class="resize-edge top" @mousedown="startResizing($event, index, 'top')"></div> <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 bottom" @mousedown="startResizing($event, index, 'bottom')"></div>
<div class="resize-edge left" @mousedown="startResizing($event, index, 'left')"></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-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-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 top-right" @mousedown="startResizing($event, index, 'top-right')"></div>
<div class="resize-corner bottom-left" @mousedown="startResizing($event, index, 'bottom-left')"></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 HandControl from '../register/components/DexHand/index.vue';
import CameraView from '../register/components/Camera/index.vue'; import CameraView from '../register/components/Camera/index.vue';
import MusicPlayer from '../register/components/Speaker/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'; import image from '@/assets/images/robot.png';
// 组件映射 // 组件映射
const componentsMap = { const componentsMap = {
HandControl, HandControl,
CameraView, CameraView,
MusicPlayer MusicPlayer,
MechanicalArm
}; };
const robotImage = image; const robotImage = image;
const robotBg = ref(null); const robotBg = ref(null);
const isTreeCollapsed = ref(false); // 控制树形控件收起/展开 const isTreeCollapsed = ref(false);
const popups = ref([]); const popups = ref([]);
const isDragging = ref(false); const isDragging = ref(false);
const isResizing = ref([]); const isResizing = ref([]);
const currentZIndex = ref(2000); // 提高初始 z-index const currentZIndex = ref(2000);
const dragIndex = ref(null); // 跟踪当前拖动的索引 const dragIndex = ref(null);
const resizeStart = ref({ index: null, startX: 0, startY: 0, originalWidth: 0, originalHeight: 0, originalTop: 0, originalLeft: 0, edge: '' }); 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([ const treeData = ref([
{ {
id: 'robot-node-config', // 添加唯一 ID 用于默认展开 id: 'robot-node-config',
label: '机器人节点配置', label: '机器人节点配置',
children: [ children: [
{ { id: 'node-1', label: '节点1', children: [{ id: 'node-1-1', label: '子节点1-1' }, { id: 'node-1-2', label: '子节点1-2' }] },
id: 'node-1', { id: 'node-2', label: '节点2', children: [{ id: 'node-2-1', label: '子节点2-1' }, { id: 'node-2-2', label: '子节点2-2' }] },
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 = { const treeProps = { children: 'children', label: 'label' };
children: 'children',
label: 'label'
};
const handleNodeClick = (data) => { const handleNodeClick = (data) => {
console.log('点击节点', data.label); 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(() => { const adjustedAreas = computed(() => {
if (!imageDimensions.value.width || !imageDimensions.value.height || !robotBg.value) return []; if (!imageDimensions.value.width || !imageDimensions.value.height || !robotBg.value) return [];
const containerWidth = robotBg.value.parentElement.offsetWidth - 40 - (isTreeCollapsed.value ? 0 : 240); // 减去外层 padding 和 tree-panel 宽度 const containerWidth = containerSize.value.width - (isTreeCollapsed.value ? 0 : 240); // 仅减去树宽度
console.log(containerWidth) const containerHeight = containerSize.value.height; // 无垂直padding
const containerHeight = robotBg.value.parentElement.offsetHeight - 40; // 减去外层 padding
const scaleX = containerWidth / imageDimensions.value.width; const scaleX = containerWidth / imageDimensions.value.width;
const scaleY = containerHeight / imageDimensions.value.height; const scaleY = containerHeight / imageDimensions.value.height;
const scale = Math.min(scaleX, scaleY); // 取最小缩放比例以适配 const scale = Math.min(scaleX, scaleY, 1); // 防止放大
const offsetX = (containerWidth - (imageDimensions.value.width * scale)) / 2; const scaledWidth = imageDimensions.value.width * scale; // 图片宽
const offsetY = (containerHeight - (imageDimensions.value.height * scale)) / 2; 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 => ({ return originalAreas.value.map(area => ({
...area, ...area,
x: (area.x * scale) + offsetX, x: area.x * scale,
y: (area.y * scale) + offsetY, y: area.y * scale,
width: area.width * scale, width: area.width * scale,
height: area.height * scale height: area.height * scale
})); }));
}); });
// 打开弹窗,考虑树宽度和padding偏移
const openPopup = (area) => { const openPopup = (area) => {
// 检查是否已存在相同 id 的窗口
const existingPopup = popups.value.find(p => p.id === area.id); const existingPopup = popups.value.find(p => p.id === area.id);
if (existingPopup) { if (existingPopup) {
console.log('Popup with id', area.id, 'is already open'); console.log('已打开ID为', area.id, '的弹窗');
return; // 如果已打开,阻止重复打开 return;
} }
const popup = { const treeWidth = isTreeCollapsed.value ? 0 : 240;
id: area.id, const leftOffset = treeWidth + 20; // tree宽度 + robot-panel padding-left
component: area.component, // 使用组件引用 const popup = {
top: area.y + 'px', id: area.id,
left: area.x + 'px', component: area.component,
top: `${area.y}px`,
left: `${area.x + leftOffset}px`,
zIndex: currentZIndex.value++, zIndex: currentZIndex.value++,
width: 640, // 默认宽度 width: 640,
height: 480, // 默认高度 height: 480,
originalWidth: 640, // 初始化原始宽度 originalWidth: 640,
originalHeight: 480, // 初始化原始高度 originalHeight: 480,
originalTop: area.y + 'px', // 初始化原始顶部位置 originalTop: `${area.y}px`,
originalLeft: area.x + 'px', // 初始化原始左侧位置 originalLeft: `${area.x + leftOffset}px`,
isMaximized: false, isMaximized: false,
props: { terminalId: 'your-terminal-id', cameraId: 'your-camera-id' } // 替换为实际值 props: { terminalId: 'your-terminal-id', cameraId: 'your-camera-id' }
}; };
popups.value.push(popup); popups.value.push(popup);
isResizing.value.push(false); 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) => { const closePopup = (index) => {
popups.value.splice(index, 1); popups.value.splice(index, 1);
isResizing.value.splice(index, 1); isResizing.value.splice(index, 1);
console.log('Closed popup at index', index); console.log('关闭弹窗索引', index);
}; };
// 最小化弹窗
const minimizePopup = (index) => { const minimizePopup = (index) => {
const popup = popups.value[index]; const popup = popups.value[index];
if (popup.isMaximized) { if (popup.isMaximized) {
popup.width = popup.originalWidth; // 恢复到原始宽度 popup.width = popup.originalWidth;
popup.height = popup.originalHeight; // 恢复到原始高度 popup.height = popup.originalHeight;
popup.top = popup.originalTop; // 恢复到原始顶部位置 popup.top = popup.originalTop;
popup.left = popup.originalLeft; // 恢复到原始左侧位置 popup.left = popup.originalLeft;
popup.isMaximized = false; 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 maximizePopup = (index) => {
const popup = popups.value[index]; const popup = popups.value[index];
if (!popup.isMaximized) { if (!popup.isMaximized) {
popup.originalWidth = popup.width; // 保存当前宽度作为原始宽度 popup.originalWidth = popup.width;
popup.originalHeight = popup.height; // 保存当前高度作为原始高度 popup.originalHeight = popup.height;
popup.originalTop = popup.top; // 保存当前顶部位置 popup.originalTop = popup.top;
popup.originalLeft = popup.left; // 保存当前左侧位置 popup.originalLeft = popup.left;
popup.width = window.innerWidth; // 全屏宽度 popup.width = window.innerWidth;
popup.height = window.innerHeight; // 全屏高度 popup.height = window.innerHeight;
popup.top = '0px'; // 从顶部开始 popup.top = '0px';
popup.left = '0px'; // 从左侧开始 popup.left = '0px';
popup.isMaximized = true; 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) => { const startDragging = (e, index) => {
isDragging.value = true; isDragging.value = true;
dragIndex.value = index; dragIndex.value = index;
@ -217,18 +219,16 @@ const startDragging = (e, index) => {
popup.zIndex = currentZIndex.value++; popup.zIndex = currentZIndex.value++;
document.addEventListener('mousemove', dragHandler); document.addEventListener('mousemove', dragHandler);
document.addEventListener('mouseup', stopDragging); document.addEventListener('mouseup', stopDragging);
console.log('Start dragging', index, 'ClientY:', e.clientY, 'Handle height:', e.currentTarget.offsetHeight);
}; };
const dragHandler = (e) => { const dragHandler = (e) => {
if (isDragging.value && dragIndex.value !== null) { if (isDragging.value && dragIndex.value !== null) {
requestAnimationFrame(() => { requestAnimationFrame(() => {
const popup = popups.value[dragIndex.value]; const popup = popups.value[dragIndex.value];
const newTop = Math.max(0, e.pageY - popup.startY); // 限制顶部不小于 0 const newTop = Math.max(0, e.pageY - popup.startY);
const newLeft = e.pageX - popup.startX; // 左侧无限制 const newLeft = e.pageX - popup.startX;
popup.top = `${newTop}px`; popup.top = `${newTop}px`;
popup.left = `${newLeft}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); document.removeEventListener('mouseup', stopDragging);
}; };
// 调整大小相关逻辑
const startResizing = (e, index, edge) => { const startResizing = (e, index, edge) => {
const popup = popups.value[index]; const popup = popups.value[index];
resizeStart.value = { resizeStart.value = {
@ -255,7 +256,6 @@ const startResizing = (e, index, edge) => {
isResizing.value[index] = true; isResizing.value[index] = true;
document.addEventListener('mousemove', handleResize); document.addEventListener('mousemove', handleResize);
document.addEventListener('mouseup', stopResizingGlobal); document.addEventListener('mouseup', stopResizingGlobal);
console.log('Start resizing', edge);
}; };
const handleResize = (e) => { const handleResize = (e) => {
@ -271,22 +271,18 @@ const handleResize = (e) => {
case 'top': case 'top':
const newHeight = Math.max(minHeight, resizeStart.value.originalHeight - diffY); const newHeight = Math.max(minHeight, resizeStart.value.originalHeight - diffY);
popup.height = newHeight; popup.height = newHeight;
popup.top = `${Math.max(0, resizeStart.value.originalTop + (resizeStart.value.originalHeight - newHeight))}px`; // 限制顶部 popup.top = `${Math.max(0, resizeStart.value.originalTop + (resizeStart.value.originalHeight - newHeight))}px`;
console.log('Resizing top', 'newHeight:', newHeight, 'newTop:', popup.top);
break; break;
case 'bottom': case 'bottom':
popup.height = Math.max(minHeight, resizeStart.value.originalHeight + diffY); popup.height = Math.max(minHeight, resizeStart.value.originalHeight + diffY);
console.log('Resizing bottom', 'newHeight:', popup.height);
break; break;
case 'left': case 'left':
const newWidth = Math.max(minWidth, resizeStart.value.originalWidth - diffX); const newWidth = Math.max(minWidth, resizeStart.value.originalWidth - diffX);
popup.width = newWidth; popup.width = newWidth;
popup.left = `${resizeStart.value.originalLeft + (resizeStart.value.originalWidth - newWidth)}px`; popup.left = `${resizeStart.value.originalLeft + (resizeStart.value.originalWidth - newWidth)}px`;
console.log('Resizing left', 'newWidth:', newWidth, 'newLeft:', popup.left);
break; break;
case 'right': case 'right':
popup.width = Math.max(minWidth, resizeStart.value.originalWidth + diffX); popup.width = Math.max(minWidth, resizeStart.value.originalWidth + diffX);
console.log('Resizing right', 'newWidth:', popup.width);
break; break;
case 'top-left': case 'top-left':
const newWidthTL = Math.max(minWidth, resizeStart.value.originalWidth - diffX); const newWidthTL = Math.max(minWidth, resizeStart.value.originalWidth - diffX);
@ -294,16 +290,14 @@ const handleResize = (e) => {
popup.width = newWidthTL; popup.width = newWidthTL;
popup.height = newHeightTL; popup.height = newHeightTL;
popup.left = `${resizeStart.value.originalLeft + (resizeStart.value.originalWidth - newWidthTL)}px`; popup.left = `${resizeStart.value.originalLeft + (resizeStart.value.originalWidth - newWidthTL)}px`;
popup.top = `${Math.max(0, resizeStart.value.originalTop + (resizeStart.value.originalHeight - newHeightTL))}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);
break; break;
case 'top-right': case 'top-right':
const newWidthTR = Math.max(minWidth, resizeStart.value.originalWidth + diffX); const newWidthTR = Math.max(minWidth, resizeStart.value.originalWidth + diffX);
const newHeightTR = Math.max(minHeight, resizeStart.value.originalHeight - diffY); const newHeightTR = Math.max(minHeight, resizeStart.value.originalHeight - diffY);
popup.width = newWidthTR; popup.width = newWidthTR;
popup.height = newHeightTR; popup.height = newHeightTR;
popup.top = `${Math.max(0, resizeStart.value.originalTop + (resizeStart.value.originalHeight - newHeightTR))}px`; // 限制顶部 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);
break; break;
case 'bottom-left': case 'bottom-left':
const newWidthBL = Math.max(minWidth, resizeStart.value.originalWidth - diffX); const newWidthBL = Math.max(minWidth, resizeStart.value.originalWidth - diffX);
@ -311,12 +305,10 @@ const handleResize = (e) => {
popup.width = newWidthBL; popup.width = newWidthBL;
popup.height = newHeightBL; popup.height = newHeightBL;
popup.left = `${resizeStart.value.originalLeft + (resizeStart.value.originalWidth - newWidthBL)}px`; popup.left = `${resizeStart.value.originalLeft + (resizeStart.value.originalWidth - newWidthBL)}px`;
console.log('Resizing bottom-left', 'newWidth:', newWidthBL, 'newHeight:', newHeightBL, 'newLeft:', popup.left);
break; break;
case 'bottom-right': case 'bottom-right':
popup.width = Math.max(minWidth, resizeStart.value.originalWidth + diffX); popup.width = Math.max(minWidth, resizeStart.value.originalWidth + diffX);
popup.height = Math.max(minHeight, resizeStart.value.originalHeight + diffY); popup.height = Math.max(minHeight, resizeStart.value.originalHeight + diffY);
console.log('Resizing bottom-right', 'newWidth:', popup.width, 'newHeight:', popup.height);
break; 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(() => { onMounted(() => {
const img = new Image(); const img = new Image();
img.src = robotImage; img.src = robotImage;
img.onload = () => { img.onload = () => {
imageDimensions.value = { width: img.width, height: img.height }; imageDimensions.value = { width: img.width, height: img.height };
console.log('图片尺寸:', imageDimensions.value.width, 'x', imageDimensions.value.height);
if (robotBg.value) { if (robotBg.value) {
watch(() => [robotBg.value.parentElement.offsetWidth, robotBg.value.parentElement.offsetHeight, isTreeCollapsed.value], ([newWidth, newHeight]) => { const observer = new ResizeObserver(updateDimensions);
const containerWidth = newWidth - 40 - (isTreeCollapsed.value ? 0 : 240); // 减去外层 padding 和 tree-panel 宽度 observer.observe(robotBg.value.parentElement);
const containerHeight = newHeight - 40; // 减去外层 padding watch(() => isTreeCollapsed.value, updateDimensions, { immediate: true });
const scale = Math.min(containerWidth / img.width, containerHeight / img.height); updateDimensions(); // 初始更新
robotBg.value.style.minWidth = `${img.width * scale}px`; onUnmounted(() => observer.disconnect());
robotBg.value.style.minHeight = `${img.height * scale}px`;
}, { immediate: true });
} }
}; };
}); });
@ -358,47 +370,48 @@ onUnmounted(() => {
.robot-container { .robot-container {
width: 100%; width: 100%;
position: relative; position: relative;
display: flex; /* 保持与外层一致 */ display: flex;
overflow: hidden; /* 防止遮挡弹窗 */ overflow: hidden;
padding: 0; /* 外层已处理 padding */ padding: 0;
background-color: #fff; background-color: #fff;
border-radius: 10px; border-radius: 10px;
} }
.tree-panel { .tree-panel {
flex-shrink: 0; /* 防止收缩 */ flex-shrink: 0;
/* padding: 10px; */
border-right: 1px solid #ddd; border-right: 1px solid #ddd;
overflow-y: auto; /* 垂直滚动 */ overflow-y: auto;
height: auto; /* 动态高度 */ height: auto;
transition: width 0.3s ease; /* 平滑过渡 */ transition: width 0.3s ease;
} }
.tree-panel:deep(.el-tree) { .tree-panel:deep(.el-tree) {
padding: 10px !important; padding: 10px !important;
} }
.robot-panel { .robot-panel {
flex-grow: 1; /* 填满剩余空间 */ flex-grow: 1;
display: flex; display: flex;
justify-content: center; /* 水平居中 */ justify-content: center;
align-items: center; /* 垂直居中 */ align-items: center;
overflow: hidden; overflow: hidden;
padding: 0 20px; /* 补偿外层 padding */ padding: 0 20px;
position: relative; /* 确保按钮定位 */ position: relative;
} }
#robot-bg { #robot-bg {
background-size: contain; background-size: contain;
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: center; background-position: center;
min-width: 0; /* 防止收缩 */ min-width: 0;
min-height: 0; /* 防止收缩 */ min-height: 0;
position: relative; /* 添加 relative 定位 */
} }
.toggle-button { .toggle-button {
position: absolute; position: absolute;
left: 20px; /* 贴近左边边 */ left: 20px;
top: 10px; /* 顶部留点间距 */ top: 10px;
width: 40px; width: 40px;
height: 40px; height: 40px;
display: flex; display: flex;
@ -408,7 +421,7 @@ onUnmounted(() => {
font-size: 24px; font-size: 24px;
background-color: #e0e0e0; background-color: #e0e0e0;
border-radius: 4px; border-radius: 4px;
z-index: 2001; /* 确保高于其他元素 */ z-index: 2001;
} }
.toggle-button:hover { .toggle-button:hover {
@ -417,7 +430,7 @@ onUnmounted(() => {
.clickable-area { .clickable-area {
cursor: pointer; cursor: pointer;
position: relative; position: absolute;
} }
.clickable-text { .clickable-text {
@ -443,23 +456,24 @@ onUnmounted(() => {
user-select: none; user-select: none;
overflow: auto; overflow: auto;
box-sizing: border-box; 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 { .popup.dragging {
transition: none; /* 拖动时禁用过渡 */ transition: none;
opacity: 0.8; opacity: 0.8;
} }
.drag-handle { .drag-handle {
height: 30px; /* 增加高度以容纳按钮 */ height: 30px;
background: #e0e0e0; /* 背景色 */ background: #e0e0e0;
cursor: move; /* 拖动光标 */ cursor: move;
position: relative; position: relative;
z-index: 1; z-index: 1;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; /* 标题居中 */ justify-content: center;
} }
.popup-title { .popup-title {
@ -471,8 +485,8 @@ onUnmounted(() => {
text-overflow: ellipsis; text-overflow: ellipsis;
position: absolute; position: absolute;
left: 10px; left: 10px;
right: 60px; /* 留出按钮空间 */ right: 60px;
text-align: center; /* 居中文本 */ text-align: center;
} }
.popup-controls { .popup-controls {
@ -484,8 +498,8 @@ onUnmounted(() => {
.control-icon { .control-icon {
cursor: pointer; cursor: pointer;
font-size: 18px; /* 放大按钮 */ font-size: 18px;
padding: 4px 8px; /* 增加内边距 */ padding: 4px 8px;
background: #f0f0f0; background: #f0f0f0;
border-radius: 4px; border-radius: 4px;
} }
@ -502,19 +516,17 @@ onUnmounted(() => {
background: #ff6666; background: #ff6666;
} }
/* 边缘拖动区域 */
.resize-edge { .resize-edge {
position: absolute; position: absolute;
background: transparent; 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; } .bottom { bottom: 0; left: 0; right: 0; height: 5px; cursor: ns-resize; }
.left { top: 0; bottom: 0; left: 0; width: 5px; cursor: ew-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; } .right { top: 0; bottom: 0; right: 0; width: 5px; cursor: ew-resize; }
/* 角落拖动区域 */
.resize-corner { .resize-corner {
position: absolute; position: absolute;
width: 10px; width: 10px;
@ -528,6 +540,6 @@ onUnmounted(() => {
.bottom-right { bottom: 0; right: 0; cursor: nwse-resize; } .bottom-right { bottom: 0; right: 0; cursor: nwse-resize; }
.popup.resizing { .popup.resizing {
transition: none; /* 缩放时禁用过渡 */ transition: none;
} }
</style> </style>

View File

@ -1,68 +1,68 @@
import {defineConfig, loadEnv, normalizePath} from 'vite' import {defineConfig, loadEnv, normalizePath} from 'vite'
import path from 'path' import path from 'path'
import createVitePlugins from './vite/plugins' import createVitePlugins from './vite/plugins'
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig(({mode, command}) => { export default defineConfig(({mode, command}) => {
const env = loadEnv(mode, process.cwd()) const env = loadEnv(mode, process.cwd())
const {VITE_APP_ENV, VITE_API_URL} = env const {VITE_APP_ENV, VITE_API_URL} = env
const variablePath = normalizePath(path.resolve('./src/style/variable.scss')); const variablePath = normalizePath(path.resolve('./src/style/variable.scss'));
return { return {
// 部署生产环境和开发环境下的URL。 // 部署生产环境和开发环境下的URL。
// 默认情况下,vite 会假设你的应用是被部署在一个域名的根路径上 // 默认情况下,vite 会假设你的应用是被部署在一个域名的根路径上
// 例如 https://www.ruoyi.vip/。如果应用被部署在一个子路径上,你就需要用这个选项指定这个子路径。例如,如果你的应用被部署在 https://www.ruoyi.vip/admin/,则设置 baseUrl 为 /admin/。 // 例如 https://www.ruoyi.vip/。如果应用被部署在一个子路径上,你就需要用这个选项指定这个子路径。例如,如果你的应用被部署在 https://www.ruoyi.vip/admin/,则设置 baseUrl 为 /admin/。
base: VITE_APP_ENV === 'production' ? '/' : '/', base: VITE_APP_ENV === 'production' ? '/' : '/',
plugins: createVitePlugins(env, command === 'build'), plugins: createVitePlugins(env, command === 'build'),
resolve: { resolve: {
// https://cn.vitejs.dev/config/#resolve-alias // https://cn.vitejs.dev/config/#resolve-alias
alias: { alias: {
// 设置路径 // 设置路径
'~': path.resolve(__dirname, './'), '~': path.resolve(__dirname, './'),
// 设置别名 // 设置别名
'@': path.resolve(__dirname, './src') '@': path.resolve(__dirname, './src')
}, },
// https://cn.vitejs.dev/config/#resolve-extensions // https://cn.vitejs.dev/config/#resolve-extensions
extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue'] extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue']
}, },
// vite 相关配置 // vite 相关配置
server: { server: {
port: 13080, port: 13080,
host: true, host: true,
open: true, open: true,
proxy: { proxy: {
// https://cn.vitejs.dev/config/#server-proxy // https://cn.vitejs.dev/config/#server-proxy
'/dev-api': { '/dev-api': {
//杨 http://192.168.0.10:13080 //杨 http://192.168.0.10:13080
//赵 http://10.148.108.58:13080 //赵 http://10.148.108.58:13080
// dev http://10.148.20.34:13080 // dev http://10.148.20.34:13080
// target: command === 'build' ? VITE_API_URL : 'http://10.148.20.34:13080', // target: command === 'build' ? VITE_API_URL : 'http://10.148.20.34:13080',
target: command === 'build' ? VITE_API_URL : 'http://192.168.0.10:13080', target: command === 'build' ? VITE_API_URL : 'http://192.168.0.10:13080',
changeOrigin: true, changeOrigin: true,
rewrite: (p) => p.replace(/^\/dev-api/, '') rewrite: (p) => p.replace(/^\/dev-api/, '')
} }
} }
}, },
//fix:error:stdin>:7356:1: warning: "@charset" must be the first rule in the file //fix:error:stdin>:7356:1: warning: "@charset" must be the first rule in the file
css: { css: {
postcss: { postcss: {
plugins: [ plugins: [
{ {
postcssPlugin: 'internal:charset-removal', postcssPlugin: 'internal:charset-removal',
AtRule: { AtRule: {
charset: (atRule) => { charset: (atRule) => {
if (atRule.name === 'charset') { if (atRule.name === 'charset') {
atRule.remove(); atRule.remove();
} }
} }
} }
} }
] ]
}, },
preprocessorOptions: { preprocessorOptions: {
scss: { scss: {
additionalData: `@import "${variablePath}";` additionalData: `@import "${variablePath}";`
}, },
}, },
} }
} }
}) })