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 的时候该路由不会再侧边栏出现 如401login等页面或者如一些编辑页面/edit/1 * hidden: true // 当设置 true 的时候该路由不会再侧边栏出现 如401login等页面或者如一些编辑页面/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}";`
}, },
}, },
} }
} }
}) })