commit 751cb3cd8ec414b631ebf3dc8f446b3df7a7adc8
Author: zhanghao <774378400@qq.com>
Date: Mon Jun 29 16:33:00 2026 +0800
first commit
diff --git a/.env.development b/.env.development
new file mode 100644
index 0000000..9ddffd1
--- /dev/null
+++ b/.env.development
@@ -0,0 +1,14 @@
+# 页面标题
+VITE_APP_TITLE = 招商车研AI测评机器人模拟测试平台
+
+# 开发环境配置
+VITE_APP_ENV = 'development'
+
+# 招商车研物联网平台/开发环境
+VITE_APP_BASE_API = '/dev-api'
+
+VITE_API_URL = http://192.168.0.201:13080
+VITE_WS_URL = ws://192.168.0.201:13080/ws
+
+# explanation
+VITE_INSPECTION_TYPE = "inspection"
\ No newline at end of file
diff --git a/.env.production b/.env.production
new file mode 100644
index 0000000..4c5aa50
--- /dev/null
+++ b/.env.production
@@ -0,0 +1,20 @@
+# 页面标题
+VITE_APP_TITLE = 智能座舱测评AI机器人
+
+# 生产环境配置
+VITE_APP_ENV = 'production'
+
+# 招商车研物联网平台/生产环境
+VITE_APP_BASE_API = '/prod-api'
+
+# 是否在打包时开启压缩,支持 gzip 和 brotli
+VITE_BUILD_COMPRESS = gzip
+
+#node-red 服务地址
+VITE_NODE_RED_URL = 'http://10.148.108.59:13080'
+
+VITE_API_URL = http://192.168.28.10:13080
+VITE_WS_URL = ws://192.168.0.201:13080/ws
+
+# explanation
+VITE_INSPECTION_TYPE = "inspection"
\ No newline at end of file
diff --git a/.env.staging b/.env.staging
new file mode 100644
index 0000000..66d8d56
--- /dev/null
+++ b/.env.staging
@@ -0,0 +1,13 @@
+# 页面标题
+VITE_APP_TITLE = 招商车研物联网平台管理系统
+
+# 生产环境配置
+VITE_APP_ENV = 'staging'
+
+# 招商车研物联网平台管理系统/生产环境
+VITE_APP_BASE_API = '/stage-api'
+
+# 是否在打包时开启压缩,支持 gzip 和 brotli
+VITE_BUILD_COMPRESS = gzip
+VITE_API_URL = http://10.148.121.100:13080
+VITE_WS_URL = ws://192.168.0.201:13080/ws
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..57a04df
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,26 @@
+.DS_Store
+node_modules/
+dist/
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+**/*.log
+
+tests/**/coverage/
+tests/e2e/reports
+selenium-debug.log
+
+# Editor directories and files
+.idea
+.vscode
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.local
+.history
+
+package-lock.json
+yarn.lock
+bin/
+pnpm-lock.yaml
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..4219e85
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,10 @@
+FROM node:18
+
+WORKDIR /app
+
+COPY package*.json ./
+
+RUN npm install
+
+
+COPY . .
diff --git a/Dockerfile.frontend b/Dockerfile.frontend
new file mode 100644
index 0000000..28e0423
--- /dev/null
+++ b/Dockerfile.frontend
@@ -0,0 +1,17 @@
+# 使用官方的 Node.js 镜像作为基础镜像
+FROM node:18
+
+# 设置工作目录
+WORKDIR /app
+
+# 复制 package.json 和 package-lock.json(如果存在)到工作目录
+COPY package*.json ./
+
+# 安装项目依赖
+RUN npm install
+
+# 复制项目文件到工作目录
+COPY . .
+
+# 构建项目
+RUN npm run build
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..8564f29
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,20 @@
+The MIT License (MIT)
+
+Copyright (c) 2018 RuoYi
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..5e715c1
--- /dev/null
+++ b/README.md
@@ -0,0 +1,35 @@
+
+## 前端运行
+
+```bash
+# 安装依赖
+yarn --registry=https://registry.npmmirror.com
+
+# 启动服务
+yarn dev
+
+# 构建测试环境 yarn build:stage
+# 构建生产环境 yarn build:prod
+# 前端访问地址 http://localhost:80
+```
+
+## 内置功能
+
+1. 用户管理:用户是系统操作者,该功能主要完成系统用户配置。
+2. 部门管理:配置系统组织机构(公司、部门、小组),树结构展现支持数据权限。
+3. 岗位管理:配置系统用户所属担任职务。
+4. 菜单管理:配置系统菜单,操作权限,按钮权限标识等。
+5. 角色管理:角色菜单权限分配、设置角色按机构进行数据范围权限划分。
+6. 字典管理:对系统中经常使用的一些较为固定的数据进行维护。
+7. 参数管理:对系统动态配置常用参数。
+8. 通知公告:系统通知公告信息发布维护。
+9. 操作日志:系统正常操作日志记录和查询;系统异常信息日志记录和查询。
+10. 登录日志:系统登录日志记录查询包含登录异常。
+11. 在线用户:当前系统中活跃用户状态监控。
+12. 定时任务:在线(添加、修改、删除)任务调度包含执行结果日志。
+13. 代码生成:前后端代码的生成(java、html、xml、sql)支持CRUD下载 。
+14. 系统接口:根据业务代码自动生成相关的api接口文档。
+15. 服务监控:监视当前系统CPU、内存、磁盘、堆栈等相关信息。
+16. 缓存监控:对系统的缓存信息查询,命令统计等。
+17. 在线构建器:拖动表单元素生成相应的HTML代码。
+18. 连接池监视:监视当前系统数据库连接池状态,可进行分析SQL找出系统性能瓶颈。
diff --git a/html/ie.html b/html/ie.html
new file mode 100644
index 0000000..052ffcd
--- /dev/null
+++ b/html/ie.html
@@ -0,0 +1,46 @@
+
+
+
+
+
+ 请升级您的浏览器
+
+
+
+
+
+
+请升级您的浏览器,以便我们更好的为您提供服务!
+您正在使用 Internet Explorer 的早期版本(IE11以下版本或使用该内核的浏览器)。这意味着在升级浏览器前,您将无法访问此网站。
+
+请注意:微软公司对Windows XP 及 Internet Explorer 早期版本的支持已经结束
+自 2016 年 1 月 12 日起,Microsoft 不再为 IE 11 以下版本提供相应支持和更新。没有关键的浏览器安全更新,您的电脑可能易受有害病毒、间谍软件和其他恶意软件的攻击,它们可以窃取或损害您的业务数据和信息。请参阅 微软对 Internet Explorer 早期版本的支持将于 2016 年 1 月 12 日结束的说明 。
+
+您可以选择更先进的浏览器
+推荐使用以下浏览器的最新版本。如果您的电脑已有以下浏览器的最新版本则直接使用该浏览器访问即可。
+
+
+
+
\ No newline at end of file
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..ceebf89
--- /dev/null
+++ b/index.html
@@ -0,0 +1,215 @@
+
+
+
+
+
+
+
+
+
+ 招商车研物联网平台管理系统
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..c97dfe2
--- /dev/null
+++ b/package.json
@@ -0,0 +1,66 @@
+{
+ "name": "CMVR-IOT-UI",
+ "version": "3.8.8",
+ "description": "CMVR-IOT平台管理系统前端UI框架",
+ "author": "CMVR",
+ "license": "MIT",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build:dev": "vite build --mode development",
+ "build:prod": "vite build --mode production",
+ "build:staging": "vite build --mode staging --verbose",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@element-plus/icons-vue": "2.3.1",
+ "@logicflow/core": "^2.0.13",
+ "@logicflow/extension": "^2.0.18",
+ "@vueup/vue-quill": "1.2.0",
+ "@vueuse/core": "10.11.0",
+ "ace-builds": "^1.43.0",
+ "axios": "0.28.1",
+ "clipboard": "2.0.11",
+ "dayjs": "^1.11.13",
+ "echarts": "5.5.1",
+ "element-plus": "2.9.4",
+ "file-saver": "2.0.5",
+ "fuse.js": "6.6.2",
+ "js-beautify": "1.14.11",
+ "js-cookie": "3.0.5",
+ "jsencrypt": "3.3.2",
+ "json-editor-vue": "^0.18.1",
+ "lodash": "^4.17.21",
+ "mitt": "^3.0.1",
+ "monaco-editor": "^0.55.1",
+ "nprogress": "0.2.0",
+ "pako": "^2.1.0",
+ "pinia": "2.1.7",
+ "pnpm": "^10.23.0",
+ "socket.io-client": "^4.8.1",
+ "sockjs-client": "^1.6.1",
+ "splitpanes": "3.1.5",
+ "three": "^0.181.2",
+ "urdf-loader": "^0.12.6",
+ "uuid": "^11.1.0",
+ "vue": "3.4.31",
+ "vue-cropper": "1.1.1",
+ "vue-router": "4.4.0",
+ "vue3-ace-editor": "^2.2.4",
+ "vue3-json-viewer": "2.3.0",
+ "vuedraggable": "4.1.0",
+ "xgplayer": "^3.0.22"
+ },
+ "devDependencies": {
+ "@vitejs/plugin-vue": "5.0.5",
+ "file-loader": "^6.2.0",
+ "sass": "1.77.5",
+ "unplugin-auto-import": "0.17.6",
+ "unplugin-vue-setup-extend-plus": "1.0.1",
+ "vite": "5.3.2",
+ "vite-plugin-compression": "0.5.1",
+ "vite-plugin-monaco-editor": "^1.1.0",
+ "vite-plugin-svg-icons": "2.0.1"
+ },
+ "packageManager": "pnpm@9.15.9+sha512.68046141893c66fad01c079231128e9afb89ef87e2691d69e4d40eee228988295fd4682181bae55b58418c3a253bde65a505ec7c5f9403ece5cc3cd37dcf2531"
+}
diff --git a/public/bot.svg b/public/bot.svg
new file mode 100644
index 0000000..808cb7b
--- /dev/null
+++ b/public/bot.svg
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/public/favicon.ico b/public/favicon.ico
new file mode 100644
index 0000000..e263760
Binary files /dev/null and b/public/favicon.ico differ
diff --git a/public/inspection/elfin10/elfin10.urdf.xacro b/public/inspection/elfin10/elfin10.urdf.xacro
new file mode 100644
index 0000000..4aa83c3
--- /dev/null
+++ b/public/inspection/elfin10/elfin10.urdf.xacro
@@ -0,0 +1,465 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/public/inspection/elfin10/elfin10_gazebo.xacro b/public/inspection/elfin10/elfin10_gazebo.xacro
new file mode 100644
index 0000000..01581e9
--- /dev/null
+++ b/public/inspection/elfin10/elfin10_gazebo.xacro
@@ -0,0 +1,134 @@
+
+
+
+
+
+
+
+
+ gazebo_ros2_control/GazeboSystem
+ $(find elfin10_ros2_gazebo)/config/elfin_arm_controller.yaml
+
+
+
+
+
+
+
+ elfin_hardware_interface/ElfinHWInterface
+
+
+
+
+ fake_components/GenericSystem
+
+
+
+ gazebo_ros2_control/GazeboSystem
+
+
+
+
+
+
+
+ -3.14
+ 3.14
+
+
+ -1.57
+ 1.57
+
+
+
+
+
+
+ -2.35
+ 2.35
+
+
+ -1.57
+ 1.57
+
+
+
+
+
+
+ -2.61
+ 2.61
+
+
+ -1.57
+ 1.57
+
+
+
+
+
+
+ -3.14
+ 3.14
+
+
+ -1.57
+ 1.57
+
+
+
+
+
+
+ -2.56
+ 2.56
+
+
+ -1.57
+ 1.57
+
+
+
+
+
+
+ -3.14
+ 3.14
+
+
+ -1.57
+ 1.57
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/public/inspection/elfin10/elfin10_transmission.xacro b/public/inspection/elfin10/elfin10_transmission.xacro
new file mode 100644
index 0000000..12dffc7
--- /dev/null
+++ b/public/inspection/elfin10/elfin10_transmission.xacro
@@ -0,0 +1,69 @@
+
+
+
+
+
+
+
+ transmission_interface/SimpleTransmission
+
+ hardware_interface/PositionJointInterface
+
+
+ 1
+
+
+
+
+ transmission_interface/SimpleTransmission
+
+ hardware_interface/PositionJointInterface
+
+
+ 1
+
+
+
+
+ transmission_interface/SimpleTransmission
+
+ hardware_interface/PositionJointInterface
+
+
+ 1
+
+
+
+
+ transmission_interface/SimpleTransmission
+
+ hardware_interface/PositionJointInterface
+
+
+ 1
+
+
+
+
+ transmission_interface/SimpleTransmission
+
+ hardware_interface/PositionJointInterface
+
+
+ 1
+
+
+
+
+ transmission_interface/SimpleTransmission
+
+ hardware_interface/PositionJointInterface
+
+
+ 1
+
+
+
+
+
+
\ No newline at end of file
diff --git a/public/inspection/elfin10/meshes/elfin10/elfin_base.STL b/public/inspection/elfin10/meshes/elfin10/elfin_base.STL
new file mode 100644
index 0000000..fa9dd2b
Binary files /dev/null and b/public/inspection/elfin10/meshes/elfin10/elfin_base.STL differ
diff --git a/public/inspection/elfin10/meshes/elfin10/elfin_end_link.STL b/public/inspection/elfin10/meshes/elfin10/elfin_end_link.STL
new file mode 100644
index 0000000..8536f5d
Binary files /dev/null and b/public/inspection/elfin10/meshes/elfin10/elfin_end_link.STL differ
diff --git a/public/inspection/elfin10/meshes/elfin10/elfin_link1.STL b/public/inspection/elfin10/meshes/elfin10/elfin_link1.STL
new file mode 100644
index 0000000..ca0e111
Binary files /dev/null and b/public/inspection/elfin10/meshes/elfin10/elfin_link1.STL differ
diff --git a/public/inspection/elfin10/meshes/elfin10/elfin_link2.STL b/public/inspection/elfin10/meshes/elfin10/elfin_link2.STL
new file mode 100644
index 0000000..e5a2628
Binary files /dev/null and b/public/inspection/elfin10/meshes/elfin10/elfin_link2.STL differ
diff --git a/public/inspection/elfin10/meshes/elfin10/elfin_link3.STL b/public/inspection/elfin10/meshes/elfin10/elfin_link3.STL
new file mode 100644
index 0000000..bcefa4f
Binary files /dev/null and b/public/inspection/elfin10/meshes/elfin10/elfin_link3.STL differ
diff --git a/public/inspection/elfin10/meshes/elfin10/elfin_link4.STL b/public/inspection/elfin10/meshes/elfin10/elfin_link4.STL
new file mode 100644
index 0000000..565329d
Binary files /dev/null and b/public/inspection/elfin10/meshes/elfin10/elfin_link4.STL differ
diff --git a/public/inspection/elfin10/meshes/elfin10/elfin_link5.STL b/public/inspection/elfin10/meshes/elfin10/elfin_link5.STL
new file mode 100644
index 0000000..a6d7816
Binary files /dev/null and b/public/inspection/elfin10/meshes/elfin10/elfin_link5.STL differ
diff --git a/public/inspection/elfin10/meshes/elfin10/elfin_link6.STL b/public/inspection/elfin10/meshes/elfin10/elfin_link6.STL
new file mode 100644
index 0000000..996d478
Binary files /dev/null and b/public/inspection/elfin10/meshes/elfin10/elfin_link6.STL differ
diff --git a/src/App.vue b/src/App.vue
new file mode 100644
index 0000000..498b169
--- /dev/null
+++ b/src/App.vue
@@ -0,0 +1,123 @@
+
+
+
+
+
+
diff --git a/src/adapters/URDFAdapter.js b/src/adapters/URDFAdapter.js
new file mode 100644
index 0000000..cbbfceb
--- /dev/null
+++ b/src/adapters/URDFAdapter.js
@@ -0,0 +1,461 @@
+/**
+ * URDF Adapter
+ * Converts urdf-loaders result to unified model
+ */
+import * as THREE from 'three';
+import { UnifiedRobotModel, Link, Joint, JointLimits, VisualGeometry, CollisionGeometry, InertialProperties } from '@/models/UnifiedRobotModel.js';
+
+export class URDFAdapter {
+ /**
+ * Convert urdf-loaders robot object to unified model
+ * @param {THREE.Group} robot - Robot object returned by urdf-loaders
+ * @param {string} urdfXML - Original URDF XML content (optional, for extracting inertial data)
+ * @returns {UnifiedRobotModel}
+ */
+ static convert(robot, urdfXML = null) {
+ const model = new UnifiedRobotModel();
+ model.name = robot.name || 'robot';
+ model.threeObject = robot;
+
+ // Mark model type as URDF
+ if (!robot.userData) robot.userData = {};
+ robot.userData.type = 'urdf';
+
+ if (!robot.links || !robot.joints) {
+ console.warn('URDF model missing links or joints information');
+ return model;
+ }
+
+ // If XML provided, parse inertial data
+ let inertialData = {};
+ if (urdfXML) {
+ inertialData = this.parseInertialFromXML(urdfXML);
+ }
+
+ // Convert links
+ Object.values(robot.links).forEach(urdfLink => {
+ const link = this.convertLink(urdfLink);
+
+ // If urdf-loader didn't parse inertial, get from XML
+ if (!link.inertial && inertialData[urdfLink.name]) {
+ link.inertial = inertialData[urdfLink.name];
+ }
+
+ model.addLink(link);
+ });
+
+ // Convert joints
+ Object.values(robot.joints).forEach(urdfJoint => {
+ const joint = this.convertJoint(urdfJoint);
+ model.addJoint(joint);
+ });
+
+ // If XML provided, supplement effort and velocity from XML (urdf-loaders may not have parsed)
+ if (urdfXML) {
+ this.supplementJointLimitsFromXML(model, urdfXML);
+ }
+
+ // Find root link (link that is not a child of any joint)
+ const allChildren = new Set(
+ Array.from(model.joints.values()).map(j => j.child).filter(c => c)
+ );
+ const rootLinks = Array.from(model.links.keys()).filter(
+ name => !allChildren.has(name)
+ );
+ if (rootLinks.length > 0) {
+ model.rootLink = rootLinks[0];
+ }
+
+ // Enhance materials for better lighting (MuJoCo style)
+ this.enhanceMaterials(robot);
+
+ return model;
+ }
+
+ /**
+ * Enhance materials for better lighting (MuJoCo style)
+ * Applies consistent shininess and specular properties to all materials
+ * Directly modifies materials in place to ensure changes persist
+ */
+ static enhanceMaterials(robotObject) {
+ robotObject.traverse((child) => {
+ if (child.isMesh && child.material) {
+ // Handle material arrays
+ if (Array.isArray(child.material)) {
+ child.material = child.material.map(mat => {
+ return this.enhanceSingleMaterial(mat);
+ });
+ } else {
+ child.material = this.enhanceSingleMaterial(child.material);
+ }
+ }
+ });
+ }
+
+ /**
+ * Enhance a single material with better lighting properties
+ * Returns enhanced material (may be cloned or modified in place)
+ * Saves original properties for lighting toggle
+ */
+ static enhanceSingleMaterial(material) {
+ if (material.isMeshPhongMaterial || material.isMeshStandardMaterial) {
+ // Save original properties if not already saved (for lighting toggle)
+ if (material.userData.originalShininess === undefined) {
+ material.userData.originalShininess = material.shininess !== undefined ? material.shininess : 30;
+ // IMPORTANT: Save original color to preserve URDF material colors
+ if (!material.userData.originalColor && material.color) {
+ material.userData.originalColor = material.color.clone();
+ }
+ // Save original specular - if material had no specular, save null
+ if (!material.specular) {
+ material.userData.originalSpecular = null;
+ } else if (material.specular.isColor) {
+ const spec = material.specular;
+ if (spec.r < 0.1 && spec.g < 0.1 && spec.b < 0.1) {
+ material.userData.originalSpecular = null; // Likely default
+ } else {
+ material.userData.originalSpecular = spec.clone();
+ }
+ } else if (typeof material.specular === 'number') {
+ if (material.specular === 0x111111 || material.specular < 0x111111) {
+ material.userData.originalSpecular = null;
+ } else {
+ material.userData.originalSpecular = new THREE.Color(material.specular);
+ }
+ } else {
+ material.userData.originalSpecular = null;
+ }
+ }
+
+ // IMPORTANT: Preserve original color from URDF material definitions
+ // Do not modify material.color - urdf-loaders already sets the correct color
+
+ // Apply enhanced lighting (default enabled)
+ // Increase shininess for better highlights
+ if (material.shininess === undefined || material.shininess < 50) {
+ material.shininess = 50;
+ }
+
+ // Enhance specular reflection - ensure it's a Color object with proper values
+ if (!material.specular) {
+ material.specular = new THREE.Color(0.3, 0.3, 0.3);
+ } else if (material.specular.isColor) {
+ // If it's already a Color object, update values
+ if (material.specular.r < 0.2 || material.specular.g < 0.2 || material.specular.b < 0.2) {
+ material.specular.setRGB(0.3, 0.3, 0.3);
+ }
+ } else if (typeof material.specular === 'number') {
+ // Convert number to Color object
+ if (material.specular < 0x333333) {
+ material.specular = new THREE.Color(0.3, 0.3, 0.3);
+ } else {
+ // Convert hex to Color
+ material.specular = new THREE.Color(material.specular);
+ if (material.specular.r < 0.2) {
+ material.specular.setRGB(0.3, 0.3, 0.3);
+ }
+ }
+ }
+
+ // Mark material as needing update
+ material.needsUpdate = true;
+ return material;
+ } else if (material.type === 'MeshBasicMaterial') {
+ // Convert MeshBasicMaterial to MeshPhongMaterial
+ const oldMaterial = material;
+ const envMap = typeof window !== 'undefined' && window.app?.sceneManager?.environmentManager?.getEnvironmentMap();
+ const newMaterial = new THREE.MeshPhongMaterial({
+ color: oldMaterial.color,
+ map: oldMaterial.map,
+ transparent: oldMaterial.transparent,
+ opacity: oldMaterial.opacity,
+ side: oldMaterial.side,
+ shininess: 50,
+ specular: new THREE.Color(0.3, 0.3, 0.3),
+ envMap: envMap || null,
+ reflectivity: envMap ? 0.3 : 0
+ });
+ // Save original properties for lighting toggle
+ newMaterial.userData.originalShininess = 30;
+ newMaterial.userData.originalSpecular = null; // MeshBasicMaterial had no specular
+ return newMaterial;
+ }
+
+ return material;
+ }
+
+ static convertLink(urdfLink) {
+ const link = new Link(urdfLink.name);
+ link.threeObject = urdfLink;
+
+ // Convert inertial properties
+ if (urdfLink.inertial) {
+ link.inertial = this.convertInertial(urdfLink.inertial);
+ }
+
+ // Note: urdf-loaders has already converted visual and collision to Three.js objects
+ // We mainly extract metadata, actual meshes are in threeObject
+
+ return link;
+ }
+
+ static convertJoint(urdfJoint) {
+ // URDF joint object has jointType property (not type)
+ const jointType = urdfJoint.jointType || urdfJoint.type || 'fixed';
+ const joint = new Joint(urdfJoint.name, jointType);
+
+ // Extract parent name (urdf-loader returns Three.js object reference)
+ joint.parent = urdfJoint.parent?.name || null;
+
+ // child may not be in urdfJoint.child, but in Three.js children array
+ // In Three.js scene graph, Joint is parent node, Child Link is Joint's child node
+ if (urdfJoint.child && urdfJoint.child.name) {
+ joint.child = urdfJoint.child.name;
+ } else if (urdfJoint.children && urdfJoint.children.length > 0) {
+ // Find Link object from children array
+ const childLink = urdfJoint.children.find(child =>
+ child.isURDFLink || child.type === 'URDFLink'
+ );
+ if (childLink) {
+ joint.child = childLink.name;
+ }
+ }
+
+ joint.threeObject = urdfJoint;
+
+ // Convert origin
+ if (urdfJoint.origin) {
+ joint.origin = {
+ xyz: urdfJoint.origin.xyz || [0, 0, 0],
+ rpy: urdfJoint.origin.rpy || [0, 0, 0]
+ };
+ }
+
+ // Convert axis
+ if (urdfJoint.axis) {
+ joint.axis = { xyz: urdfJoint.axis.xyz || [0, 0, 1] };
+ }
+
+ // Convert limits
+ if (urdfJoint.limit) {
+ joint.limits = this.convertLimits(urdfJoint.limit);
+ }
+
+ // Get current value
+ if (urdfJoint.angle !== undefined) {
+ joint.currentValue = urdfJoint.angle;
+ } else if (urdfJoint.jointValue !== undefined) {
+ joint.currentValue = urdfJoint.jointValue;
+ }
+
+ return joint;
+ }
+
+ static convertLimits(limit) {
+ const limits = new JointLimits();
+ if (limit.lower !== undefined) limits.lower = limit.lower;
+ if (limit.upper !== undefined) limits.upper = limit.upper;
+ if (limit.effort !== undefined) limits.effort = limit.effort;
+ if (limit.velocity !== undefined) limits.velocity = limit.velocity;
+ return limits;
+ }
+
+ /**
+ * Supplement joint effort and velocity information from URDF XML
+ * (because urdf-loaders may not have parsed these attributes)
+ */
+ static supplementJointLimitsFromXML(model, urdfXML) {
+ try {
+ const parser = new DOMParser();
+ const doc = parser.parseFromString(urdfXML, 'text/xml');
+
+ const jointElements = doc.querySelectorAll('joint');
+ jointElements.forEach(jointEl => {
+ const jointName = jointEl.getAttribute('name');
+ const joint = model.joints.get(jointName);
+
+ if (joint) {
+ const limitEl = jointEl.querySelector('limit');
+ if (limitEl) {
+ if (!joint.limits) {
+ joint.limits = new JointLimits();
+ }
+
+ // Read effort and velocity
+ const effort = limitEl.getAttribute('effort');
+ const velocity = limitEl.getAttribute('velocity');
+
+ if (effort !== null) {
+ joint.limits.effort = parseFloat(effort);
+ }
+ if (velocity !== null) {
+ joint.limits.velocity = parseFloat(velocity);
+ }
+
+ // Also ensure lower and upper are correctly set
+ const lower = limitEl.getAttribute('lower');
+ const upper = limitEl.getAttribute('upper');
+ if (lower !== null) {
+ joint.limits.lower = parseFloat(lower);
+ }
+ if (upper !== null) {
+ joint.limits.upper = parseFloat(upper);
+ }
+ }
+ }
+ });
+ } catch (error) {
+ console.error('Failed to supplement joint limit information from XML:', error);
+ }
+ }
+
+ static convertInertial(inertial) {
+ const props = new InertialProperties();
+ if (inertial.mass !== undefined) props.mass = inertial.mass;
+ if (inertial.origin) {
+ props.origin = {
+ xyz: inertial.origin.xyz || [0, 0, 0],
+ rpy: inertial.origin.rpy || [0, 0, 0]
+ };
+ }
+ if (inertial.ixx !== undefined) props.ixx = inertial.ixx;
+ if (inertial.iyy !== undefined) props.iyy = inertial.iyy;
+ if (inertial.izz !== undefined) props.izz = inertial.izz;
+ if (inertial.ixy !== undefined) props.ixy = inertial.ixy;
+ if (inertial.ixz !== undefined) props.ixz = inertial.ixz;
+ if (inertial.iyz !== undefined) props.iyz = inertial.iyz;
+ return props;
+ }
+
+ /**
+ * Parse inertial data from URDF XML
+ * @param {string} xmlContent - URDF XML content
+ * @returns {Object} Mapping from link names to inertial data
+ */
+ static parseInertialFromXML(xmlContent) {
+ const inertialData = {};
+
+ try {
+ const parser = new DOMParser();
+ const xmlDoc = parser.parseFromString(xmlContent, 'text/xml');
+
+ // Get all link elements
+ const links = xmlDoc.getElementsByTagName('link');
+
+ for (let i = 0; i < links.length; i++) {
+ const linkElement = links[i];
+ const linkName = linkElement.getAttribute('name');
+
+ // Find inertial element
+ const inertialElement = linkElement.getElementsByTagName('inertial')[0];
+ if (!inertialElement) continue;
+
+ const inertial = new InertialProperties();
+
+ // Parse mass
+ const massElement = inertialElement.getElementsByTagName('mass')[0];
+ if (massElement) {
+ inertial.mass = parseFloat(massElement.getAttribute('value'));
+ }
+
+ // Parse origin
+ const originElement = inertialElement.getElementsByTagName('origin')[0];
+ if (originElement) {
+ const xyz = originElement.getAttribute('xyz');
+ const rpy = originElement.getAttribute('rpy');
+ inertial.origin = {
+ xyz: xyz ? xyz.split(' ').map(parseFloat) : [0, 0, 0],
+ rpy: rpy ? rpy.split(' ').map(parseFloat) : [0, 0, 0]
+ };
+ } else {
+ inertial.origin = { xyz: [0, 0, 0], rpy: [0, 0, 0] };
+ }
+
+ // Parse inertia
+ const inertiaElement = inertialElement.getElementsByTagName('inertia')[0];
+ if (inertiaElement) {
+ inertial.ixx = parseFloat(inertiaElement.getAttribute('ixx')) || 0;
+ inertial.iyy = parseFloat(inertiaElement.getAttribute('iyy')) || 0;
+ inertial.izz = parseFloat(inertiaElement.getAttribute('izz')) || 0;
+ inertial.ixy = parseFloat(inertiaElement.getAttribute('ixy')) || 0;
+ inertial.ixz = parseFloat(inertiaElement.getAttribute('ixz')) || 0;
+ inertial.iyz = parseFloat(inertiaElement.getAttribute('iyz')) || 0;
+ }
+
+ inertialData[linkName] = inertial;
+ }
+ } catch (error) {
+ console.error('Failed to parse URDF XML inertial data:', error);
+ }
+
+ return inertialData;
+ }
+
+ /**
+ * Set joint angle (using urdf-loaders' setJointValue method)
+ * Reference URDFClasses.js setJointValue implementation
+ */
+ static setJointAngle(joint, angle, ignoreLimits = false) {
+ joint.currentValue = angle;
+
+ // URDF format: use urdf-loader's setJointValue method
+ if (joint.threeObject) {
+ // If ignoring limits, temporarily modify URDF joint object's limit values
+ let originalLimits = null;
+ if (ignoreLimits && joint.threeObject.limit) {
+ originalLimits = {
+ lower: joint.threeObject.limit.lower,
+ upper: joint.threeObject.limit.upper
+ };
+ joint.threeObject.limit.lower = -Math.PI * 2;
+ joint.threeObject.limit.upper = Math.PI * 2;
+ }
+
+ // Prefer setJointValue method (urdf-loader's standard method)
+ if (typeof joint.threeObject.setJointValue === 'function') {
+ joint.threeObject.setJointValue(angle);
+
+ // Restore original limits
+ if (originalLimits && joint.threeObject.limit) {
+ joint.threeObject.limit.lower = originalLimits.lower;
+ joint.threeObject.limit.upper = originalLimits.upper;
+ }
+ return;
+ } else if (typeof joint.threeObject.setAngle === 'function') {
+ joint.threeObject.setAngle(angle);
+
+ // Restore original limits
+ if (originalLimits && joint.threeObject.limit) {
+ joint.threeObject.limit.lower = originalLimits.lower;
+ joint.threeObject.limit.upper = originalLimits.upper;
+ }
+ return;
+ }
+
+ // Restore original limits (if none of the above methods executed)
+ if (originalLimits && joint.threeObject.limit) {
+ joint.threeObject.limit.lower = originalLimits.lower;
+ joint.threeObject.limit.upper = originalLimits.upper;
+ }
+
+ // If none available, manually set rotation
+ // Note: For URDF, we usually shouldn't reach here, as urdf-loader should provide setJointValue
+ console.warn(`Joint ${joint.name} has no setJointValue or setAngle method, attempting manual setup`);
+ if (joint.type === 'revolute' || joint.type === 'continuous') {
+ const axis = joint.axis ? new THREE.Vector3(...joint.axis.xyz).normalize() : new THREE.Vector3(0, 0, 1);
+ // Note: joint.threeObject may not be a Three.js object, cannot directly set rotation
+ if (joint.threeObject.rotation) {
+ joint.threeObject.rotation.setFromAxisAngle(axis, angle);
+ }
+ } else if (joint.type === 'prismatic') {
+ const axis = joint.axis ? new THREE.Vector3(...joint.axis.xyz).normalize() : new THREE.Vector3(1, 0, 0);
+ if (joint.threeObject.position) {
+ joint.threeObject.position.copy(axis.multiplyScalar(angle));
+ }
+ }
+ } else {
+ console.warn(`Joint ${joint.name} has no threeObject`);
+ }
+ }
+}
+
diff --git a/src/adapters/XacroAdapter.js b/src/adapters/XacroAdapter.js
new file mode 100644
index 0000000..9a7275f
--- /dev/null
+++ b/src/adapters/XacroAdapter.js
@@ -0,0 +1,624 @@
+/**
+ * Xacro Adapter
+ *
+ * Parses ROS Xacro files and converts them to URDF format using xacro-parser.
+ *
+ * Supported Features:
+ * - Property definitions and substitutions (xacro:property)
+ * - Macro definitions with parameters (xacro:macro)
+ * - Conditional blocks (xacro:if, xacro:unless)
+ * - File inclusions (xacro:include) with package:// and relative paths
+ * - Python-style boolean constants (True/False)
+ * - Arithmetic expressions and property evaluation
+ *
+ * Compatibility:
+ * - ROS Jade and later (inOrder=true, requirePrefix=true, localProperties=true)
+ * - Automatically injects True/False constants for compatibility with ROS xacro files
+ *
+ * Usage:
+ * - Upload all related xacro files (main file and included files)
+ * - The adapter automatically resolves file inclusions from the uploaded file map
+ * - Mesh files are resolved using package:// paths or relative paths
+ */
+import { XacroParser } from 'xacro-parser';
+import { URDFAdapter } from './URDFAdapter.js';
+
+export class XacroAdapter {
+ /**
+ * Parse xacro content and convert to unified model
+ * @param {string} xacroContent - Xacro file content
+ * @param {string} fileName - Xacro file name (for working path)
+ * @param {Map} fileMap - File map (path -> File object)
+ * @param {File} file - Original file object (optional)
+ * @returns {Promise}
+ */
+ static async parse(xacroContent, fileName, fileMap = null, file = null) {
+ try {
+ // Create xacro parser
+ const parser = new XacroParser();
+
+ // Configure parser for ROS Jade and later (default settings)
+ parser.inOrder = true;
+ parser.requirePrefix = true;
+ parser.localProperties = true;
+
+ // Set working path (directory where xacro file is located)
+ const workingPath = fileName.includes('/')
+ ? fileName.substring(0, fileName.lastIndexOf('/') + 1)
+ : '';
+ parser.workingPath = workingPath;
+
+ // Inject Python-style boolean constants as xacro properties
+ // Some xacro files use True/False (capitalized) in conditions
+ xacroContent = this.injectBooleanConstants(xacroContent);
+
+ // Extract and set xacro arguments with their default values
+ // Also add common ROS arguments that might be used without definition
+ const xacroArgs = this.extractXacroArguments(xacroContent);
+
+ // Add common ROS arguments with sensible defaults if not already defined
+ if (!xacroArgs.hasOwnProperty('DEBUG')) {
+ xacroArgs.DEBUG = 'false';
+ }
+ if (!xacroArgs.hasOwnProperty('SELF_COLLIDE')) {
+ xacroArgs.SELF_COLLIDE = 'false';
+ }
+
+ parser.arguments = xacroArgs;
+
+ // If fileMap provided, setup custom file loader
+ if (fileMap) {
+ parser.getFileContents = async (path) => {
+ return await this.loadFileFromMap(path, fileMap, workingPath);
+ };
+ }
+
+ // Parse xacro to URDF XML
+ const urdfXML = await parser.parse(xacroContent);
+
+
+ // Convert XMLDocument to string
+ const serializer = new XMLSerializer();
+ let urdfString = serializer.serializeToString(urdfXML);
+
+ // Clean up the XML string - remove empty xmlns attributes that might cause issues
+ urdfString = urdfString.replace(/\sxmlns=""/g, '');
+
+ // Re-parse the URDF string with DOMParser to create a clean XMLDocument
+ // This removes any xacro-specific nodes that urdf-loader might not handle
+ const domParser = new DOMParser();
+ const cleanUrdfXML = domParser.parseFromString(urdfString, 'text/xml');
+
+ // Check for parsing errors
+ const parseError = cleanUrdfXML.querySelector('parsererror');
+ if (parseError) {
+ console.error('[XacroAdapter] XML parsing error:', parseError.textContent);
+ throw new Error('Generated URDF has invalid XML: ' + parseError.textContent);
+ }
+
+ // Remove problematic elements that urdf-loader might not handle well
+ // Remove gazebo elements (plugins, sensors, etc.)
+ const gazeboElements = cleanUrdfXML.querySelectorAll('gazebo');
+ gazeboElements.forEach(el => el.parentNode?.removeChild(el));
+
+ // Remove transmission elements (these are for Gazebo/ROS control)
+ const transmissionElements = cleanUrdfXML.querySelectorAll('transmission');
+ transmissionElements.forEach(el => el.parentNode?.removeChild(el));
+
+ // Remove visual/collision elements with empty geometry tags
+ this.removeEmptyGeometry(cleanUrdfXML);
+
+ // Clean up empty text nodes and comments that might cause issues
+ this.cleanXMLNodes(cleanUrdfXML.documentElement);
+
+ // Convert the clean XMLDocument back to string for URDFLoader
+ const finalUrdfString = serializer.serializeToString(cleanUrdfXML);
+
+ // Now use existing URDF loading infrastructure
+ // Import URDFLoader dynamically
+ const urdfModule = await import('urdf-loader');
+ const URDFLoader = urdfModule.URDFLoader || urdfModule.default || urdfModule;
+
+ return new Promise((resolve, reject) => {
+ const loader = new URDFLoader();
+ loader.parseCollision = true;
+
+ // Extract directory where URDF file is located
+ const urdfDir = workingPath;
+
+ // If file map provided, setup resource loader (same as URDF loading)
+ if (fileMap) {
+ // Parse URDF content, find all used package names
+ const packages = this.extractPackagesFromURDF(finalUrdfString);
+
+ // Build package map
+ const packageMap = {};
+ packages.forEach(pkg => {
+ packageMap[pkg] = pkg;
+ });
+ packageMap[''] = '';
+
+ loader.packages = packageMap;
+
+ // Set URL Modifier (same as URDF loading)
+ const urlModifier = (url) => {
+ // Handle blob URLs
+ if (url.startsWith('blob:')) {
+ const blobMatch = url.match(/^blob:https?:\/\/[^\/]+\/(.+)$/);
+ if (blobMatch && blobMatch[1]) {
+ const fileName = blobMatch[1];
+ if (/\.(jpg|jpeg|png|gif|bmp|tga|tiff|webp|dae|stl|obj|gltf|glb)$/i.test(fileName)) {
+ url = fileName;
+ } else {
+ return url;
+ }
+ } else {
+ return url;
+ }
+ }
+
+ const isTextureFile = /\.(jpg|jpeg|png|gif|bmp|tga|tiff|webp)$/i.test(url);
+ const isMeshFile = /\.(dae|stl|obj|gltf|glb)$/i.test(url);
+
+ let meshPath = url;
+
+ // Remove http:// or https:// prefix
+ if (meshPath.startsWith('http://') || meshPath.startsWith('https://')) {
+ try {
+ const urlObj = new URL(meshPath);
+ meshPath = urlObj.pathname;
+ if (meshPath.startsWith('/')) {
+ meshPath = meshPath.substring(1);
+ }
+ } catch (e) {
+ // Invalid URL, use as-is
+ }
+ }
+
+ // Remove package:// prefix
+ if (meshPath.startsWith('package://')) {
+ meshPath = meshPath.replace(/^package:\/\//, '');
+ const parts = meshPath.split('/');
+ if (parts.length > 1) {
+ meshPath = parts.slice(1).join('/');
+ }
+ }
+
+ // Remove leading ./
+ meshPath = meshPath.replace(/^\.\//, '');
+
+ // Handle relative paths
+ let normalizedPath = meshPath;
+ if (meshPath.includes('../')) {
+ const parts = meshPath.split('/');
+ const resolvedParts = [];
+ for (const part of parts) {
+ if (part === '..') {
+ resolvedParts.pop();
+ } else if (part !== '.' && part !== '') {
+ resolvedParts.push(part);
+ }
+ }
+ normalizedPath = resolvedParts.join('/');
+ }
+
+ // Build full path
+ const fullPath = urdfDir + normalizedPath;
+ const altPath = urdfDir + meshPath;
+
+ // Find file in fileMap
+ let matchedFile = fileMap.get(fullPath);
+
+ if (!matchedFile && altPath !== fullPath) {
+ matchedFile = fileMap.get(altPath);
+ }
+
+ if (!matchedFile) {
+ matchedFile = fileMap.get(normalizedPath);
+ }
+
+ if (!matchedFile) {
+ matchedFile = fileMap.get(meshPath);
+ }
+
+ if (!matchedFile) {
+ const targetFileName = normalizedPath.split('/').pop() || meshPath.split('/').pop();
+ for (const [key, file] of fileMap.entries()) {
+ const keyFileName = key.split('/').pop();
+ if (keyFileName === targetFileName) {
+ matchedFile = file;
+ break;
+ }
+ }
+ }
+
+ if (matchedFile) {
+ const bloburl = URL.createObjectURL(matchedFile);
+ return bloburl;
+ }
+
+ return url;
+ };
+
+ loader.manager.setURLModifier(urlModifier);
+
+ // Custom loadMeshCb (same as URDF loading)
+ const originalLoadMeshCb = loader.loadMeshCb || loader.defaultMeshLoader.bind(loader);
+ loader.loadMeshCb = (path, manager, done) => {
+ this.findFileInMapByPath(path, fileMap, urdfDir).then(file => {
+ if (file) {
+ const ext = (file.name || path).toLowerCase().split('.').pop();
+ this.loadMeshFileAsync(file, ext, manager).then(meshObject => {
+ if (meshObject) {
+ done(meshObject, null);
+ } else {
+ done(null, new Error(`Failed to load mesh file: ${path}`));
+ }
+ }).catch(err => {
+ console.error(`Failed to load mesh: ${path}`, err);
+ done(null, err);
+ });
+ } else {
+ let stlPath = path
+ stlPath = stlPath.replace('file:///', 'inspection/elfin10')
+ console.log(stlPath, manager, done)
+ originalLoadMeshCb(path, manager, done);
+ }
+ }).catch(error => {
+ console.error(`Failed to find file: ${path}`, error);
+ originalLoadMeshCb(path, manager, done);
+ });
+ };
+ }
+
+ // Create temporary URL for URDF content
+ const blob = new Blob([finalUrdfString], { type: 'text/xml' });
+ const url = URL.createObjectURL(blob);
+
+ loader.load(url, (robot) => {
+ URL.revokeObjectURL(url);
+
+ // Convert to unified model using URDFAdapter
+ try {
+ const model = URDFAdapter.convert(robot, finalUrdfString);
+ resolve(model);
+ } catch (error) {
+ console.error('[XacroAdapter] URDF conversion error:', error);
+ reject(new Error('URDF conversion failed: ' + error.message));
+ }
+ }, undefined, (error) => {
+ URL.revokeObjectURL(url);
+ console.error('[XacroAdapter] URDF loading error:', error);
+ reject(new Error('URDF loading failed: ' + (error.message || error)));
+ });
+ });
+ } catch (error) {
+ console.error('[XacroAdapter] Xacro parsing error:', error);
+ throw new Error('Xacro parsing failed: ' + error.message);
+ }
+ }
+
+ /**
+ * Load file from fileMap for xacro includes
+ * @param {string} path - File path
+ * @param {Map} fileMap - File map
+ * @param {string} workingPath - Working directory
+ * @returns {Promise}
+ */
+ static async loadFileFromMap(path, fileMap, workingPath) {
+ console.log('path', path)
+ console.log('fileMap', fileMap)
+ console.log('workingPath', workingPath)
+ // Clean path - remove leading slash if present
+ let cleanPath = path;
+ // if (cleanPath.startsWith('/')) {
+ // cleanPath = cleanPath.substring(1);
+ // }
+
+ // // Remove leading ./
+ // cleanPath = cleanPath.replace(/^\.\//, '');
+ cleanPath = cleanPath.split('///')[1];
+ console.log('cleanPath', cleanPath)
+ // Try different path combinations
+ const possiblePaths = [
+ cleanPath,
+ workingPath + cleanPath,
+ path,
+ workingPath + path.replace(/^\/+/, ''),
+ // Also try without the first directory component
+ cleanPath.includes('/') ? cleanPath.substring(cleanPath.indexOf('/') + 1) : cleanPath,
+ ];
+ console.log('possiblePaths', possiblePaths)
+ // Try each path
+ for (const tryPath of possiblePaths) {
+ console.log('tryPath', tryPath)
+ const file = fileMap.get(tryPath);
+ if (file) {
+ console.log(12345)
+ const content = await file.text();
+ return content;
+ }
+ }
+
+ // Try filename only match
+ const fileName = cleanPath.split('/').pop();
+ for (const [key, file] of fileMap.entries()) {
+ const keyFileName = key.split('/').pop();
+ if (keyFileName === fileName) {
+ const content = await file.text();
+ return content;
+ }
+ }
+
+ console.error('[XacroAdapter] Cannot find included file:', path);
+ throw new Error(`Cannot find included file: ${path}`);
+ }
+
+ /**
+ * Remove visual/collision elements with empty geometry tags
+ * xacro-parser may not fully expand conditional geometry (xacro:if)
+ * We'll just remove these empty visual elements rather than try to fix them
+ * @param {XMLDocument} xmlDoc - XML document to clean
+ */
+ static removeEmptyGeometry(xmlDoc) {
+ // Find all geometry elements
+ const geometryElements = xmlDoc.querySelectorAll('geometry');
+
+ geometryElements.forEach(geom => {
+ // Check if geometry is empty (no child elements)
+ if (!geom.children || geom.children.length === 0) {
+ // Find the parent visual or collision element
+ let parent = geom.parentNode;
+ if (parent) {
+ const grandParent = parent.parentNode;
+ if (grandParent) {
+ if (parent.nodeName === 'visual' || parent.nodeName === 'collision') {
+ // Remove empty visual/collision elements
+ grandParent.removeChild(parent);
+ }
+ }
+ }
+ }
+ });
+ }
+
+ /**
+ * Recursively clean XML nodes - remove empty text nodes and comments
+ * @param {Element} element - Element to clean
+ */
+ static cleanXMLNodes(element) {
+ if (!element || !element.childNodes) return;
+
+ const nodesToRemove = [];
+
+ // Collect nodes to remove
+ for (let i = 0; i < element.childNodes.length; i++) {
+ const node = element.childNodes[i];
+
+ // Remove comments
+ if (node.nodeType === Node.COMMENT_NODE) {
+ nodesToRemove.push(node);
+ }
+ // Remove empty or whitespace-only text nodes
+ else if (node.nodeType === Node.TEXT_NODE) {
+ if (!node.nodeValue || node.nodeValue.trim() === '') {
+ nodesToRemove.push(node);
+ }
+ }
+ // Recursively clean element nodes
+ else if (node.nodeType === Node.ELEMENT_NODE) {
+ this.cleanXMLNodes(node);
+ }
+ }
+
+ // Remove collected nodes
+ nodesToRemove.forEach(node => {
+ element.removeChild(node);
+ });
+ }
+
+ /**
+ * Extract package names from URDF content
+ * @param {string} urdfContent - URDF content
+ * @returns {Set}
+ */
+ static extractPackagesFromURDF(urdfContent) {
+ const packages = new Set();
+ const packageRegex = /package:\/\/([^\/]+)/g;
+ let match;
+ while ((match = packageRegex.exec(urdfContent)) !== null) {
+ packages.add(match[1]);
+ }
+ return packages;
+ }
+
+ /**
+ * Find file in fileMap by path
+ * @param {string} path - File path
+ * @param {Map} fileMap - File map
+ * @param {string} urdfDir - URDF directory
+ * @returns {Promise}
+ */
+ static async findFileInMapByPath(path, fileMap, urdfDir) {
+ let meshPath = path;
+
+ // Remove blob: prefix
+ meshPath = meshPath.replace(/^blob:[^\/]+\//, '');
+
+ // Remove package:// prefix
+ if (meshPath.startsWith('package://')) {
+ meshPath = meshPath.replace(/^package:\/\//, '');
+ const parts = meshPath.split('/');
+ if (parts.length > 1) {
+ meshPath = parts.slice(1).join('/');
+ }
+ }
+
+ // Remove leading ./
+ meshPath = meshPath.replace(/^\.\//, '');
+
+ // Build full path
+ const fullPath = urdfDir + meshPath;
+
+ // Try full path
+ let file = fileMap.get(fullPath);
+ if (file) return file;
+
+ // Try path without directory
+ file = fileMap.get(meshPath);
+ if (file) return file;
+
+ // Try filename match
+ const targetFileName = meshPath.split('/').pop();
+ for (const [key, f] of fileMap.entries()) {
+ const keyFileName = key.split('/').pop();
+ if (keyFileName === targetFileName) {
+ return f;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Load mesh file asynchronously
+ * @param {File} file - File object
+ * @param {string} ext - File extension
+ * @param {THREE.LoadingManager} manager - Loading manager
+ * @returns {Promise}
+ */
+ static async loadMeshFileAsync(file, ext, manager) {
+ const THREE = await import('three');
+ const blobUrl = URL.createObjectURL(file);
+
+ try {
+ let meshObject = null;
+
+ switch (ext) {
+ case 'stl': {
+ const { STLLoader } = await import('three/examples/jsm/loaders/STLLoader.js');
+ const stlLoader = new STLLoader(manager);
+ const stlGeometry = await new Promise((resolve, reject) => {
+ stlLoader.load(blobUrl, resolve, undefined, reject);
+ });
+ const stlMaterial = new THREE.MeshPhongMaterial();
+ meshObject = new THREE.Mesh(stlGeometry, stlMaterial);
+ break;
+ }
+
+ case 'dae': {
+ const { ColladaLoader } = await import('three/examples/jsm/loaders/ColladaLoader.js');
+ const colladaLoader = new ColladaLoader(manager);
+ const colladaModel = await new Promise((resolve, reject) => {
+ colladaLoader.load(blobUrl, resolve, undefined, reject);
+ });
+ meshObject = colladaModel.scene;
+
+ // Remove lights
+ if (meshObject && meshObject.traverse) {
+ const lightsToRemove = [];
+ meshObject.traverse(child => {
+ if (child.isLight) {
+ lightsToRemove.push(child);
+ }
+ });
+ lightsToRemove.forEach(light => {
+ if (light.parent) {
+ light.parent.remove(light);
+ }
+ });
+ }
+ break;
+ }
+
+ case 'obj': {
+ const { OBJLoader } = await import('three/examples/jsm/loaders/OBJLoader.js');
+ const objLoader = new OBJLoader(manager);
+ meshObject = await new Promise((resolve, reject) => {
+ objLoader.load(blobUrl, resolve, undefined, reject);
+ });
+ break;
+ }
+
+ case 'gltf':
+ case 'glb': {
+ const { GLTFLoader } = await import('three/examples/jsm/loaders/GLTFLoader.js');
+ const gltfLoader = new GLTFLoader(manager);
+ const gltfModel = await new Promise((resolve, reject) => {
+ gltfLoader.load(blobUrl, resolve, undefined, reject);
+ });
+ meshObject = gltfModel.scene;
+ break;
+ }
+
+ default:
+ console.warn(`Unsupported file format: ${ext}`);
+ URL.revokeObjectURL(blobUrl);
+ return null;
+ }
+
+ URL.revokeObjectURL(blobUrl);
+ return meshObject;
+ } catch (error) {
+ URL.revokeObjectURL(blobUrl);
+ console.error(`Failed to load mesh file: ${file.name}`, error);
+ throw error;
+ }
+ }
+
+ /**
+ * Inject Python-style boolean constants (True/False) into xacro content
+ * Some xacro files use capitalized True/False in conditional expressions
+ * The parameters are passed as strings ("True", "False") but compared to constants
+ * We define the constants to match the string values for comparison to work
+ * @param {string} xacroContent - Original xacro content
+ * @returns {string} - Modified xacro content with boolean constants defined
+ */
+ static injectBooleanConstants(xacroContent) {
+ // Find the robot tag opening
+ const robotTagMatch = xacroContent.match(/(]*>)/);
+ if (!robotTagMatch) {
+ return xacroContent;
+ }
+
+ // Define True/False as string properties for comparison
+ // When mirror_dae="True" is passed, it becomes the string "True"
+ // We want ${mirror_dae == True} to work, so True must also be "True"
+ const booleanProperties = `
+
+
+
+`;
+
+ const insertPosition = robotTagMatch.index + robotTagMatch[0].length;
+ return xacroContent.substring(0, insertPosition) +
+ booleanProperties +
+ xacroContent.substring(insertPosition);
+ }
+
+ /**
+ * Extract xacro arguments and their default values from xacro content
+ * Xacro files can define arguments using
+ * These need to be provided to the parser via parser.arguments
+ * @param {string} xacroContent - Xacro file content
+ * @returns {Object} - Object with argument names as keys and default values
+ */
+ static extractXacroArguments(xacroContent) {
+ const args = {};
+
+ // Match
+ // Also match (without xacro: prefix)
+ const argPattern = /<(?:xacro:)?arg\s+name=["']([^"']+)["'](?:\s+default=["']([^"']*)["'])?/g;
+
+ let match;
+ while ((match = argPattern.exec(xacroContent)) !== null) {
+ const argName = match[1];
+ const defaultValue = match[2] || 'false'; // Default to 'false' if no default specified
+ args[argName] = defaultValue;
+ }
+
+ return args;
+ }
+}
+
+
diff --git a/src/api/device/arm.js b/src/api/device/arm.js
new file mode 100644
index 0000000..00e859c
--- /dev/null
+++ b/src/api/device/arm.js
@@ -0,0 +1,18 @@
+import request from '@/utils/request'
+// 移动机械臂
+export function moveJ(data) {
+ return request({
+ url: '/api/humanoid/moveJ',
+ method: 'post',
+ data: data
+ })
+}
+
+// 获取机械臂状态
+export function status(data) {
+ return request({
+ url: '/api/humanoid/status',
+ method: 'post',
+ data: data
+ })
+}
\ No newline at end of file
diff --git a/src/api/device/dexHand.js b/src/api/device/dexHand.js
new file mode 100644
index 0000000..0d8793d
--- /dev/null
+++ b/src/api/device/dexHand.js
@@ -0,0 +1,19 @@
+import request from '@/utils/request'
+
+// 获取灵巧手状态
+export function status(query) {
+ return request({
+ url: '/api/dexHand/status',
+ method: 'get',
+ params: query
+ })
+}
+
+// 设置灵巧手弯曲角度
+export function setDexHandAngle(query) {
+ return request({
+ url: '/api/dexHand/angle',
+ method: 'get',
+ params: query
+ })
+}
diff --git a/src/api/device/flow.js b/src/api/device/flow.js
new file mode 100644
index 0000000..d6f7e5d
--- /dev/null
+++ b/src/api/device/flow.js
@@ -0,0 +1,63 @@
+import request from '@/utils/request'
+
+// 发布流程
+export function flowDeploy(data) {
+ return request({
+ url: '/flow/publish',
+ method: 'post',
+ data: data
+ })
+}
+
+export function flowExecute(data) {
+ return request({
+ url: '/flow/execute',
+ method: 'post',
+ data: data
+ })
+}
+
+export function flowAction(data) {
+ return request({
+ url: '/flow/action',
+ method: 'post',
+ data: data
+ })
+}
+
+export function flowExecuteTrial(data) {
+ return request({
+ url: '/flow/executeTrial',
+ method: 'post',
+ data: data
+ })
+}
+
+export function flowView(data) {
+ return request({
+ url: '/flow/view',
+ method: 'post',
+ data: data
+ })
+}
+
+export function flowPause(instId) {
+ return request({
+ url: `/flow/pause/${instId}`,
+ method: 'post'
+ })
+}
+
+export function flowResume(instId) {
+ return request({
+ url: `/flow/resume/${instId}`,
+ method: 'post'
+ })
+}
+
+export function flowStop(instId) {
+ return request({
+ url: `/flow/stop/${instId}`,
+ method: 'post'
+ })
+}
\ No newline at end of file
diff --git a/src/api/device/microphone.js b/src/api/device/microphone.js
new file mode 100644
index 0000000..e5fa841
--- /dev/null
+++ b/src/api/device/microphone.js
@@ -0,0 +1,46 @@
+import request from '@/utils/request'
+
+// 开始录音
+export function startMicrophoneApi(data) {
+ return request({
+ url: '/api/Microphone/start',
+ method: 'post',
+ params: data
+ })
+}
+
+// 暂停录音
+export function pauseMicrophoneApi(data) {
+ return request({
+ url: '/api/Microphone/pause',
+ method: 'post',
+ params: data
+ })
+}
+
+// 恢复录音
+export function resumeMicrophoneApi(data) {
+ return request({
+ url: '/api/Microphone/resume',
+ method: 'post',
+ params: data
+ })
+}
+
+// 停止录音
+export function stopMicrophoneApi(data) {
+ return request({
+ url: '/api/Microphone/stop',
+ method: 'post',
+ params: data
+ })
+}
+
+// 播放音频
+export function playSpeaker(data) {
+ return request({
+ url: '/api/Speaker/play',
+ method: 'post',
+ params: data
+ })
+}
\ No newline at end of file
diff --git a/src/api/device/profile.js b/src/api/device/profile.js
new file mode 100644
index 0000000..8b88dc8
--- /dev/null
+++ b/src/api/device/profile.js
@@ -0,0 +1,45 @@
+import request from '@/utils/request'
+
+// 查询设备配置信息列表
+export function listProfile(query) {
+ return request({
+ url: '/device/profile/list',
+ method: 'get',
+ params: query
+ })
+}
+
+// 查询设备配置信息详细
+export function getProfile(query) {
+ return request({
+ url: '/device/profile/parameterInfo/',
+ method: 'get',
+ params: query
+ })
+}
+
+// 新增设备配置信息
+export function addProfile(data) {
+ return request({
+ url: '/device/profile',
+ method: 'post',
+ data: data
+ })
+}
+
+// 修改设备配置信息
+export function updateProfile(data) {
+ return request({
+ url: '/device/profile',
+ method: 'put',
+ data: data
+ })
+}
+
+// 删除设备配置信息
+export function delProfile(id) {
+ return request({
+ url: '/device/profile/' + id,
+ method: 'delete'
+ })
+}
diff --git a/src/api/device/register.js b/src/api/device/register.js
new file mode 100644
index 0000000..2234a78
--- /dev/null
+++ b/src/api/device/register.js
@@ -0,0 +1,146 @@
+import request from '@/utils/request'
+
+// 查询设备注册列表
+export function listRegister(query) {
+ return request({
+ url: '/device/register/list',
+ method: 'get',
+ params: query
+ })
+}
+
+// 查询设备注册详细
+export function getRegister(id) {
+ return request({
+ url: '/device/register/' + id,
+ method: 'get'
+ })
+}
+
+// 新增设备注册
+export function addRegister(data) {
+ return request({
+ url: '/device/register',
+ method: 'post',
+ data: data
+ })
+}
+
+// 修改设备注册
+export function updateRegister(data) {
+ return request({
+ url: '/device/register',
+ method: 'put',
+ data: data
+ })
+}
+
+// 删除设备注册
+export function delRegister(id) {
+ return request({
+ url: '/device/register/' + id,
+ method: 'delete'
+ })
+}
+
+// 设备示教
+export function deviceTeach(id) {
+ return request({
+ url: '/device/profile/' + id,
+ method: 'get',
+ })
+}
+
+// 设备示教参数修改
+export function deviceUpdateProfile(data) {
+ return request({
+ url: '/device/profile/updateProfile',
+ method: 'put',
+ data: data,
+ })
+}
+
+// 新增音色
+export function createTimbre(data) {
+ return request({
+ url: '/show/tts/create',
+ method: 'post',
+ headers: {
+ 'Content-Type': 'multipart/form-data',
+ Authorization: 'eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6ImFiNmEyY2Y0LWM3MjQtNDgyMy1hM2NiLTk4N2NhZWNmMzZiNCJ9.oq-JVRbQuq_ujQ2V0h2jXC3woHPdMQKhnYJ8ixRm-C6Ez4GqJWyHYgmnkw5dbDyhWGdIUH02uv6XT8UiboHD4g',
+ isToken: false
+ },
+ data: data,
+ })
+}
+
+// 获取音色
+export function getTts() {
+ return request({
+ url: '/show/tts/query',
+ headers: {
+ Authorization: 'eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6ImFiNmEyY2Y0LWM3MjQtNDgyMy1hM2NiLTk4N2NhZWNmMzZiNCJ9.oq-JVRbQuq_ujQ2V0h2jXC3woHPdMQKhnYJ8ixRm-C6Ez4GqJWyHYgmnkw5dbDyhWGdIUH02uv6XT8UiboHD4g',
+ isToken: false
+ },
+ method: 'get',
+ })
+}
+
+// 生产语料
+export function createCorpus(data) {
+ return request({
+ url: '/show/tts/corpus',
+ headers: {
+ Authorization: 'eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6ImFiNmEyY2Y0LWM3MjQtNDgyMy1hM2NiLTk4N2NhZWNmMzZiNCJ9.oq-JVRbQuq_ujQ2V0h2jXC3woHPdMQKhnYJ8ixRm-C6Ez4GqJWyHYgmnkw5dbDyhWGdIUH02uv6XT8UiboHD4g',
+ isToken: false
+ },
+ method: 'post',
+ data: data
+ })
+}
+
+export function getSynthesize(data, config = {}) {
+ return request({
+ url: '/show/tts/synthesize',
+ headers: {
+ Authorization: 'eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6ImFiNmEyY2Y0LWM3MjQtNDgyMy1hM2NiLTk4N2NhZWNmMzZiNCJ9.oq-JVRbQuq_ujQ2V0h2jXC3woHPdMQKhnYJ8ixRm-C6Ez4GqJWyHYgmnkw5dbDyhWGdIUH02uv6XT8UiboHD4g',
+ isToken: false
+ },
+ responseType: 'blob',
+ method: 'post',
+ data: data
+ }, {
+ ...config
+ })
+}
+
+export function createAdvancedCorpus(data) {
+ return request({
+ url: '/show/tts/corpus/advanced',
+ headers: {
+ Authorization: 'eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6ImFiNmEyY2Y0LWM3MjQtNDgyMy1hM2NiLTk4N2NhZWNmMzZiNCJ9.oq-JVRbQuq_ujQ2V0h2jXC3woHPdMQKhnYJ8ixRm-C6Ez4GqJWyHYgmnkw5dbDyhWGdIUH02uv6XT8UiboHD4g',
+ isToken: false
+ },
+ method: 'post',
+ data: data
+ })
+}
+
+export function getAdvancedSynthesize(data, config = {}) {
+ return request({
+ url: '/show/tts/synthesize/advanced',
+ headers: {
+ Authorization: 'eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6ImFiNmEyY2Y0LWM3MjQtNDgyMy1hM2NiLTk4N2NhZWNmMzZiNCJ9.oq-JVRbQuq_ujQ2V0h2jXC3woHPdMQKhnYJ8ixRm-C6Ez4GqJWyHYgmnkw5dbDyhWGdIUH02uv6XT8UiboHD4g',
+ isToken: false
+ },
+ responseType: 'blob',
+ method: 'post',
+ data: data
+ }, {
+ ...config
+ })
+}
+
+export const getFlowById = (id) => {
+ return request.get(import.meta.env.VITE_NODE_RED_URL + '/flow/' + id, {})
+}
\ No newline at end of file
diff --git a/src/api/device/robot.js b/src/api/device/robot.js
new file mode 100644
index 0000000..b725429
--- /dev/null
+++ b/src/api/device/robot.js
@@ -0,0 +1,44 @@
+import request from '@/utils/request'
+
+// 查询机器人配置列表
+export function listRobot(query) {
+ return request({
+ url: '/device/robot/list',
+ method: 'get',
+ params: query
+ })
+}
+
+// 查询机器人配置详细
+export function getRobot(id) {
+ return request({
+ url: '/device/robot/' + id,
+ method: 'get'
+ })
+}
+
+// 新增机器人配置
+export function addRobot(data) {
+ return request({
+ url: '/device/robot',
+ method: 'post',
+ data: data
+ })
+}
+
+// 修改机器人配置
+export function updateRobot(data) {
+ return request({
+ url: '/device/robot',
+ method: 'put',
+ data: data
+ })
+}
+
+// 删除机器人配置
+export function delRobot(id) {
+ return request({
+ url: '/device/robot/' + id,
+ method: 'delete'
+ })
+}
diff --git a/src/api/device/speaker.js b/src/api/device/speaker.js
new file mode 100644
index 0000000..4b40916
--- /dev/null
+++ b/src/api/device/speaker.js
@@ -0,0 +1,72 @@
+import request from '@/utils/request'
+
+// 暂停播放
+export function pause(data) {
+ return request({
+ url: '/api/Speaker/pause',
+ method: 'post',
+ params: data
+ })
+}
+
+// 播放音频
+export function play(data) {
+ return request({
+ url: '/api/Speaker/play',
+ method: 'post',
+ params: data
+ })
+}
+
+// 恢复播放
+export function resume(data) {
+ return request({
+ url: '/api/Speaker/resume',
+ method: 'post',
+ params: data
+ })
+}
+
+// 获取扬声器状态
+export function status(query) {
+ return request({
+ url: '/api/Speaker/status',
+ method: 'get',
+ params: query
+ })
+}
+
+// 停止播放
+export function stop(data) {
+ return request({
+ url: '/api/Speaker/stop',
+ method: 'post',
+ params: data
+ })
+}
+
+// 获取音量
+export function getVolume(query) {
+ return request({
+ url: '/api/Speaker/volume',
+ method: 'get',
+ params: query
+ })
+}
+
+// 设置音量
+export function setVolume(data) {
+ return request({
+ url: '/api/Speaker/volume',
+ method: 'post',
+ params: data
+ })
+}
+
+// 设置音量
+export function getAll() {
+ return request({
+ url: '/api/Speaker/getAll',
+ method: 'get',
+ })
+}
\ No newline at end of file
diff --git a/src/api/device/terminal.js b/src/api/device/terminal.js
new file mode 100644
index 0000000..f1d85a4
--- /dev/null
+++ b/src/api/device/terminal.js
@@ -0,0 +1,86 @@
+import request from '@/utils/request'
+
+// 查询设备终端配置列表
+export function listTerminal(query) {
+ return request({
+ url: '/device/terminal/list',
+ method: 'get',
+ params: query
+ })
+}
+
+// 查询设备终端配置详细
+export function getTerminal(id) {
+ return request({
+ url: '/device/terminal/' + id,
+ method: 'get'
+ })
+}
+
+// 新增设备终端配置
+export function addTerminal(data) {
+ return request({
+ url: '/device/terminal',
+ method: 'post',
+ data: data
+ })
+}
+
+// 修改设备终端配置
+export function updateTerminal(data) {
+ return request({
+ url: '/device/terminal',
+ method: 'put',
+ data: data
+ })
+}
+
+// 删除设备终端配置
+export function delTerminal(id) {
+ return request({
+ url: '/device/terminal/' + id,
+ method: 'delete'
+ })
+}
+
+// 获取实例信息
+export function getInstInfo(id) {
+ return request({
+ url: '/test/inst/context/' + id,
+ method: 'get'
+ })
+}
+
+// 暂停任务
+export function pauseInst(id) {
+ return request({
+ url: `/test/inst/pause/${id}`,
+ method: 'post'
+ })
+}
+
+// 恢复任务
+export function resumeInst(id) {
+ return request({
+ url: `/test/inst/resume/${id}`,
+ method: 'post'
+ })
+}
+
+// 恢复任务
+export function stopInst(id) {
+ return request({
+ url: `/test/inst/stop/${id}`,
+ method: 'post'
+ })
+}
+
+export function getFlowParamsApi(taskId) {
+ return request({
+ url: '/test/orchestration',
+ method: 'get',
+ params: {
+ taskId
+ }
+ })
+}
\ No newline at end of file
diff --git a/src/api/evaluate/indicator.js b/src/api/evaluate/indicator.js
new file mode 100644
index 0000000..ed9541a
--- /dev/null
+++ b/src/api/evaluate/indicator.js
@@ -0,0 +1,68 @@
+import request from "@/utils/request";
+
+// 查询根据层级指标库
+export function getIndicatorByParentId(parentId, type) {
+ return request({
+ url: '/evaluate/indicator/getIndicatorByParentId',
+ method: "get",
+ params: {
+ parentId,
+ type
+ }
+ });
+}
+
+/**
+ * 新增指标
+ * @param {*} data
+ * @returns
+ */
+export function addIndicator(data) {
+ return request({
+ url: `/evaluate/indicator/indicator/insert`,
+ method: "post",
+ data
+ });
+}
+
+/**
+ * 删除指标
+ * @param {*} id
+ * @returns
+ */
+export function deleteIndicator(id) {
+ return request({
+ url: `/evaluate/indicator/${id}`,
+ method: "delete",
+ });
+}
+
+/**
+ * 修改指标
+ * @param {*} id
+ * @returns
+ */
+export function updateIndicator(data) {
+ return request({
+ url: `/evaluate/indicator`,
+ method: "put",
+ data
+ });
+}
+
+/**
+ * 获取指定层级的指标
+ * @param {*} id
+ * @returns
+ */
+export function getChildrenByParentIdWithLevelLimitApi(parentId, maxLevel, type) {
+ return request({
+ url: `/evaluate/indicator/getChildrenByParentIdWithLevelLimit`,
+ method: "get",
+ params: {
+ parentId,
+ maxLevel,
+ type
+ }
+ });
+}
\ No newline at end of file
diff --git a/src/api/inspection/map.js b/src/api/inspection/map.js
new file mode 100644
index 0000000..ec8c562
--- /dev/null
+++ b/src/api/inspection/map.js
@@ -0,0 +1,33 @@
+import request from '@/utils/request'
+
+
+export function getMapList(params) {
+ return request({
+ url: '/inspection/map/list',
+ method: 'get',
+ params: params
+ })
+}
+
+export function addMap(data) {
+ return request({
+ url: '/inspection/map',
+ method: 'post',
+ data: data
+ })
+}
+
+export function updateMap(data) {
+ return request({
+ url: '/inspection/map',
+ method: 'put',
+ data: data
+ })
+}
+
+export function deleteMap(id) {
+ return request({
+ url: `/inspection/map/${id}`,
+ method: 'delete'
+ })
+}
\ No newline at end of file
diff --git a/src/api/inspection/point.js b/src/api/inspection/point.js
new file mode 100644
index 0000000..5f4b17e
--- /dev/null
+++ b/src/api/inspection/point.js
@@ -0,0 +1,32 @@
+import request from '@/utils/request'
+
+export function getPointList(params) {
+ return request({
+ url: '/inspection/waypoint/list',
+ method: 'get',
+ params: params
+ })
+}
+
+export function addPoint(data) {
+ return request({
+ url: '/inspection/waypoint',
+ method: 'post',
+ data: data
+ })
+}
+
+export function updatePoint(data) {
+ return request({
+ url: '/inspection/waypoint',
+ method: 'put',
+ data: data
+ })
+}
+
+export function deletePoint(id) {
+ return request({
+ url: `/inspection/waypoint/${id}`,
+ method: 'delete'
+ })
+}
\ No newline at end of file
diff --git a/src/api/inspection/robot.js b/src/api/inspection/robot.js
new file mode 100644
index 0000000..c4963f2
--- /dev/null
+++ b/src/api/inspection/robot.js
@@ -0,0 +1,57 @@
+import request from '@/utils/request'
+
+export function getRobotList(params) {
+ return request({
+ url: '/inspection/robot/list',
+ method: 'get',
+ params: params
+ })
+}
+
+export function addRobot(data) {
+ return request({
+ url: '/inspection/robot',
+ method: 'post',
+ data: data
+ })
+}
+
+export function updateRobot(data) {
+ return request({
+ url: '/inspection/robot',
+ method: 'put',
+ data: data
+ })
+}
+
+export function deleteRobot(id) {
+ return request({
+ url: `/inspection/robot/${id}`,
+ method: 'delete'
+ })
+}
+
+/**
+ * 获取机器人地图
+ * @param {*} robotId
+ * @returns
+ */
+export function getMapByRobot(robotId) {
+ return request({
+ url: `/inspection/robot/${robotId}/maps`,
+ method: 'get'
+ })
+}
+
+/**
+ * 获取地图JSON数据
+ * @param {*} params
+ * @returns
+ */
+export function getMapJson(params) {
+ return request({
+ url: `/inspection/robot/${params.robotId}/download-map`,
+ method: 'get',
+ params
+ })
+}
\ No newline at end of file
diff --git a/src/api/inspection/runTask.js b/src/api/inspection/runTask.js
new file mode 100644
index 0000000..fb2c8b4
--- /dev/null
+++ b/src/api/inspection/runTask.js
@@ -0,0 +1,75 @@
+import request from '@/utils/request'
+
+export function getRunTaskList(params) {
+ return request({
+ url: '/inspection/taskInstance/list',
+ method: 'get',
+ params: params
+ })
+}
+
+export function getRunTaskDetail(id) {
+ return request({
+ url: `/inspection/taskInstance/${id}`,
+ method: 'get'
+ })
+}
+
+export function addRunTask(data) {
+ return request({
+ url: '/inspection/taskInstance',
+ method: 'post',
+ data: data
+ })
+}
+
+export function updateRunTask(data) {
+ return request({
+ url: '/inspection/taskInstance',
+ method: 'put',
+ data: data
+ })
+}
+
+export function deleteRunTask(id) {
+ return request({
+ url: `/inspection/taskInstance/${id}`,
+ method: 'delete'
+ })
+}
+
+export function startRunTask(id) {
+ return request({
+ url: `/inspection/taskInstance/start/${id}`,
+ method: 'put'
+ })
+}
+
+export function stopRunTask(id) {
+ return request({
+ url: `/inspection/taskInstance/stop/${id}`,
+ method: 'put'
+ })
+}
+
+export function pauseRunTask(id) {
+ return request({
+ url: `/inspection/taskInstance/pause/${id}`,
+ method: 'put'
+ })
+}
+
+export function resumeRunTask(id) {
+ return request({
+ url: `/inspection/taskInstance/resume/${id}`,
+ method: 'put'
+ })
+}
+
+export function getTaskLogListApi(params) {
+ return request({
+ url: '/inspection/taskLog/list',
+ method: 'get',
+ params: params
+ })
+}
\ No newline at end of file
diff --git a/src/api/inspection/task.js b/src/api/inspection/task.js
new file mode 100644
index 0000000..9a04f33
--- /dev/null
+++ b/src/api/inspection/task.js
@@ -0,0 +1,47 @@
+import request from '@/utils/request'
+
+export function getTaskList(params) {
+ return request({
+ url: '/inspection/task/list',
+ method: 'get',
+ params: params
+ })
+}
+
+export function addTask(data) {
+ return request({
+ url: '/inspection/task',
+ method: 'post',
+ data: data
+ })
+}
+
+export function updateTask(data) {
+ return request({
+ url: '/inspection/task',
+ method: 'put',
+ data: data
+ })
+}
+
+export function deleteTask(id) {
+ return request({
+ url: `/inspection/task/${id}`,
+ method: 'delete'
+ })
+}
+
+export function getWaypointByTaskId(taskId) {
+ return request({
+ url: `/inspection/task/${taskId}/waypoints`,
+ method: 'get'
+ })
+}
+
+export function bindWaypointsToTask(taskId, waypointIds) {
+ return request({
+ url: `/inspection/task/${taskId}/waypoints`,
+ method: 'post',
+ data: waypointIds
+ })
+}
\ No newline at end of file
diff --git a/src/api/intelligenceTest/car.js b/src/api/intelligenceTest/car.js
new file mode 100644
index 0000000..5d8c25e
--- /dev/null
+++ b/src/api/intelligenceTest/car.js
@@ -0,0 +1,32 @@
+import request from '@/utils/request'
+
+export function addVehicle(data) {
+ return request({
+ url: '/aima/vehicle',
+ method: 'post',
+ data: data
+ })
+}
+
+export function getVehicleList(params) {
+ return request({
+ url: '/aima/vehicle/list',
+ method: 'get',
+ params: params
+ })
+}
+
+export function updateVehicle(data) {
+ return request({
+ url: '/aima/vehicle',
+ method: 'put',
+ data: data
+ })
+}
+
+export function deleteVehicle(ids) {
+ return request({
+ url: `/aima/vehicle/${ids}`,
+ method: 'delete'
+ })
+}
\ No newline at end of file
diff --git a/src/api/intelligenceTest/mobilePhone.js b/src/api/intelligenceTest/mobilePhone.js
new file mode 100644
index 0000000..b683fc2
--- /dev/null
+++ b/src/api/intelligenceTest/mobilePhone.js
@@ -0,0 +1,32 @@
+import request from '@/utils/request'
+
+export function addPhone(data) {
+ return request({
+ url: '/aima/phone',
+ method: 'post',
+ data: data
+ })
+}
+
+export function getPhoneList(params) {
+ return request({
+ url: '/aima/phone/list',
+ method: 'get',
+ params: params
+ })
+}
+
+export function updatePhone(data) {
+ return request({
+ url: '/aima/phone',
+ method: 'put',
+ data: data
+ })
+}
+
+export function deletePhone(ids) {
+ return request({
+ url: `/aima/phone/${ids}`,
+ method: 'delete'
+ })
+}
\ No newline at end of file
diff --git a/src/api/intelligenceTest/runningTask.js b/src/api/intelligenceTest/runningTask.js
new file mode 100644
index 0000000..adfd01e
--- /dev/null
+++ b/src/api/intelligenceTest/runningTask.js
@@ -0,0 +1,69 @@
+import request from '@/utils/request'
+import { method } from 'lodash'
+
+export function addTaskinstance(data) {
+ return request({
+ url: '/aima/taskinstance',
+ method: 'post',
+ data: data
+ })
+}
+
+export function getTaskinstanceList(params) {
+ return request({
+ url: '/aima/taskinstance/list',
+ method: 'get',
+ params: params
+ })
+}
+
+export function updateTaskinstance(data) {
+ return request({
+ url: '/aima/taskinstance',
+ method: 'put',
+ data: data
+ })
+}
+
+export function deleteTaskinstance(ids) {
+ return request({
+ url: `/aima/taskinstance/{ids}`,
+ method: 'delete'
+ })
+}
+
+export function startTaskinstance(id) {
+ return request({
+ url: `/aima/taskinstance/start/${id}`,
+ method: 'put'
+ })
+}
+
+export function pauseTaskinstance(id) {
+ return request({
+ url: `/aima/taskinstance/pause/${id}`,
+ method: 'put'
+ })
+}
+
+export function stopTaskinstance(id) {
+ return request({
+ url: `/aima/taskinstance/stop/${id}`,
+ method: 'put'
+ })
+}
+
+export function resumeTaskinstance(id) {
+ return request({
+ url: `/aima/taskinstance/resume/${id}`,
+ method: 'put'
+ })
+}
+
+export function getTaskLogListApi(params) {
+ return request({
+ url: '/aima/testlog/list',
+ method: 'get',
+ params: params
+ })
+}
\ No newline at end of file
diff --git a/src/api/intelligenceTest/taskManage.js b/src/api/intelligenceTest/taskManage.js
new file mode 100644
index 0000000..457d33f
--- /dev/null
+++ b/src/api/intelligenceTest/taskManage.js
@@ -0,0 +1,47 @@
+import request from '@/utils/request'
+
+export function addTask(data) {
+ return request({
+ url: '/aima/task',
+ method: 'post',
+ data: data
+ })
+}
+
+export function getTaskList(params) {
+ return request({
+ url: '/aima/task/list',
+ method: 'get',
+ params: params
+ })
+}
+
+export function updateTask(data) {
+ return request({
+ url: '/aima/task',
+ method: 'put',
+ data: data
+ })
+}
+
+export function deleteTask(ids) {
+ return request({
+ url: `/aima/task/${ids}`,
+ method: 'delete'
+ })
+}
+
+export function getTestCaseByTaskId(taskId) {
+ return request({
+ url: `/aima/task/testCases/${taskId}`,
+ method: 'get',
+ })
+}
+
+export function bindTastCase(taskId, ids) {
+ return request({
+ url: `/aima/task/bindTestCases/${taskId}`,
+ method: 'post',
+ data: ids
+ })
+}
\ No newline at end of file
diff --git a/src/api/intelligenceTest/testCase.js b/src/api/intelligenceTest/testCase.js
new file mode 100644
index 0000000..374ebe1
--- /dev/null
+++ b/src/api/intelligenceTest/testCase.js
@@ -0,0 +1,32 @@
+import request from '@/utils/request'
+
+export function addTestCase(data) {
+ return request({
+ url: '/aima/testcase',
+ method: 'post',
+ data: data
+ })
+}
+
+export function getTestCaseList(params) {
+ return request({
+ url: '/aima/testcase/list',
+ method: 'get',
+ params: params
+ })
+}
+
+export function updateTestCase(data) {
+ return request({
+ url: '/aima/testcase',
+ method: 'put',
+ data: data
+ })
+}
+
+export function deleteTestCase(ids) {
+ return request({
+ url: `/aima/testcase/${ids}`,
+ method: 'delete'
+ })
+}
\ No newline at end of file
diff --git a/src/api/is/func/index.js b/src/api/is/func/index.js
new file mode 100644
index 0000000..0503469
--- /dev/null
+++ b/src/api/is/func/index.js
@@ -0,0 +1,63 @@
+import request from '@/utils/request'
+
+// 查询语音交互-方案列表
+export function getTreeDataApi() {
+ return request({
+ url: '/ti/config/list',
+ method: 'get'
+ })
+}
+
+export function postTreeData(data) {
+ return request({
+ url: '/ti/config',
+ method: 'post',
+ data
+ })
+}
+
+export function updateTreeData(data) {
+ return request({
+ url: '/ti/config',
+ method: 'put',
+ data
+ })
+}
+
+export function deleteTreeData(ids) {
+ return request({
+ url: `/ti/config/${ids}`,
+ method: 'delete'
+ })
+}
+
+export function addFunction(data) {
+ return request({
+ url: '/ti/function',
+ method: 'post',
+ data
+ })
+}
+
+export function getFunction(params) {
+ return request({
+ url: '/ti/function/list',
+ method: 'get',
+ params
+ })
+}
+
+export function updateFunction(data) {
+ return request({
+ url: '/ti/function',
+ method: 'put',
+ data
+ })
+}
+
+export function deleteFunction(ids) {
+ return request({
+ url: `/ti/function/${ids}`,
+ method: 'delete'
+ })
+}
\ No newline at end of file
diff --git a/src/api/is/im/index.js b/src/api/is/im/index.js
new file mode 100644
index 0000000..5cd060e
--- /dev/null
+++ b/src/api/is/im/index.js
@@ -0,0 +1,32 @@
+import request from '@/utils/request'
+
+export function addUi(data) {
+ return request({
+ url: '/ti/ui',
+ method: 'post',
+ data
+ })
+}
+
+export function getUiList(params) {
+ return request({
+ url: '/ti/ui/list',
+ methods: 'get',
+ params
+ })
+}
+
+export function deleteUiList(ids) {
+ return request({
+ url: `/ti/ui/${ids}`,
+ method: 'delete'
+ })
+}
+
+export function updateUI(data) {
+ return request({
+ url: '/ti/ui',
+ method: 'put',
+ data
+ })
+}
\ No newline at end of file
diff --git a/src/api/is/project/index.js b/src/api/is/project/index.js
new file mode 100644
index 0000000..79e88c6
--- /dev/null
+++ b/src/api/is/project/index.js
@@ -0,0 +1,76 @@
+import request from '@/utils/request'
+
+export const getDictList = (params) => {
+ return request({
+ url: '/system/dict/data/list',
+ method: 'get',
+ params
+ })
+}
+
+export const addProject = (data) => {
+ return request({
+ url: '/ti/project',
+ method: 'post',
+ data
+ })
+}
+
+export const updateProject = (data) => {
+ return request({
+ url: '/ti/project',
+ method: 'put',
+ data
+ })
+}
+
+export const getProjectList = (params) => {
+ return request({
+ url: '/ti/project/list',
+ method: 'get',
+ params
+ })
+}
+
+export const deleteProject = (projectIds) => {
+ return request({
+ url: '/ti/project/' + projectIds,
+ method: 'delete'
+ })
+}
+
+export const getExecuteRunParams = (params) => {
+ return request({
+ url: '/ti/project/queryExecuteRunParams',
+ method: 'get',
+ params
+ })
+}
+
+export const projectExecute = (data) => {
+ return request({
+ url: '/ti/project/execute',
+ method: 'post',
+ data
+ })
+}
+
+export const executeProject = (data) => {
+ return request({
+ url: '/ti/project/evaluation',
+ method: 'post',
+ data
+ })
+}
+
+/**
+ * 获取评估
+ * @param {*} projectId
+ * @returns
+ */
+export function getValuationProject(projectId) {
+ return request({
+ url: '/ti/project/evaluation/' + projectId,
+ method: 'get'
+ })
+}
\ No newline at end of file
diff --git a/src/api/login.js b/src/api/login.js
new file mode 100644
index 0000000..7b7388f
--- /dev/null
+++ b/src/api/login.js
@@ -0,0 +1,60 @@
+import request from '@/utils/request'
+
+// 登录方法
+export function login(username, password, code, uuid) {
+ const data = {
+ username,
+ password,
+ code,
+ uuid
+ }
+ return request({
+ url: '/login',
+ headers: {
+ isToken: false,
+ repeatSubmit: false
+ },
+ method: 'post',
+ data: data
+ })
+}
+
+// 注册方法
+export function register(data) {
+ return request({
+ url: '/register',
+ headers: {
+ isToken: false
+ },
+ method: 'post',
+ data: data
+ })
+}
+
+// 获取用户详细信息
+export function getInfo() {
+ return request({
+ url: '/getInfo',
+ method: 'get'
+ })
+}
+
+// 退出方法
+export function logout() {
+ return request({
+ url: '/logout',
+ method: 'post'
+ })
+}
+
+// 获取验证码
+export function getCodeImg() {
+ return request({
+ url: '/captchaImage',
+ headers: {
+ isToken: false
+ },
+ method: 'get',
+ timeout: 20000
+ })
+}
\ No newline at end of file
diff --git a/src/api/menu.js b/src/api/menu.js
new file mode 100644
index 0000000..faef101
--- /dev/null
+++ b/src/api/menu.js
@@ -0,0 +1,9 @@
+import request from '@/utils/request'
+
+// 获取路由
+export const getRouters = () => {
+ return request({
+ url: '/getRouters',
+ method: 'get'
+ })
+}
\ No newline at end of file
diff --git a/src/api/monitor/cache.js b/src/api/monitor/cache.js
new file mode 100644
index 0000000..72c5f6a
--- /dev/null
+++ b/src/api/monitor/cache.js
@@ -0,0 +1,57 @@
+import request from '@/utils/request'
+
+// 查询缓存详细
+export function getCache() {
+ return request({
+ url: '/monitor/cache',
+ method: 'get'
+ })
+}
+
+// 查询缓存名称列表
+export function listCacheName() {
+ return request({
+ url: '/monitor/cache/getNames',
+ method: 'get'
+ })
+}
+
+// 查询缓存键名列表
+export function listCacheKey(cacheName) {
+ return request({
+ url: '/monitor/cache/getKeys/' + cacheName,
+ method: 'get'
+ })
+}
+
+// 查询缓存内容
+export function getCacheValue(cacheName, cacheKey) {
+ return request({
+ url: '/monitor/cache/getValue/' + cacheName + '/' + cacheKey,
+ method: 'get'
+ })
+}
+
+// 清理指定名称缓存
+export function clearCacheName(cacheName) {
+ return request({
+ url: '/monitor/cache/clearCacheName/' + cacheName,
+ method: 'delete'
+ })
+}
+
+// 清理指定键名缓存
+export function clearCacheKey(cacheKey) {
+ return request({
+ url: '/monitor/cache/clearCacheKey/' + cacheKey,
+ method: 'delete'
+ })
+}
+
+// 清理全部缓存
+export function clearCacheAll() {
+ return request({
+ url: '/monitor/cache/clearCacheAll',
+ method: 'delete'
+ })
+}
diff --git a/src/api/monitor/job.js b/src/api/monitor/job.js
new file mode 100644
index 0000000..3815569
--- /dev/null
+++ b/src/api/monitor/job.js
@@ -0,0 +1,71 @@
+import request from '@/utils/request'
+
+// 查询定时任务调度列表
+export function listJob(query) {
+ return request({
+ url: '/monitor/job/list',
+ method: 'get',
+ params: query
+ })
+}
+
+// 查询定时任务调度详细
+export function getJob(jobId) {
+ return request({
+ url: '/monitor/job/' + jobId,
+ method: 'get'
+ })
+}
+
+// 新增定时任务调度
+export function addJob(data) {
+ return request({
+ url: '/monitor/job',
+ method: 'post',
+ data: data
+ })
+}
+
+// 修改定时任务调度
+export function updateJob(data) {
+ return request({
+ url: '/monitor/job',
+ method: 'put',
+ data: data
+ })
+}
+
+// 删除定时任务调度
+export function delJob(jobId) {
+ return request({
+ url: '/monitor/job/' + jobId,
+ method: 'delete'
+ })
+}
+
+// 任务状态修改
+export function changeJobStatus(jobId, status) {
+ const data = {
+ jobId,
+ status
+ }
+ return request({
+ url: '/monitor/job/changeStatus',
+ method: 'put',
+ data: data
+ })
+}
+
+
+// 定时任务立即执行一次
+export function runJob(jobId, jobGroup) {
+ const data = {
+ jobId,
+ jobGroup
+ }
+ return request({
+ url: '/monitor/job/run',
+ method: 'put',
+ data: data
+ })
+}
\ No newline at end of file
diff --git a/src/api/monitor/jobLog.js b/src/api/monitor/jobLog.js
new file mode 100644
index 0000000..6e0be61
--- /dev/null
+++ b/src/api/monitor/jobLog.js
@@ -0,0 +1,26 @@
+import request from '@/utils/request'
+
+// 查询调度日志列表
+export function listJobLog(query) {
+ return request({
+ url: '/monitor/jobLog/list',
+ method: 'get',
+ params: query
+ })
+}
+
+// 删除调度日志
+export function delJobLog(jobLogId) {
+ return request({
+ url: '/monitor/jobLog/' + jobLogId,
+ method: 'delete'
+ })
+}
+
+// 清空调度日志
+export function cleanJobLog() {
+ return request({
+ url: '/monitor/jobLog/clean',
+ method: 'delete'
+ })
+}
diff --git a/src/api/monitor/logininfor.js b/src/api/monitor/logininfor.js
new file mode 100644
index 0000000..4d112b7
--- /dev/null
+++ b/src/api/monitor/logininfor.js
@@ -0,0 +1,34 @@
+import request from '@/utils/request'
+
+// 查询登录日志列表
+export function list(query) {
+ return request({
+ url: '/monitor/logininfor/list',
+ method: 'get',
+ params: query
+ })
+}
+
+// 删除登录日志
+export function delLogininfor(infoId) {
+ return request({
+ url: '/monitor/logininfor/' + infoId,
+ method: 'delete'
+ })
+}
+
+// 解锁用户登录状态
+export function unlockLogininfor(userName) {
+ return request({
+ url: '/monitor/logininfor/unlock/' + userName,
+ method: 'get'
+ })
+}
+
+// 清空登录日志
+export function cleanLogininfor() {
+ return request({
+ url: '/monitor/logininfor/clean',
+ method: 'delete'
+ })
+}
diff --git a/src/api/monitor/online.js b/src/api/monitor/online.js
new file mode 100644
index 0000000..bd22137
--- /dev/null
+++ b/src/api/monitor/online.js
@@ -0,0 +1,18 @@
+import request from '@/utils/request'
+
+// 查询在线用户列表
+export function list(query) {
+ return request({
+ url: '/monitor/online/list',
+ method: 'get',
+ params: query
+ })
+}
+
+// 强退用户
+export function forceLogout(tokenId) {
+ return request({
+ url: '/monitor/online/' + tokenId,
+ method: 'delete'
+ })
+}
diff --git a/src/api/monitor/operlog.js b/src/api/monitor/operlog.js
new file mode 100644
index 0000000..a04bca8
--- /dev/null
+++ b/src/api/monitor/operlog.js
@@ -0,0 +1,26 @@
+import request from '@/utils/request'
+
+// 查询操作日志列表
+export function list(query) {
+ return request({
+ url: '/monitor/operlog/list',
+ method: 'get',
+ params: query
+ })
+}
+
+// 删除操作日志
+export function delOperlog(operId) {
+ return request({
+ url: '/monitor/operlog/' + operId,
+ method: 'delete'
+ })
+}
+
+// 清空操作日志
+export function cleanOperlog() {
+ return request({
+ url: '/monitor/operlog/clean',
+ method: 'delete'
+ })
+}
diff --git a/src/api/monitor/server.js b/src/api/monitor/server.js
new file mode 100644
index 0000000..e1f9ca2
--- /dev/null
+++ b/src/api/monitor/server.js
@@ -0,0 +1,9 @@
+import request from '@/utils/request'
+
+// 获取服务信息
+export function getServer() {
+ return request({
+ url: '/monitor/server',
+ method: 'get'
+ })
+}
\ No newline at end of file
diff --git a/src/api/system/config.js b/src/api/system/config.js
new file mode 100644
index 0000000..a404d82
--- /dev/null
+++ b/src/api/system/config.js
@@ -0,0 +1,60 @@
+import request from '@/utils/request'
+
+// 查询参数列表
+export function listConfig(query) {
+ return request({
+ url: '/system/config/list',
+ method: 'get',
+ params: query
+ })
+}
+
+// 查询参数详细
+export function getConfig(configId) {
+ return request({
+ url: '/system/config/' + configId,
+ method: 'get'
+ })
+}
+
+// 根据参数键名查询参数值
+export function getConfigKey(configKey) {
+ return request({
+ url: '/system/config/configKey/' + configKey,
+ method: 'get'
+ })
+}
+
+// 新增参数配置
+export function addConfig(data) {
+ return request({
+ url: '/system/config',
+ method: 'post',
+ data: data
+ })
+}
+
+// 修改参数配置
+export function updateConfig(data) {
+ return request({
+ url: '/system/config',
+ method: 'put',
+ data: data
+ })
+}
+
+// 删除参数配置
+export function delConfig(configId) {
+ return request({
+ url: '/system/config/' + configId,
+ method: 'delete'
+ })
+}
+
+// 刷新参数缓存
+export function refreshCache() {
+ return request({
+ url: '/system/config/refreshCache',
+ method: 'delete'
+ })
+}
diff --git a/src/api/system/dept.js b/src/api/system/dept.js
new file mode 100644
index 0000000..fc943cd
--- /dev/null
+++ b/src/api/system/dept.js
@@ -0,0 +1,52 @@
+import request from '@/utils/request'
+
+// 查询部门列表
+export function listDept(query) {
+ return request({
+ url: '/system/dept/list',
+ method: 'get',
+ params: query
+ })
+}
+
+// 查询部门列表(排除节点)
+export function listDeptExcludeChild(deptId) {
+ return request({
+ url: '/system/dept/list/exclude/' + deptId,
+ method: 'get'
+ })
+}
+
+// 查询部门详细
+export function getDept(deptId) {
+ return request({
+ url: '/system/dept/' + deptId,
+ method: 'get'
+ })
+}
+
+// 新增部门
+export function addDept(data) {
+ return request({
+ url: '/system/dept',
+ method: 'post',
+ data: data
+ })
+}
+
+// 修改部门
+export function updateDept(data) {
+ return request({
+ url: '/system/dept',
+ method: 'put',
+ data: data
+ })
+}
+
+// 删除部门
+export function delDept(deptId) {
+ return request({
+ url: '/system/dept/' + deptId,
+ method: 'delete'
+ })
+}
\ No newline at end of file
diff --git a/src/api/system/dict/data.js b/src/api/system/dict/data.js
new file mode 100644
index 0000000..6c9eb79
--- /dev/null
+++ b/src/api/system/dict/data.js
@@ -0,0 +1,52 @@
+import request from '@/utils/request'
+
+// 查询字典数据列表
+export function listData(query) {
+ return request({
+ url: '/system/dict/data/list',
+ method: 'get',
+ params: query
+ })
+}
+
+// 查询字典数据详细
+export function getData(dictCode) {
+ return request({
+ url: '/system/dict/data/' + dictCode,
+ method: 'get'
+ })
+}
+
+// 根据字典类型查询字典数据信息
+export function getDicts(dictType) {
+ return request({
+ url: '/system/dict/data/type/' + dictType,
+ method: 'get'
+ })
+}
+
+// 新增字典数据
+export function addData(data) {
+ return request({
+ url: '/system/dict/data',
+ method: 'post',
+ data: data
+ })
+}
+
+// 修改字典数据
+export function updateData(data) {
+ return request({
+ url: '/system/dict/data',
+ method: 'put',
+ data: data
+ })
+}
+
+// 删除字典数据
+export function delData(dictCode) {
+ return request({
+ url: '/system/dict/data/' + dictCode,
+ method: 'delete'
+ })
+}
diff --git a/src/api/system/dict/type.js b/src/api/system/dict/type.js
new file mode 100644
index 0000000..a0254ba
--- /dev/null
+++ b/src/api/system/dict/type.js
@@ -0,0 +1,60 @@
+import request from '@/utils/request'
+
+// 查询字典类型列表
+export function listType(query) {
+ return request({
+ url: '/system/dict/type/list',
+ method: 'get',
+ params: query
+ })
+}
+
+// 查询字典类型详细
+export function getType(dictId) {
+ return request({
+ url: '/system/dict/type/' + dictId,
+ method: 'get'
+ })
+}
+
+// 新增字典类型
+export function addType(data) {
+ return request({
+ url: '/system/dict/type',
+ method: 'post',
+ data: data
+ })
+}
+
+// 修改字典类型
+export function updateType(data) {
+ return request({
+ url: '/system/dict/type',
+ method: 'put',
+ data: data
+ })
+}
+
+// 删除字典类型
+export function delType(dictId) {
+ return request({
+ url: '/system/dict/type/' + dictId,
+ method: 'delete'
+ })
+}
+
+// 刷新字典缓存
+export function refreshCache() {
+ return request({
+ url: '/system/dict/type/refreshCache',
+ method: 'delete'
+ })
+}
+
+// 获取字典选择框列表
+export function optionselect() {
+ return request({
+ url: '/system/dict/type/optionselect',
+ method: 'get'
+ })
+}
diff --git a/src/api/system/file.js b/src/api/system/file.js
new file mode 100644
index 0000000..9d254bf
--- /dev/null
+++ b/src/api/system/file.js
@@ -0,0 +1,44 @@
+import request from '@/utils/request'
+
+// 查询文件管理列表
+export function listFile(query) {
+ return request({
+ url: '/system/file/list',
+ method: 'get',
+ params: query
+ })
+}
+
+// 查询文件管理详细
+export function getFile(fileId) {
+ return request({
+ url: '/system/file/' + fileId,
+ method: 'get'
+ })
+}
+
+// 新增文件管理
+export function addFile(data) {
+ return request({
+ url: '/system/file',
+ method: 'post',
+ data: data
+ })
+}
+
+// 修改文件管理
+export function updateFile(data) {
+ return request({
+ url: '/system/file',
+ method: 'put',
+ data: data
+ })
+}
+
+// 删除文件管理
+export function delFile(fileId) {
+ return request({
+ url: '/system/file/' + fileId,
+ method: 'delete'
+ })
+}
diff --git a/src/api/system/group.js b/src/api/system/group.js
new file mode 100644
index 0000000..71defe0
--- /dev/null
+++ b/src/api/system/group.js
@@ -0,0 +1,44 @@
+import request from '@/utils/request'
+
+// 查询通用分组配置列表
+export function listGroup(query) {
+ return request({
+ url: '/system/group/list',
+ method: 'get',
+ params: query
+ })
+}
+
+// 查询通用分组配置详细
+export function getGroup(groupId) {
+ return request({
+ url: '/system/group/' + groupId,
+ method: 'get'
+ })
+}
+
+// 新增通用分组配置
+export function addGroup(data) {
+ return request({
+ url: '/system/group',
+ method: 'post',
+ data: data
+ })
+}
+
+// 修改通用分组配置
+export function updateGroup(data) {
+ return request({
+ url: '/system/group',
+ method: 'put',
+ data: data
+ })
+}
+
+// 删除通用分组配置
+export function delGroup(groupId) {
+ return request({
+ url: '/system/group/' + groupId,
+ method: 'delete'
+ })
+}
diff --git a/src/api/system/menu.js b/src/api/system/menu.js
new file mode 100644
index 0000000..f6415c6
--- /dev/null
+++ b/src/api/system/menu.js
@@ -0,0 +1,60 @@
+import request from '@/utils/request'
+
+// 查询菜单列表
+export function listMenu(query) {
+ return request({
+ url: '/system/menu/list',
+ method: 'get',
+ params: query
+ })
+}
+
+// 查询菜单详细
+export function getMenu(menuId) {
+ return request({
+ url: '/system/menu/' + menuId,
+ method: 'get'
+ })
+}
+
+// 查询菜单下拉树结构
+export function treeselect() {
+ return request({
+ url: '/system/menu/treeselect',
+ method: 'get'
+ })
+}
+
+// 根据角色ID查询菜单下拉树结构
+export function roleMenuTreeselect(roleId) {
+ return request({
+ url: '/system/menu/roleMenuTreeselect/' + roleId,
+ method: 'get'
+ })
+}
+
+// 新增菜单
+export function addMenu(data) {
+ return request({
+ url: '/system/menu',
+ method: 'post',
+ data: data
+ })
+}
+
+// 修改菜单
+export function updateMenu(data) {
+ return request({
+ url: '/system/menu',
+ method: 'put',
+ data: data
+ })
+}
+
+// 删除菜单
+export function delMenu(menuId) {
+ return request({
+ url: '/system/menu/' + menuId,
+ method: 'delete'
+ })
+}
\ No newline at end of file
diff --git a/src/api/system/notice.js b/src/api/system/notice.js
new file mode 100644
index 0000000..c274ea5
--- /dev/null
+++ b/src/api/system/notice.js
@@ -0,0 +1,44 @@
+import request from '@/utils/request'
+
+// 查询公告列表
+export function listNotice(query) {
+ return request({
+ url: '/system/notice/list',
+ method: 'get',
+ params: query
+ })
+}
+
+// 查询公告详细
+export function getNotice(noticeId) {
+ return request({
+ url: '/system/notice/' + noticeId,
+ method: 'get'
+ })
+}
+
+// 新增公告
+export function addNotice(data) {
+ return request({
+ url: '/system/notice',
+ method: 'post',
+ data: data
+ })
+}
+
+// 修改公告
+export function updateNotice(data) {
+ return request({
+ url: '/system/notice',
+ method: 'put',
+ data: data
+ })
+}
+
+// 删除公告
+export function delNotice(noticeId) {
+ return request({
+ url: '/system/notice/' + noticeId,
+ method: 'delete'
+ })
+}
\ No newline at end of file
diff --git a/src/api/system/post.js b/src/api/system/post.js
new file mode 100644
index 0000000..1a8e9ca
--- /dev/null
+++ b/src/api/system/post.js
@@ -0,0 +1,44 @@
+import request from '@/utils/request'
+
+// 查询岗位列表
+export function listPost(query) {
+ return request({
+ url: '/system/post/list',
+ method: 'get',
+ params: query
+ })
+}
+
+// 查询岗位详细
+export function getPost(postId) {
+ return request({
+ url: '/system/post/' + postId,
+ method: 'get'
+ })
+}
+
+// 新增岗位
+export function addPost(data) {
+ return request({
+ url: '/system/post',
+ method: 'post',
+ data: data
+ })
+}
+
+// 修改岗位
+export function updatePost(data) {
+ return request({
+ url: '/system/post',
+ method: 'put',
+ data: data
+ })
+}
+
+// 删除岗位
+export function delPost(postId) {
+ return request({
+ url: '/system/post/' + postId,
+ method: 'delete'
+ })
+}
diff --git a/src/api/system/role.js b/src/api/system/role.js
new file mode 100644
index 0000000..f13e6f4
--- /dev/null
+++ b/src/api/system/role.js
@@ -0,0 +1,119 @@
+import request from '@/utils/request'
+
+// 查询角色列表
+export function listRole(query) {
+ return request({
+ url: '/system/role/list',
+ method: 'get',
+ params: query
+ })
+}
+
+// 查询角色详细
+export function getRole(roleId) {
+ return request({
+ url: '/system/role/' + roleId,
+ method: 'get'
+ })
+}
+
+// 新增角色
+export function addRole(data) {
+ return request({
+ url: '/system/role',
+ method: 'post',
+ data: data
+ })
+}
+
+// 修改角色
+export function updateRole(data) {
+ return request({
+ url: '/system/role',
+ method: 'put',
+ data: data
+ })
+}
+
+// 角色数据权限
+export function dataScope(data) {
+ return request({
+ url: '/system/role/dataScope',
+ method: 'put',
+ data: data
+ })
+}
+
+// 角色状态修改
+export function changeRoleStatus(roleId, status) {
+ const data = {
+ roleId,
+ status
+ }
+ return request({
+ url: '/system/role/changeStatus',
+ method: 'put',
+ data: data
+ })
+}
+
+// 删除角色
+export function delRole(roleId) {
+ return request({
+ url: '/system/role/' + roleId,
+ method: 'delete'
+ })
+}
+
+// 查询角色已授权用户列表
+export function allocatedUserList(query) {
+ return request({
+ url: '/system/role/authUser/allocatedList',
+ method: 'get',
+ params: query
+ })
+}
+
+// 查询角色未授权用户列表
+export function unallocatedUserList(query) {
+ return request({
+ url: '/system/role/authUser/unallocatedList',
+ method: 'get',
+ params: query
+ })
+}
+
+// 取消用户授权角色
+export function authUserCancel(data) {
+ return request({
+ url: '/system/role/authUser/cancel',
+ method: 'put',
+ data: data
+ })
+}
+
+// 批量取消用户授权角色
+export function authUserCancelAll(data) {
+ return request({
+ url: '/system/role/authUser/cancelAll',
+ method: 'put',
+ params: data
+ })
+}
+
+// 授权用户选择
+export function authUserSelectAll(data) {
+ return request({
+ url: '/system/role/authUser/selectAll',
+ method: 'put',
+ params: data
+ })
+}
+
+// 根据角色ID查询部门树结构
+export function deptTreeSelect(roleId) {
+ return request({
+ url: '/system/role/deptTree/' + roleId,
+ method: 'get'
+ })
+}
diff --git a/src/api/system/user.js b/src/api/system/user.js
new file mode 100644
index 0000000..9b0211a
--- /dev/null
+++ b/src/api/system/user.js
@@ -0,0 +1,136 @@
+import request from '@/utils/request'
+import { parseStrEmpty } from "@/utils/ruoyi";
+
+// 查询用户列表
+export function listUser(query) {
+ return request({
+ url: '/system/user/list',
+ method: 'get',
+ params: query
+ })
+}
+
+// 查询用户详细
+export function getUser(userId) {
+ return request({
+ url: '/system/user/' + parseStrEmpty(userId),
+ method: 'get'
+ })
+}
+
+// 新增用户
+export function addUser(data) {
+ return request({
+ url: '/system/user',
+ method: 'post',
+ data: data
+ })
+}
+
+// 修改用户
+export function updateUser(data) {
+ return request({
+ url: '/system/user',
+ method: 'put',
+ data: data
+ })
+}
+
+// 删除用户
+export function delUser(userId) {
+ return request({
+ url: '/system/user/' + userId,
+ method: 'delete'
+ })
+}
+
+// 用户密码重置
+export function resetUserPwd(userId, password) {
+ const data = {
+ userId,
+ password
+ }
+ return request({
+ url: '/system/user/resetPwd',
+ method: 'put',
+ data: data
+ })
+}
+
+// 用户状态修改
+export function changeUserStatus(userId, status) {
+ const data = {
+ userId,
+ status
+ }
+ return request({
+ url: '/system/user/changeStatus',
+ method: 'put',
+ data: data
+ })
+}
+
+// 查询用户个人信息
+export function getUserProfile() {
+ return request({
+ url: '/system/user/profile',
+ method: 'get'
+ })
+}
+
+// 修改用户个人信息
+export function updateUserProfile(data) {
+ return request({
+ url: '/system/user/profile',
+ method: 'put',
+ data: data
+ })
+}
+
+// 用户密码重置
+export function updateUserPwd(oldPassword, newPassword) {
+ const data = {
+ oldPassword,
+ newPassword
+ }
+ return request({
+ url: '/system/user/profile/updatePwd',
+ method: 'put',
+ params: data
+ })
+}
+
+// 用户头像上传
+export function uploadAvatar(data) {
+ return request({
+ url: '/system/user/profile/avatar',
+ method: 'post',
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
+ data: data
+ })
+}
+
+// 查询授权角色
+export function getAuthRole(userId) {
+ return request({
+ url: '/system/user/authRole/' + userId,
+ method: 'get'
+ })
+}
+
+// 保存授权角色
+export function updateAuthRole(data) {
+ return request({
+ url: '/system/user/authRole',
+ method: 'put',
+ params: data
+ })
+}
+
+// 查询部门下拉树结构
+export function deptTreeSelect() {
+ return request({
+ url: '/system/user/deptTree',
+ method: 'get'
+ })
+}
diff --git a/src/api/system/vehOper.js b/src/api/system/vehOper.js
new file mode 100644
index 0000000..9c1170a
--- /dev/null
+++ b/src/api/system/vehOper.js
@@ -0,0 +1,44 @@
+import request from '@/utils/request'
+
+// 查询车机操作配置列表
+export function listVehOper(query) {
+ return request({
+ url: '/system/vehOper/list',
+ method: 'get',
+ params: query
+ })
+}
+
+// 查询车机操作配置详细
+export function getVehOper(configId) {
+ return request({
+ url: '/system/vehOper/' + configId,
+ method: 'get'
+ })
+}
+
+// 新增车机操作配置
+export function addVehOper(data) {
+ return request({
+ url: '/system/vehOper',
+ method: 'post',
+ data: data
+ })
+}
+
+// 修改车机操作配置
+export function updateVehOper(data) {
+ return request({
+ url: '/system/vehOper',
+ method: 'put',
+ data: data
+ })
+}
+
+// 删除车机操作配置
+export function delVehOper(configId) {
+ return request({
+ url: '/system/vehOper/' + configId,
+ method: 'delete'
+ })
+}
diff --git a/src/api/test/config.js b/src/api/test/config.js
new file mode 100644
index 0000000..5ec8531
--- /dev/null
+++ b/src/api/test/config.js
@@ -0,0 +1,78 @@
+import request from '@/utils/request'
+
+// 查询任务流程编排列表
+export function listConfig(query) {
+ return request({
+ url: '/test/config/list',
+ method: 'get',
+ params: query
+ })
+}
+
+// 查询任务流程编排详细
+export function getConfig(id) {
+ return request({
+ url: '/test/config/' + id,
+ method: 'get'
+ })
+}
+
+// 新增任务流程编排
+export function addConfig(data) {
+ return request({
+ url: '/test/config',
+ method: 'post',
+ data: data
+ })
+}
+
+// 修改任务流程编排
+export function updateConfig(data) {
+ return request({
+ url: '/test/config',
+ method: 'put',
+ data: data
+ })
+}
+
+// 删除任务流程编排
+export function delConfig(id) {
+ return request({
+ url: '/test/config/' + id,
+ method: 'delete'
+ })
+}
+
+// 流程编排
+export function taskConfig(data) {
+ return request({
+ url: '/test/orchestration',
+ method: 'post',
+ data: data
+ })
+}
+
+// 流程编排查询
+export function getTaskConfig(params) {
+ return request({
+ url: `/test/orchestration`,
+ method: 'get',
+ params: params
+ })
+}
+
+// 获取任务执行参数
+export function getTaskParamsById(id) {
+ return request({
+ url: `/test/param/query/${id}`,
+ method: 'get',
+ })
+}
+// 任务执行
+export function taskExecute(data) {
+ return request({
+ url: `/flow/execute`,
+ method: 'post',
+ data: data
+ })
+}
diff --git a/src/api/test/detect.js b/src/api/test/detect.js
new file mode 100644
index 0000000..8cf79c2
--- /dev/null
+++ b/src/api/test/detect.js
@@ -0,0 +1,44 @@
+import request from '@/utils/request'
+
+// 查询检测项配置列表
+export function listDetect(query) {
+ return request({
+ url: '/test/detect/list',
+ method: 'get',
+ params: query
+ })
+}
+
+// 查询检测项配置详细
+export function getDetect(id) {
+ return request({
+ url: '/test/detect/' + id,
+ method: 'get'
+ })
+}
+
+// 新增检测项配置
+export function addDetect(data) {
+ return request({
+ url: '/test/detect',
+ method: 'post',
+ data: data
+ })
+}
+
+// 修改检测项配置
+export function updateDetect(data) {
+ return request({
+ url: '/test/detect',
+ method: 'put',
+ data: data
+ })
+}
+
+// 删除检测项配置
+export function delDetect(id) {
+ return request({
+ url: '/test/detect/' + id,
+ method: 'delete'
+ })
+}
diff --git a/src/api/test/log.js b/src/api/test/log.js
new file mode 100644
index 0000000..d357ed4
--- /dev/null
+++ b/src/api/test/log.js
@@ -0,0 +1,72 @@
+import request from '@/utils/request'
+
+// 查询检测项日志列表
+export function listLog(query) {
+ return request({
+ url: '/test/log/list',
+ method: 'get',
+ params: query
+ })
+}
+
+// 查询检测项日志详细
+export function getLog(id) {
+ return request({
+ url: '/test/log/' + id,
+ method: 'get'
+ })
+}
+
+// 新增检测项日志
+export function addLog(data) {
+ return request({
+ url: '/test/log',
+ method: 'post',
+ data: data
+ })
+}
+
+// 修改检测项日志
+export function updateLog(data) {
+ return request({
+ url: '/test/log',
+ method: 'put',
+ data: data
+ })
+}
+
+// 删除检测项日志
+export function delLog(id) {
+ return request({
+ url: '/test/log/' + id,
+ method: 'delete'
+ })
+}
+
+// 查询任务实例列表
+export function listInst(query) {
+ return request({
+ url: '/test/inst/list',
+ method: 'get',
+ params: query
+ })
+}
+
+// 查询任务实例日志
+export function getInst(query) {
+ return request({
+ url: '/test/inst/log/detail',
+ method: 'get',
+ params: query
+ })
+}
+
+// 查询任务实例日志
+export function getLogicFlow(query) {
+ return request({
+ url: '/test/inst/log/query',
+ method: 'get',
+ params: query
+ })
+}
+
diff --git a/src/api/test/params.js b/src/api/test/params.js
new file mode 100644
index 0000000..1cde454
--- /dev/null
+++ b/src/api/test/params.js
@@ -0,0 +1,60 @@
+import request from '@/utils/request'
+
+// 查询参数配置列表
+export function listParams(query) {
+ return request({
+ url: '/test/param/list',
+ method: 'get',
+ params: query
+ })
+}
+
+// 查询参数配置详细
+export function getParams(id) {
+ return request({
+ url: '/test/param/' + id,
+ method: 'get'
+ })
+}
+
+// 新增参数配置
+export function addParams(data) {
+ return request({
+ url: '/test/param',
+ method: 'post',
+ data: data
+ })
+}
+
+// 修改参数配置
+export function updateParams(data) {
+ return request({
+ url: '/test/param',
+ method: 'put',
+ data: data
+ })
+}
+
+// 删除参数配置
+export function delParams(id) {
+ return request({
+ url: '/test/param/' + id,
+ method: 'delete'
+ })
+}
+
+// 参数来源
+export function getParamSources(id) {
+ return request({
+ url: '/test/param/sources',
+ method: 'get'
+ })
+}
+
+// 参数类型
+export function getParamTypes(id) {
+ return request({
+ url: '/test/param/types',
+ method: 'get'
+ })
+}
diff --git a/src/api/tool/gen.js b/src/api/tool/gen.js
new file mode 100644
index 0000000..2075677
--- /dev/null
+++ b/src/api/tool/gen.js
@@ -0,0 +1,85 @@
+import request from '@/utils/request'
+
+// 查询生成表数据
+export function listTable(query) {
+ return request({
+ url: '/tool/gen/list',
+ method: 'get',
+ params: query
+ })
+}
+// 查询db数据库列表
+export function listDbTable(query) {
+ return request({
+ url: '/tool/gen/db/list',
+ method: 'get',
+ params: query
+ })
+}
+
+// 查询表详细信息
+export function getGenTable(tableId) {
+ return request({
+ url: '/tool/gen/' + tableId,
+ method: 'get'
+ })
+}
+
+// 修改代码生成信息
+export function updateGenTable(data) {
+ return request({
+ url: '/tool/gen',
+ method: 'put',
+ data: data
+ })
+}
+
+// 导入表
+export function importTable(data) {
+ return request({
+ url: '/tool/gen/importTable',
+ method: 'post',
+ params: data
+ })
+}
+
+// 创建表
+export function createTable(data) {
+ return request({
+ url: '/tool/gen/createTable',
+ method: 'post',
+ params: data
+ })
+}
+
+// 预览生成代码
+export function previewTable(tableId) {
+ return request({
+ url: '/tool/gen/preview/' + tableId,
+ method: 'get'
+ })
+}
+
+// 删除表数据
+export function delTable(tableId) {
+ return request({
+ url: '/tool/gen/' + tableId,
+ method: 'delete'
+ })
+}
+
+// 生成代码(自定义路径)
+export function genCode(tableName) {
+ return request({
+ url: '/tool/gen/genCode/' + tableName,
+ method: 'get'
+ })
+}
+
+// 同步数据库
+export function synchDb(tableName) {
+ return request({
+ url: '/tool/gen/synchDb/' + tableName,
+ method: 'get'
+ })
+}
diff --git a/src/api/touch_interaction.js b/src/api/touch_interaction.js
new file mode 100644
index 0000000..9182549
--- /dev/null
+++ b/src/api/touch_interaction.js
@@ -0,0 +1,14 @@
+import request from "@/utils/request.js";
+
+export function coordinateMarking(data) {
+ return request({
+ url: '/show/touch/coordinates',
+ method: 'post',
+ headers: {
+ isToken: false,
+ 'Content-Type': 'multipart/form-data',
+ 'Authorization':'Bearer eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6ImIzY2QwYjFjLWNlNjgtNDEwNy1hYzA1LTdmYTBlNmQ4NzJmNiJ9.2N9E9IDrFeCe-yRIBly60N0kYYmCscdrhyd8KHmhw2dxLi7B_A1i8bcWRTtx9YryFB3AhkvZkGauMx0CdZ0TDg'
+ },
+ data: data
+ })
+}
diff --git a/src/api/vi/corpus.js b/src/api/vi/corpus.js
new file mode 100644
index 0000000..f049e87
--- /dev/null
+++ b/src/api/vi/corpus.js
@@ -0,0 +1,77 @@
+import request from "@/utils/request";
+import { method } from "lodash";
+
+// 查询语料库列表
+export function listCorpus(query) {
+ return request({
+ url: "/vi/corpus/single/list",
+ method: "get",
+ params: query,
+ });
+}
+
+// 查询语料库详细
+export function getCorpus(corpusId) {
+ return request({
+ url: "/vi/corpus/" + corpusId,
+ method: "get",
+ });
+}
+
+// 新增语料库
+export function addCorpus(data) {
+ return request({
+ url: "/vi/corpus",
+ method: "post",
+ data: data,
+ });
+}
+
+// 修改语料库
+export function updateCorpus(data) {
+ return request({
+ url: "/vi/corpus",
+ method: "put",
+ data: data,
+ });
+}
+
+// 删除语料库
+export function delCorpus(corpusId) {
+ return request({
+ url: "/vi/corpus/" + corpusId,
+ method: "delete",
+ });
+}
+
+export function batchAddCorpus(data) {
+ return request({
+ url: "/vi/corpus/continuous/insert",
+ method: "post",
+ data: data,
+ });
+}
+
+export function batchUpdateCorpus(data) {
+ return request({
+ url: "/vi/corpus/continuous/update",
+ method: "post",
+ data: data,
+ });
+}
+
+export function getBatchCorpus(query) {
+ return request({
+ url: "/vi/corpus/continuous/list",
+ method: "get",
+ params: query,
+ });
+}
+
+export function deleteBatchCorpus(corpusIds) {
+ return request({
+ url: `/vi/corpus/continuous`,
+ method: "delete",
+ data: corpusIds,
+ });
+}
diff --git a/src/api/vi/plan.js b/src/api/vi/plan.js
new file mode 100644
index 0000000..73f0f0e
--- /dev/null
+++ b/src/api/vi/plan.js
@@ -0,0 +1,44 @@
+import request from '@/utils/request'
+
+// 查询语音交互-方案列表
+export function listScheme(query) {
+ return request({
+ url: '/vi/scheme/list',
+ method: 'get',
+ params: query
+ })
+}
+
+// 查询语音交互-方案详细
+export function getScheme(schemeId) {
+ return request({
+ url: '/vi/scheme/' + schemeId,
+ method: 'get'
+ })
+}
+
+// 新增语音交互-方案
+export function addScheme(data) {
+ return request({
+ url: '/vi/scheme',
+ method: 'post',
+ data: data
+ })
+}
+
+// 修改语音交互-方案
+export function updateScheme(data) {
+ return request({
+ url: '/vi/scheme',
+ method: 'put',
+ data: data
+ })
+}
+
+// 删除语音交互-方案
+export function delScheme(schemeId) {
+ return request({
+ url: '/vi/scheme/' + schemeId,
+ method: 'delete'
+ })
+}
diff --git a/src/api/vi/project.js b/src/api/vi/project.js
new file mode 100644
index 0000000..0b0397b
--- /dev/null
+++ b/src/api/vi/project.js
@@ -0,0 +1,83 @@
+import request from '@/utils/request'
+
+// 查询语音交互-项目列表
+export function listProject(query) {
+ return request({
+ url: '/vi/project/list',
+ method: 'get',
+ params: query
+ })
+}
+
+// 查询语音交互-项目详细
+export function getProject(projectId) {
+ return request({
+ url: '/vi/project/' + projectId,
+ method: 'get'
+ })
+}
+
+// 新增语音交互-项目
+export function addProject(data) {
+ return request({
+ url: '/vi/project',
+ method: 'post',
+ data: data
+ })
+}
+
+// 修改语音交互-项目
+export function updateProject(data) {
+ return request({
+ url: '/vi/project',
+ method: 'put',
+ data: data
+ })
+}
+
+// 删除语音交互-项目
+export function delProject(projectId) {
+ return request({
+ url: '/vi/project/' + projectId,
+ method: 'delete'
+ })
+}
+
+export function getExecuteRunParams(projectId) {
+ return request({
+ url: '/vi/project/queryExecuteRunParams',
+ method: 'get',
+ params: {
+ projectId
+ }
+ })
+}
+
+export function projectExecute(data) {
+ return request({
+ url: '/vi/project/execute',
+ method: 'post',
+ data
+ })
+}
+
+export function queryExecuteInstId(projectId) {
+ return request({
+ url: `/vi/project/queryExecuteInstId/${projectId}`,
+ method: 'get',
+
+ })
+}
+
+
+/**
+ * 获取评估
+ * @param {*} projectId
+ * @returns
+ */
+export function getValuationProject(projectId) {
+ return request({
+ url: '/vi/project/evaluation/' + projectId,
+ method: 'get'
+ })
+}
diff --git a/src/assets/401_images/401.gif b/src/assets/401_images/401.gif
new file mode 100644
index 0000000..cd6e0d9
Binary files /dev/null and b/src/assets/401_images/401.gif differ
diff --git a/src/assets/404_images/404.png b/src/assets/404_images/404.png
new file mode 100644
index 0000000..3d8e230
Binary files /dev/null and b/src/assets/404_images/404.png differ
diff --git a/src/assets/404_images/404_cloud.png b/src/assets/404_images/404_cloud.png
new file mode 100644
index 0000000..c6281d0
Binary files /dev/null and b/src/assets/404_images/404_cloud.png differ
diff --git a/src/assets/icons/svg/404.svg b/src/assets/icons/svg/404.svg
new file mode 100644
index 0000000..6df5019
--- /dev/null
+++ b/src/assets/icons/svg/404.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/bot.svg b/src/assets/icons/svg/bot.svg
new file mode 100644
index 0000000..808cb7b
--- /dev/null
+++ b/src/assets/icons/svg/bot.svg
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/bug.svg b/src/assets/icons/svg/bug.svg
new file mode 100644
index 0000000..05a150d
--- /dev/null
+++ b/src/assets/icons/svg/bug.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/build.svg b/src/assets/icons/svg/build.svg
new file mode 100644
index 0000000..97c4688
--- /dev/null
+++ b/src/assets/icons/svg/build.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/button.svg b/src/assets/icons/svg/button.svg
new file mode 100644
index 0000000..904fddc
--- /dev/null
+++ b/src/assets/icons/svg/button.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/cascader.svg b/src/assets/icons/svg/cascader.svg
new file mode 100644
index 0000000..e256024
--- /dev/null
+++ b/src/assets/icons/svg/cascader.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/chart.svg b/src/assets/icons/svg/chart.svg
new file mode 100644
index 0000000..27728fb
--- /dev/null
+++ b/src/assets/icons/svg/chart.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/checkbox.svg b/src/assets/icons/svg/checkbox.svg
new file mode 100644
index 0000000..013fd3a
--- /dev/null
+++ b/src/assets/icons/svg/checkbox.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/chevron-down.svg b/src/assets/icons/svg/chevron-down.svg
new file mode 100644
index 0000000..2d3b12a
--- /dev/null
+++ b/src/assets/icons/svg/chevron-down.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/src/assets/icons/svg/chevron-left.svg b/src/assets/icons/svg/chevron-left.svg
new file mode 100644
index 0000000..c07e6b1
--- /dev/null
+++ b/src/assets/icons/svg/chevron-left.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/src/assets/icons/svg/chevron-right.svg b/src/assets/icons/svg/chevron-right.svg
new file mode 100644
index 0000000..29a2824
--- /dev/null
+++ b/src/assets/icons/svg/chevron-right.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/src/assets/icons/svg/chevron-stop.svg b/src/assets/icons/svg/chevron-stop.svg
new file mode 100644
index 0000000..714f927
--- /dev/null
+++ b/src/assets/icons/svg/chevron-stop.svg
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/assets/icons/svg/chevron-up.svg b/src/assets/icons/svg/chevron-up.svg
new file mode 100644
index 0000000..c981751
--- /dev/null
+++ b/src/assets/icons/svg/chevron-up.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/src/assets/icons/svg/clipboard.svg b/src/assets/icons/svg/clipboard.svg
new file mode 100644
index 0000000..90923ff
--- /dev/null
+++ b/src/assets/icons/svg/clipboard.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/code.svg b/src/assets/icons/svg/code.svg
new file mode 100644
index 0000000..5f9c5ab
--- /dev/null
+++ b/src/assets/icons/svg/code.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/color.svg b/src/assets/icons/svg/color.svg
new file mode 100644
index 0000000..44a81aa
--- /dev/null
+++ b/src/assets/icons/svg/color.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/component.svg b/src/assets/icons/svg/component.svg
new file mode 100644
index 0000000..29c3458
--- /dev/null
+++ b/src/assets/icons/svg/component.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/cpu.svg b/src/assets/icons/svg/cpu.svg
new file mode 100644
index 0000000..1b336d8
--- /dev/null
+++ b/src/assets/icons/svg/cpu.svg
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/assets/icons/svg/dashboard.svg b/src/assets/icons/svg/dashboard.svg
new file mode 100644
index 0000000..5317d37
--- /dev/null
+++ b/src/assets/icons/svg/dashboard.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/date-range.svg b/src/assets/icons/svg/date-range.svg
new file mode 100644
index 0000000..fda571e
--- /dev/null
+++ b/src/assets/icons/svg/date-range.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/date.svg b/src/assets/icons/svg/date.svg
new file mode 100644
index 0000000..52dc73e
--- /dev/null
+++ b/src/assets/icons/svg/date.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/dict.svg b/src/assets/icons/svg/dict.svg
new file mode 100644
index 0000000..4849377
--- /dev/null
+++ b/src/assets/icons/svg/dict.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/documentation.svg b/src/assets/icons/svg/documentation.svg
new file mode 100644
index 0000000..7043122
--- /dev/null
+++ b/src/assets/icons/svg/documentation.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/download.svg b/src/assets/icons/svg/download.svg
new file mode 100644
index 0000000..c896951
--- /dev/null
+++ b/src/assets/icons/svg/download.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/drag.svg b/src/assets/icons/svg/drag.svg
new file mode 100644
index 0000000..4185d3c
--- /dev/null
+++ b/src/assets/icons/svg/drag.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/druid.svg b/src/assets/icons/svg/druid.svg
new file mode 100644
index 0000000..a2b4b4e
--- /dev/null
+++ b/src/assets/icons/svg/druid.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/edit.svg b/src/assets/icons/svg/edit.svg
new file mode 100644
index 0000000..d26101f
--- /dev/null
+++ b/src/assets/icons/svg/edit.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/education.svg b/src/assets/icons/svg/education.svg
new file mode 100644
index 0000000..7bfb01d
--- /dev/null
+++ b/src/assets/icons/svg/education.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/email.svg b/src/assets/icons/svg/email.svg
new file mode 100644
index 0000000..74d25e2
--- /dev/null
+++ b/src/assets/icons/svg/email.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/example.svg b/src/assets/icons/svg/example.svg
new file mode 100644
index 0000000..46f42b5
--- /dev/null
+++ b/src/assets/icons/svg/example.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/excel.svg b/src/assets/icons/svg/excel.svg
new file mode 100644
index 0000000..74d97b8
--- /dev/null
+++ b/src/assets/icons/svg/excel.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/exit-fullscreen.svg b/src/assets/icons/svg/exit-fullscreen.svg
new file mode 100644
index 0000000..485c128
--- /dev/null
+++ b/src/assets/icons/svg/exit-fullscreen.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/eye-open.svg b/src/assets/icons/svg/eye-open.svg
new file mode 100644
index 0000000..88dcc98
--- /dev/null
+++ b/src/assets/icons/svg/eye-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/eye.svg b/src/assets/icons/svg/eye.svg
new file mode 100644
index 0000000..16ed2d8
--- /dev/null
+++ b/src/assets/icons/svg/eye.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/form.svg b/src/assets/icons/svg/form.svg
new file mode 100644
index 0000000..dcbaa18
--- /dev/null
+++ b/src/assets/icons/svg/form.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/fullscreen.svg b/src/assets/icons/svg/fullscreen.svg
new file mode 100644
index 0000000..0e86b6f
--- /dev/null
+++ b/src/assets/icons/svg/fullscreen.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/gamepad.svg b/src/assets/icons/svg/gamepad.svg
new file mode 100644
index 0000000..37f522b
--- /dev/null
+++ b/src/assets/icons/svg/gamepad.svg
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/src/assets/icons/svg/github.svg b/src/assets/icons/svg/github.svg
new file mode 100644
index 0000000..db0a0d4
--- /dev/null
+++ b/src/assets/icons/svg/github.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/guide.svg b/src/assets/icons/svg/guide.svg
new file mode 100644
index 0000000..b271001
--- /dev/null
+++ b/src/assets/icons/svg/guide.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/icon.svg b/src/assets/icons/svg/icon.svg
new file mode 100644
index 0000000..82be8ee
--- /dev/null
+++ b/src/assets/icons/svg/icon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/input.svg b/src/assets/icons/svg/input.svg
new file mode 100644
index 0000000..ab91381
--- /dev/null
+++ b/src/assets/icons/svg/input.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/international.svg b/src/assets/icons/svg/international.svg
new file mode 100644
index 0000000..e9b56ee
--- /dev/null
+++ b/src/assets/icons/svg/international.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/job.svg b/src/assets/icons/svg/job.svg
new file mode 100644
index 0000000..2a93a25
--- /dev/null
+++ b/src/assets/icons/svg/job.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/language.svg b/src/assets/icons/svg/language.svg
new file mode 100644
index 0000000..0082b57
--- /dev/null
+++ b/src/assets/icons/svg/language.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/link.svg b/src/assets/icons/svg/link.svg
new file mode 100644
index 0000000..48197ba
--- /dev/null
+++ b/src/assets/icons/svg/link.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/list.svg b/src/assets/icons/svg/list.svg
new file mode 100644
index 0000000..20259ed
--- /dev/null
+++ b/src/assets/icons/svg/list.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/locate-fixed.svg b/src/assets/icons/svg/locate-fixed.svg
new file mode 100644
index 0000000..f6dc1bb
--- /dev/null
+++ b/src/assets/icons/svg/locate-fixed.svg
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/assets/icons/svg/lock.svg b/src/assets/icons/svg/lock.svg
new file mode 100644
index 0000000..74fee54
--- /dev/null
+++ b/src/assets/icons/svg/lock.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/log.svg b/src/assets/icons/svg/log.svg
new file mode 100644
index 0000000..d879d33
--- /dev/null
+++ b/src/assets/icons/svg/log.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/logininfor.svg b/src/assets/icons/svg/logininfor.svg
new file mode 100644
index 0000000..267f844
--- /dev/null
+++ b/src/assets/icons/svg/logininfor.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/map.svg b/src/assets/icons/svg/map.svg
new file mode 100644
index 0000000..1893335
--- /dev/null
+++ b/src/assets/icons/svg/map.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/src/assets/icons/svg/message.svg b/src/assets/icons/svg/message.svg
new file mode 100644
index 0000000..14ca817
--- /dev/null
+++ b/src/assets/icons/svg/message.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/money.svg b/src/assets/icons/svg/money.svg
new file mode 100644
index 0000000..c1580de
--- /dev/null
+++ b/src/assets/icons/svg/money.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/monitor.svg b/src/assets/icons/svg/monitor.svg
new file mode 100644
index 0000000..bc308cb
--- /dev/null
+++ b/src/assets/icons/svg/monitor.svg
@@ -0,0 +1,2 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/moon.svg b/src/assets/icons/svg/moon.svg
new file mode 100644
index 0000000..ec72d77
--- /dev/null
+++ b/src/assets/icons/svg/moon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/navigation.svg b/src/assets/icons/svg/navigation.svg
new file mode 100644
index 0000000..819fd1b
--- /dev/null
+++ b/src/assets/icons/svg/navigation.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/src/assets/icons/svg/nested.svg b/src/assets/icons/svg/nested.svg
new file mode 100644
index 0000000..06713a8
--- /dev/null
+++ b/src/assets/icons/svg/nested.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/number.svg b/src/assets/icons/svg/number.svg
new file mode 100644
index 0000000..ad5ce9a
--- /dev/null
+++ b/src/assets/icons/svg/number.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/online.svg b/src/assets/icons/svg/online.svg
new file mode 100644
index 0000000..330a202
--- /dev/null
+++ b/src/assets/icons/svg/online.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/password.svg b/src/assets/icons/svg/password.svg
new file mode 100644
index 0000000..6c64def
--- /dev/null
+++ b/src/assets/icons/svg/password.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/pause.svg b/src/assets/icons/svg/pause.svg
new file mode 100644
index 0000000..0468653
--- /dev/null
+++ b/src/assets/icons/svg/pause.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/pdf.svg b/src/assets/icons/svg/pdf.svg
new file mode 100644
index 0000000..957aa0c
--- /dev/null
+++ b/src/assets/icons/svg/pdf.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/people.svg b/src/assets/icons/svg/people.svg
new file mode 100644
index 0000000..2bd54ae
--- /dev/null
+++ b/src/assets/icons/svg/people.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/peoples.svg b/src/assets/icons/svg/peoples.svg
new file mode 100644
index 0000000..aab852e
--- /dev/null
+++ b/src/assets/icons/svg/peoples.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/phone.svg b/src/assets/icons/svg/phone.svg
new file mode 100644
index 0000000..ab8e8c4
--- /dev/null
+++ b/src/assets/icons/svg/phone.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/post.svg b/src/assets/icons/svg/post.svg
new file mode 100644
index 0000000..2922c61
--- /dev/null
+++ b/src/assets/icons/svg/post.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/qq.svg b/src/assets/icons/svg/qq.svg
new file mode 100644
index 0000000..ee13d4e
--- /dev/null
+++ b/src/assets/icons/svg/qq.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/question.svg b/src/assets/icons/svg/question.svg
new file mode 100644
index 0000000..cf75bd4
--- /dev/null
+++ b/src/assets/icons/svg/question.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/radio.svg b/src/assets/icons/svg/radio.svg
new file mode 100644
index 0000000..0cde345
--- /dev/null
+++ b/src/assets/icons/svg/radio.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/rate.svg b/src/assets/icons/svg/rate.svg
new file mode 100644
index 0000000..aa3b14d
--- /dev/null
+++ b/src/assets/icons/svg/rate.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/redis-list.svg b/src/assets/icons/svg/redis-list.svg
new file mode 100644
index 0000000..98a15b2
--- /dev/null
+++ b/src/assets/icons/svg/redis-list.svg
@@ -0,0 +1,2 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/redis.svg b/src/assets/icons/svg/redis.svg
new file mode 100644
index 0000000..2f1d62d
--- /dev/null
+++ b/src/assets/icons/svg/redis.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/remove.svg b/src/assets/icons/svg/remove.svg
new file mode 100644
index 0000000..808f25c
--- /dev/null
+++ b/src/assets/icons/svg/remove.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/row.svg b/src/assets/icons/svg/row.svg
new file mode 100644
index 0000000..0780992
--- /dev/null
+++ b/src/assets/icons/svg/row.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/search.svg b/src/assets/icons/svg/search.svg
new file mode 100644
index 0000000..84233dd
--- /dev/null
+++ b/src/assets/icons/svg/search.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/select.svg b/src/assets/icons/svg/select.svg
new file mode 100644
index 0000000..d628382
--- /dev/null
+++ b/src/assets/icons/svg/select.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/server.svg b/src/assets/icons/svg/server.svg
new file mode 100644
index 0000000..eb287e3
--- /dev/null
+++ b/src/assets/icons/svg/server.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/shopping.svg b/src/assets/icons/svg/shopping.svg
new file mode 100644
index 0000000..87513e7
--- /dev/null
+++ b/src/assets/icons/svg/shopping.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/size.svg b/src/assets/icons/svg/size.svg
new file mode 100644
index 0000000..ddb25b8
--- /dev/null
+++ b/src/assets/icons/svg/size.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/skill.svg b/src/assets/icons/svg/skill.svg
new file mode 100644
index 0000000..a3b7312
--- /dev/null
+++ b/src/assets/icons/svg/skill.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/slider.svg b/src/assets/icons/svg/slider.svg
new file mode 100644
index 0000000..fbe4f39
--- /dev/null
+++ b/src/assets/icons/svg/slider.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/star.svg b/src/assets/icons/svg/star.svg
new file mode 100644
index 0000000..6cf86e6
--- /dev/null
+++ b/src/assets/icons/svg/star.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/start.svg b/src/assets/icons/svg/start.svg
new file mode 100644
index 0000000..6c9ef5b
--- /dev/null
+++ b/src/assets/icons/svg/start.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/stop.svg b/src/assets/icons/svg/stop.svg
new file mode 100644
index 0000000..47f964d
--- /dev/null
+++ b/src/assets/icons/svg/stop.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/sunny.svg b/src/assets/icons/svg/sunny.svg
new file mode 100644
index 0000000..cc628bf
--- /dev/null
+++ b/src/assets/icons/svg/sunny.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/swagger.svg b/src/assets/icons/svg/swagger.svg
new file mode 100644
index 0000000..05d4e7b
--- /dev/null
+++ b/src/assets/icons/svg/swagger.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/switch.svg b/src/assets/icons/svg/switch.svg
new file mode 100644
index 0000000..0ba61e3
--- /dev/null
+++ b/src/assets/icons/svg/switch.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/system.svg b/src/assets/icons/svg/system.svg
new file mode 100644
index 0000000..5992593
--- /dev/null
+++ b/src/assets/icons/svg/system.svg
@@ -0,0 +1,2 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/tab.svg b/src/assets/icons/svg/tab.svg
new file mode 100644
index 0000000..b4b48e4
--- /dev/null
+++ b/src/assets/icons/svg/tab.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/table.svg b/src/assets/icons/svg/table.svg
new file mode 100644
index 0000000..0e3dc9d
--- /dev/null
+++ b/src/assets/icons/svg/table.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/textarea.svg b/src/assets/icons/svg/textarea.svg
new file mode 100644
index 0000000..2709f29
--- /dev/null
+++ b/src/assets/icons/svg/textarea.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/theme.svg b/src/assets/icons/svg/theme.svg
new file mode 100644
index 0000000..5982a2f
--- /dev/null
+++ b/src/assets/icons/svg/theme.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/time-range.svg b/src/assets/icons/svg/time-range.svg
new file mode 100644
index 0000000..13c1202
--- /dev/null
+++ b/src/assets/icons/svg/time-range.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/time.svg b/src/assets/icons/svg/time.svg
new file mode 100644
index 0000000..b376e32
--- /dev/null
+++ b/src/assets/icons/svg/time.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/tool.svg b/src/assets/icons/svg/tool.svg
new file mode 100644
index 0000000..48e0e35
--- /dev/null
+++ b/src/assets/icons/svg/tool.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/tree-table.svg b/src/assets/icons/svg/tree-table.svg
new file mode 100644
index 0000000..8aafdb8
--- /dev/null
+++ b/src/assets/icons/svg/tree-table.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/tree.svg b/src/assets/icons/svg/tree.svg
new file mode 100644
index 0000000..dd4b7dd
--- /dev/null
+++ b/src/assets/icons/svg/tree.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/upload.svg b/src/assets/icons/svg/upload.svg
new file mode 100644
index 0000000..bae49c0
--- /dev/null
+++ b/src/assets/icons/svg/upload.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/user.svg b/src/assets/icons/svg/user.svg
new file mode 100644
index 0000000..0ba0716
--- /dev/null
+++ b/src/assets/icons/svg/user.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/validCode.svg b/src/assets/icons/svg/validCode.svg
new file mode 100644
index 0000000..cfb1021
--- /dev/null
+++ b/src/assets/icons/svg/validCode.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/wechat.svg b/src/assets/icons/svg/wechat.svg
new file mode 100644
index 0000000..c586e55
--- /dev/null
+++ b/src/assets/icons/svg/wechat.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/zip.svg b/src/assets/icons/svg/zip.svg
new file mode 100644
index 0000000..f806fc4
--- /dev/null
+++ b/src/assets/icons/svg/zip.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/svg/zoom-in.svg b/src/assets/icons/svg/zoom-in.svg
new file mode 100644
index 0000000..ffa20bd
--- /dev/null
+++ b/src/assets/icons/svg/zoom-in.svg
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/src/assets/icons/svg/zoom-out.svg b/src/assets/icons/svg/zoom-out.svg
new file mode 100644
index 0000000..451be83
--- /dev/null
+++ b/src/assets/icons/svg/zoom-out.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/src/assets/images/arm.png b/src/assets/images/arm.png
new file mode 100644
index 0000000..6b38d40
Binary files /dev/null and b/src/assets/images/arm.png differ
diff --git a/src/assets/images/dark.svg b/src/assets/images/dark.svg
new file mode 100644
index 0000000..f646bd7
--- /dev/null
+++ b/src/assets/images/dark.svg
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/assets/images/hand.png b/src/assets/images/hand.png
new file mode 100644
index 0000000..c3d1d36
Binary files /dev/null and b/src/assets/images/hand.png differ
diff --git a/src/assets/images/hand2.png b/src/assets/images/hand2.png
new file mode 100644
index 0000000..1f8cafc
Binary files /dev/null and b/src/assets/images/hand2.png differ
diff --git a/src/assets/images/index-background.jpg b/src/assets/images/index-background.jpg
new file mode 100644
index 0000000..a93ee06
Binary files /dev/null and b/src/assets/images/index-background.jpg differ
diff --git a/src/assets/images/light.svg b/src/assets/images/light.svg
new file mode 100644
index 0000000..ab7cc08
--- /dev/null
+++ b/src/assets/images/light.svg
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/assets/images/login-background-back.jpg b/src/assets/images/login-background-back.jpg
new file mode 100644
index 0000000..8a89eb8
Binary files /dev/null and b/src/assets/images/login-background-back.jpg differ
diff --git a/src/assets/images/login-background.jpg b/src/assets/images/login-background.jpg
new file mode 100644
index 0000000..242ca39
Binary files /dev/null and b/src/assets/images/login-background.jpg differ
diff --git a/src/assets/images/pay.png b/src/assets/images/pay.png
new file mode 100644
index 0000000..bb8b967
Binary files /dev/null and b/src/assets/images/pay.png differ
diff --git a/src/assets/images/profile.jpg b/src/assets/images/profile.jpg
new file mode 100644
index 0000000..b3a940b
Binary files /dev/null and b/src/assets/images/profile.jpg differ
diff --git a/src/assets/images/robot.png b/src/assets/images/robot.png
new file mode 100644
index 0000000..21d510f
Binary files /dev/null and b/src/assets/images/robot.png differ
diff --git a/src/assets/logo/logo.png b/src/assets/logo/logo.png
new file mode 100644
index 0000000..e263760
Binary files /dev/null and b/src/assets/logo/logo.png differ
diff --git a/src/assets/styles/btn.scss b/src/assets/styles/btn.scss
new file mode 100644
index 0000000..3590d8d
--- /dev/null
+++ b/src/assets/styles/btn.scss
@@ -0,0 +1,99 @@
+@import './variables.module.scss';
+
+@mixin colorBtn($color) {
+ background: $color;
+
+ &:hover {
+ color: $color;
+
+ &:before,
+ &:after {
+ background: $color;
+ }
+ }
+}
+
+.blue-btn {
+ @include colorBtn($blue)
+}
+
+.light-blue-btn {
+ @include colorBtn($light-blue)
+}
+
+.red-btn {
+ @include colorBtn($red)
+}
+
+.pink-btn {
+ @include colorBtn($pink)
+}
+
+.green-btn {
+ @include colorBtn($green)
+}
+
+.tiffany-btn {
+ @include colorBtn($tiffany)
+}
+
+.yellow-btn {
+ @include colorBtn($yellow)
+}
+
+.pan-btn {
+ font-size: 14px;
+ color: #fff;
+ padding: 14px 36px;
+ border-radius: 8px;
+ border: none;
+ outline: none;
+ transition: 600ms ease all;
+ position: relative;
+ display: inline-block;
+
+ &:hover {
+ background: #fff;
+
+ &:before,
+ &:after {
+ width: 100%;
+ transition: 600ms ease all;
+ }
+ }
+
+ &:before,
+ &:after {
+ content: '';
+ position: absolute;
+ top: 0;
+ right: 0;
+ height: 2px;
+ width: 0;
+ transition: 400ms ease all;
+ }
+
+ &::after {
+ right: inherit;
+ top: inherit;
+ left: 0;
+ bottom: 0;
+ }
+}
+
+.custom-button {
+ display: inline-block;
+ line-height: 1;
+ white-space: nowrap;
+ cursor: pointer;
+ background: #fff;
+ color: #fff;
+ -webkit-appearance: none;
+ text-align: center;
+ box-sizing: border-box;
+ outline: 0;
+ margin: 0;
+ padding: 10px 15px;
+ font-size: 14px;
+ border-radius: 4px;
+}
diff --git a/src/assets/styles/element-ui.scss b/src/assets/styles/element-ui.scss
new file mode 100644
index 0000000..0f175f2
--- /dev/null
+++ b/src/assets/styles/element-ui.scss
@@ -0,0 +1,96 @@
+// cover some element-ui styles
+
+.el-breadcrumb__inner,
+.el-breadcrumb__inner a {
+ font-weight: 400 !important;
+}
+
+.el-upload {
+ input[type="file"] {
+ display: none !important;
+ }
+}
+
+.el-upload__input {
+ display: none;
+}
+
+.cell {
+ .el-tag {
+ margin-right: 0px;
+ }
+}
+
+.small-padding {
+ .cell {
+ padding-left: 5px;
+ padding-right: 5px;
+ }
+}
+
+.fixed-width {
+ .el-button--mini {
+ padding: 7px 10px;
+ width: 60px;
+ }
+}
+
+.status-col {
+ .cell {
+ padding: 0 10px;
+ text-align: center;
+
+ .el-tag {
+ margin-right: 0px;
+ }
+ }
+}
+
+// to fixed https://github.com/ElemeFE/element/issues/2461
+.el-dialog {
+ transform: none;
+ left: 0;
+ position: relative;
+ margin: 0 auto;
+}
+
+// refine element ui upload
+.upload-container {
+ .el-upload {
+ width: 100%;
+
+ .el-upload-dragger {
+ width: 100%;
+ height: 200px;
+ }
+ }
+}
+
+// dropdown
+.el-dropdown-menu {
+ a {
+ display: block
+ }
+}
+
+// fix date-picker ui bug in filter-item
+.el-range-editor.el-input__inner {
+ display: inline-flex !important;
+}
+
+// to fix el-date-picker css style
+.el-range-separator {
+ box-sizing: content-box;
+}
+
+.el-menu--collapse
+ > div
+ > .el-submenu
+ > .el-submenu__title
+ .el-submenu__icon-arrow {
+ display: none;
+}
+
+.el-dropdown .el-dropdown-link{
+ color: var(--el-color-primary) !important;
+}
\ No newline at end of file
diff --git a/src/assets/styles/index.scss b/src/assets/styles/index.scss
new file mode 100644
index 0000000..dbc678a
--- /dev/null
+++ b/src/assets/styles/index.scss
@@ -0,0 +1,187 @@
+@import './variables.module.scss';
+@import './mixin.scss';
+@import './transition.scss';
+@import './element-ui.scss';
+@import './sidebar.scss';
+@import './btn.scss';
+@import './ruoyi.scss';
+
+body {
+ height: 100%;
+ margin: 0;
+ -moz-osx-font-smoothing: grayscale;
+ -webkit-font-smoothing: antialiased;
+ text-rendering: optimizeLegibility;
+ font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
+}
+
+label {
+ font-weight: 700;
+}
+
+html {
+ height: 100%;
+ box-sizing: border-box;
+}
+
+#app {
+ height: 100%;
+}
+
+*,
+*:before,
+*:after {
+ box-sizing: inherit;
+}
+
+.no-padding {
+ padding: 0px !important;
+}
+
+.padding-content {
+ padding: 4px 0;
+}
+
+a:focus,
+a:active {
+ outline: none;
+}
+
+a,
+a:focus,
+a:hover {
+ cursor: pointer;
+ color: inherit;
+ text-decoration: none;
+}
+
+div:focus {
+ outline: none;
+}
+
+.fr {
+ float: right;
+}
+
+.fl {
+ float: left;
+}
+
+.pr-5 {
+ padding-right: 5px;
+}
+
+.pl-5 {
+ padding-left: 5px;
+}
+
+.block {
+ display: block;
+}
+
+.pointer {
+ cursor: pointer;
+}
+
+.inlineBlock {
+ display: block;
+}
+
+.clearfix {
+ &:after {
+ visibility: hidden;
+ display: block;
+ font-size: 0;
+ content: " ";
+ clear: both;
+ height: 0;
+ }
+}
+
+aside {
+ background: #eef1f6;
+ padding: 8px 24px;
+ margin-bottom: 20px;
+ border-radius: 2px;
+ display: block;
+ line-height: 32px;
+ font-size: 16px;
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
+ color: #2c3e50;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+
+ a {
+ color: #337ab7;
+ cursor: pointer;
+
+ &:hover {
+ color: rgb(32, 160, 255);
+ }
+ }
+}
+
+//main-container全局样式
+.app-container {
+ width: 100%;
+ padding: 20px;
+ background-color: var(--el-bg-color);
+ border-radius: 4px;
+}
+
+.components-container {
+ margin: 30px 50px;
+ position: relative;
+}
+
+.pagination-container {
+ margin-top: 30px;
+}
+
+.text-center {
+ text-align: center
+}
+
+.sub-navbar {
+ height: 50px;
+ line-height: 50px;
+ position: relative;
+ width: 100%;
+ text-align: right;
+ padding-right: 20px;
+ transition: 600ms ease position;
+ background: linear-gradient(90deg, rgba(32, 182, 249, 1) 0%, rgba(32, 182, 249, 1) 0%, rgba(33, 120, 241, 1) 100%, rgba(33, 120, 241, 1) 100%);
+
+ .subtitle {
+ font-size: 20px;
+ color: #fff;
+ }
+
+ &.draft {
+ background: #d0d0d0;
+ }
+
+ &.deleted {
+ background: #d0d0d0;
+ }
+}
+
+.link-type,
+.link-type:focus {
+ color: #337ab7;
+ cursor: pointer;
+
+ &:hover {
+ color: rgb(32, 160, 255);
+ }
+}
+
+.filter-container {
+ padding-bottom: 10px;
+
+ .filter-item {
+ display: inline-block;
+ vertical-align: middle;
+ margin-bottom: 10px;
+ }
+}
diff --git a/src/assets/styles/mixin.scss b/src/assets/styles/mixin.scss
new file mode 100644
index 0000000..06fa061
--- /dev/null
+++ b/src/assets/styles/mixin.scss
@@ -0,0 +1,66 @@
+@mixin clearfix {
+ &:after {
+ content: "";
+ display: table;
+ clear: both;
+ }
+}
+
+@mixin scrollBar {
+ &::-webkit-scrollbar-track-piece {
+ background: #d3dce6;
+ }
+
+ &::-webkit-scrollbar {
+ width: 6px;
+ }
+
+ &::-webkit-scrollbar-thumb {
+ background: #99a9bf;
+ border-radius: 20px;
+ }
+}
+
+@mixin relative {
+ position: relative;
+ width: 100%;
+ height: 100%;
+}
+
+@mixin pct($pct) {
+ width: #{$pct};
+ position: relative;
+ margin: 0 auto;
+}
+
+@mixin triangle($width, $height, $color, $direction) {
+ $width: $width/2;
+ $color-border-style: $height solid $color;
+ $transparent-border-style: $width solid transparent;
+ height: 0;
+ width: 0;
+
+ @if $direction==up {
+ border-bottom: $color-border-style;
+ border-left: $transparent-border-style;
+ border-right: $transparent-border-style;
+ }
+
+ @else if $direction==right {
+ border-left: $color-border-style;
+ border-top: $transparent-border-style;
+ border-bottom: $transparent-border-style;
+ }
+
+ @else if $direction==down {
+ border-top: $color-border-style;
+ border-left: $transparent-border-style;
+ border-right: $transparent-border-style;
+ }
+
+ @else if $direction==left {
+ border-right: $color-border-style;
+ border-top: $transparent-border-style;
+ border-bottom: $transparent-border-style;
+ }
+}
diff --git a/src/assets/styles/ruoyi.scss b/src/assets/styles/ruoyi.scss
new file mode 100644
index 0000000..252be35
--- /dev/null
+++ b/src/assets/styles/ruoyi.scss
@@ -0,0 +1,301 @@
+/**
+ * 通用css样式布局处理
+ * Copyright (c) 2019 ruoyi
+ */
+
+ /** 基础通用 **/
+.pt5 {
+ padding-top: 5px;
+}
+.pr5 {
+ padding-right: 5px;
+}
+.pb5 {
+ padding-bottom: 5px;
+}
+.mt5 {
+ margin-top: 5px;
+}
+.mr5 {
+ margin-right: 5px;
+}
+.mb5 {
+ margin-bottom: 5px;
+}
+.mb8 {
+ margin-bottom: 8px;
+}
+.ml5 {
+ margin-left: 5px;
+}
+.mt10 {
+ margin-top: 10px;
+}
+.mr10 {
+ margin-right: 10px;
+}
+.mb10 {
+ margin-bottom: 10px;
+}
+.ml10 {
+ margin-left: 10px;
+}
+.mt20 {
+ margin-top: 20px;
+}
+.mr20 {
+ margin-right: 20px;
+}
+.mb20 {
+ margin-bottom: 20px;
+}
+.ml20 {
+ margin-left: 20px;
+}
+
+.h1, .h2, .h3, .h4, .h5, .h6, h1, h2, h3, h4, h5, h6 {
+ font-family: inherit;
+ font-weight: 500;
+ line-height: 1.1;
+ color: inherit;
+}
+
+.el-form .el-form-item__label {
+ font-weight: 700;
+}
+.el-dialog:not(.is-fullscreen) {
+ margin-top: 6vh !important;
+}
+
+.el-dialog.scrollbar .el-dialog__body {
+ overflow: auto;
+ overflow-x: hidden;
+ max-height: 70vh;
+ padding: 10px 20px 0;
+}
+
+.el-table {
+ .el-table__header-wrapper, .el-table__fixed-header-wrapper {
+ th {
+ word-break: break-word;
+ background-color: #f8f8f9 !important;
+ color: #515a6e;
+ height: 40px !important;
+ font-size: 13px;
+ }
+ }
+ .el-table__body-wrapper {
+ .el-button [class*="el-icon-"] + span {
+ margin-left: 1px;
+ }
+ }
+}
+
+/** 表单布局 **/
+.form-header {
+ font-size:15px;
+ color:#6379bb;
+ border-bottom:1px solid #ddd;
+ margin:8px 10px 25px 10px;
+ padding-bottom:5px
+}
+
+/** 表格布局 **/
+.pagination-container {
+ position: relative;
+ height: 25px;
+ margin-bottom: 10px;
+ margin-top: 15px;
+ padding: 10px 20px !important;
+ background-color: transparent !important;
+}
+
+/* 分页器定位 */
+.pagination-container .el-pagination {
+ position: absolute;
+ right: 0;
+ top: 0;
+}
+
+/* 弹窗中的分页器 */
+.el-dialog .pagination-container {
+ position: static !important;
+ margin: 10px 0 0 0;
+ padding: 0 !important;
+
+ .el-pagination {
+ position: static;
+ }
+}
+
+/* 移动端适配 */
+@media (max-width: 768px) {
+ .pagination-container {
+ .el-pagination {
+ > .el-pagination__jump {
+ display: none !important;
+ }
+ > .el-pagination__sizes {
+ display: none !important;
+ }
+ }
+ }
+}
+
+/* tree border */
+.tree-border {
+ margin-top: 5px;
+ border: 1px solid var(--el-border-color-light, #e5e6e7);
+ background: var(--el-bg-color, #FFFFFF) none;
+ border-radius:4px;
+ width: 100%;
+}
+
+.el-table .fixed-width .el-button--small {
+ padding-left: 0;
+ padding-right: 0;
+ width: inherit;
+}
+
+/** 表格更多操作下拉样式 */
+.el-table .el-dropdown-link {
+ cursor: pointer;
+ color: #409EFF;
+ margin-left: 10px;
+}
+
+.el-table .el-dropdown, .el-icon-arrow-down {
+ font-size: 12px;
+}
+
+.el-tree-node__content > .el-checkbox {
+ margin-right: 8px;
+}
+
+.list-group-striped > .list-group-item {
+ border-left: 0;
+ border-right: 0;
+ border-radius: 0;
+ padding-left: 0;
+ padding-right: 0;
+}
+
+.list-group {
+ padding-left: 0px;
+ list-style: none;
+}
+
+.list-group-item {
+ border-bottom: 1px solid #e7eaec;
+ border-top: 1px solid #e7eaec;
+ margin-bottom: -1px;
+ padding: 11px 0px;
+ font-size: 13px;
+}
+
+.pull-right {
+ float: right !important;
+}
+
+.el-card__header {
+ padding: 14px 15px 7px !important;
+ min-height: 40px;
+}
+
+.el-card__body {
+ padding: 15px 20px 20px 20px !important;
+}
+
+.card-box {
+ padding-right: 15px;
+ padding-left: 15px;
+ margin-bottom: 10px;
+}
+
+/* button color */
+.el-button--cyan.is-active,
+.el-button--cyan:active {
+ background: #20B2AA;
+ border-color: #20B2AA;
+ color: #FFFFFF;
+}
+
+.el-button--cyan:focus,
+.el-button--cyan:hover {
+ background: #48D1CC;
+ border-color: #48D1CC;
+ color: #FFFFFF;
+}
+
+.el-button--cyan {
+ background-color: #20B2AA;
+ border-color: #20B2AA;
+ color: #FFFFFF;
+}
+
+/* text color */
+.text-navy {
+ color: #1ab394;
+}
+
+.text-primary {
+ color: inherit;
+}
+
+.text-success {
+ color: #1c84c6;
+}
+
+.text-info {
+ color: #23c6c8;
+}
+
+.text-warning {
+ color: #f8ac59;
+}
+
+.text-danger {
+ color: #ed5565;
+}
+
+.text-muted {
+ color: #888888;
+}
+
+/* image */
+.img-circle {
+ border-radius: 50%;
+}
+
+.img-lg {
+ width: 120px;
+ height: 120px;
+}
+
+.avatar-upload-preview {
+ position: absolute;
+ top: 50%;
+ transform: translate(50%, -50%);
+ width: 200px;
+ height: 200px;
+ border-radius: 50%;
+ box-shadow: 0 0 4px #ccc;
+ overflow: hidden;
+}
+
+/* 拖拽列样式 */
+.sortable-ghost{
+ opacity: .8;
+ color: #fff!important;
+ background: #42b983!important;
+}
+
+/* 表格右侧工具栏样式 */
+.top-right-btn {
+ margin-left: auto;
+}
+
+/* 分割面板样式 */
+.splitpanes.default-theme .splitpanes__pane {
+ background-color: var(--splitpanes-default-bg) !important;
+}
diff --git a/src/assets/styles/sidebar.scss b/src/assets/styles/sidebar.scss
new file mode 100644
index 0000000..7758f8b
--- /dev/null
+++ b/src/assets/styles/sidebar.scss
@@ -0,0 +1,236 @@
+#app {
+
+ .main-container {
+ min-height: 100%;
+ transition: margin-left .28s;
+ margin-left: $base-sidebar-width;
+ position: relative;
+ }
+
+ .sidebarHide {
+ margin-left: 0!important;
+ }
+
+ .sidebar-container {
+ transition: width 0.28s;
+ width: $base-sidebar-width !important;
+ height: 100%;
+ position: fixed;
+ font-size: 0px;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ z-index: 1001;
+ overflow: hidden;
+ -webkit-box-shadow: 2px 0 6px rgba(0,21,41,.35);
+ box-shadow: 2px 0 6px rgba(0,21,41,.35);
+
+ // reset element-ui css
+ .horizontal-collapse-transition {
+ transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out;
+ }
+
+ .scrollbar-wrapper {
+ overflow-x: hidden !important;
+ }
+
+ .el-scrollbar__bar.is-vertical {
+ right: 0px;
+ }
+
+ .el-scrollbar {
+ height: 100%;
+ }
+
+ &.has-logo {
+ .el-scrollbar {
+ height: calc(100% - 50px);
+ }
+ }
+
+ .is-horizontal {
+ display: none;
+ }
+
+ a {
+ display: inline-block;
+ width: 100%;
+ overflow: hidden;
+ }
+
+ .svg-icon {
+ margin-right: 16px;
+ }
+
+ .el-menu {
+ border: none;
+ height: 100%;
+ width: 100% !important;
+ }
+
+ .el-menu-item, .menu-title {
+ overflow: hidden !important;
+ text-overflow: ellipsis !important;
+ white-space: nowrap !important;
+ }
+
+ .el-menu-item .el-menu-tooltip__trigger {
+ display: inline-block !important;
+ }
+
+ // menu hover
+ .sub-menu-title-noDropdown,
+ .el-sub-menu__title {
+ &:hover {
+ background-color: rgba(0, 0, 0, 0.06) !important;
+ }
+ }
+
+ & .theme-dark .is-active > .el-sub-menu__title {
+ color: $base-menu-color-active !important;
+ }
+
+ & .nest-menu .el-sub-menu>.el-sub-menu__title,
+ & .el-sub-menu .el-menu-item {
+ min-width: $base-sidebar-width !important;
+
+ &:hover {
+ background-color: rgba(0, 0, 0, 0.06) !important;
+ }
+ }
+
+ & .theme-dark .nest-menu .el-sub-menu>.el-sub-menu__title,
+ & .theme-dark .el-sub-menu .el-menu-item {
+ background-color: $base-sub-menu-background;
+
+ &:hover {
+ background-color: $base-sub-menu-hover !important;
+ }
+ }
+ }
+
+ .hideSidebar {
+ .sidebar-container {
+ width: 54px !important;
+ }
+
+ .main-container {
+ margin-left: 54px;
+ }
+
+ .sub-menu-title-noDropdown {
+ padding: 0 !important;
+ position: relative;
+
+ .el-tooltip {
+ padding: 0 !important;
+
+ .svg-icon {
+ margin-left: 20px;
+ }
+ }
+ }
+
+ .el-sub-menu {
+ overflow: hidden;
+
+ &>.el-sub-menu__title {
+ padding: 0 !important;
+
+ .svg-icon {
+ margin-left: 20px;
+ }
+
+ }
+ }
+
+ .el-menu--collapse {
+ .el-sub-menu {
+ &>.el-sub-menu__title {
+ &>span {
+ height: 0;
+ width: 0;
+ overflow: hidden;
+ visibility: hidden;
+ display: inline-block;
+ }
+ &>i {
+ height: 0;
+ width: 0;
+ overflow: hidden;
+ visibility: hidden;
+ display: inline-block;
+ }
+ }
+ }
+ }
+ }
+
+ .el-menu--collapse .el-menu .el-sub-menu {
+ min-width: $base-sidebar-width !important;
+ }
+
+ // mobile responsive
+ .mobile {
+ .main-container {
+ margin-left: 0px;
+ }
+
+ .sidebar-container {
+ transition: transform .28s;
+ width: $base-sidebar-width !important;
+ }
+
+ &.hideSidebar {
+ .sidebar-container {
+ pointer-events: none;
+ transition-duration: 0.3s;
+ transform: translate3d(-$base-sidebar-width, 0, 0);
+ }
+ }
+ }
+
+ .withoutAnimation {
+
+ .main-container,
+ .sidebar-container {
+ transition: none;
+ }
+ }
+}
+
+// when menu collapsed
+.el-menu--vertical {
+ &>.el-menu {
+ .svg-icon {
+ margin-right: 16px;
+ }
+ }
+
+ .nest-menu .el-sub-menu>.el-sub-menu__title,
+ .el-menu-item {
+ &:hover {
+ // you can use $sub-menuHover
+ background-color: rgba(0, 0, 0, 0.06) !important;
+ }
+ }
+
+ // the scroll bar appears when the sub-menu is too long
+ >.el-menu--popup {
+ max-height: 100vh;
+ overflow-y: auto;
+
+ &::-webkit-scrollbar-track-piece {
+ background: #d3dce6;
+ }
+
+ &::-webkit-scrollbar {
+ width: 6px;
+ }
+
+ &::-webkit-scrollbar-thumb {
+ background: #99a9bf;
+ border-radius: 20px;
+ }
+ }
+}
diff --git a/src/assets/styles/transition.scss b/src/assets/styles/transition.scss
new file mode 100644
index 0000000..073f8c6
--- /dev/null
+++ b/src/assets/styles/transition.scss
@@ -0,0 +1,49 @@
+// global transition css
+
+/* fade */
+.fade-enter-active,
+.fade-leave-active {
+ transition: opacity 0.28s;
+}
+
+.fade-enter,
+.fade-leave-active {
+ opacity: 0;
+}
+
+/* fade-transform */
+.fade-transform--move,
+.fade-transform-leave-active,
+.fade-transform-enter-active {
+ transition: all .5s;
+}
+
+.fade-transform-enter {
+ opacity: 0;
+ transform: translateX(-30px);
+}
+
+.fade-transform-leave-to {
+ opacity: 0;
+ transform: translateX(30px);
+}
+
+/* breadcrumb transition */
+.breadcrumb-enter-active,
+.breadcrumb-leave-active {
+ transition: all .5s;
+}
+
+.breadcrumb-enter,
+.breadcrumb-leave-active {
+ opacity: 0;
+ transform: translateX(20px);
+}
+
+.breadcrumb-move {
+ transition: all .5s;
+}
+
+.breadcrumb-leave-active {
+ position: absolute;
+}
diff --git a/src/assets/styles/variables.module.scss b/src/assets/styles/variables.module.scss
new file mode 100644
index 0000000..98c265a
--- /dev/null
+++ b/src/assets/styles/variables.module.scss
@@ -0,0 +1,286 @@
+// base color
+$blue: #324157;
+$light-blue: #333c46;
+$red: #c03639;
+$pink: #e65d6e;
+$green: #30b08f;
+$tiffany: #4ab7bd;
+$yellow: #fec171;
+$panGreen: #30b08f;
+
+// 默认主题变量
+$menuText: #bfcbd9;
+$menuActiveText: #409eff;
+$menuBg: #304156;
+$menuHover: #263445;
+
+// 浅色主题theme-light
+$menuLightBg: #ffffff;
+$menuLightHover: #f0f1f5;
+$menuLightText: #303133;
+$menuLightActiveText: #409eff;
+
+// 基础变量
+$base-sidebar-width: 200px;
+$sideBarWidth: 200px;
+
+// 菜单暗色变量
+$base-menu-color: #bfcbd9;
+$base-menu-color-active: #f4f4f5;
+$base-menu-background: #304156;
+$base-sub-menu-background: #1f2d3d;
+$base-sub-menu-hover: #001528;
+
+// 组件变量
+$--color-primary: #409eff;
+$--color-success: #67c23a;
+$--color-warning: #e6a23c;
+$--color-danger: #f56c6c;
+$--color-info: #909399;
+
+:export {
+ menuText: $menuText;
+ menuActiveText: $menuActiveText;
+ menuBg: $menuBg;
+ menuHover: $menuHover;
+ menuLightBg: $menuLightBg;
+ menuLightHover: $menuLightHover;
+ menuLightText: $menuLightText;
+ menuLightActiveText: $menuLightActiveText;
+ sideBarWidth: $sideBarWidth;
+ // 导出基础颜色
+ blue: $blue;
+ lightBlue: $light-blue;
+ red: $red;
+ pink: $pink;
+ green: $green;
+ tiffany: $tiffany;
+ yellow: $yellow;
+ panGreen: $panGreen;
+ // 导出组件颜色
+ colorPrimary: $--color-primary;
+ colorSuccess: $--color-success;
+ colorWarning: $--color-warning;
+ colorDanger: $--color-danger;
+ colorInfo: $--color-info;
+}
+
+// CSS变量定义
+:root {
+ /* 亮色模式变量 */
+ --sidebar-bg: #{$menuBg};
+ --sidebar-text: #{$menuText};
+ --menu-hover: #{$menuHover};
+
+ --navbar-bg: #ffffff;
+ --navbar-text: #303133;
+
+ /* splitpanes default-theme 变量 */
+ --splitpanes-default-bg: #ffffff;
+
+ --main-fill-bg: rgb(242, 243, 245);
+}
+
+// 暗黑模式变量
+html.dark {
+ --main-fill-bg: rgba(255, 255, 255, 0.08);
+ /* 默认通用 */
+ --el-bg-color: #141414;
+ --el-bg-color-overlay: #1d1e1f;
+ --el-text-color-primary: #ffffff;
+ --el-text-color-regular: #d0d0d0;
+ --el-border-color: #434343;
+ --el-border-color-light: #434343;
+
+ /* 侧边栏 */
+ --sidebar-bg: #141414;
+ --sidebar-text: #ffffff;
+ --menu-hover: #2d2d2d;
+ --menu-active-text: #{$menuActiveText};
+
+ /* 顶部导航栏 */
+ --navbar-bg: #141414;
+ --navbar-text: #ffffff;
+ --navbar-hover: #141414;
+
+ /* 标签栏 */
+ --tags-bg: #141414;
+ --tags-item-bg: #1d1e1f;
+ --tags-item-border: #303030;
+ --tags-item-text: #d0d0d0;
+ --tags-item-hover: #2d2d2d;
+ --tags-close-hover: #64666a;
+
+ /* splitpanes 组件暗黑模式变量 */
+ --splitpanes-bg: #141414;
+ --splitpanes-border: #303030;
+ --splitpanes-splitter-bg: #1d1e1f;
+ --splitpanes-splitter-hover-bg: #2d2d2d;
+
+ /* blockquote 暗黑模式变量 */
+ --blockquote-bg: #1d1e1f;
+ --blockquote-border: #303030;
+ --blockquote-text: #d0d0d0;
+
+ /* Cron 时间表达式 模式变量 */
+ --cron-border: #303030;
+
+ /* splitpanes default-theme 暗黑模式变量 */
+ --splitpanes-default-bg: #141414;
+
+ /* 侧边栏菜单覆盖 */
+ .sidebar-container {
+ .el-menu-item,
+ .menu-title {
+ color: var(--el-text-color-regular);
+ }
+ & .theme-dark .nest-menu .el-sub-menu > .el-sub-menu__title,
+ & .theme-dark .el-sub-menu .el-menu-item {
+ background-color: var(--el-bg-color) !important;
+ }
+ }
+
+ /* 顶部栏栏菜单覆盖 */
+ .el-menu--horizontal {
+ .el-menu-item {
+ &:not(.is-disabled) {
+ &:hover,
+ &:focus {
+ background-color: var(--navbar-hover) !important;
+ }
+ }
+ }
+ }
+
+ /* 分割窗格覆盖 */
+ .splitpanes {
+ background-color: var(--splitpanes-bg);
+
+ .splitpanes__pane {
+ background-color: var(--splitpanes-bg);
+ border-color: var(--splitpanes-border);
+ }
+
+ .splitpanes__splitter {
+ background-color: var(--splitpanes-splitter-bg);
+ border-color: var(--splitpanes-border);
+
+ &:hover {
+ background-color: var(--splitpanes-splitter-hover-bg);
+ }
+
+ &:before,
+ &:after {
+ background-color: var(--splitpanes-border);
+ }
+ }
+ }
+
+ /* 表格样式覆盖 */
+ .el-table {
+ --el-table-header-bg-color: var(--el-bg-color-overlay) !important;
+ --el-table-header-text-color: var(--el-text-color-regular) !important;
+ --el-table-border-color: var(--el-border-color-light) !important;
+ --el-table-row-hover-bg-color: var(--el-bg-color-overlay) !important;
+
+ .el-table__header-wrapper,
+ .el-table__fixed-header-wrapper {
+ th {
+ background-color: var(--el-bg-color-overlay, #f8f8f9) !important;
+ color: var(--el-text-color-regular, #515a6e);
+ }
+ }
+ }
+
+ /* 树组件高亮样式覆盖 */
+ .el-tree {
+ .el-tree-node.is-current > .el-tree-node__content {
+ background-color: var(--el-bg-color-overlay) !important;
+ color: var(--el-color-primary);
+ }
+
+ .el-tree-node__content:hover {
+ background-color: var(--el-bg-color-overlay);
+ }
+ }
+
+ /* 下拉菜单样式覆盖 */
+ .el-dropdown-menu__item:not(.is-disabled):focus,
+ .el-dropdown-menu__item:not(.is-disabled):hover {
+ background-color: var(--navbar-hover) !important;
+ }
+
+ /* blockquote样式覆盖 */
+ blockquote {
+ background-color: var(--blockquote-bg) !important;
+ border-left-color: var(--blockquote-border) !important;
+ color: var(--blockquote-text) !important;
+ }
+
+ /* 时间表达式标题样式覆盖 */
+ .popup-result .title {
+ background: var(--cron-border);
+ }
+
+ .el-aside {
+ background: #4d4d4d;
+ }
+
+ .el-header {
+ border-bottom: 1px solid #282828 !important;
+ }
+
+ .el-collapse {
+ --el-collapse-header-bg-color: #000;
+
+ .collapseItem {
+ .el-collapse-item__wrap {
+ background-color: #3c3c3c;
+
+ .node {
+ background-color: #000;
+
+ .title_container {
+ color: #fff;
+ }
+
+ .node_desc {
+ color: #f7f7f7;
+ }
+ }
+ }
+ }
+ }
+
+ .node__container {
+ background: #000;
+ // border: 2px solid #000;
+
+ .subTitle {
+ color: #fbfbfb !important;
+ }
+ }
+
+ .node__box {
+ background: #000;
+ border: 2px solid #000;
+
+ .subTitle {
+ color: #fbfbfb !important;
+ }
+
+ .input__container {
+ background: #2d2d2d;
+
+ .title {
+ .text {
+ color: #fbfbfb !important;
+ }
+ }
+ }
+ }
+
+ .lf-canvas-overlay {
+ background: #3e3e3e !important;
+ }
+}
diff --git a/src/components/Breadcrumb/index.vue b/src/components/Breadcrumb/index.vue
new file mode 100644
index 0000000..20cb850
--- /dev/null
+++ b/src/components/Breadcrumb/index.vue
@@ -0,0 +1,98 @@
+
+
+
+
+ {{ item.meta.title }}
+ {{ item.meta.title }}
+
+
+
+
+
+
+
+
diff --git a/src/components/Crontab/day.vue b/src/components/Crontab/day.vue
new file mode 100644
index 0000000..8f67324
--- /dev/null
+++ b/src/components/Crontab/day.vue
@@ -0,0 +1,174 @@
+
+
+
+
+ 日,允许的通配符[, - * ? / L W]
+
+
+
+
+
+ 不指定
+
+
+
+
+
+ 周期从
+ -
+ 日
+
+
+
+
+
+ 从
+ 号开始,每
+ 日执行一次
+
+
+
+
+
+ 每月
+ 号最近的那个工作日
+
+
+
+
+
+ 本月最后一天
+
+
+
+
+
+ 指定
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/Crontab/hour.vue b/src/components/Crontab/hour.vue
new file mode 100644
index 0000000..c6f9a13
--- /dev/null
+++ b/src/components/Crontab/hour.vue
@@ -0,0 +1,133 @@
+
+
+
+
+ 小时,允许的通配符[, - * /]
+
+
+
+
+
+ 周期从
+ -
+ 时
+
+
+
+
+
+ 从
+ 时开始,每
+ 小时执行一次
+
+
+
+
+
+ 指定
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/Crontab/index.vue b/src/components/Crontab/index.vue
new file mode 100644
index 0000000..ef0fe97
--- /dev/null
+++ b/src/components/Crontab/index.vue
@@ -0,0 +1,309 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 确定
+ 重置
+ 取消
+
+
+
+
+
+
+
+
diff --git a/src/components/Crontab/min.vue b/src/components/Crontab/min.vue
new file mode 100644
index 0000000..21de620
--- /dev/null
+++ b/src/components/Crontab/min.vue
@@ -0,0 +1,126 @@
+
+
+
+
+ 分钟,允许的通配符[, - * /]
+
+
+
+
+
+ 周期从
+ -
+ 分钟
+
+
+
+
+
+ 从
+ 分钟开始, 每
+ 分钟执行一次
+
+
+
+
+
+ 指定
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/Crontab/month.vue b/src/components/Crontab/month.vue
new file mode 100644
index 0000000..45e9d0f
--- /dev/null
+++ b/src/components/Crontab/month.vue
@@ -0,0 +1,141 @@
+
+
+
+
+ 月,允许的通配符[, - * /]
+
+
+
+
+
+ 周期从
+ -
+ 月
+
+
+
+
+
+ 从
+ 月开始,每
+ 月月执行一次
+
+
+
+
+
+ 指定
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/Crontab/result.vue b/src/components/Crontab/result.vue
new file mode 100644
index 0000000..0c18922
--- /dev/null
+++ b/src/components/Crontab/result.vue
@@ -0,0 +1,540 @@
+
+
+
+
+
diff --git a/src/components/Crontab/second.vue b/src/components/Crontab/second.vue
new file mode 100644
index 0000000..1a47424
--- /dev/null
+++ b/src/components/Crontab/second.vue
@@ -0,0 +1,128 @@
+
+
+
+
+ 秒,允许的通配符[, - * /]
+
+
+
+
+
+ 周期从
+ -
+ 秒
+
+
+
+
+
+ 从
+ 秒开始,每
+ 秒执行一次
+
+
+
+
+
+ 指定
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/Crontab/week.vue b/src/components/Crontab/week.vue
new file mode 100644
index 0000000..669d781
--- /dev/null
+++ b/src/components/Crontab/week.vue
@@ -0,0 +1,197 @@
+
+
+
+
+ 周,允许的通配符[, - * ? / L #]
+
+
+
+
+
+ 不指定
+
+
+
+
+
+ 周期从
+
+ {{item.value}}
+
+ -
+
+ {{item.value}}
+
+
+
+
+
+
+ 第
+ 周的
+
+
+
+
+
+
+
+
+ 本月最后一个
+
+
+
+
+
+
+
+
+ 指定
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/Crontab/year.vue b/src/components/Crontab/year.vue
new file mode 100644
index 0000000..3a47aec
--- /dev/null
+++ b/src/components/Crontab/year.vue
@@ -0,0 +1,149 @@
+
+
+
+
+ 不填,允许的通配符[, - * /]
+
+
+
+
+
+ 每年
+
+
+
+
+
+ 周期从
+ -
+
+
+
+
+
+
+ 从
+ 年开始,每
+ 年执行一次
+
+
+
+
+
+
+ 指定
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/DictTag/index.vue b/src/components/DictTag/index.vue
new file mode 100644
index 0000000..19e24e4
--- /dev/null
+++ b/src/components/DictTag/index.vue
@@ -0,0 +1,82 @@
+
+
+
+
+ {{ item.label + " " }}
+ {{ item.label + " " }}
+
+
+
+ {{ unmatchArray | handleArray }}
+
+
+
+
+
+
+
diff --git a/src/components/EditTable/index.vue b/src/components/EditTable/index.vue
new file mode 100644
index 0000000..e111423
--- /dev/null
+++ b/src/components/EditTable/index.vue
@@ -0,0 +1,299 @@
+
+
+
+
+ 执行顺序
+ 检测项
+ 参数中文名
+ 参数英文名
+ 操作类型
+ 参数来源
+ 参数值
+ 是否必填
+ 备注
+ 操作
+
+
+
+
+
+
+ ⋮⋮
+
+
+
+
+
+
+
+ {{ element.paramEnName }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ element.paramCnName }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 是
+ 否
+
+
+
+
+
+
+
+
+
+
+
+
+ 保存
+
+ 取消
+
+
+ 编辑
+
+
+
+
+
+
+
+
+ 添加参数
+ 查看
+
+
+
+
+
+
diff --git a/src/components/Editor/index.vue b/src/components/Editor/index.vue
new file mode 100644
index 0000000..1bd01ed
--- /dev/null
+++ b/src/components/Editor/index.vue
@@ -0,0 +1,251 @@
+
+
+
+
+
+
+
+ $emit('update:modelValue', content)"
+ :options="options"
+ :style="styles"
+ />
+
+
+
+
+
+
diff --git a/src/components/FileUpload/index.vue b/src/components/FileUpload/index.vue
new file mode 100644
index 0000000..8aaef5c
--- /dev/null
+++ b/src/components/FileUpload/index.vue
@@ -0,0 +1,213 @@
+
+
+
+
+ 选取文件
+
+
+
+ 请上传
+ 大小不超过 {{ fileSize }}MB
+ 格式为 {{ fileType.join("/") }}
+ 的文件
+
+
+
+
+
+ {{ getFileName(file.name) }}
+
+
+ 删除
+
+
+
+
+
+
+
+
+
diff --git a/src/components/Hamburger/index.vue b/src/components/Hamburger/index.vue
new file mode 100644
index 0000000..02d58e9
--- /dev/null
+++ b/src/components/Hamburger/index.vue
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
diff --git a/src/components/HeaderSearch/index.vue b/src/components/HeaderSearch/index.vue
new file mode 100644
index 0000000..4728f6a
--- /dev/null
+++ b/src/components/HeaderSearch/index.vue
@@ -0,0 +1,187 @@
+
+
+
+
+
+
+
diff --git a/src/components/IconSelect/index.vue b/src/components/IconSelect/index.vue
new file mode 100644
index 0000000..f2c2aa3
--- /dev/null
+++ b/src/components/IconSelect/index.vue
@@ -0,0 +1,111 @@
+
+
+
+
+
+
+
diff --git a/src/components/IconSelect/requireIcons.js b/src/components/IconSelect/requireIcons.js
new file mode 100644
index 0000000..ac22fd7
--- /dev/null
+++ b/src/components/IconSelect/requireIcons.js
@@ -0,0 +1,8 @@
+let icons = []
+const modules = import.meta.glob('./../../assets/icons/svg/*.svg');
+for (const path in modules) {
+ const p = path.split('assets/icons/svg/')[1].split('.svg')[0];
+ icons.push(p);
+}
+
+export default icons
\ No newline at end of file
diff --git a/src/components/ImagePreview/index.vue b/src/components/ImagePreview/index.vue
new file mode 100644
index 0000000..7e3d2b6
--- /dev/null
+++ b/src/components/ImagePreview/index.vue
@@ -0,0 +1,92 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/ImageUpload/index.vue b/src/components/ImageUpload/index.vue
new file mode 100644
index 0000000..3cf43e3
--- /dev/null
+++ b/src/components/ImageUpload/index.vue
@@ -0,0 +1,216 @@
+
+
+
+
+
+
+
+ 请上传
+
+ 大小不超过 {{ fileSize }}MB
+
+
+ 格式为 {{ fileType.join("/") }}
+
+ 的文件
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/Pagination/index.vue b/src/components/Pagination/index.vue
new file mode 100644
index 0000000..3b29fe4
--- /dev/null
+++ b/src/components/Pagination/index.vue
@@ -0,0 +1,105 @@
+
+
+
+
+
+
+
diff --git a/src/components/ParentView/index.vue b/src/components/ParentView/index.vue
new file mode 100644
index 0000000..7bf6148
--- /dev/null
+++ b/src/components/ParentView/index.vue
@@ -0,0 +1,3 @@
+
+
+
diff --git a/src/components/RightToolbar/index.vue b/src/components/RightToolbar/index.vue
new file mode 100644
index 0000000..99387f6
--- /dev/null
+++ b/src/components/RightToolbar/index.vue
@@ -0,0 +1,134 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/RuoYi/Doc/index.vue b/src/components/RuoYi/Doc/index.vue
new file mode 100644
index 0000000..cd0389d
--- /dev/null
+++ b/src/components/RuoYi/Doc/index.vue
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
diff --git a/src/components/RuoYi/Git/index.vue b/src/components/RuoYi/Git/index.vue
new file mode 100644
index 0000000..b4459f3
--- /dev/null
+++ b/src/components/RuoYi/Git/index.vue
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
diff --git a/src/components/Screenfull/index.vue b/src/components/Screenfull/index.vue
new file mode 100644
index 0000000..9c9f980
--- /dev/null
+++ b/src/components/Screenfull/index.vue
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/components/SizeSelect/index.vue b/src/components/SizeSelect/index.vue
new file mode 100644
index 0000000..a8e09ce
--- /dev/null
+++ b/src/components/SizeSelect/index.vue
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+ {{ item.label }}
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/SvgIcon/index.vue b/src/components/SvgIcon/index.vue
new file mode 100644
index 0000000..ec10049
--- /dev/null
+++ b/src/components/SvgIcon/index.vue
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
diff --git a/src/components/SvgIcon/svgicon.js b/src/components/SvgIcon/svgicon.js
new file mode 100644
index 0000000..4431719
--- /dev/null
+++ b/src/components/SvgIcon/svgicon.js
@@ -0,0 +1,10 @@
+import * as components from '@element-plus/icons-vue'
+
+export default {
+ install: (app) => {
+ for (const key in components) {
+ const componentConfig = components[key];
+ app.component(componentConfig.name, componentConfig);
+ }
+ },
+};
diff --git a/src/components/TableSearch/index.vue b/src/components/TableSearch/index.vue
new file mode 100644
index 0000000..6bb4556
--- /dev/null
+++ b/src/components/TableSearch/index.vue
@@ -0,0 +1,69 @@
+
+
+
+
+
+
+
+
+
+ 搜索
+ 重置
+
+
+
+
+
+ {{ formData.search ? "收起" : "展开" }}
+
+
+
+
+
+
+
+
diff --git a/src/components/TableTags/index.vue b/src/components/TableTags/index.vue
new file mode 100644
index 0000000..5d161f7
--- /dev/null
+++ b/src/components/TableTags/index.vue
@@ -0,0 +1,24 @@
+
+
+
+
+ {{ item.label }}
+
+
+
+
diff --git a/src/components/TopNav/index.vue b/src/components/TopNav/index.vue
new file mode 100644
index 0000000..cf67821
--- /dev/null
+++ b/src/components/TopNav/index.vue
@@ -0,0 +1,216 @@
+
+
+
+
+
+ {{ item.meta.title }}
+
+
+
+
+
+ 更多菜单
+
+
+
+ {{ item.meta.title }}
+
+
+
+
+
+
+
+
+
diff --git a/src/components/Video/index.vue b/src/components/Video/index.vue
new file mode 100644
index 0000000..5ed53a4
--- /dev/null
+++ b/src/components/Video/index.vue
@@ -0,0 +1,27 @@
+
+
+
+
\ No newline at end of file
diff --git a/src/components/iFrame/index.vue b/src/components/iFrame/index.vue
new file mode 100644
index 0000000..091b1a2
--- /dev/null
+++ b/src/components/iFrame/index.vue
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
diff --git a/src/controllers/FileHandler.js b/src/controllers/FileHandler.js
new file mode 100644
index 0000000..1843e8e
--- /dev/null
+++ b/src/controllers/FileHandler.js
@@ -0,0 +1,553 @@
+/**
+ * FileHandler - File handling module
+ * Handles file drag-drop, loading, type detection, etc.
+ */
+import * as THREE from 'three';
+import { ModelLoaderFactory } from '../loaders/ModelLoaderFactory.js';
+import { readFileContent, getFileFromEntry, getFileTypeFromExtension, getFileDisplayType } from '../utils/FileUtils.js';
+
+export class FileHandler {
+ constructor() {
+ this.fileMap = new Map();
+ this.availableModels = [];
+ this.currentModelFile = null;
+ this.onModelLoaded = null; // Callback function
+ this.usdViewerManager = null; // USD viewer manager (lazy loaded)
+ }
+
+ /**
+ * Set USD viewer manager
+ */
+ setUSDViewerManager(manager) {
+ this.usdViewerManager = manager;
+ }
+
+ /**
+ * Set USD viewer initializer callback (for lazy loading)
+ */
+ setUSDViewerInitializer(initializer) {
+ this.usdViewerInitializer = initializer;
+ }
+
+ /**
+ * Setup file drag-drop
+ */
+ setupFileDrop() {
+ const body = document.body;
+
+ const preventDefaults = (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ };
+
+ ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
+ body.addEventListener(eventName, preventDefaults, false);
+ });
+
+ let dragCounter = 0;
+ body.addEventListener('dragenter', (e) => {
+ dragCounter++;
+ const dropZone = document.getElementById('drop-zone');
+ if (dropZone) dropZone.classList.add('drag-over');
+ }, false);
+
+ body.addEventListener('dragleave', (e) => {
+ dragCounter--;
+ if (dragCounter === 0) {
+ const dropZone = document.getElementById('drop-zone');
+ if (dropZone) dropZone.classList.remove('drag-over');
+ }
+ }, false);
+
+ body.addEventListener('drop', (e) => {
+ dragCounter = 0;
+ const dropZone = document.getElementById('drop-zone');
+ if (dropZone) dropZone.classList.remove('drag-over');
+ this.handleDrop(e);
+ }, false);
+ }
+
+
+ /**
+ * Handle file drop
+ */
+ async handleDrop(e) {
+ const items = e.dataTransfer.items;
+ if (!items || items.length === 0) return;
+
+ this.fileMap.clear();
+
+ const entries = [];
+ for (let i = 0; i < items.length; i++) {
+ const item = items[i];
+ if (item.webkitGetAsEntry) {
+ const entry = item.webkitGetAsEntry();
+ if (entry) {
+ entries.push(entry);
+ }
+ }
+ }
+
+ if (entries.length > 0) {
+ await this.processEntries(entries);
+ } else {
+ const files = e.dataTransfer.files;
+ if (files.length > 0) {
+ for (const file of files) {
+ const path = file.webkitRelativePath || file.name;
+ this.fileMap.set(path, file);
+ // Only add file.name as a separate key if there's no webkitRelativePath
+ // This prevents duplicate entries in the file tree
+ if (!file.webkitRelativePath) {
+ this.fileMap.set(file.name, file);
+ }
+ }
+
+ const loadableFiles = await this.findAllLoadableFiles(Array.from(files));
+ if (loadableFiles.length > 0) {
+ this.availableModels = loadableFiles;
+ this.onFilesLoaded?.(loadableFiles);
+ await this.loadFileOrMesh(loadableFiles[0]);
+ }
+ }
+ }
+ }
+
+ /**
+ * Process file system entries
+ */
+ async processEntries(entries) {
+ const files = [];
+
+ for (const entry of entries) {
+ if (entry.isFile) {
+ const file = await getFileFromEntry(entry);
+ const path = entry.fullPath || entry.name;
+ this.fileMap.set(path, file);
+ files.push(file);
+ } else if (entry.isDirectory) {
+ const dirFiles = await this.readDirectory(entry);
+ files.push(...dirFiles);
+ }
+ }
+
+ if (files.length === 0) return;
+
+ const loadableFiles = await this.findAllLoadableFiles(files);
+
+ if (loadableFiles.length === 0) {
+ this.onFilesLoaded?.([]);
+ return;
+ }
+
+ this.availableModels = loadableFiles;
+ this.onFilesLoaded?.(loadableFiles);
+
+ if (loadableFiles.length > 0) {
+ await this.loadFileOrMesh(loadableFiles[0]);
+ }
+ }
+
+ /**
+ * Recursively read directory
+ */
+ async readDirectory(dirEntry) {
+ const files = [];
+
+ return new Promise((resolve, reject) => {
+ const reader = dirEntry.createReader();
+
+ const readEntries = () => {
+ reader.readEntries(async (entries) => {
+ if (entries.length === 0) {
+ resolve(files);
+ return;
+ }
+
+ for (const entry of entries) {
+ if (entry.isFile) {
+ const file = await getFileFromEntry(entry);
+ const path = entry.fullPath || entry.name;
+ this.fileMap.set(path, file);
+ files.push(file);
+ } else if (entry.isDirectory) {
+ const subFiles = await this.readDirectory(entry);
+ files.push(...subFiles);
+ }
+ }
+
+ readEntries();
+ }, reject);
+ };
+
+ readEntries();
+ });
+ }
+
+ /**
+ * Find all loadable files
+ */
+ async findAllLoadableFiles(files) {
+ const supportedExtensions = {
+ model: ['urdf', 'xacro', 'xml', 'usd', 'usda', 'usdc', 'usdz'],
+ mesh: ['dae', 'stl', 'obj', 'collada']
+ };
+ const loadableFiles = [];
+
+ const checkPromises = files.map(async (file) => {
+ const ext = file.name.toLowerCase().split('.').pop();
+
+ if (supportedExtensions.model.includes(ext)) {
+ if (ext === 'xml') {
+ try {
+ const content = await readFileContent(file);
+ const fileType = ModelLoaderFactory.detectFileType(file.name, content);
+
+ if (!fileType) {
+ return null;
+ }
+
+ return {
+ file: file,
+ name: file.name,
+ type: fileType,
+ path: file.webkitRelativePath || file.name,
+ category: 'model'
+ };
+ } catch (error) {
+ console.error(`Failed to read XML file: ${file.name}`, error);
+ return null;
+ }
+ } else {
+ const fileType = getFileTypeFromExtension(ext);
+ return {
+ file: file,
+ name: file.name,
+ type: fileType,
+ path: file.webkitRelativePath || file.name,
+ category: 'model'
+ };
+ }
+ } else if (supportedExtensions.mesh.includes(ext)) {
+ return {
+ file: file,
+ name: file.name,
+ type: 'mesh',
+ path: file.webkitRelativePath || file.name,
+ category: 'mesh'
+ };
+ }
+ return null;
+ });
+
+ const results = await Promise.all(checkPromises);
+
+ results.forEach(result => {
+ if (result) {
+ loadableFiles.push(result);
+ }
+ });
+
+ loadableFiles.sort((a, b) => {
+ if (a.category !== b.category) {
+ return a.category === 'model' ? -1 : 1;
+ }
+ return a.name.localeCompare(b.name);
+ });
+
+ return loadableFiles;
+ }
+
+ /**
+ * Load file or mesh
+ */
+ async loadFileOrMesh(fileInfo) {
+ if (fileInfo.category === 'model') {
+ await this.loadFile(fileInfo.file);
+ } else if (fileInfo.category === 'mesh') {
+ await this.loadMeshAsModel(fileInfo.file, fileInfo.name);
+ }
+ }
+
+ /**
+ * Load model file
+ */
+ async loadFile(file) {
+ this.currentModelFile = file;
+
+ try {
+ const fileName = file.name.toLowerCase();
+
+ // Get the full path from fileMap
+ let fullPath = file.name;
+ for (const [path, f] of this.fileMap.entries()) {
+ if (f === file) {
+ fullPath = path;
+ break;
+ }
+ }
+
+ // Handle USD format files (all USD formats use WASM)
+ const isUSD = fileName.endsWith('.usd') || fileName.endsWith('.usda') ||
+ fileName.endsWith('.usdc') || fileName.endsWith('.usdz');
+ if (isUSD) {
+ // Ensure USD viewer is initialized
+ if (!this.usdViewerManager && this.usdViewerInitializer) {
+ try {
+ await this.usdViewerInitializer();
+ } catch (error) {
+ console.error('USD viewer initialization failed:', error);
+ return;
+ }
+ }
+
+ if (this.fileMap.size === 0) {
+ this.fileMap.set(file.name, file);
+ }
+
+ // USD files need File object passed
+ const model = await ModelLoaderFactory.loadModel(
+ 'usd',
+ null,
+ fullPath,
+ this.fileMap,
+ file,
+ { usdViewerManager: this.usdViewerManager }
+ );
+
+ this.onModelLoaded?.(model, file, false, null);
+ document.getElementById('drop-zone')?.classList.remove('show');
+ document.getElementById('drop-zone')?.classList.remove('drag-over');
+ return;
+ }
+
+ // For other files, read text content
+ const content = await readFileContent(file);
+
+ // Detect if USDC binary format (based on content)
+ if (this.isUSDCBinaryContent(content)) {
+ console.error('Cannot load USDC binary format, please convert to USDZ or USDA');
+ return;
+ }
+
+ const fileType = ModelLoaderFactory.detectFileType(file.name, content);
+
+ if (!fileType) {
+ console.error(`${window.i18n.t('unsupportedFormat')}: ${file.name}`);
+ return;
+ }
+
+ if (this.fileMap.size === 0) {
+ this.fileMap.set(file.name, file);
+ }
+
+ const model = await ModelLoaderFactory.loadModel(
+ fileType,
+ content,
+ fullPath, // Use full path instead of just file.name
+ this.fileMap,
+ file,
+ { usdViewerManager: this.usdViewerManager }
+ );
+
+ // Notify model loaded (pass null as snapshot, let main.js create it)
+ this.onModelLoaded?.(model, file, false, null);
+
+ document.getElementById('drop-zone')?.classList.remove('show');
+ document.getElementById('drop-zone')?.classList.remove('drag-over');
+
+ } catch (error) {
+ console.error('Failed to load file:', error);
+
+ // If error message contains USDC related content
+ if (error.message && (error.message.includes('USDC') || error.message.includes('binary format'))) {
+ console.error('Cannot load USDC binary format, please convert to USDZ or USDA');
+ } else {
+ console.error(`${window.i18n.t('loadFailed')}: ${error.message}`);
+ }
+
+ // Remove snapshot if exists
+ const snapshot = document.getElementById('canvas-snapshot');
+ if (snapshot?.parentNode) {
+ snapshot.parentNode.removeChild(snapshot);
+ }
+ }
+ }
+
+ /**
+ * Detect if file content is USDC binary format
+ */
+ isUSDCBinaryContent(content) {
+ if (!content || typeof content !== 'string') {
+ return false;
+ }
+
+ // Detect USDC binary format characteristics
+ // 1. Starts with "PXR-USDC" (USDC file magic bytes)
+ if (content.startsWith('PXR-USDC')) {
+ return true;
+ }
+
+ // 2. Check first 100 bytes, if contains many null bytes, likely binary
+ const firstBytes = content.substring(0, 100);
+ const nullCount = (firstBytes.match(/\x00/g) || []).length;
+ if (nullCount > 10) {
+ return true;
+ }
+
+ // 3. Check if valid ASCII USD file
+ // Valid USDA files usually start with #usda or contain def keywords
+ if (content.includes('#usda') ||
+ (content.includes('def ') && content.includes('{'))) {
+ return false;
+ }
+
+ // 4. File starts with non-printable characters (excluding newline, carriage return, tab, space)
+ const firstChar = content.charCodeAt(0);
+ if (firstChar < 32 && firstChar !== 10 && firstChar !== 13 && firstChar !== 9 && firstChar !== 32) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Create loading snapshot
+ */
+ createLoadingSnapshot() {
+ const canvas = document.getElementById('canvas');
+ if (!canvas) return null;
+
+ try {
+ const dataURL = canvas.toDataURL('image/png');
+ const snapshot = document.createElement('div');
+ snapshot.id = 'canvas-snapshot';
+ snapshot.style.cssText = `
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background-image: url(${dataURL});
+ background-size: cover;
+ background-position: center;
+ background-color: var(--bg-primary);
+ background-repeat: no-repeat;
+ z-index: 2;
+ pointer-events: none;
+ `;
+
+ const canvasContainer = document.getElementById('canvas-container');
+ if (canvasContainer) {
+ canvasContainer.appendChild(snapshot);
+ }
+
+ return snapshot;
+ } catch (error) {
+ console.error('Failed to create snapshot:', error);
+ return null;
+ }
+ }
+
+ /**
+ * Remove loading snapshot
+ */
+ removeLoadingSnapshot(snapshot) {
+ if (!snapshot || !snapshot.parentNode) return;
+
+ snapshot.style.transition = 'opacity 0.3s ease';
+ snapshot.style.opacity = '0';
+
+ setTimeout(() => {
+ if (snapshot.parentNode) {
+ snapshot.parentNode.removeChild(snapshot);
+ }
+ }, 300);
+ }
+
+ /**
+ * Load single mesh file as model
+ */
+ async loadMeshAsModel(file, fileName) {
+ try {
+ const meshObject = await ModelLoaderFactory.loadMeshFileDirect(file, fileName);
+
+ if (!meshObject) {
+ throw new Error(window.i18n.t('cannotLoadMesh'));
+ }
+
+ // Ensure mesh materials support lighting and shadows
+ meshObject.traverse((child) => {
+ if (child.isMesh) {
+ if (child.material?.type === 'MeshBasicMaterial') {
+ const oldMaterial = child.material;
+ child.material = new THREE.MeshPhongMaterial({
+ color: oldMaterial.color,
+ map: oldMaterial.map,
+ transparent: oldMaterial.transparent,
+ opacity: oldMaterial.opacity,
+ side: oldMaterial.side,
+ shininess: 30
+ });
+ if (child.material.map) {
+ child.material.map.colorSpace = THREE.SRGBColorSpace;
+ }
+ }
+ child.castShadow = true;
+ child.receiveShadow = true;
+ }
+ });
+
+ const simpleMeshModel = {
+ name: fileName,
+ rootLink: 'mesh_root',
+ links: new Map([
+ ['mesh_root', {
+ name: 'mesh_root',
+ threeObject: meshObject,
+ visuals: [{
+ threeObject: meshObject,
+ geometry: { mesh: fileName }
+ }],
+ inertial: null
+ }]
+ ]),
+ joints: new Map(),
+ threeObject: meshObject
+ };
+
+ this.currentModelFile = file;
+ this.onModelLoaded?.(simpleMeshModel, file, true, null);
+
+ } catch (error) {
+ console.error('Failed to load mesh file:', error);
+
+ const snapshot = document.getElementById('canvas-snapshot');
+ if (snapshot?.parentNode) {
+ snapshot.parentNode.removeChild(snapshot);
+ }
+ }
+ }
+
+ /**
+ * Get file map
+ */
+ getFileMap() {
+ return this.fileMap;
+ }
+
+ /**
+ * Get available models list
+ */
+ getAvailableModels() {
+ return this.availableModels;
+ }
+
+ /**
+ * Get current model file
+ */
+ getCurrentModelFile() {
+ return this.currentModelFile;
+ }
+}
+
diff --git a/src/directive/common/copyText.js b/src/directive/common/copyText.js
new file mode 100644
index 0000000..7063df8
--- /dev/null
+++ b/src/directive/common/copyText.js
@@ -0,0 +1,66 @@
+/**
+* v-copyText 复制文本内容
+* Copyright (c) 2022 ruoyi
+*/
+
+export default {
+ beforeMount(el, { value, arg }) {
+ if (arg === "callback") {
+ el.$copyCallback = value;
+ } else {
+ el.$copyValue = value;
+ const handler = () => {
+ copyTextToClipboard(el.$copyValue);
+ if (el.$copyCallback) {
+ el.$copyCallback(el.$copyValue);
+ }
+ };
+ el.addEventListener("click", handler);
+ el.$destroyCopy = () => el.removeEventListener("click", handler);
+ }
+ }
+}
+
+function copyTextToClipboard(input, { target = document.body } = {}) {
+ const element = document.createElement('textarea');
+ const previouslyFocusedElement = document.activeElement;
+
+ element.value = input;
+
+ // Prevent keyboard from showing on mobile
+ element.setAttribute('readonly', '');
+
+ element.style.contain = 'strict';
+ element.style.position = 'absolute';
+ element.style.left = '-9999px';
+ element.style.fontSize = '12pt'; // Prevent zooming on iOS
+
+ const selection = document.getSelection();
+ const originalRange = selection.rangeCount > 0 && selection.getRangeAt(0);
+
+ target.append(element);
+ element.select();
+
+ // Explicit selection workaround for iOS
+ element.selectionStart = 0;
+ element.selectionEnd = input.length;
+
+ let isSuccess = false;
+ try {
+ isSuccess = document.execCommand('copy');
+ } catch { }
+
+ element.remove();
+
+ if (originalRange) {
+ selection.removeAllRanges();
+ selection.addRange(originalRange);
+ }
+
+ // Get the focus back on the previously focused element, if any
+ if (previouslyFocusedElement) {
+ previouslyFocusedElement.focus();
+ }
+
+ return isSuccess;
+}
diff --git a/src/directive/index.js b/src/directive/index.js
new file mode 100644
index 0000000..86b8f88
--- /dev/null
+++ b/src/directive/index.js
@@ -0,0 +1,9 @@
+import hasRole from './permission/hasRole'
+import hasPermi from './permission/hasPermi'
+import copyText from './common/copyText'
+
+export default function directive(app){
+ app.directive('hasRole', hasRole)
+ app.directive('hasPermi', hasPermi)
+ app.directive('copyText', copyText)
+}
\ No newline at end of file
diff --git a/src/directive/permission/hasPermi.js b/src/directive/permission/hasPermi.js
new file mode 100644
index 0000000..44ef3f8
--- /dev/null
+++ b/src/directive/permission/hasPermi.js
@@ -0,0 +1,28 @@
+ /**
+ * v-hasPermi 操作权限处理
+ * Copyright (c) 2019 ruoyi
+ */
+
+import useUserStore from '@/store/modules/user'
+
+export default {
+ mounted(el, binding, vnode) {
+ const { value } = binding
+ const all_permission = "*:*:*";
+ const permissions = useUserStore().permissions
+
+ if (value && value instanceof Array && value.length > 0) {
+ const permissionFlag = value
+
+ const hasPermissions = permissions.some(permission => {
+ return all_permission === permission || permissionFlag.includes(permission)
+ })
+
+ if (!hasPermissions) {
+ el.parentNode && el.parentNode.removeChild(el)
+ }
+ } else {
+ throw new Error(`请设置操作权限标签值`)
+ }
+ }
+}
diff --git a/src/directive/permission/hasRole.js b/src/directive/permission/hasRole.js
new file mode 100644
index 0000000..8774bdd
--- /dev/null
+++ b/src/directive/permission/hasRole.js
@@ -0,0 +1,28 @@
+ /**
+ * v-hasRole 角色权限处理
+ * Copyright (c) 2019 ruoyi
+ */
+
+import useUserStore from '@/store/modules/user'
+
+export default {
+ mounted(el, binding, vnode) {
+ const { value } = binding
+ const super_admin = "admin";
+ const roles = useUserStore().roles
+
+ if (value && value instanceof Array && value.length > 0) {
+ const roleFlag = value
+
+ const hasRole = roles.some(role => {
+ return super_admin === role || roleFlag.includes(role)
+ })
+
+ if (!hasRole) {
+ el.parentNode && el.parentNode.removeChild(el)
+ }
+ } else {
+ throw new Error(`请设置角色权限标签值`)
+ }
+ }
+}
diff --git a/src/enum/index.js b/src/enum/index.js
new file mode 100644
index 0000000..cbfc14a
--- /dev/null
+++ b/src/enum/index.js
@@ -0,0 +1,110 @@
+export const controlPageList = [
+ "/mechanical_arm",
+ "/Motor",
+ "/Camera"
+]
+export const controlModuleList = [
+ {
+ label: "机械臂",
+ value: "mechanical_arm"
+ },
+ {
+ label: "电机",
+ value: "motor"
+ },
+ {
+ label: "舵机",
+ value: "servo"
+ },
+ {
+ label: "执行器",
+ value: "executor"
+ },
+ {
+ label: "麦克风",
+ value: "microphone"
+ },
+ {
+ label: "扬声器",
+ value: "speaker"
+ },
+ {
+ label: "摄像头",
+ value: "Camera"
+ },
+ {
+ label: "电源",
+ value: "power"
+ },
+ {
+ label: "灵巧手",
+ value: "hand"
+ },
+ {
+ label: "人工头",
+ value: "head"
+ }
+]
+
+export const fileType = [
+ {
+ label: "普通文件",
+ value: 0
+ },
+ {
+ label: "图片",
+ value: 1
+ },
+ {
+ label: "音频",
+ value: 2
+ },
+ {
+ label: "视频",
+ value: 3
+ }
+]
+
+//0:普通文件、1:图片、2:音频、3:视频
+export const supportType = [
+ //json
+ {label: "application/json", value: 0},
+ {label: "text/plain", value: 0},
+ {label: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", value: 0},
+ {label: "image/jpeg", value: 1},
+ {label: "image/png", value: 1},
+ {label: "audio/mpeg", value: 2},
+ {label: "video/mp4", value: 3},
+]
+
+export const deviceParamsMap = {
+ "mechanical_arm": [
+ {
+ "label": "工作模式", "key": "workModule", "value": "1", "formType": "select", "options": [
+ {
+ "label": "真实机械臂",
+ "value": "1"
+ },
+ {
+ "label": "仿真机械臂",
+ "value": "2"
+ }
+ ]
+ },
+ {"label": "位置步长", "key": "posStep", "value": 0, "formType": "input_number"},
+ {"label": "姿态步长", "key": "postureStep", "value": 0, "formType": "input_number"},
+ {"label": "关节步长", "key": "jointStep", "value": 0, "formType": "input_number"},
+ {"label": "位置( X )", "key": "x", "value": 0, "formType": "input_number"},
+ {"label": "位置( y )", "key": "y", "value": 0, "formType": "input_number"},
+ {"label": "位置( z )", "key": "z", "value": 0, "formType": "input_number"},
+ {"label": "姿态( rx )", "key": "rx", "value": 0, "formType": "input_number"},
+ {"label": "姿态( ry )", "key": "ry", "value": 0, "formType": "input_number"},
+ {"label": "姿态( rz )", "key": "rz", "value": 0, "formType": "input_number"},
+ {"label": "关节1( deg )", "key": "joint1", "value": 0, "formType": "input_number"},
+ {"label": "关节2( deg )", "key": "joint2", "value": 0, "formType": "input_number"},
+ {"label": "关节3( deg )", "key": "joint3", "value": 0, "formType": "input_number"},
+ {"label": "关节4( deg )", "key": "joint4", "value": 0, "formType": "input_number"},
+ {"label": "关节5( deg )", "key": "joint5", "value": 0, "formType": "input_number"},
+ {"label": "关节6( deg )", "key": "joint6", "value": 0, "formType": "input_number"},
+ ],
+}
diff --git a/src/hooks/tableHeight.ts b/src/hooks/tableHeight.ts
new file mode 100644
index 0000000..f254147
--- /dev/null
+++ b/src/hooks/tableHeight.ts
@@ -0,0 +1,21 @@
+import { useElementSize } from '@vueuse/core'
+import { computed } from 'vue'
+
+/**
+ * 监听容器高度变化的hook
+ * @param {Ref} containerRef - 容器的ref引用
+ * @param {boolean} [immediate=true] - 是否立即执行一次
+ * @returns {Ref} 容器的高度
+ */
+export function useContainerHeight(containerRef, immediate = true) {
+ // 存储容器高度的响应式变量
+
+ const { height } = useElementSize(containerRef)
+ const containerHeight = computed(() => {
+ let offset = height.value + 84
+ return { height: `calc(100% - ${offset}px)` }
+ })
+
+
+ return containerHeight;
+}
diff --git a/src/hooks/usePlayAudio.ts b/src/hooks/usePlayAudio.ts
new file mode 100644
index 0000000..3e3675e
--- /dev/null
+++ b/src/hooks/usePlayAudio.ts
@@ -0,0 +1,38 @@
+import { computed } from "vue";
+
+/**
+ * 播放音频
+ */
+export function usePlayAudio() {
+ let audio: HTMLAudioElement | null = new Audio();
+ // 播放结束后自动清理
+ // 先定义cleanup,再绑定事件(避免作用域问题)
+ let cleanup: any;
+ cleanup = () => {
+ // 1. 先移除所有事件监听器,切断循环触发链路
+ audio!.removeEventListener("ended", cleanup);
+ audio!.removeEventListener("error", cleanup);
+ // 2. 停止播放(而非清空src),避免触发新error
+ audio!.pause();
+ // 可选:解除引用,帮助GC(若后续无其他引用)
+ audio = null;
+ console.log("音频资源已清理");
+ };
+
+ audio.addEventListener("ended", cleanup);
+
+ // 播放错误时也清理
+ audio.addEventListener("error", cleanup);
+
+ const playAudio = (url: string) => {
+ audio!.src = url
+ audio!.play().catch((error) => {
+ console.error("播放失败:", error);
+ cleanup();
+ });
+ };
+
+ return {
+ playAudio
+ }
+}
diff --git a/src/layout/components/AppMain.vue b/src/layout/components/AppMain.vue
new file mode 100644
index 0000000..e3204de
--- /dev/null
+++ b/src/layout/components/AppMain.vue
@@ -0,0 +1,88 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/src/layout/components/IframeToggle/index.vue b/src/layout/components/IframeToggle/index.vue
new file mode 100644
index 0000000..a407b74
--- /dev/null
+++ b/src/layout/components/IframeToggle/index.vue
@@ -0,0 +1,25 @@
+
+
+
+
+
diff --git a/src/layout/components/InnerLink/index.vue b/src/layout/components/InnerLink/index.vue
new file mode 100644
index 0000000..53a903c
--- /dev/null
+++ b/src/layout/components/InnerLink/index.vue
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
diff --git a/src/layout/components/Navbar.vue b/src/layout/components/Navbar.vue
new file mode 100644
index 0000000..5b15602
--- /dev/null
+++ b/src/layout/components/Navbar.vue
@@ -0,0 +1,205 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/layout/components/Settings/index.vue b/src/layout/components/Settings/index.vue
new file mode 100644
index 0000000..cc1a377
--- /dev/null
+++ b/src/layout/components/Settings/index.vue
@@ -0,0 +1,204 @@
+
+
+
+
主题风格设置
+
+
+
+
+
+
+
+
+
+
+
+
+ 主题颜色
+
+
+
+
+
+
+ 系统布局配置
+
+
+ 开启 TopNav
+
+
+
+
+
+
+ 开启 Tags-Views
+
+
+
+
+
+
+ 固定 Header
+
+
+
+
+
+
+ 显示 Logo
+
+
+
+
+
+
+ 动态标题
+
+
+
+
+
+
+
+ 保存配置
+ 重置配置
+
+
+
+
+
+
+
diff --git a/src/layout/components/Sidebar/Link.vue b/src/layout/components/Sidebar/Link.vue
new file mode 100644
index 0000000..53793ca
--- /dev/null
+++ b/src/layout/components/Sidebar/Link.vue
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
diff --git a/src/layout/components/Sidebar/Logo.vue b/src/layout/components/Sidebar/Logo.vue
new file mode 100644
index 0000000..c23c92a
--- /dev/null
+++ b/src/layout/components/Sidebar/Logo.vue
@@ -0,0 +1,99 @@
+
+
+
+
+
+
+
diff --git a/src/layout/components/Sidebar/SidebarItem.vue b/src/layout/components/Sidebar/SidebarItem.vue
new file mode 100644
index 0000000..b43bca7
--- /dev/null
+++ b/src/layout/components/Sidebar/SidebarItem.vue
@@ -0,0 +1,100 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/layout/components/Sidebar/index.vue b/src/layout/components/Sidebar/index.vue
new file mode 100644
index 0000000..f0fdf88
--- /dev/null
+++ b/src/layout/components/Sidebar/index.vue
@@ -0,0 +1,104 @@
+
+
+
+
+
+
+
diff --git a/src/layout/components/TagsView/ScrollPane.vue b/src/layout/components/TagsView/ScrollPane.vue
new file mode 100644
index 0000000..f4fc81b
--- /dev/null
+++ b/src/layout/components/TagsView/ScrollPane.vue
@@ -0,0 +1,107 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/layout/components/TagsView/index.vue b/src/layout/components/TagsView/index.vue
new file mode 100644
index 0000000..5844331
--- /dev/null
+++ b/src/layout/components/TagsView/index.vue
@@ -0,0 +1,365 @@
+
+
+
+
+ {{ tag.title }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/layout/components/index.js b/src/layout/components/index.js
new file mode 100644
index 0000000..fd57731
--- /dev/null
+++ b/src/layout/components/index.js
@@ -0,0 +1,4 @@
+export { default as AppMain } from './AppMain'
+export { default as Navbar } from './Navbar'
+export { default as Settings } from './Settings'
+export { default as TagsView } from './TagsView/index.vue'
diff --git a/src/layout/index.vue b/src/layout/index.vue
new file mode 100644
index 0000000..de92549
--- /dev/null
+++ b/src/layout/index.vue
@@ -0,0 +1,114 @@
+
+
+
+
+
+
+
diff --git a/src/main.js b/src/main.js
new file mode 100644
index 0000000..8be21ea
--- /dev/null
+++ b/src/main.js
@@ -0,0 +1,95 @@
+import { createApp } from 'vue'
+
+import Cookies from 'js-cookie'
+
+import ElementPlus from 'element-plus'
+import 'element-plus/dist/index.css'
+import 'element-plus/theme-chalk/dark/css-vars.css'
+import locale from 'element-plus/es/locale/lang/zh-cn'
+
+import '@/assets/styles/index.scss' // global css
+
+import App from './App'
+import store from './store'
+import router from './router'
+import directive from './directive' // directive
+import WebSocketPlugin from './plugins/websocket';
+
+// 注册指令
+import plugins from './plugins' // plugins
+import { download } from '@/utils/request'
+
+// svg图标
+import 'virtual:svg-icons-register'
+import SvgIcon from '@/components/SvgIcon'
+import elementIcons from '@/components/SvgIcon/svgicon'
+
+import './permission' // permission control
+
+import { useDict } from '@/utils/dict'
+import { parseTime, resetForm, addDateRange, handleTree, selectDictLabel, selectDictLabels } from '@/utils/ruoyi'
+
+// 分页组件
+import Pagination from '@/components/Pagination'
+// 自定义表格工具组件
+import RightToolbar from '@/components/RightToolbar'
+// 富文本组件
+import Editor from "@/components/Editor"
+// 文件上传组件
+import FileUpload from "@/components/FileUpload"
+// 图片上传组件
+import ImageUpload from "@/components/ImageUpload"
+// 图片预览组件
+import ImagePreview from "@/components/ImagePreview"
+// 字典标签组件
+import DictTag from '@/components/DictTag'
+
+// ace.config.js(独立配置文件)
+import ace from 'ace-builds';
+import modeJsonUrl from 'ace-builds/src-noconflict/mode-json?url';
+ace.config.setModuleUrl('ace/mode/json', modeJsonUrl); // 动态绑定语法模块路径
+
+const app = createApp(App)
+
+// 全局方法挂载
+app.config.globalProperties.useDict = useDict
+ app.config.globalProperties.download = download
+app.config.globalProperties.parseTime = parseTime
+app.config.globalProperties.resetForm = resetForm
+app.config.globalProperties.handleTree = handleTree
+app.config.globalProperties.addDateRange = addDateRange
+app.config.globalProperties.selectDictLabel = selectDictLabel
+app.config.globalProperties.selectDictLabels = selectDictLabels
+
+// 全局组件挂载
+app.component('DictTag', DictTag)
+app.component('Pagination', Pagination)
+app.component('FileUpload', FileUpload)
+app.component('ImageUpload', ImageUpload)
+app.component('ImagePreview', ImagePreview)
+app.component('RightToolbar', RightToolbar)
+app.component('Editor', Editor)
+
+app.use(router)
+app.use(store)
+app.use(plugins)
+app.use(elementIcons)
+app.component('svg-icon', SvgIcon)
+
+directive(app)
+
+// 使用element-plus 并且设置全局的大小
+app.use(ElementPlus, {
+ locale: locale,
+ // 支持 large、default、small
+ size: Cookies.get('size') || 'default'
+})
+
+
+app.use(WebSocketPlugin, {
+ url: import.meta.env.VITE_WS_URL,
+ // url: 'ws://192.168.0.201:13080/ws',
+ userId: 'your_user_id'
+});
+
+app.mount('#app')
diff --git a/src/models/UnifiedRobotModel.js b/src/models/UnifiedRobotModel.js
new file mode 100644
index 0000000..1ef44f8
--- /dev/null
+++ b/src/models/UnifiedRobotModel.js
@@ -0,0 +1,181 @@
+/**
+ * Unified robot model data interface
+ * All formats (URDF, MJCF, USD) are converted to this unified format
+ */
+export class UnifiedRobotModel {
+ constructor() {
+ this.name = '';
+ this.links = new Map(); // Map
+ this.joints = new Map(); // Map
+ this.materials = new Map(); // Map
+ this.constraints = new Map(); // Map - for parallel mechanism constraints
+ this.rootLink = null; // Root link name
+ this.threeObject = null; // Three.js object (if available)
+ }
+
+ addLink(link) {
+ this.links.set(link.name, link);
+ }
+
+ addJoint(joint) {
+ this.joints.set(joint.name, joint);
+ }
+
+ addConstraint(constraint) {
+ this.constraints.set(constraint.name, constraint);
+ }
+
+ getLink(name) {
+ return this.links.get(name);
+ }
+
+ getJoint(name) {
+ return this.joints.get(name);
+ }
+
+ getConstraint(name) {
+ return this.constraints.get(name);
+ }
+}
+
+/**
+ * Link interface
+ */
+export class Link {
+ constructor(name) {
+ this.name = name;
+ this.visuals = []; // VisualGeometry[]
+ this.collisions = []; // CollisionGeometry[]
+ this.inertial = null; // InertialProperties
+ this.threeObject = null; // Three.js object
+ this.userData = {}; // User-defined data (for adapters to store additional information)
+ }
+}
+
+/**
+ * VisualGeometry interface
+ */
+export class VisualGeometry {
+ constructor() {
+ this.name = '';
+ this.origin = { xyz: [0, 0, 0], rpy: [0, 0, 0] };
+ this.geometry = null; // GeometryType
+ this.material = null; // Material
+ this.threeObject = null; // Three.js Mesh
+ }
+}
+
+/**
+ * CollisionGeometry interface
+ */
+export class CollisionGeometry {
+ constructor() {
+ this.name = '';
+ this.origin = { xyz: [0, 0, 0], rpy: [0, 0, 0] };
+ this.geometry = null; // GeometryType
+ this.threeObject = null; // Three.js Mesh
+ }
+}
+
+/**
+ * GeometryType interface
+ */
+export class GeometryType {
+ constructor(type) {
+ this.type = type; // 'box' | 'sphere' | 'cylinder' | 'mesh'
+ this.size = null; // Size parameters (varies by type)
+ this.filename = null; // Mesh file path (if mesh type)
+ }
+
+ clone() {
+ const cloned = new GeometryType(this.type);
+ cloned.size = this.size ? { ...this.size } : null;
+ cloned.filename = this.filename;
+ return cloned;
+ }
+}
+
+/**
+ * InertialProperties interface
+ */
+export class InertialProperties {
+ constructor() {
+ this.mass = 0;
+ this.origin = { xyz: [0, 0, 0], rpy: [0, 0, 0] };
+ this.ixx = 0;
+ this.iyy = 0;
+ this.izz = 0;
+ this.ixy = 0;
+ this.ixz = 0;
+ this.iyz = 0;
+ }
+}
+
+/**
+ * Joint interface
+ */
+export class Joint {
+ constructor(name, type) {
+ this.name = name;
+ this.type = type; // 'revolute' | 'prismatic' | 'fixed' | 'continuous'
+ this.parent = null; // Parent link name
+ this.child = null; // Child link name
+ this.origin = { xyz: [0, 0, 0], rpy: [0, 0, 0] };
+ this.axis = { xyz: [0, 0, 1] }; // Default z-axis
+ this.limits = null; // JointLimits
+ this.currentValue = 0; // Current joint value
+ this.threeObject = null; // Three.js object (if available)
+ }
+}
+
+/**
+ * JointLimits interface
+ */
+export class JointLimits {
+ constructor() {
+ this.lower = -Math.PI;
+ this.upper = Math.PI;
+ this.effort = null;
+ this.velocity = null;
+ }
+}
+
+/**
+ * Material interface
+ */
+export class Material {
+ constructor(name) {
+ this.name = name;
+ this.color = { r: 0.8, g: 0.8, b: 0.8 };
+ this.texture = null;
+ }
+}
+
+/**
+ * Constraint interface - for describing closed-chain constraints of parallel mechanisms
+ * Supports MuJoCo equality constraint types
+ */
+export class Constraint {
+ constructor(name, type) {
+ this.name = name;
+ this.type = type; // 'connect' | 'weld' | 'joint' | 'tendon' | 'distance'
+
+ // Constraint objects (may be body, geom, joint, etc. depending on type)
+ this.body1 = null;
+ this.body2 = null;
+ this.anchor = null; // Connection point coordinates
+ this.torquescale = null; // Torque scale
+
+ // Joint constraint specific properties
+ this.joint1 = null;
+ this.joint2 = null;
+ this.polycoef = null; // Polynomial coefficients [a0, a1, a2, a3, a4]
+
+ // Visualization object
+ this.threeObject = null; // Three.js object for displaying constraint
+
+ // Original data (for debugging)
+ this.userData = {};
+ }
+}
+
diff --git a/src/permission.js b/src/permission.js
new file mode 100644
index 0000000..c9cbd87
--- /dev/null
+++ b/src/permission.js
@@ -0,0 +1,75 @@
+import router from './router'
+import { ElMessage } from 'element-plus'
+import NProgress from 'nprogress'
+import 'nprogress/nprogress.css'
+import { getToken } from '@/utils/auth'
+import { isHttp, isPathMatch } from '@/utils/validate'
+import { isRelogin } from '@/utils/request'
+import useUserStore from '@/store/modules/user'
+import useSettingsStore from '@/store/modules/settings'
+import usePermissionStore from '@/store/modules/permission'
+
+NProgress.configure({ showSpinner: false })
+
+const whiteList = ['/login', '/register']
+
+const isWhiteList = (path) => {
+ return whiteList.some(pattern => isPathMatch(pattern, path))
+}
+
+router.beforeEach((to, from, next) => {
+ if (to.meta?.noLogin) {
+ next()
+ return
+ }
+ NProgress.start()
+ if (getToken()) {
+ to.meta.title && useSettingsStore().setTitle(to.meta.title)
+ /* has token*/
+ if (to.path === '/login') {
+ next({ path: '/' })
+ NProgress.done()
+ } else if (isWhiteList(to.path)) {
+ next()
+ } else {
+ if (useUserStore().roles.length === 0) {
+ isRelogin.show = true
+ // 判断当前用户是否已拉取完user_info信息
+ useUserStore().getInfo().then(() => {
+ isRelogin.show = false
+ usePermissionStore().generateRoutes().then(accessRoutes => {
+ console.log('accessRoutes', accessRoutes)
+ // 根据roles权限生成可访问的路由表
+ accessRoutes.forEach(route => {
+ if (!isHttp(route.path)) {
+ router.addRoute(route) // 动态添加可访问路由表
+ }
+ })
+ console.log('router', router)
+ next({ ...to, replace: true }) // hack方法 确保addRoutes已完成
+ })
+ }).catch(err => {
+ useUserStore().logOut().then(() => {
+ ElMessage.error(err)
+ next({ path: '/' })
+ })
+ })
+ } else {
+ next()
+ }
+ }
+ } else {
+ // 没有token
+ if (isWhiteList(to.path)) {
+ // 在免登录白名单,直接进入
+ next()
+ } else {
+ next(`/login?redirect=${to.fullPath}`) // 否则全部重定向到登录页
+ NProgress.done()
+ }
+ }
+})
+
+router.afterEach(() => {
+ NProgress.done()
+})
diff --git a/src/plugins/auth.js b/src/plugins/auth.js
new file mode 100644
index 0000000..5e8c28d
--- /dev/null
+++ b/src/plugins/auth.js
@@ -0,0 +1,60 @@
+import useUserStore from '@/store/modules/user'
+
+function authPermission(permission) {
+ const all_permission = "*:*:*";
+ const permissions = useUserStore().permissions
+ if (permission && permission.length > 0) {
+ return permissions.some(v => {
+ return all_permission === v || v === permission
+ })
+ } else {
+ return false
+ }
+}
+
+function authRole(role) {
+ const super_admin = "admin";
+ const roles = useUserStore().roles
+ if (role && role.length > 0) {
+ return roles.some(v => {
+ return super_admin === v || v === role
+ })
+ } else {
+ return false
+ }
+}
+
+export default {
+ // 验证用户是否具备某权限
+ hasPermi(permission) {
+ return authPermission(permission);
+ },
+ // 验证用户是否含有指定权限,只需包含其中一个
+ hasPermiOr(permissions) {
+ return permissions.some(item => {
+ return authPermission(item)
+ })
+ },
+ // 验证用户是否含有指定权限,必须全部拥有
+ hasPermiAnd(permissions) {
+ return permissions.every(item => {
+ return authPermission(item)
+ })
+ },
+ // 验证用户是否具备某角色
+ hasRole(role) {
+ return authRole(role);
+ },
+ // 验证用户是否含有指定角色,只需包含其中一个
+ hasRoleOr(roles) {
+ return roles.some(item => {
+ return authRole(item)
+ })
+ },
+ // 验证用户是否含有指定角色,必须全部拥有
+ hasRoleAnd(roles) {
+ return roles.every(item => {
+ return authRole(item)
+ })
+ }
+}
diff --git a/src/plugins/cache.js b/src/plugins/cache.js
new file mode 100644
index 0000000..b67d453
--- /dev/null
+++ b/src/plugins/cache.js
@@ -0,0 +1,79 @@
+const sessionCache = {
+ set (key, value) {
+ if (!sessionStorage) {
+ return
+ }
+ if (key != null && value != null) {
+ sessionStorage.setItem(key, value)
+ }
+ },
+ get (key) {
+ if (!sessionStorage) {
+ return null
+ }
+ if (key == null) {
+ return null
+ }
+ return sessionStorage.getItem(key)
+ },
+ setJSON (key, jsonValue) {
+ if (jsonValue != null) {
+ this.set(key, JSON.stringify(jsonValue))
+ }
+ },
+ getJSON (key) {
+ const value = this.get(key)
+ if (value != null) {
+ return JSON.parse(value)
+ }
+ return null
+ },
+ remove (key) {
+ sessionStorage.removeItem(key);
+ }
+}
+const localCache = {
+ set (key, value) {
+ if (!localStorage) {
+ return
+ }
+ if (key != null && value != null) {
+ localStorage.setItem(key, value)
+ }
+ },
+ get (key) {
+ if (!localStorage) {
+ return null
+ }
+ if (key == null) {
+ return null
+ }
+ return localStorage.getItem(key)
+ },
+ setJSON (key, jsonValue) {
+ if (jsonValue != null) {
+ this.set(key, JSON.stringify(jsonValue))
+ }
+ },
+ getJSON (key) {
+ const value = this.get(key)
+ if (value != null) {
+ return JSON.parse(value)
+ }
+ return null
+ },
+ remove (key) {
+ localStorage.removeItem(key);
+ }
+}
+
+export default {
+ /**
+ * 会话级缓存
+ */
+ session: sessionCache,
+ /**
+ * 本地缓存
+ */
+ local: localCache
+}
diff --git a/src/plugins/download.js b/src/plugins/download.js
new file mode 100644
index 0000000..1a89efd
--- /dev/null
+++ b/src/plugins/download.js
@@ -0,0 +1,79 @@
+import axios from 'axios'
+import { ElLoading, ElMessage } from 'element-plus'
+import { saveAs } from 'file-saver'
+import { getToken } from '@/utils/auth'
+import errorCode from '@/utils/errorCode'
+import { blobValidate } from '@/utils/ruoyi'
+
+const baseURL = import.meta.env.VITE_APP_BASE_API
+let downloadLoadingInstance;
+
+export default {
+ name(name, isDelete = true) {
+ var url = baseURL + "/common/download?fileName=" + encodeURIComponent(name) + "&delete=" + isDelete
+ axios({
+ method: 'get',
+ url: url,
+ responseType: 'blob',
+ headers: { 'Authorization': 'Bearer ' + getToken() }
+ }).then((res) => {
+ const isBlob = blobValidate(res.data);
+ if (isBlob) {
+ const blob = new Blob([res.data])
+ this.saveAs(blob, decodeURIComponent(res.headers['download-filename']))
+ } else {
+ this.printErrMsg(res.data);
+ }
+ })
+ },
+ resource(resource) {
+ var url = baseURL + "/common/download/resource?resource=" + encodeURIComponent(resource);
+ axios({
+ method: 'get',
+ url: url,
+ responseType: 'blob',
+ headers: { 'Authorization': 'Bearer ' + getToken() }
+ }).then((res) => {
+ const isBlob = blobValidate(res.data);
+ if (isBlob) {
+ const blob = new Blob([res.data])
+ this.saveAs(blob, decodeURIComponent(res.headers['download-filename']))
+ } else {
+ this.printErrMsg(res.data);
+ }
+ })
+ },
+ zip(url, name) {
+ var url = baseURL + url
+ downloadLoadingInstance = ElLoading.service({ text: "正在下载数据,请稍候", background: "rgba(0, 0, 0, 0.7)", })
+ axios({
+ method: 'get',
+ url: url,
+ responseType: 'blob',
+ headers: { 'Authorization': 'Bearer ' + getToken() }
+ }).then((res) => {
+ const isBlob = blobValidate(res.data);
+ if (isBlob) {
+ const blob = new Blob([res.data], { type: 'application/zip' })
+ this.saveAs(blob, name)
+ } else {
+ this.printErrMsg(res.data);
+ }
+ downloadLoadingInstance.close();
+ }).catch((r) => {
+ console.error(r)
+ ElMessage.error('下载文件出现错误,请联系管理员!')
+ downloadLoadingInstance.close();
+ })
+ },
+ saveAs(text, name, opts) {
+ saveAs(text, name, opts);
+ },
+ async printErrMsg(data) {
+ const resText = await data.text();
+ const rspObj = JSON.parse(resText);
+ const errMsg = errorCode[rspObj.code] || rspObj.msg || errorCode['default']
+ ElMessage.error(errMsg);
+ }
+}
+
diff --git a/src/plugins/index.js b/src/plugins/index.js
new file mode 100644
index 0000000..47d1b41
--- /dev/null
+++ b/src/plugins/index.js
@@ -0,0 +1,18 @@
+import tab from './tab'
+import auth from './auth'
+import cache from './cache'
+import modal from './modal'
+import download from './download'
+
+export default function installPlugins(app){
+ // 页签操作
+ app.config.globalProperties.$tab = tab
+ // 认证对象
+ app.config.globalProperties.$auth = auth
+ // 缓存对象
+ app.config.globalProperties.$cache = cache
+ // 模态框对象
+ app.config.globalProperties.$modal = modal
+ // 下载文件
+ app.config.globalProperties.$download = download
+}
diff --git a/src/plugins/modal.js b/src/plugins/modal.js
new file mode 100644
index 0000000..b59e14d
--- /dev/null
+++ b/src/plugins/modal.js
@@ -0,0 +1,82 @@
+import { ElMessage, ElMessageBox, ElNotification, ElLoading } from 'element-plus'
+
+let loadingInstance;
+
+export default {
+ // 消息提示
+ msg(content) {
+ ElMessage.info(content)
+ },
+ // 错误消息
+ msgError(content) {
+ ElMessage.error(content)
+ },
+ // 成功消息
+ msgSuccess(content) {
+ ElMessage.success(content)
+ },
+ // 警告消息
+ msgWarning(content) {
+ ElMessage.warning(content)
+ },
+ // 弹出提示
+ alert(content) {
+ ElMessageBox.alert(content, "系统提示")
+ },
+ // 错误提示
+ alertError(content) {
+ ElMessageBox.alert(content, "系统提示", { type: 'error' })
+ },
+ // 成功提示
+ alertSuccess(content) {
+ ElMessageBox.alert(content, "系统提示", { type: 'success' })
+ },
+ // 警告提示
+ alertWarning(content) {
+ ElMessageBox.alert(content, "系统提示", { type: 'warning' })
+ },
+ // 通知提示
+ notify(content) {
+ ElNotification.info(content)
+ },
+ // 错误通知
+ notifyError(content) {
+ ElNotification.error(content);
+ },
+ // 成功通知
+ notifySuccess(content) {
+ ElNotification.success(content)
+ },
+ // 警告通知
+ notifyWarning(content) {
+ ElNotification.warning(content)
+ },
+ // 确认窗体
+ confirm(content) {
+ return ElMessageBox.confirm(content, "系统提示", {
+ confirmButtonText: '确定',
+ cancelButtonText: '取消',
+ type: "warning",
+ })
+ },
+ // 提交内容
+ prompt(content) {
+ return ElMessageBox.prompt(content, "系统提示", {
+ confirmButtonText: '确定',
+ cancelButtonText: '取消',
+ type: "warning",
+ })
+ },
+ // 打开遮罩层
+ loading(content) {
+ loadingInstance = ElLoading.service({
+ lock: true,
+ text: content,
+ background: "rgba(0, 0, 0, 0.7)",
+ })
+ },
+ // 关闭遮罩层
+ closeLoading() {
+ loadingInstance.close();
+ }
+}
diff --git a/src/plugins/tab.js b/src/plugins/tab.js
new file mode 100644
index 0000000..7b51cf5
--- /dev/null
+++ b/src/plugins/tab.js
@@ -0,0 +1,69 @@
+import useTagsViewStore from '@/store/modules/tagsView'
+import router from '@/router'
+
+export default {
+ // 刷新当前tab页签
+ refreshPage(obj) {
+ const { path, query, matched } = router.currentRoute.value;
+ if (obj === undefined) {
+ matched.forEach((m) => {
+ if (m.components && m.components.default && m.components.default.name) {
+ if (!['Layout', 'ParentView'].includes(m.components.default.name)) {
+ obj = { name: m.components.default.name, path: path, query: query };
+ }
+ }
+ });
+ }
+ return useTagsViewStore().delCachedView(obj).then(() => {
+ const { path, query } = obj
+ router.replace({
+ path: '/redirect' + path,
+ query: query
+ })
+ })
+ },
+ // 关闭当前tab页签,打开新页签
+ closeOpenPage(obj) {
+ useTagsViewStore().delView(router.currentRoute.value);
+ if (obj !== undefined) {
+ return router.push(obj);
+ }
+ },
+ // 关闭指定tab页签
+ closePage(obj) {
+ if (obj === undefined) {
+ return useTagsViewStore().delView(router.currentRoute.value).then(({ visitedViews }) => {
+ const latestView = visitedViews.slice(-1)[0]
+ if (latestView) {
+ return router.push(latestView.fullPath)
+ }
+ return router.push('/');
+ });
+ }
+ return useTagsViewStore().delView(obj);
+ },
+ // 关闭所有tab页签
+ closeAllPage() {
+ return useTagsViewStore().delAllViews();
+ },
+ // 关闭左侧tab页签
+ closeLeftPage(obj) {
+ return useTagsViewStore().delLeftTags(obj || router.currentRoute.value);
+ },
+ // 关闭右侧tab页签
+ closeRightPage(obj) {
+ return useTagsViewStore().delRightTags(obj || router.currentRoute.value);
+ },
+ // 关闭其他tab页签
+ closeOtherPage(obj) {
+ return useTagsViewStore().delOthersViews(obj || router.currentRoute.value);
+ },
+ // 打开tab页签
+ openPage(url) {
+ return router.push(url);
+ },
+ // 修改tab页签
+ updatePage(obj) {
+ return useTagsViewStore().updateVisitedView(obj);
+ }
+}
diff --git a/src/plugins/websocket.js b/src/plugins/websocket.js
new file mode 100644
index 0000000..338a2ae
--- /dev/null
+++ b/src/plugins/websocket.js
@@ -0,0 +1,183 @@
+import { fa } from 'element-plus/es/locale/index.mjs';
+import { throttle } from 'lodash';
+
+class WebSocketManager {
+ constructor(url, userId) {
+ this.url = url;
+ this.userId = userId;
+ this.ws = null;
+ this.events = {};
+ this.reconnectTimer = null;
+ this.heartbeatTimer = null;
+ this.isManualClose = false;
+ this.messageQueue = [];
+ this.maxQueueSize = 1;
+ this.status = false;
+ }
+
+ connect() {
+ if (!window.WebSocket) {
+ console.error('浏览器不支持 WebSocket');
+ return;
+ }
+
+ this.ws = new WebSocket(`${this.url}?userId=${this.userId}`, [], { perMessageDeflate: true });
+ this.ws.binaryType = 'arraybuffer'; // 确保接收 ArrayBuffer
+
+ this.ws.onopen = () => {
+ console.log('WebSocket 已连接');
+ this.status = true;
+ this.startHeartbeat();
+ this.processMessageQueue();
+ };
+
+ this.ws.onmessage = (event) => {
+ if (this.messageQueue.length < this.maxQueueSize) {
+ this.messageQueue.push(event);
+ this.processMessageQueue();
+ } else {
+ console.warn('消息队列已满,丢弃消息');
+ }
+ };
+
+ this.ws.onclose = () => {
+ console.log('WebSocket 断开');
+ this.status = false;
+ this.stopHeartbeat();
+ if (!this.isManualClose) {
+ this.reconnect();
+ }
+ };
+
+ this.ws.onerror = (err) => {
+ console.error('WebSocket 错误:', err);
+ };
+ }
+
+ async processMessageQueue() {
+ if (!this.ws || this.messageQueue.length === 0) return;
+
+ const event = this.messageQueue.shift();
+ try {
+ // 处理消息数据
+ console.log('收到消息数据类型:', event.data.constructor.name);
+ let compressedData;
+
+ // 处理 Blob 或 ArrayBuffer
+ if (event.data instanceof Blob) {
+ console.log('处理 Blob 数据,大小:', event.data.size);
+ compressedData = new Uint8Array(await event.data.arrayBuffer());
+ } else if (event.data instanceof ArrayBuffer) {
+ console.log('处理 ArrayBuffer 数据,大小:', event.data.byteLength);
+ compressedData = new Uint8Array(event.data);
+ } else {
+ throw new Error('不支持的数据类型: ' + event.data.constructor.name);
+ }
+
+ // 使用 DecompressionStream 解压 GZIP 数据
+ const decompressionStream = new DecompressionStream('gzip');
+ const writer = decompressionStream.writable.getWriter();
+ writer.write(compressedData);
+ writer.close();
+
+ const decompressedStream = decompressionStream.readable;
+ const reader = decompressedStream.getReader();
+ let decompressedData = new Uint8Array(0);
+
+ while (true) {
+ const { done, value } = await reader.read();
+ if (done) break;
+ const newData = new Uint8Array(decompressedData.length + value.length);
+ newData.set(decompressedData);
+ newData.set(value, decompressedData.length);
+ decompressedData = newData;
+ }
+
+ // 将解压后的数据转换为字符串
+ const textDecoder = new TextDecoder();
+ const decompressedString = textDecoder.decode(decompressedData);
+ console.log('解压后的数据:', JSON.parse(decompressedString));
+
+ // 解析 JSON
+ const { type, channel, payload,timestamp } = JSON.parse(decompressedString);
+ console.log('后端到前端延迟:', ((new Date()).getTime() - timestamp));
+
+ if (channel && this.events[channel]) {
+ this.events[channel].forEach(callback => callback(payload));
+ } else if (type && this.events[type]) {
+ this.events[type].forEach(callback => callback(payload));
+ }
+ setTimeout(() => this.processMessageQueue(), 0);
+ } catch (err) {
+ console.error('WebSocket 消息解析失败:', err);
+ console.error('错误堆栈:', err.stack);
+ console.error('原始数据类型:', event.data.constructor.name);
+ console.error('原始数据大小:', event.data instanceof Blob ? event.data.size : event.data.byteLength || '未知');
+ }
+ }
+
+ on(eventName, callback) {
+ if (!this.events[eventName]) {
+ this.events[eventName] = [];
+ }
+ this.events[eventName].push(callback);
+ }
+
+ off(eventName, callback) {
+ if (this.events[eventName]) {
+ const index = this.events[eventName].indexOf(callback);
+ if (index !== -1) {
+ this.events[eventName].splice(index, 1);
+ }
+ }
+ }
+
+ send(payload) {
+ if (this.ws && this.ws.readyState === 1) {
+ this.ws.send(JSON.stringify(payload));
+ } else {
+ console.warn('WebSocket 未连接,消息发送失败');
+ }
+ }
+
+ close() {
+ this.isManualClose = true;
+ this.status = false;
+ this.stopHeartbeat();
+ if (this.ws) {
+ this.ws.close();
+ }
+ }
+
+ reconnect() {
+ clearTimeout(this.reconnectTimer);
+ this.reconnectTimer = setTimeout(() => {
+ console.log('尝试重新连接 WebSocket...');
+ this.connect();
+ }, 5000);
+ }
+
+ startHeartbeat() {
+ this.heartbeatTimer = setInterval(() => {
+ this.send({ type: 'heartbeat' });
+ }, 30000);
+ }
+
+ stopHeartbeat() {
+ clearInterval(this.heartbeatTimer);
+ }
+
+ status() {
+ return this.status;
+ }
+}
+
+export default {
+ install(app, options) {
+ const { url, userId } = options;
+ const wsManager = new WebSocketManager(url, userId);
+ wsManager.connect();
+ app.config.globalProperties.$ws = wsManager;
+ app.provide('ws', wsManager);
+ }
+};
\ No newline at end of file
diff --git a/src/router/index.js b/src/router/index.js
new file mode 100644
index 0000000..7bd4be0
--- /dev/null
+++ b/src/router/index.js
@@ -0,0 +1,162 @@
+import { createWebHistory, createRouter } from "vue-router";
+/* Layout */
+import Layout from "@/layout";
+import path from "path";
+
+/**
+ * Note: 路由配置项
+ *
+ * hidden: true // 当设置 true 的时候该路由不会再侧边栏出现 如401,login等页面,或者如一些编辑页面/edit/1
+ * alwaysShow: true // 当你一个路由下面的 children 声明的路由大于1个时,自动会变成嵌套的模式--如组件页面
+ * // 只有一个时,会将那个子路由当做根路由显示在侧边栏--如引导页面
+ * // 若你想不管路由下面的 children 声明的个数都显示你的根路由
+ * // 你可以设置 alwaysShow: true,这样它就会忽略之前定义的规则,一直显示根路由
+ * redirect: noRedirect // 当设置 noRedirect 的时候该路由在面包屑导航中不可被点击
+ * name:'router-name' // 设定路由的名字,一定要填写不然使用时会出现各种问题
+ * query: '{"id": 1, "name": "ry"}' // 访问路由的默认传递参数
+ * roles: ['admin', 'common'] // 访问路由的角色权限
+ * permissions: ['a:a:a', 'b:b:b'] // 访问路由的菜单权限
+ * meta : {
+ noCache: true // 如果设置为true,则不会被 缓存(默认 false)
+ title: 'title' // 设置该路由在侧边栏和面包屑中展示的名字
+ icon: 'svg-name' // 设置该路由的图标,对应路径src/assets/icons/svg
+ breadcrumb: false // 如果设置为false,则不会在breadcrumb面包屑中显示
+ activeMenu: '/system/user' // 当路由设置了该属性,则会高亮相对应的侧边栏。
+ }
+ */
+
+// 公共路由
+export const constantRoutes = [
+ {
+ path: "/redirect",
+ component: Layout,
+ hidden: true,
+ children: [
+ {
+ path: "/redirect/:path(.*)",
+ component: () => import("@/views/redirect/index.vue"),
+ },
+ ],
+ },
+ {
+ path: "/login",
+ component: () => import("@/views/login"),
+ hidden: true,
+ },
+ {
+ path: "/register",
+ component: () => import("@/views/register"),
+ hidden: true,
+ },
+ {
+ path: "/:pathMatch(.*)*",
+ component: () => import("@/views/error/404"),
+ hidden: true,
+ },
+ {
+ path: "/401",
+ component: () => import("@/views/error/401"),
+ hidden: true,
+ },
+ {
+ //cockpit
+ path: "/inspection/cockpit",
+ component: () =>
+ import("@/views/inspection/cockpit/index.vue"),
+ meta: { title: "logicFlow", noLogin: true },
+ hidden: true,
+ },
+ {
+ //logicFlow
+ path: "/flow",
+ component: () => import("@/views/flow/index.vue"),
+ meta: { title: "flow", noLogin: true },
+ hidden: true,
+ },
+ {
+ path: "",
+ component: Layout,
+ redirect: "/index",
+ 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: [
+ {
+ path: "role/:userId(\\d+)",
+ component: () => import("@/views/system/user/authRole"),
+ name: "AuthRole",
+ meta: { title: "分配角色", activeMenu: "/system/user" },
+ },
+ ],
+ },
+ {
+ path: "/system/role-auth",
+ component: Layout,
+ hidden: true,
+ permissions: ["system:role:edit"],
+ children: [
+ {
+ path: "user/:roleId(\\d+)",
+ component: () => import("@/views/system/role/authUser"),
+ name: "AuthUser",
+ meta: { title: "分配用户", activeMenu: "/system/role" },
+ },
+ ],
+ },
+ {
+ path: "/system/dict-data",
+ component: Layout,
+ hidden: true,
+ permissions: ["system:dict:list"],
+ children: [
+ {
+ path: "index/:dictId(\\d+)",
+ component: () => import("@/views/system/dict/data"),
+ name: "Data",
+ meta: { title: "字典数据", activeMenu: "/system/dict" },
+ },
+ ],
+ },
+ {
+ path: "/tool/gen-edit",
+ component: Layout,
+ hidden: true,
+ permissions: ["tool:gen:edit"],
+ children: [
+ {
+ path: "index/:tableId(\\d+)",
+ component: () => import("@/views/tool/gen/editTable"),
+ name: "GenEdit",
+ meta: { title: "修改生成配置", activeMenu: "/tool/gen" },
+ },
+ ],
+ },
+];
+
+const router = createRouter({
+ history: createWebHistory(),
+ routes: constantRoutes,
+ scrollBehavior(to, from, savedPosition) {
+ if (savedPosition) {
+ return savedPosition;
+ }
+ return { top: 0 };
+ },
+});
+
+export default router;
diff --git a/src/settings.js b/src/settings.js
new file mode 100644
index 0000000..08a0108
--- /dev/null
+++ b/src/settings.js
@@ -0,0 +1,47 @@
+export default {
+ /**
+ * 网页标题
+ */
+ title: import.meta.env.VITE_APP_TITLE,
+ /**
+ * 侧边栏主题 深色主题theme-dark,浅色主题theme-light
+ */
+ sideTheme: 'theme-dark',
+ /**
+ * 是否系统布局配置
+ */
+ showSettings: true,
+
+ /**
+ * 是否显示顶部导航
+ */
+ topNav: false,
+
+ /**
+ * 是否显示 tagsView
+ */
+ tagsView: true,
+
+ /**
+ * 是否固定头部
+ */
+ fixedHeader: false,
+
+ /**
+ * 是否显示logo
+ */
+ sidebarLogo: true,
+
+ /**
+ * 是否显示动态标题
+ */
+ dynamicTitle: false,
+
+ /**
+ * @type {string | array} 'production' | ['production', 'development']
+ * @description Need show err logs component.
+ * The default is only used in the production env
+ * If you want to also use it in dev, you can pass ['production', 'development']
+ */
+ errorLog: 'production'
+}
diff --git a/src/store/index.js b/src/store/index.js
new file mode 100644
index 0000000..f10f389
--- /dev/null
+++ b/src/store/index.js
@@ -0,0 +1,3 @@
+const store = createPinia()
+
+export default store
\ No newline at end of file
diff --git a/src/store/modules/app.js b/src/store/modules/app.js
new file mode 100644
index 0000000..0b57159
--- /dev/null
+++ b/src/store/modules/app.js
@@ -0,0 +1,46 @@
+import Cookies from 'js-cookie'
+
+const useAppStore = defineStore(
+ 'app',
+ {
+ state: () => ({
+ sidebar: {
+ opened: Cookies.get('sidebarStatus') ? !!+Cookies.get('sidebarStatus') : true,
+ withoutAnimation: false,
+ hide: false
+ },
+ device: 'desktop',
+ size: Cookies.get('size') || 'default'
+ }),
+ actions: {
+ toggleSideBar(withoutAnimation) {
+ if (this.sidebar.hide) {
+ return false;
+ }
+ this.sidebar.opened = !this.sidebar.opened
+ this.sidebar.withoutAnimation = withoutAnimation
+ if (this.sidebar.opened) {
+ Cookies.set('sidebarStatus', 1)
+ } else {
+ Cookies.set('sidebarStatus', 0)
+ }
+ },
+ closeSideBar({ withoutAnimation }) {
+ Cookies.set('sidebarStatus', 0)
+ this.sidebar.opened = false
+ this.sidebar.withoutAnimation = withoutAnimation
+ },
+ toggleDevice(device) {
+ this.device = device
+ },
+ setSize(size) {
+ this.size = size;
+ Cookies.set('size', size)
+ },
+ toggleSideBarHide(status) {
+ this.sidebar.hide = status
+ }
+ }
+ })
+
+export default useAppStore
diff --git a/src/store/modules/dict.js b/src/store/modules/dict.js
new file mode 100644
index 0000000..27fc308
--- /dev/null
+++ b/src/store/modules/dict.js
@@ -0,0 +1,57 @@
+const useDictStore = defineStore(
+ 'dict',
+ {
+ state: () => ({
+ dict: new Array()
+ }),
+ actions: {
+ // 获取字典
+ getDict(_key) {
+ if (_key == null && _key == "") {
+ return null;
+ }
+ try {
+ for (let i = 0; i < this.dict.length; i++) {
+ if (this.dict[i].key == _key) {
+ return this.dict[i].value;
+ }
+ }
+ } catch (e) {
+ return null;
+ }
+ },
+ // 设置字典
+ setDict(_key, value) {
+ if (_key !== null && _key !== "") {
+ this.dict.push({
+ key: _key,
+ value: value
+ });
+ }
+ },
+ // 删除字典
+ removeDict(_key) {
+ var bln = false;
+ try {
+ for (let i = 0; i < this.dict.length; i++) {
+ if (this.dict[i].key == _key) {
+ this.dict.splice(i, 1);
+ return true;
+ }
+ }
+ } catch (e) {
+ bln = false;
+ }
+ return bln;
+ },
+ // 清空字典
+ cleanDict() {
+ this.dict = new Array();
+ },
+ // 初始字典
+ initDict() {
+ }
+ }
+ })
+
+export default useDictStore
diff --git a/src/store/modules/flow.js b/src/store/modules/flow.js
new file mode 100644
index 0000000..287ae45
--- /dev/null
+++ b/src/store/modules/flow.js
@@ -0,0 +1,22 @@
+import { defineStore } from 'pinia'
+
+export const useFlowStore = defineStore('flow', {
+ state: () => ({
+ disableForm: false,
+ deviceList: []
+ }),
+ actions: {
+ updateDisableForm(value) {
+ this.disableForm = value
+ },
+
+ async getDeviceList() {
+ const data = await new Promise((resolve, reject) => {
+ resolve( ['cam1', 'cam2', 'cam3', 'cam4'])
+ })
+ this.deviceList = data
+ return data
+ }
+ }
+}
+)
diff --git a/src/store/modules/inspection.js b/src/store/modules/inspection.js
new file mode 100644
index 0000000..08efa03
--- /dev/null
+++ b/src/store/modules/inspection.js
@@ -0,0 +1,37 @@
+import { update } from 'lodash'
+import { defineStore } from 'pinia'
+
+export const useInspectionStore = defineStore('inspection', {
+ state: () => ({
+ gridOffscreen: null, // 栅格图像(离屏Canvas)
+ hoveredElement: null, // 当前悬停的元素 {kind, index}
+ animationTimeSeconds: 0, // 全局动画时间戳(秒),用于脉冲动画效果
+ camera: { // 相机参数
+ centerX: 0, // 相机中心的世界坐标 X
+ centerY: 0, // 相机中心的世界坐标 Y
+ pixelsPerMeter: 20, // 缩放:每米对应的屏幕像素数
+ },
+ canvasWidthPx: 0, // Canvas CSS 逻辑宽度(像素)
+ canvasHeightPx: 0, // Canvas CSS 逻辑高度(像素)
+ devicePixelRatio: 1, // 设备像素比(高DPI屏幕 > 1)
+ }),
+ actions: {
+ updateCamera(value) {
+ this.camera = { ...this.camera, ...value }
+ },
+ updateCanvasSize(width, height) {
+ this.canvasWidthPx = width
+ this.canvasHeightPx = height
+ this.devicePixelRatio = window.devicePixelRatio || 1
+ },
+ async getCanvasWH() {
+ const data = {
+ canvasWidthPx: this.canvasWidthPx,
+ canvasHeightPx: this.canvasHeightPx,
+ devicePixelRatio: this.devicePixelRatio,
+ }
+ return data
+ }
+ }
+}
+)
diff --git a/src/store/modules/permission.js b/src/store/modules/permission.js
new file mode 100644
index 0000000..958fe63
--- /dev/null
+++ b/src/store/modules/permission.js
@@ -0,0 +1,142 @@
+import auth from '@/plugins/auth'
+import router, { constantRoutes, dynamicRoutes } from '@/router'
+import { getRouters } from '@/api/menu'
+import Layout from '@/layout/index'
+import ParentView from '@/components/ParentView'
+import InnerLink from '@/layout/components/InnerLink'
+
+// 匹配views里面所有的.vue文件
+const modules = import.meta.glob('./../../views/**/*.vue')
+
+const usePermissionStore = defineStore(
+ 'permission',
+ {
+ state: () => ({
+ routes: [],
+ addRoutes: [],
+ defaultRoutes: [],
+ topbarRouters: [],
+ sidebarRouters: []
+ }),
+ actions: {
+ setRoutes(routes) {
+ this.addRoutes = routes
+ this.routes = constantRoutes.concat(routes)
+ },
+ setDefaultRoutes(routes) {
+ this.defaultRoutes = constantRoutes.concat(routes)
+ },
+ setTopbarRoutes(routes) {
+ this.topbarRouters = routes
+ },
+ setSidebarRouters(routes) {
+ this.sidebarRouters = routes
+ },
+ generateRoutes(roles) {
+ return new Promise(resolve => {
+ // 向后端请求路由数据
+ getRouters().then(res => {
+ const sdata = JSON.parse(JSON.stringify(res.data))
+ const rdata = JSON.parse(JSON.stringify(res.data))
+ const defaultData = JSON.parse(JSON.stringify(res.data))
+ const sidebarRoutes = filterAsyncRouter(sdata)
+ const rewriteRoutes = filterAsyncRouter(rdata, false, true)
+ const defaultRoutes = filterAsyncRouter(defaultData)
+ const asyncRoutes = filterDynamicRoutes(dynamicRoutes)
+ asyncRoutes.forEach(route => { router.addRoute(route) })
+ this.setRoutes(rewriteRoutes)
+ this.setSidebarRouters(constantRoutes.concat(sidebarRoutes))
+ this.setDefaultRoutes(sidebarRoutes)
+ this.setTopbarRoutes(defaultRoutes)
+ resolve(rewriteRoutes)
+ })
+ })
+ }
+ }
+ })
+
+// 遍历后台传来的路由字符串,转换为组件对象
+function filterAsyncRouter(asyncRouterMap, lastRouter = false, type = false) {
+ return asyncRouterMap.filter(route => {
+ if (type && route.children) {
+ route.children = filterChildren(route.children)
+ }
+ if (route.component) {
+ // Layout ParentView 组件特殊处理
+ if (route.component === 'Layout') {
+ route.component = Layout
+ } else if (route.component === 'ParentView') {
+ route.component = ParentView
+ } else if (route.component === 'InnerLink') {
+ route.component = InnerLink
+ } else {
+ route.component = loadView(route.component)
+ }
+ }
+ if (route.children != null && route.children && route.children.length) {
+ route.children = filterAsyncRouter(route.children, route, type)
+ } else {
+ delete route['children']
+ delete route['redirect']
+ }
+ return true
+ })
+}
+
+function filterChildren(childrenMap, lastRouter = false) {
+ var children = []
+ childrenMap.forEach((el, index) => {
+ if (el.children && el.children.length) {
+ if (el.component === 'ParentView' && !lastRouter) {
+ el.children.forEach(c => {
+ c.path = el.path + '/' + c.path
+ if (c.children && c.children.length) {
+ children = children.concat(filterChildren(c.children, c))
+ return
+ }
+ children.push(c)
+ })
+ return
+ }
+ }
+ if (lastRouter) {
+ el.path = lastRouter.path + '/' + el.path
+ if (el.children && el.children.length) {
+ children = children.concat(filterChildren(el.children, el))
+ return
+ }
+ }
+ children = children.concat(el)
+ })
+ return children
+}
+
+// 动态路由遍历,验证是否具备权限
+export function filterDynamicRoutes(routes) {
+ const res = []
+ routes.forEach(route => {
+ if (route.permissions) {
+ if (auth.hasPermiOr(route.permissions)) {
+ res.push(route)
+ }
+ } else if (route.roles) {
+ if (auth.hasRoleOr(route.roles)) {
+ res.push(route)
+ }
+ }
+ })
+ return res
+}
+
+export const loadView = (view) => {
+ let res;
+ for (const path in modules) {
+ const dir = path.split('views/')[1].split('.vue')[0];
+ if (dir === view) {
+ res = () => modules[path]();
+ }
+ }
+ return res;
+}
+
+export default usePermissionStore
diff --git a/src/store/modules/settings.js b/src/store/modules/settings.js
new file mode 100644
index 0000000..194d678
--- /dev/null
+++ b/src/store/modules/settings.js
@@ -0,0 +1,48 @@
+import defaultSettings from '@/settings'
+import { useDark, useToggle } from '@vueuse/core'
+import { useDynamicTitle } from '@/utils/dynamicTitle'
+
+const isDark = useDark()
+const toggleDark = useToggle(isDark)
+
+const { sideTheme, showSettings, topNav, tagsView, fixedHeader, sidebarLogo, dynamicTitle } = defaultSettings
+
+const storageSetting = JSON.parse(localStorage.getItem('layout-setting')) || ''
+
+const useSettingsStore = defineStore(
+ 'settings',
+ {
+ state: () => ({
+ title: '',
+ theme: storageSetting.theme || '#409EFF',
+ sideTheme: storageSetting.sideTheme || sideTheme,
+ showSettings: showSettings,
+ topNav: storageSetting.topNav === undefined ? topNav : storageSetting.topNav,
+ tagsView: storageSetting.tagsView === undefined ? tagsView : storageSetting.tagsView,
+ fixedHeader: storageSetting.fixedHeader === undefined ? fixedHeader : storageSetting.fixedHeader,
+ sidebarLogo: storageSetting.sidebarLogo === undefined ? sidebarLogo : storageSetting.sidebarLogo,
+ dynamicTitle: storageSetting.dynamicTitle === undefined ? dynamicTitle : storageSetting.dynamicTitle,
+ isDark: isDark.value
+ }),
+ actions: {
+ // 修改布局设置
+ changeSetting(data) {
+ const { key, value } = data
+ if (this.hasOwnProperty(key)) {
+ this[key] = value
+ }
+ },
+ // 设置网页标题
+ setTitle(title) {
+ this.title = title
+ useDynamicTitle()
+ },
+ // 切换暗黑模式
+ toggleTheme() {
+ this.isDark = !this.isDark
+ toggleDark()
+ }
+ }
+ })
+
+export default useSettingsStore
diff --git a/src/store/modules/tagsView.js b/src/store/modules/tagsView.js
new file mode 100644
index 0000000..9d07f33
--- /dev/null
+++ b/src/store/modules/tagsView.js
@@ -0,0 +1,182 @@
+const useTagsViewStore = defineStore(
+ 'tags-view',
+ {
+ state: () => ({
+ visitedViews: [],
+ cachedViews: [],
+ iframeViews: []
+ }),
+ actions: {
+ addView(view) {
+ this.addVisitedView(view)
+ this.addCachedView(view)
+ },
+ addIframeView(view) {
+ if (this.iframeViews.some(v => v.path === view.path)) return
+ this.iframeViews.push(
+ Object.assign({}, view, {
+ title: view.meta.title || 'no-name'
+ })
+ )
+ },
+ addVisitedView(view) {
+ if (this.visitedViews.some(v => v.path === view.path)) return
+ this.visitedViews.push(
+ Object.assign({}, view, {
+ title: view.meta.title || 'no-name'
+ })
+ )
+ },
+ addCachedView(view) {
+ if (this.cachedViews.includes(view.name)) return
+ if (!view.meta.noCache) {
+ this.cachedViews.push(view.name)
+ }
+ },
+ delView(view) {
+ return new Promise(resolve => {
+ this.delVisitedView(view)
+ this.delCachedView(view)
+ resolve({
+ visitedViews: [...this.visitedViews],
+ cachedViews: [...this.cachedViews]
+ })
+ })
+ },
+ delVisitedView(view) {
+ return new Promise(resolve => {
+ for (const [i, v] of this.visitedViews.entries()) {
+ if (v.path === view.path) {
+ this.visitedViews.splice(i, 1)
+ break
+ }
+ }
+ this.iframeViews = this.iframeViews.filter(item => item.path !== view.path)
+ resolve([...this.visitedViews])
+ })
+ },
+ delIframeView(view) {
+ return new Promise(resolve => {
+ this.iframeViews = this.iframeViews.filter(item => item.path !== view.path)
+ resolve([...this.iframeViews])
+ })
+ },
+ delCachedView(view) {
+ return new Promise(resolve => {
+ const index = this.cachedViews.indexOf(view.name)
+ index > -1 && this.cachedViews.splice(index, 1)
+ resolve([...this.cachedViews])
+ })
+ },
+ delOthersViews(view) {
+ return new Promise(resolve => {
+ this.delOthersVisitedViews(view)
+ this.delOthersCachedViews(view)
+ resolve({
+ visitedViews: [...this.visitedViews],
+ cachedViews: [...this.cachedViews]
+ })
+ })
+ },
+ delOthersVisitedViews(view) {
+ return new Promise(resolve => {
+ this.visitedViews = this.visitedViews.filter(v => {
+ return v.meta.affix || v.path === view.path
+ })
+ this.iframeViews = this.iframeViews.filter(item => item.path === view.path)
+ resolve([...this.visitedViews])
+ })
+ },
+ delOthersCachedViews(view) {
+ return new Promise(resolve => {
+ const index = this.cachedViews.indexOf(view.name)
+ if (index > -1) {
+ this.cachedViews = this.cachedViews.slice(index, index + 1)
+ } else {
+ this.cachedViews = []
+ }
+ resolve([...this.cachedViews])
+ })
+ },
+ delAllViews(view) {
+ return new Promise(resolve => {
+ this.delAllVisitedViews(view)
+ this.delAllCachedViews(view)
+ resolve({
+ visitedViews: [...this.visitedViews],
+ cachedViews: [...this.cachedViews]
+ })
+ })
+ },
+ delAllVisitedViews(view) {
+ return new Promise(resolve => {
+ const affixTags = this.visitedViews.filter(tag => tag.meta.affix)
+ this.visitedViews = affixTags
+ this.iframeViews = []
+ resolve([...this.visitedViews])
+ })
+ },
+ delAllCachedViews(view) {
+ return new Promise(resolve => {
+ this.cachedViews = []
+ resolve([...this.cachedViews])
+ })
+ },
+ updateVisitedView(view) {
+ for (let v of this.visitedViews) {
+ if (v.path === view.path) {
+ v = Object.assign(v, view)
+ break
+ }
+ }
+ },
+ delRightTags(view) {
+ return new Promise(resolve => {
+ const index = this.visitedViews.findIndex(v => v.path === view.path)
+ if (index === -1) {
+ return
+ }
+ this.visitedViews = this.visitedViews.filter((item, idx) => {
+ if (idx <= index || (item.meta && item.meta.affix)) {
+ return true
+ }
+ const i = this.cachedViews.indexOf(item.name)
+ if (i > -1) {
+ this.cachedViews.splice(i, 1)
+ }
+ if(item.meta.link) {
+ const fi = this.iframeViews.findIndex(v => v.path === item.path)
+ this.iframeViews.splice(fi, 1)
+ }
+ return false
+ })
+ resolve([...this.visitedViews])
+ })
+ },
+ delLeftTags(view) {
+ return new Promise(resolve => {
+ const index = this.visitedViews.findIndex(v => v.path === view.path)
+ if (index === -1) {
+ return
+ }
+ this.visitedViews = this.visitedViews.filter((item, idx) => {
+ if (idx >= index || (item.meta && item.meta.affix)) {
+ return true
+ }
+ const i = this.cachedViews.indexOf(item.name)
+ if (i > -1) {
+ this.cachedViews.splice(i, 1)
+ }
+ if(item.meta.link) {
+ const fi = this.iframeViews.findIndex(v => v.path === item.path)
+ this.iframeViews.splice(fi, 1)
+ }
+ return false
+ })
+ resolve([...this.visitedViews])
+ })
+ }
+ }
+ })
+
+export default useTagsViewStore
diff --git a/src/store/modules/user.js b/src/store/modules/user.js
new file mode 100644
index 0000000..1e59417
--- /dev/null
+++ b/src/store/modules/user.js
@@ -0,0 +1,75 @@
+import { login, logout, getInfo } from '@/api/login'
+import { getToken, setToken, removeToken } from '@/utils/auth'
+import { isHttp, isEmpty } from "@/utils/validate"
+import defAva from '@/assets/images/profile.jpg'
+
+const useUserStore = defineStore(
+ 'user',
+ {
+ state: () => ({
+ token: getToken(),
+ id: '',
+ name: '',
+ avatar: '',
+ roles: [],
+ permissions: []
+ }),
+ actions: {
+ // 登录
+ login(userInfo) {
+ const username = userInfo.username.trim()
+ const password = userInfo.password
+ const code = userInfo.code
+ const uuid = userInfo.uuid
+ return new Promise((resolve, reject) => {
+ login(username, password, code, uuid).then(res => {
+ setToken(res.token)
+ this.token = res.token
+ resolve()
+ }).catch(error => {
+ reject(error)
+ })
+ })
+ },
+ // 获取用户信息
+ getInfo() {
+ return new Promise((resolve, reject) => {
+ getInfo().then(res => {
+ const user = res.user
+ let avatar = user.avatar || ""
+ if (!isHttp(avatar)) {
+ avatar = (isEmpty(avatar)) ? defAva : import.meta.env.VITE_APP_BASE_API + avatar
+ }
+ if (res.roles && res.roles.length > 0) { // 验证返回的roles是否是一个非空数组
+ this.roles = res.roles
+ this.permissions = res.permissions
+ } else {
+ this.roles = ['ROLE_DEFAULT']
+ }
+ this.id = user.userId
+ this.name = user.userName
+ this.avatar = avatar
+ resolve(res)
+ }).catch(error => {
+ reject(error)
+ })
+ })
+ },
+ // 退出系统
+ logOut() {
+ return new Promise((resolve, reject) => {
+ logout(this.token).then(() => {
+ this.token = ''
+ this.roles = []
+ this.permissions = []
+ removeToken()
+ resolve()
+ }).catch(error => {
+ reject(error)
+ })
+ })
+ }
+ }
+ })
+
+export default useUserStore
diff --git a/src/style/variable.scss b/src/style/variable.scss
new file mode 100644
index 0000000..5d8b52f
--- /dev/null
+++ b/src/style/variable.scss
@@ -0,0 +1,71 @@
+$br: 1px solid #e4e7ed;
+$brdeg: 8px;
+@mixin br6 {
+ border: $br;
+ border-radius: $brdeg;
+}
+
+.p10 {
+ padding: 10px;
+ height: 100%;
+}
+
+.mb10 {
+ margin-bottom: 10px;
+ user-select: none;
+}
+
+.mb5 {
+ margin-bottom: 5px;
+}
+
+.flex {
+ display: flex;
+}
+
+.flex1 {
+ flex: 1
+}
+
+.align-center {
+ align-items: center;
+}
+
+.flex-col {
+ flex-direction: column;
+}
+
+.between {
+ justify-content: space-between;
+}
+
+.justify-content-center {
+ justify-content: center;
+}
+
+.bg {
+ background-color: #409eff;
+}
+
+.scrollable {
+ //scrollbar-gutter: stable;
+ scrollbar-color: rgba(0, 0, 0, .8) transparent;
+ transition: scrollbar-color .1s ease-out
+}
+
+.scrollable:hover {
+ scrollbar-color: rgba(0, 0, 0, .15) transparent
+}
+
+.scrollable::-webkit-scrollbar {
+ background-color: transparent
+}
+
+.scrollable::-webkit-scrollbar-thumb {
+ background-color: rgba(0, 0, 0, .8)
+}
+
+.scrollable::-webkit-scrollbar-track {
+ background-color: transparent
+}
+
diff --git a/src/utils/FileUtils.js b/src/utils/FileUtils.js
new file mode 100644
index 0000000..59ec411
--- /dev/null
+++ b/src/utils/FileUtils.js
@@ -0,0 +1,144 @@
+/**
+ * File operation utility functions
+ */
+
+/**
+ * Read file content as text
+ */
+export function readFileContent(file) {
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.onload = (e) => resolve(e.target.result);
+ reader.onerror = reject;
+ reader.readAsText(file);
+ });
+}
+
+/**
+ * Get File object from file system entry
+ */
+export function getFileFromEntry(entry) {
+ return new Promise((resolve, reject) => {
+ entry.file(resolve, reject);
+ });
+}
+
+/**
+ * Recursively read directory
+ */
+export async function readDirectory(dirEntry, fileMap) {
+ const files = [];
+
+ return new Promise((resolve, reject) => {
+ const reader = dirEntry.createReader();
+
+ function readEntries() {
+ reader.readEntries(async (entries) => {
+ if (entries.length === 0) {
+ resolve(files);
+ return;
+ }
+
+ for (const entry of entries) {
+ if (entry.isFile) {
+ const file = await getFileFromEntry(entry);
+ const path = entry.fullPath || entry.name;
+ fileMap.set(path, file);
+ files.push(file);
+ } else if (entry.isDirectory) {
+ const subFiles = await readDirectory(entry, fileMap);
+ files.push(...subFiles);
+ }
+ }
+
+ readEntries();
+ }, reject);
+ }
+
+ readEntries();
+ });
+}
+
+/**
+ * Get file type from extension
+ */
+export function getFileTypeFromExtension(ext) {
+ const typeMap = {
+ 'urdf': 'urdf',
+ 'xacro': 'xacro',
+ 'xml': 'mjcf',
+ 'usd': 'usd',
+ 'usda': 'usd',
+ 'usdc': 'usd',
+ 'usdz': 'usd'
+ };
+ return typeMap[ext] || 'unknown';
+}
+
+/**
+ * Get file display type
+ */
+export function getFileDisplayType(ext, fileName) {
+ const modelExts = ['urdf', 'xacro', 'xml', 'usd', 'usda', 'usdc', 'usdz'];
+ const meshExts = ['dae', 'stl', 'obj', 'collada'];
+
+ if (modelExts.includes(ext)) {
+ return 'model';
+ } else if (meshExts.includes(ext)) {
+ return 'mesh';
+ }
+ return 'file';
+}
+
+/**
+ * Normalize path
+ */
+export function normalizePath(path) {
+ if (!path) return '';
+ return path.replace(/\\/g, '/').replace(/^\/+/, '').replace(/\/+/g, '/');
+}
+
+export const detectFileType = (fileName, content) => {
+ const ext = fileName.toLowerCase().split('.').pop();
+
+ switch (ext) {
+ case 'urdf':
+ return 'urdf';
+ case 'xacro':
+ return 'xacro';
+ case 'xml':
+ if (content) {
+ if (isValidRobotMJCF(content)) {
+ return 'mjcf';
+ } else {
+ return null;
+ }
+ }
+ return null;
+ case 'usd':
+ case 'usda':
+ case 'usdc':
+ case 'usdz':
+ return 'usd';
+ default:
+ return null;
+ }
+}
+
+const isValidRobotMJCF = (content) => {
+ if (!content || typeof content !== 'string') {
+ return false;
+ }
+
+ if (!content.includes(' {
+ let nodeRedData = []
+ const data = await getFlowById(id)
+ if (data.nodes) {
+ nodeRedData = data.nodes
+ } else {
+ console.error('请求失败')
+ }
+ return nodeRedData
+}
+
+const mapNodeType = (nodeRedType) => {
+ const typeMap = {
+ 'group': 'customGroup', // Node-RED的inject节点转为圆形
+ // 'inject': 'circle', // Node-RED的inject节点转为圆形
+ // 'debug': 'diamond', // debug节点转为菱形
+ // 'function': 'rect' // 函数节点转为矩形
+ };
+ return typeMap[nodeRedType] || 'CustomNode'; // 默认矩形
+}
+
+export const convertNodeRedToLogicFlow = async (id) => {
+ const nodeRedData = await filterNodeRedData(id)
+ // 节点数据
+ const nodes = nodeRedData.map((node) => {
+ return {
+ id: node.id, // 或生成新ID:`node-${index}`
+ type: mapNodeType(node.type), // 映射节点类型
+ x: node.x * 2,
+ y: node.y * 2,
+ text: node.name || node.type, // 使用节点名称或类型作为文本
+ children: node.type === 'group' ? node.nodes : null,
+ properties: {
+ ...node,
+ state: 'unexecuted',
+ }
+ };
+ });
+
+ const edges = []
+ // 连线数据
+ nodeRedData.forEach((item) => {
+ if (item.wires && item.wires.length > 0) {
+ item.wires.forEach((w, index) => {
+ w.forEach((id) => {
+ const obj = {
+ id: `edge-${item.id}-${id}`, // 或使用唯一标识生成方式
+ type: 'bezier',
+ sourceNodeId: item.id,
+ targetNodeId: id,
+ sourceAnchorId: item.id + '_1',
+ targetAnchorId: id + '_3'
+ }
+ edges.push(obj)
+ })
+ })
+ }
+ })
+ return { nodes, edges }
+}
+
+export const recursiveFilter = (data, id, result = [], type = 'customGroup') => {
+ data.forEach((item) => {
+ if (item.type === type && item.properties.nodes && item.properties.nodes.includes(id)) {
+ result.push(item)
+ recursiveFilter(data, item.id, result)
+ }
+ });
+ return result
+}
+
+export const changeOtherNodeState = (nodes, lf) => {
+ const filterArr = ['inject', 'cmvrLoop', 'startCamera', 'getImage', 'stopCamera', 'startVideo', 'stopVideo', 'end']
+ const loopNodes = nodes.filter(item => {
+ return !filterArr.includes(item.properties.type)
+ })
+ loopNodes.forEach(l => {
+ nodes.forEach(n => {
+ if (l.properties.wires[0].includes(n.id)) {
+ const { state } = lf.getProperties(n.id)
+ if (state !== 'unexecuted') {
+ lf.setProperties(l.id, {
+ state: 'success'
+ })
+ }
+ }
+
+ if (l.properties?.wires[1]?.includes(n.id)) {
+ const { state } = lf.getProperties(n.id)
+ if (state !== 'failed') {
+ lf.setProperties(l.id, {
+ state: 'running'
+ })
+ } else {
+ lf.setProperties(l.id, {
+ state: 'failed'
+ })
+ }
+ }
+ })
+ })
+}
\ No newline at end of file
diff --git a/src/utils/dict.js b/src/utils/dict.js
new file mode 100644
index 0000000..9648f14
--- /dev/null
+++ b/src/utils/dict.js
@@ -0,0 +1,24 @@
+import useDictStore from '@/store/modules/dict'
+import { getDicts } from '@/api/system/dict/data'
+
+/**
+ * 获取字典数据
+ */
+export function useDict(...args) {
+ const res = ref({});
+ return (() => {
+ args.forEach((dictType, index) => {
+ res.value[dictType] = [];
+ const dicts = useDictStore().getDict(dictType);
+ if (dicts) {
+ res.value[dictType] = dicts;
+ } else {
+ getDicts(dictType).then(resp => {
+ res.value[dictType] = resp.data.map(p => ({ label: p.dictLabel, value: p.dictValue, elTagType: p.listClass, elTagClass: p.cssClass }))
+ useDictStore().setDict(dictType, res.value[dictType]);
+ })
+ }
+ })
+ return toRefs(res.value);
+ })()
+}
\ No newline at end of file
diff --git a/src/utils/dynamicTitle.js b/src/utils/dynamicTitle.js
new file mode 100644
index 0000000..64404b2
--- /dev/null
+++ b/src/utils/dynamicTitle.js
@@ -0,0 +1,15 @@
+import store from '@/store'
+import defaultSettings from '@/settings'
+import useSettingsStore from '@/store/modules/settings'
+
+/**
+ * 动态修改标题
+ */
+export function useDynamicTitle() {
+ const settingsStore = useSettingsStore();
+ if (settingsStore.dynamicTitle) {
+ document.title = settingsStore.title + ' - ' + defaultSettings.title;
+ } else {
+ document.title = defaultSettings.title;
+ }
+}
\ No newline at end of file
diff --git a/src/utils/errorCode.js b/src/utils/errorCode.js
new file mode 100644
index 0000000..d2111ee
--- /dev/null
+++ b/src/utils/errorCode.js
@@ -0,0 +1,6 @@
+export default {
+ '401': '认证失败,无法访问系统资源',
+ '403': '当前操作没有权限',
+ '404': '访问资源不存在',
+ 'default': '系统未知错误,请反馈给管理员'
+}
diff --git a/src/utils/eventBus.js b/src/utils/eventBus.js
new file mode 100644
index 0000000..48c05ea
--- /dev/null
+++ b/src/utils/eventBus.js
@@ -0,0 +1,3 @@
+
+import mitt from 'mitt';
+export const emitter = mitt();
diff --git a/src/utils/flow.js b/src/utils/flow.js
new file mode 100644
index 0000000..677076b
--- /dev/null
+++ b/src/utils/flow.js
@@ -0,0 +1,370 @@
+export const getInput = (nodeId, isStrict = true) => {
+ const data = lf.getGraphData();
+ // let flag = false
+ // if (isStrict) {
+ // data.edges.forEach(item => {
+ // if (item.targetNodeId === nodeId) {
+ // flag = true
+ // }
+ // });
+ // } else {
+ // flag = true
+ // }
+
+ // if (flag) {
+ const arr = []
+ const filterNodes = ['sleep', 'stopLoop', 'subEnd', 'subStart', 'branch', 'end']
+ data.nodes.forEach(item => {
+ if (item.id === nodeId) {
+ return
+ }
+
+ if (filterNodes.includes(item.type)) {
+ return
+ }
+
+ const obj = {
+ value: item.id,
+ label: item.properties.name,
+ children: [{
+ value: 'input',
+ label: '输入',
+ type: 'input',
+ children: []
+ }, {
+ value: 'output',
+ label: '输出',
+ type: 'output',
+ children: []
+ }]
+ }
+
+ if (item.properties.inputParams) {
+ transformTree(item.properties.inputParams, obj.children[0].children, 'input')
+ }
+ if (item.properties.nodeParams) {
+ transformTree(item.properties.nodeParams, obj.children[0].children, 'input')
+ }
+ if (item.properties.outputParams) {
+ transformTree(item.properties.outputParams, obj.children[1].children, 'output')
+ }
+ arr.push(obj)
+ })
+ return arr
+ // }
+ // return flag
+}
+
+const transformTree = (arr, parent = [], type) => {
+ arr.forEach(item => {
+ const currentNode = {
+ value: item.name,
+ label: item.name,
+ type,
+ children: []
+ };
+ parent.push(currentNode);
+
+ if (item.children?.length > 0) {
+ transformTree(item.children, currentNode.children, type);
+ } else {
+ delete currentNode.children
+ }
+ });
+ return parent;
+};
+
+export const getStartNodeFormData = () => {
+ const flowData = lf.getGraphData();
+ const startNode = flowData.nodes.find(item => item.type === 'start')
+ const data = addValueProperty(startNode.properties?.inputParams || [])
+ return data
+}
+
+export const addValueProperty = (data, schemeId = '') => {
+ // 处理单个对象或对象数组
+ const processItem = (item, index, path) => {
+ // 根据类型设置默认值
+ item.value = item.name === 'schemeId' ? schemeId : item.type.includes('array') ? '[0]' : item.type === 'number' ? 0 : item.type === 'boolean' ? false : '';
+ item.propPath = `${path}.${index}.`
+ // if (item.type.includes('array') && !item.type.includes('object')) {
+ // const type = item.type.match(/<([^>]+)>/)[1]
+ // item.children = [{ name: 'Array Item', type, value: '', propPath: item.propPath + '.0'}]
+ // }
+ // 递归处理子项
+ if (item.children && item.children.length > 0) {
+ item.children = item.children.map((cItem, cIndex) => {
+ return processItem(cItem, cIndex , item.propPath + 'children')
+ });
+ }
+
+ return item;
+ };
+
+ // 如果传入的是数组,处理每个元素;否则处理单个对象
+ return Array.isArray(data) ? data.map((item, index) => processItem(item, index, '')) : processItem(data, 0, '')
+}
+
+export const recursiveFilter = (data, id, type = ['loop', 'customGroup'], result = []) => {
+ data.forEach((item) => {
+ if (type.includes(item.type) && item.children && item.children.includes(id)) {
+ result.push(item)
+ recursiveFilter(data, item.id, type, result)
+ }
+ });
+ return result
+}
+
+export const formatTableData = (arr, data = {}) => {
+ arr.forEach(item => {
+ data[item.name] = item.value
+ if (item?.children && item.children.length > 0) {
+ data[item.name] = {}
+ formatTableData(item.children, data[item.name])
+ }
+ })
+ return data
+}
+
+export const addNewEdge = (nodeId) => {
+ const { edges } = lf.getGraphData();
+ const ttt = edges.slice(); // 浅拷贝(比 JSON 深拷贝高效 10x+)
+
+ const edgesToDelete = ttt.filter(_edge =>
+ _edge.sourceNodeId === nodeId || _edge.targetNodeId === nodeId
+ );
+
+ edgesToDelete.forEach(edge => lf.deleteEdge(edge.id));
+
+ if (window.addNewEdgeTimer) {
+ clearTimeout(window.addNewEdgeTimer); // 清理上一次未执行的定时器
+ }
+
+ window.addNewEdgeTimer = setTimeout(() => {
+ edgesToDelete.forEach(item => {
+ lf.addEdge({
+ type: "bezier",
+ sourceNodeId: item.sourceNodeId,
+ targetNodeId: item.targetNodeId,
+ sourceAnchorId: item.sourceAnchorId,
+ targetAnchorId: item.targetAnchorId
+ });
+ })
+ delete window.addNewEdgeTimer;
+ }, 50)
+}
+
+export const newEdge = (nodeId) => {
+ const { edges } = lf.getGraphData();
+ const arr = JSON.parse(JSON.stringify(edges))
+ arr.forEach(_edge => {
+ const { sourceNodeId, targetNodeId, sourceAnchorId, targetAnchorId } = _edge
+ if (sourceNodeId === nodeId || targetNodeId === nodeId) {
+ lf.deleteEdge(_edge.id);
+ lf.addEdge({
+ type: "bezier",
+ sourceNodeId: sourceNodeId,
+ targetNodeId: targetNodeId,
+ sourceAnchorId: sourceAnchorId,
+ targetAnchorId: targetAnchorId
+ });
+ }
+ })
+}
+
+export const initNodeZoom = (modelId, nodeZoom, className, init=false) => {
+ const nodes = document.getElementsByClassName(modelId)
+ if (nodes.length > 0) {
+ const node = nodes[0]
+ const parent = node.closest(className);
+ if (nodeZoom) {
+ parent.style.width = '680px'
+ } else {
+ parent.style.width = '380px'
+ }
+ if (!init) {
+ addNewEdge(modelId)
+ }
+ }
+}
+
+export const getInputNumber = (nodeId) => {
+ const data = lf.getGraphData();
+ let flag = false
+ data.edges.forEach(item => {
+ if (item.targetNodeId === nodeId) {
+ flag = true
+ }
+ });
+ if (flag) {
+ const arr = []
+ data.nodes.forEach(item => {
+ if (item.properties.inputParams) {
+ item.properties.inputParams.forEach(item => {
+ if (item.type === 'number') {
+ arr.push(item)
+ }
+ })
+ }
+ })
+ return arr
+ }
+ return flag
+}
+
+export const removeSwitchEdge = (nodeId, sourceAnchorId) => {
+ const { edges } = lf.getGraphData();
+ const edge = edges.find(item => {
+ return item.sourceNodeId === nodeId && item.sourceAnchorId === sourceAnchorId
+ })
+
+ if (edge) {
+ lf.deleteEdge(edge.id);
+ }
+}
+
+export const convertToTree = (data) => {
+ // 创建 id 到节点的映射
+ const nodeMap = new Map();
+ data.forEach(node => {
+ nodeMap.set(node.id, { ...node, name: node.properties.name || '' }); // 浅拷贝节点
+ });
+
+ // 标记非根节点(被其他节点引用的节点)
+ const nonRootIds = new Set();
+ data.forEach(node => {
+ if (node.children) {
+ node.children.forEach(childId => {
+ nonRootIds.add(childId);
+ });
+ }
+ });
+
+ // 构建树形结构
+ const result = [];
+ data.forEach(node => {
+ // 只处理根节点(未被引用的节点)
+ if (!nonRootIds.has(node.id)) {
+ const rootNode = nodeMap.get(node.id);
+ // 递归处理子节点
+ const processNode = (currentNode) => {
+ if (currentNode.children) {
+ currentNode.children = currentNode.children.map(childId => {
+ const childNode = nodeMap.get(childId);
+ processNode(childNode); // 递归处理子节点
+ return childNode;
+ });
+ }
+ return currentNode;
+ };
+
+ result.push(processNode(rootNode));
+ }
+ });
+
+ return result;
+}
+
+
+export const filterEmptyName = (obj) => {
+ // 1. 如果是数组,遍历每一项递归处理
+ if (Array.isArray(obj)) {
+ return obj
+ .map(item => filterEmptyName(item)) // 先递归处理子项
+ .filter(item => item !== null); // 过滤掉被剔除的空项
+ }
+
+ // 2. 如果是对象,先检查 name 是否为空,为空直接返回 null(剔除)
+ if (typeof obj === 'object' && obj !== null) {
+ // 核心:name 为空字符串 → 直接剔除这个对象
+ if (obj.name === '') {
+ return null;
+ }
+
+ // 创建新对象,保留原有属性
+ const newObj = { ...obj };
+
+ // 如果有 children,递归过滤子节点
+ if (newObj.children && Array.isArray(newObj.children)) {
+ newObj.children = filterEmptyName(newObj.children);
+ }
+
+ return newObj;
+ }
+
+ // 基础类型直接返回
+ return obj;
+}
+
+/**
+ * 将原始表单配置数组 转换为 目标对象格式
+ * @param {Array} source - 原始数据数组
+ * @returns {Object} 转换后的目标格式对象
+ */
+export const transformHttpNodeData = (source) => {
+ // 初始化结果对象
+ const result = {};
+
+ // 遍历每一个顶级节点
+ source.forEach(item => {
+ const { name, children } = item;
+ if (!name) return; // 过滤无name的无效节点
+
+ // 处理普通节点(config/headers/params):直接取 children 数组
+ if (name !== 'body') {
+ result[name] = children || [];
+ return;
+ }
+
+ // 专门处理 body 节点(特殊结构)
+ if (name === 'body') {
+ const bodyObj = {
+ type: '',
+ json: '',
+ formData: []
+ };
+
+ // 遍历body的子项,赋值到对应字段
+ children?.forEach(child => {
+ const childName = child.name;
+ if (childName === 'bodyType') {
+ bodyObj.type = child.input;
+ }
+ if (childName === 'json') {
+ bodyObj.json = child.input;
+ }
+ if (childName === 'formData') {
+ bodyObj.formData = child.children || [];
+ }
+ });
+
+ result.body = bodyObj;
+ }
+ });
+
+ return result;
+}
+
+export const transformSdAgentNodeData = (source) => {
+ // 初始化结果对象
+ const result = {};
+
+ // 遍历每一个顶级节点
+ source.forEach(item => {
+ const { name, children } = item;
+ if (!name) return; // 过滤无name的无效节点
+
+ // 处理普通节点(config/headers/params):直接取 children 数组
+ if (name !== 'invokeTts') {
+ result[name] = children || [];
+ return;
+ }
+
+ // 专门处理 tts 节点(特殊结构)
+ if (name === 'invokeTts') {
+ result.invokeTts = item.input;
+ }
+ });
+
+ return result;
+}
\ No newline at end of file
diff --git a/src/utils/fn.js b/src/utils/fn.js
new file mode 100644
index 0000000..f2963bc
--- /dev/null
+++ b/src/utils/fn.js
@@ -0,0 +1,94 @@
+/**
+ * 防抖函数(在指定时间内,如果事件被频繁触发,则只有在最后一次触发后指定的时间间隔内没有再次触发时,才执行函数)
+ */
+export function debounce(func, delay) {
+ let timer; // 用于存储定时器的标识符
+ return function (...args) {
+ // 每次触发事件时,清除之前的定时器
+ clearTimeout(timer);
+ // 设置新的定时器
+ timer = setTimeout(() => {
+ func.apply(this, args); // 执行目标函数
+ }, delay);
+ };
+}
+
+/**
+ * 节流函数(在指定时间内,无论事件触发多少次,只执行一次函数)
+ */
+export function throttle(func, limit) {
+ let inThrottle;
+ return function (...args) {
+ if (!inThrottle) {
+ func.apply(this, args);
+ inThrottle = true;
+ setTimeout(() => {
+ inThrottle = false;
+ }, limit);
+ }
+ };
+}
+
+/*
+* 找出两个对象之间的不同点
+* */
+export function findDifferences(oldData, newData) {
+ // 创建一个结果对象来存储不同的键值对
+ const differences = {};
+
+ // 获取两个对象的所有键
+ const keys = new Set([...Object.keys(oldData), ...Object.keys(newData)]);
+
+ // 遍历所有键
+ keys.forEach((key) => {
+ // 检查两个对象中该键的值是否不同
+ if (oldData[key] !== newData[key]) {
+ differences[key] = newData[key];
+ }
+ });
+ return differences;
+}
+
+/*
+* 将对象中的数值转换为字符串
+* */
+export function convertNumbersToStringAndRemoveNulls(obj) {
+ const result = {};
+ for (const key in obj) {
+ if (obj.hasOwnProperty(key)) {
+ const value = obj[key];
+ // 如果值为 null,跳过该属性
+ if (value === null) {
+ continue;
+ }
+ // 检查值是否为数字
+ if (typeof value === 'number') {
+ result[key] = value.toString();
+ } else {
+ result[key] = value;
+ }
+ }
+ }
+ return result;
+}
+
+/*
+* 参数编码拼接
+* */
+export function appendParamsToPath(path, params) {
+ // 创建一个 URLSearchParams 对象
+ const searchParams = new URLSearchParams();
+
+ // 遍历参数对象,将每个键值对添加到 URLSearchParams 中
+ for (const key in params) {
+ if (params.hasOwnProperty(key)) {
+ const value = params[key];
+ // 对值进行编码
+ searchParams.append(key, encodeURIComponent(value));
+ }
+ }
+
+ // 将查询字符串拼接到路径后面
+ const queryString = searchParams.toString();
+ return `${path}?${queryString}`;
+}
diff --git a/src/utils/generator/config.js b/src/utils/generator/config.js
new file mode 100644
index 0000000..449715f
--- /dev/null
+++ b/src/utils/generator/config.js
@@ -0,0 +1,452 @@
+export const formConf = {
+ formRef: 'formRef',
+ formModel: 'formData',
+ size: 'default',
+ labelPosition: 'right',
+ labelWidth: 100,
+ formRules: 'rules',
+ gutter: 15,
+ disabled: false,
+ span: 24,
+ formBtns: true,
+}
+
+export const inputComponents = [
+ {
+ label: '单行文本',
+ tag: 'el-input',
+ tagIcon: 'input',
+ type: 'text',
+ placeholder: '请输入',
+ defaultValue: undefined,
+ span: 24,
+ labelWidth: null,
+ style: { width: '100%' },
+ clearable: true,
+ prepend: '',
+ append: '',
+ 'prefix-icon': '',
+ 'suffix-icon': '',
+ maxlength: null,
+ 'show-word-limit': false,
+ readonly: false,
+ disabled: false,
+ required: true,
+ regList: [],
+ changeTag: true,
+ document: 'https://element-plus.org/zh-CN/component/input',
+ },
+ {
+ label: '多行文本',
+ tag: 'el-input',
+ tagIcon: 'textarea',
+ type: 'textarea',
+ placeholder: '请输入',
+ defaultValue: undefined,
+ span: 24,
+ labelWidth: null,
+ autosize: {
+ minRows: 4,
+ maxRows: 4,
+ },
+ style: { width: '100%' },
+ maxlength: null,
+ 'show-word-limit': false,
+ readonly: false,
+ disabled: false,
+ required: true,
+ regList: [],
+ changeTag: true,
+ document: 'https://element-plus.org/zh-CN/component/input',
+ },
+ {
+ label: '密码',
+ tag: 'el-input',
+ tagIcon: 'password',
+ type: 'password',
+ placeholder: '请输入',
+ defaultValue: undefined,
+ span: 24,
+ 'show-password': true,
+ labelWidth: null,
+ style: { width: '100%' },
+ clearable: true,
+ prepend: '',
+ append: '',
+ 'prefix-icon': '',
+ 'suffix-icon': '',
+ maxlength: null,
+ 'show-word-limit': false,
+ readonly: false,
+ disabled: false,
+ required: true,
+ regList: [],
+ changeTag: true,
+ document: 'https://element-plus.org/zh-CN/component/input',
+ },
+ {
+ label: '计数器',
+ tag: 'el-input-number',
+ tagIcon: 'number',
+ placeholder: '',
+ defaultValue: undefined,
+ span: 24,
+ labelWidth: null,
+ min: undefined,
+ max: undefined,
+ step: undefined,
+ 'step-strictly': false,
+ precision: undefined,
+ 'controls-position': '',
+ disabled: false,
+ required: true,
+ regList: [],
+ changeTag: true,
+ document: 'https://element-plus.org/zh-CN/component/input-number',
+ },
+]
+
+export const selectComponents = [
+ {
+ label: '下拉选择',
+ tag: 'el-select',
+ tagIcon: 'select',
+ placeholder: '请选择',
+ defaultValue: undefined,
+ span: 24,
+ labelWidth: null,
+ style: { width: '100%' },
+ clearable: true,
+ disabled: false,
+ required: true,
+ filterable: false,
+ multiple: false,
+ options: [
+ {
+ label: '选项一',
+ value: 1,
+ },
+ {
+ label: '选项二',
+ value: 2,
+ },
+ ],
+ regList: [],
+ changeTag: true,
+ document: 'https://element-plus.org/zh-CN/component/select',
+ },
+ {
+ label: '级联选择',
+ tag: 'el-cascader',
+ tagIcon: 'cascader',
+ placeholder: '请选择',
+ defaultValue: [],
+ span: 24,
+ labelWidth: null,
+ style: { width: '100%' },
+ props: {
+ props: {
+ multiple: false,
+ },
+ },
+ 'show-all-levels': true,
+ disabled: false,
+ clearable: true,
+ filterable: false,
+ required: true,
+ options: [
+ {
+ id: 1,
+ value: 1,
+ label: '选项1',
+ children: [
+ {
+ id: 2,
+ value: 2,
+ label: '选项1-1',
+ },
+ ],
+ },
+ ],
+ dataType: 'dynamic',
+ labelKey: 'label',
+ valueKey: 'value',
+ childrenKey: 'children',
+ separator: '/',
+ regList: [],
+ changeTag: true,
+ document: 'https://element-plus.org/zh-CN/component/cascader',
+ },
+ {
+ label: '单选框组',
+ tag: 'el-radio-group',
+ tagIcon: 'radio',
+ defaultValue: 0,
+ span: 24,
+ labelWidth: null,
+ style: {},
+ optionType: 'default',
+ border: false,
+ size: 'default',
+ disabled: false,
+ required: true,
+ options: [
+ {
+ label: '选项一',
+ value: 1,
+ },
+ {
+ label: '选项二',
+ value: 2,
+ },
+ ],
+ regList: [],
+ changeTag: true,
+ document: 'https://element-plus.org/zh-CN/component/radio',
+ },
+ {
+ label: '多选框组',
+ tag: 'el-checkbox-group',
+ tagIcon: 'checkbox',
+ defaultValue: [],
+ span: 24,
+ labelWidth: null,
+ style: {},
+ optionType: 'default',
+ border: false,
+ size: 'default',
+ disabled: false,
+ required: true,
+ options: [
+ {
+ label: '选项一',
+ value: 1,
+ },
+ {
+ label: '选项二',
+ value: 2,
+ },
+ ],
+ regList: [],
+ changeTag: true,
+ document: 'https://element-plus.org/zh-CN/component/checkbox',
+ },
+ {
+ label: '开关',
+ tag: 'el-switch',
+ tagIcon: 'switch',
+ defaultValue: false,
+ span: 24,
+ labelWidth: null,
+ style: {},
+ disabled: false,
+ required: true,
+ 'active-text': '',
+ 'inactive-text': '',
+ 'active-color': null,
+ 'inactive-color': null,
+ 'active-value': true,
+ 'inactive-value': false,
+ regList: [],
+ changeTag: true,
+ document: 'https://element-plus.org/zh-CN/component/switch',
+ },
+ {
+ label: '滑块',
+ tag: 'el-slider',
+ tagIcon: 'slider',
+ defaultValue: null,
+ span: 24,
+ labelWidth: null,
+ disabled: false,
+ required: true,
+ min: 0,
+ max: 100,
+ step: 1,
+ 'show-stops': false,
+ range: false,
+ regList: [],
+ changeTag: true,
+ document: 'https://element-plus.org/zh-CN/component/slider',
+ },
+ {
+ label: '时间选择',
+ tag: 'el-time-picker',
+ tagIcon: 'time',
+ placeholder: '请选择',
+ defaultValue: '',
+ span: 24,
+ labelWidth: null,
+ style: { width: '100%' },
+ disabled: false,
+ clearable: true,
+ required: true,
+ format: 'HH:mm:ss',
+ 'value-format': 'HH:mm:ss',
+ regList: [],
+ changeTag: true,
+ document: 'https://element-plus.org/zh-CN/component/time-picker',
+ },
+ {
+ label: '时间范围',
+ tag: 'el-time-picker',
+ tagIcon: 'time-range',
+ defaultValue: null,
+ span: 24,
+ labelWidth: null,
+ style: { width: '100%' },
+ disabled: false,
+ clearable: true,
+ required: true,
+ 'is-range': true,
+ 'range-separator': '至',
+ 'start-placeholder': '开始时间',
+ 'end-placeholder': '结束时间',
+ format: 'HH:mm:ss',
+ 'value-format': 'HH:mm:ss',
+ regList: [],
+ changeTag: true,
+ document: 'https://element-plus.org/zh-CN/component/time-picker',
+ },
+ {
+ label: '日期选择',
+ tag: 'el-date-picker',
+ tagIcon: 'date',
+ placeholder: '请选择',
+ defaultValue: null,
+ type: 'date',
+ span: 24,
+ labelWidth: null,
+ style: { width: '100%' },
+ disabled: false,
+ clearable: true,
+ required: true,
+ format: 'YYYY-MM-DD',
+ 'value-format': 'YYYY-MM-DD',
+ readonly: false,
+ regList: [],
+ changeTag: true,
+ document: 'https://element-plus.org/zh-CN/component/date-picker',
+ },
+ {
+ label: '日期范围',
+ tag: 'el-date-picker',
+ tagIcon: 'date-range',
+ defaultValue: null,
+ span: 24,
+ labelWidth: null,
+ style: { width: '100%' },
+ type: 'daterange',
+ 'range-separator': '至',
+ 'start-placeholder': '开始日期',
+ 'end-placeholder': '结束日期',
+ disabled: false,
+ clearable: true,
+ required: true,
+ format: 'YYYY-MM-DD',
+ 'value-format': 'YYYY-MM-DD',
+ readonly: false,
+ regList: [],
+ changeTag: true,
+ document: 'https://element-plus.org/zh-CN/component/date-picker',
+ },
+ {
+ label: '评分',
+ tag: 'el-rate',
+ tagIcon: 'rate',
+ defaultValue: 0,
+ span: 24,
+ labelWidth: null,
+ style: {},
+ max: 5,
+ 'allow-half': false,
+ 'show-text': false,
+ 'show-score': false,
+ disabled: false,
+ required: true,
+ regList: [],
+ changeTag: true,
+ document: 'https://element-plus.org/zh-CN/component/rate',
+ },
+ {
+ label: '颜色选择',
+ tag: 'el-color-picker',
+ tagIcon: 'color',
+ defaultValue: null,
+ labelWidth: null,
+ 'show-alpha': false,
+ 'color-format': '',
+ disabled: false,
+ required: true,
+ size: 'default',
+ regList: [],
+ changeTag: true,
+ document: 'https://element-plus.org/zh-CN/component/color-picker',
+ },
+ {
+ label: '上传',
+ tag: 'el-upload',
+ tagIcon: 'upload',
+ action: 'https://jsonplaceholder.typicode.com/posts/',
+ defaultValue: null,
+ labelWidth: null,
+ disabled: false,
+ required: true,
+ accept: '',
+ name: 'file',
+ 'auto-upload': true,
+ showTip: false,
+ buttonText: '点击上传',
+ fileSize: 2,
+ sizeUnit: 'MB',
+ 'list-type': 'text',
+ multiple: false,
+ regList: [],
+ changeTag: true,
+ document: 'https://element-plus.org/zh-CN/component/upload',
+ tip: '只能上传不超过 2MB 的文件',
+ style: { width: '100%' },
+ },
+]
+
+export const layoutComponents = [
+ {
+ layout: 'rowFormItem',
+ tagIcon: 'row',
+ type: 'default',
+ justify: 'start',
+ align: 'top',
+ label: '行容器',
+ layoutTree: true,
+ children: [],
+ document: 'https://element-plus.org/zh-CN/component/layout',
+ },
+ {
+ layout: 'colFormItem',
+ label: '按钮',
+ changeTag: true,
+ labelWidth: null,
+ tag: 'el-button',
+ tagIcon: 'button',
+ span: 24,
+ default: '主要按钮',
+ type: 'primary',
+ icon: 'Search',
+ size: 'default',
+ disabled: false,
+ document: 'https://element-plus.org/zh-CN/component/button',
+ },
+]
+
+// 组件rule的触发方式,无触发方式的组件不生成rule
+export const trigger = {
+ 'el-input': 'blur',
+ 'el-input-number': 'blur',
+ 'el-select': 'change',
+ 'el-radio-group': 'change',
+ 'el-checkbox-group': 'change',
+ 'el-cascader': 'change',
+ 'el-time-picker': 'change',
+ 'el-date-picker': 'change',
+ 'el-rate': 'change',
+}
diff --git a/src/utils/generator/css.js b/src/utils/generator/css.js
new file mode 100644
index 0000000..c1c62e6
--- /dev/null
+++ b/src/utils/generator/css.js
@@ -0,0 +1,18 @@
+const styles = {
+ 'el-rate': '.el-rate{display: inline-block; vertical-align: text-top;}',
+ 'el-upload': '.el-upload__tip{line-height: 1.2;}'
+}
+
+function addCss(cssList, el) {
+ const css = styles[el.tag]
+ css && cssList.indexOf(css) === -1 && cssList.push(css)
+ if (el.children) {
+ el.children.forEach(el2 => addCss(cssList, el2))
+ }
+}
+
+export function makeUpCss(conf) {
+ const cssList = []
+ conf.fields.forEach(el => addCss(cssList, el))
+ return cssList.join('\n')
+}
diff --git a/src/utils/generator/drawingDefalut.js b/src/utils/generator/drawingDefalut.js
new file mode 100644
index 0000000..1105c28
--- /dev/null
+++ b/src/utils/generator/drawingDefalut.js
@@ -0,0 +1,29 @@
+export default [
+ {
+ layout: 'colFormItem',
+ tagIcon: 'input',
+ label: '手机号',
+ vModel: 'mobile',
+ formId: 6,
+ tag: 'el-input',
+ placeholder: '请输入手机号',
+ defaultValue: '',
+ span: 24,
+ style: { width: '100%' },
+ clearable: true,
+ prepend: '',
+ append: '',
+ 'prefix-icon': 'Cellphone',
+ 'suffix-icon': '',
+ maxlength: 11,
+ 'show-word-limit': true,
+ readonly: false,
+ disabled: false,
+ required: true,
+ changeTag: true,
+ regList: [{
+ pattern: '/^1(3|4|5|7|8|9)\\d{9}$/',
+ message: '手机号格式错误'
+ }]
+ }
+]
diff --git a/src/utils/generator/html.js b/src/utils/generator/html.js
new file mode 100644
index 0000000..bf2e40a
--- /dev/null
+++ b/src/utils/generator/html.js
@@ -0,0 +1,359 @@
+/* eslint-disable max-len */
+import { trigger } from './config'
+
+let confGlobal
+let someSpanIsNot24
+
+export function dialogWrapper(str) {
+ return `
+ ${str}
+
+ 取消
+ 确定
+
+ `
+}
+
+export function vueTemplate(str) {
+ return `
+
+ ${str}
+
+ `
+}
+
+export function vueScript(str) {
+ return ``
+}
+
+export function cssStyle(cssStr) {
+ return ``
+}
+
+function buildFormTemplate(conf, child, type) {
+ let labelPosition = ''
+ if (conf.labelPosition !== 'right') {
+ labelPosition = `label-position="${conf.labelPosition}"`
+ }
+ const disabled = conf.disabled ? `:disabled="${conf.disabled}"` : ''
+ let str = `
+ ${child}
+ ${buildFromBtns(conf, type)}
+ `
+ if (someSpanIsNot24) {
+ str = `
+ ${str}
+ `
+ }
+ return str
+}
+
+function buildFromBtns(conf, type) {
+ let str = ''
+ if (conf.formBtns && type === 'file') {
+ str = `
+ 提交
+ 重置
+ `
+ if (someSpanIsNot24) {
+ str = `
+ ${str}
+ `
+ }
+ }
+ return str
+}
+
+// span不为24的用el-col包裹
+function colWrapper(element, str) {
+ if (someSpanIsNot24 || element.span !== 24) {
+ return `
+ ${str}
+ `
+ }
+ return str
+}
+
+const layouts = {
+ colFormItem(element) {
+ let labelWidth = ''
+ if (element.labelWidth && element.labelWidth !== confGlobal.labelWidth) {
+ labelWidth = `label-width="${element.labelWidth}px"`
+ }
+ const required = !trigger[element.tag] && element.required ? 'required' : ''
+ const tagDom = tags[element.tag] ? tags[element.tag](element) : null
+ let str = `
+ ${tagDom}
+ `
+ str = colWrapper(element, str)
+ return str
+ },
+ rowFormItem(element) {
+ const type = element.type === 'default' ? '' : `type="${element.type}"`
+ const justify = element.type === 'default' ? '' : `justify="${element.justify}"`
+ const align = element.type === 'default' ? '' : `align="${element.align}"`
+ const gutter = element.gutter ? `gutter="${element.gutter}"` : ''
+ const children = element.children.map(el => layouts[el.layout](el))
+ let str = `
+ ${children.join('\n')}
+ `
+ str = colWrapper(element, str)
+ return str
+ }
+}
+
+const tags = {
+ 'el-button': el => {
+ const {
+ tag, disabled
+ } = attrBuilder(el)
+ const type = el.type ? `type="${el.type}"` : ''
+ const icon = el.icon ? `icon="${el.icon}"` : ''
+ const size = el.size ? `size="${el.size}"` : ''
+ let child = buildElButtonChild(el)
+
+ if (child) child = `\n${child}\n` // 换行
+ return `<${el.tag} ${type} ${icon} ${size} ${disabled}>${child}${el.tag}>`
+ },
+ 'el-input': el => {
+ const {
+ disabled, vModel, clearable, placeholder, width
+ } = attrBuilder(el)
+ const maxlength = el.maxlength ? `:maxlength="${el.maxlength}"` : ''
+ const showWordLimit = el['show-word-limit'] ? 'show-word-limit' : ''
+ const readonly = el.readonly ? 'readonly' : ''
+ const prefixIcon = el['prefix-icon'] ? `prefix-icon='${el['prefix-icon']}'` : ''
+ const suffixIcon = el['suffix-icon'] ? `suffix-icon='${el['suffix-icon']}'` : ''
+ const showPassword = el['show-password'] ? 'show-password' : ''
+ const type = el.type ? `type="${el.type}"` : ''
+ const autosize = el.autosize && el.autosize.minRows
+ ? `:autosize="{minRows: ${el.autosize.minRows}, maxRows: ${el.autosize.maxRows}}"`
+ : ''
+ let child = buildElInputChild(el)
+
+ if (child) child = `\n${child}\n` // 换行
+ return `<${el.tag} ${vModel} ${type} ${placeholder} ${maxlength} ${showWordLimit} ${readonly} ${disabled} ${clearable} ${prefixIcon} ${suffixIcon} ${showPassword} ${autosize} ${width}>${child}${el.tag}>`
+ },
+ 'el-input-number': el => {
+ const { disabled, vModel, placeholder } = attrBuilder(el)
+ const controlsPosition = el['controls-position'] ? `controls-position=${el['controls-position']}` : ''
+ const min = el.min ? `:min='${el.min}'` : ''
+ const max = el.max ? `:max='${el.max}'` : ''
+ const step = el.step ? `:step='${el.step}'` : ''
+ const stepStrictly = el['step-strictly'] ? 'step-strictly' : ''
+ const precision = el.precision ? `:precision='${el.precision}'` : ''
+
+ return `<${el.tag} ${vModel} ${placeholder} ${step} ${stepStrictly} ${precision} ${controlsPosition} ${min} ${max} ${disabled}>${el.tag}>`
+ },
+ 'el-select': el => {
+ const {
+ disabled, vModel, clearable, placeholder, width
+ } = attrBuilder(el)
+ const filterable = el.filterable ? 'filterable' : ''
+ const multiple = el.multiple ? 'multiple' : ''
+ let child = buildElSelectChild(el)
+
+ if (child) child = `\n${child}\n` // 换行
+ return `<${el.tag} ${vModel} ${placeholder} ${disabled} ${multiple} ${filterable} ${clearable} ${width}>${child}${el.tag}>`
+ },
+ 'el-radio-group': el => {
+ const { disabled, vModel } = attrBuilder(el)
+ const size = `size="${el.size}"`
+ let child = buildElRadioGroupChild(el)
+
+ if (child) child = `\n${child}\n` // 换行
+ return `<${el.tag} ${vModel} ${size} ${disabled}>${child}${el.tag}>`
+ },
+ 'el-checkbox-group': el => {
+ const { disabled, vModel } = attrBuilder(el)
+ const size = `size="${el.size}"`
+ const min = el.min ? `:min="${el.min}"` : ''
+ const max = el.max ? `:max="${el.max}"` : ''
+ let child = buildElCheckboxGroupChild(el)
+
+ if (child) child = `\n${child}\n` // 换行
+ return `<${el.tag} ${vModel} ${min} ${max} ${size} ${disabled}>${child}${el.tag}>`
+ },
+ 'el-switch': el => {
+ const { disabled, vModel } = attrBuilder(el)
+ const activeText = el['active-text'] ? `active-text="${el['active-text']}"` : ''
+ const inactiveText = el['inactive-text'] ? `inactive-text="${el['inactive-text']}"` : ''
+ const activeColor = el['active-color'] ? `active-color="${el['active-color']}"` : ''
+ const inactiveColor = el['inactive-color'] ? `inactive-color="${el['inactive-color']}"` : ''
+ const activeValue = el['active-value'] !== true ? `:active-value='${JSON.stringify(el['active-value'])}'` : ''
+ const inactiveValue = el['inactive-value'] !== false ? `:inactive-value='${JSON.stringify(el['inactive-value'])}'` : ''
+
+ return `<${el.tag} ${vModel} ${activeText} ${inactiveText} ${activeColor} ${inactiveColor} ${activeValue} ${inactiveValue} ${disabled}>${el.tag}>`
+ },
+ 'el-cascader': el => {
+ const {
+ disabled, vModel, clearable, placeholder, width
+ } = attrBuilder(el)
+ const options = el.options ? `:options="${el.vModel}Options"` : ''
+ const props = el.props ? `:props="${el.vModel}Props"` : ''
+ const showAllLevels = el['show-all-levels'] ? '' : ':show-all-levels="false"'
+ const filterable = el.filterable ? 'filterable' : ''
+ const separator = el.separator === '/' ? '' : `separator="${el.separator}"`
+
+ return `<${el.tag} ${vModel} ${options} ${props} ${width} ${showAllLevels} ${placeholder} ${separator} ${filterable} ${clearable} ${disabled}>${el.tag}>`
+ },
+ 'el-slider': el => {
+ const { disabled, vModel } = attrBuilder(el)
+ const min = el.min ? `:min='${el.min}'` : ''
+ const max = el.max ? `:max='${el.max}'` : ''
+ const step = el.step ? `:step='${el.step}'` : ''
+ const range = el.range ? 'range' : ''
+ const showStops = el['show-stops'] ? `:show-stops="${el['show-stops']}"` : ''
+
+ return `<${el.tag} ${min} ${max} ${step} ${vModel} ${range} ${showStops} ${disabled}>${el.tag}>`
+ },
+ 'el-time-picker': el => {
+ const {
+ disabled, vModel, clearable, placeholder, width
+ } = attrBuilder(el)
+ const startPlaceholder = el['start-placeholder'] ? `start-placeholder="${el['start-placeholder']}"` : ''
+ const endPlaceholder = el['end-placeholder'] ? `end-placeholder="${el['end-placeholder']}"` : ''
+ const rangeSeparator = el['range-separator'] ? `range-separator="${el['range-separator']}"` : ''
+ const isRange = el['is-range'] ? 'is-range' : ''
+ const format = el.format ? `format="${el.format}"` : ''
+ const valueFormat = el['value-format'] ? `value-format="${el['value-format']}"` : ''
+ const pickerOptions = el['picker-options'] ? `:picker-options='${JSON.stringify(el['picker-options'])}'` : ''
+
+ return `<${el.tag} ${vModel} ${isRange} ${format} ${valueFormat} ${pickerOptions} ${width} ${placeholder} ${startPlaceholder} ${endPlaceholder} ${rangeSeparator} ${clearable} ${disabled}>${el.tag}>`
+ },
+ 'el-date-picker': el => {
+ const {
+ disabled, vModel, clearable, placeholder, width
+ } = attrBuilder(el)
+ const startPlaceholder = el['start-placeholder'] ? `start-placeholder="${el['start-placeholder']}"` : ''
+ const endPlaceholder = el['end-placeholder'] ? `end-placeholder="${el['end-placeholder']}"` : ''
+ const rangeSeparator = el['range-separator'] ? `range-separator="${el['range-separator']}"` : ''
+ const format = el.format ? `format="${el.format}"` : ''
+ const valueFormat = el['value-format'] ? `value-format="${el['value-format']}"` : ''
+ const type = el.type === 'date' ? '' : `type="${el.type}"`
+ const readonly = el.readonly ? 'readonly' : ''
+
+ return `<${el.tag} ${type} ${vModel} ${format} ${valueFormat} ${width} ${placeholder} ${startPlaceholder} ${endPlaceholder} ${rangeSeparator} ${clearable} ${readonly} ${disabled}>${el.tag}>`
+ },
+ 'el-rate': el => {
+ const { disabled, vModel } = attrBuilder(el)
+ const max = el.max ? `:max='${el.max}'` : ''
+ const allowHalf = el['allow-half'] ? 'allow-half' : ''
+ const showText = el['show-text'] ? 'show-text' : ''
+ const showScore = el['show-score'] ? 'show-score' : ''
+
+ return `<${el.tag} ${vModel} ${allowHalf} ${showText} ${showScore} ${disabled}>${el.tag}>`
+ },
+ 'el-color-picker': el => {
+ const { disabled, vModel } = attrBuilder(el)
+ const size = `size="${el.size}"`
+ const showAlpha = el['show-alpha'] ? 'show-alpha' : ''
+ const colorFormat = el['color-format'] ? `color-format="${el['color-format']}"` : ''
+
+ return `<${el.tag} ${vModel} ${size} ${showAlpha} ${colorFormat} ${disabled}>${el.tag}>`
+ },
+ 'el-upload': el => {
+ const disabled = el.disabled ? ':disabled=\'true\'' : ''
+ const action = el.action ? `:action="${el.vModel}Action"` : ''
+ const multiple = el.multiple ? 'multiple' : ''
+ const listType = el['list-type'] !== 'text' ? `list-type="${el['list-type']}"` : ''
+ const accept = el.accept ? `accept="${el.accept}"` : ''
+ const name = el.name !== 'file' ? `name="${el.name}"` : ''
+ const autoUpload = el['auto-upload'] === false ? ':auto-upload="false"' : ''
+ const beforeUpload = `:before-upload="${el.vModel}BeforeUpload"`
+ const fileList = `:file-list="${el.vModel}fileList"`
+ const ref = `ref="${el.vModel}"`
+ let child = buildElUploadChild(el)
+
+ if (child) child = `\n${child}\n` // 换行
+ return `<${el.tag} ${ref} ${fileList} ${action} ${autoUpload} ${multiple} ${beforeUpload} ${listType} ${accept} ${name} ${disabled}>${child}${el.tag}>`
+ }
+}
+
+function attrBuilder(el) {
+ return {
+ vModel: `v-model="${confGlobal.formModel}.${el.vModel}"`,
+ clearable: el.clearable ? 'clearable' : '',
+ placeholder: el.placeholder ? `placeholder="${el.placeholder}"` : '',
+ width: el.style && el.style.width ? ':style="{width: \'100%\'}"' : '',
+ disabled: el.disabled ? ':disabled=\'true\'' : ''
+ }
+}
+
+// el-buttin 子级
+function buildElButtonChild(conf) {
+ const children = []
+ if (conf.default) {
+ children.push(conf.default)
+ }
+ return children.join('\n')
+}
+
+// el-input innerHTML
+function buildElInputChild(conf) {
+ const children = []
+ if (conf.prepend) {
+ children.push(`${conf.prepend} `)
+ }
+ if (conf.append) {
+ children.push(`${conf.append} `)
+ }
+ return children.join('\n')
+}
+
+function buildElSelectChild(conf) {
+ const children = []
+ if (conf.options && conf.options.length) {
+ children.push(` `)
+ }
+ return children.join('\n')
+}
+
+function buildElRadioGroupChild(conf) {
+ const children = []
+ if (conf.options && conf.options.length) {
+ const tag = conf.optionType === 'button' ? 'el-radio-button' : 'el-radio'
+ const border = conf.border ? 'border' : ''
+ children.push(`<${tag} v-for="(item, index) in ${conf.vModel}Options" :key="index" :label="item.value" :disabled="item.disabled" ${border}>{{item.label}}${tag}>`)
+ }
+ return children.join('\n')
+}
+
+function buildElCheckboxGroupChild(conf) {
+ const children = []
+ if (conf.options && conf.options.length) {
+ const tag = conf.optionType === 'button' ? 'el-checkbox-button' : 'el-checkbox'
+ const border = conf.border ? 'border' : ''
+ children.push(`<${tag} v-for="(item, index) in ${conf.vModel}Options" :key="index" :label="item.value" :disabled="item.disabled" ${border}>{{item.label}}${tag}>`)
+ }
+ return children.join('\n')
+}
+
+function buildElUploadChild(conf) {
+ const list = []
+ if (conf['list-type'] === 'picture-card') list.push(' ')
+ else list.push(`${conf.buttonText} `)
+ if (conf.showTip) list.push(`只能上传不超过 ${conf.fileSize}${conf.sizeUnit} 的${conf.accept}文件
`)
+ return list.join('\n')
+}
+
+export function makeUpHtml(conf, type) {
+ const htmlList = []
+ confGlobal = conf
+ someSpanIsNot24 = conf.fields.some(item => item.span !== 24)
+ conf.fields.forEach(el => {
+ htmlList.push(layouts[el.layout](el))
+ })
+ const htmlStr = htmlList.join('\n')
+
+ let temp = buildFormTemplate(conf, htmlStr, type)
+ if (type === 'dialog') {
+ temp = dialogWrapper(temp)
+ }
+ confGlobal = null
+ return temp
+}
diff --git a/src/utils/generator/icon.json b/src/utils/generator/icon.json
new file mode 100644
index 0000000..2d9999a
--- /dev/null
+++ b/src/utils/generator/icon.json
@@ -0,0 +1 @@
+["platform-eleme","eleme","delete-solid","delete","s-tools","setting","user-solid","user","phone","phone-outline","more","more-outline","star-on","star-off","s-goods","goods","warning","warning-outline","question","info","remove","circle-plus","success","error","zoom-in","zoom-out","remove-outline","circle-plus-outline","circle-check","circle-close","s-help","help","minus","plus","check","close","picture","picture-outline","picture-outline-round","upload","upload2","download","camera-solid","camera","video-camera-solid","video-camera","message-solid","bell","s-cooperation","s-order","s-platform","s-fold","s-unfold","s-operation","s-promotion","s-home","s-release","s-ticket","s-management","s-open","s-shop","s-marketing","s-flag","s-comment","s-finance","s-claim","s-custom","s-opportunity","s-data","s-check","s-grid","menu","share","d-caret","caret-left","caret-right","caret-bottom","caret-top","bottom-left","bottom-right","back","right","bottom","top","top-left","top-right","arrow-left","arrow-right","arrow-down","arrow-up","d-arrow-left","d-arrow-right","video-pause","video-play","refresh","refresh-right","refresh-left","finished","sort","sort-up","sort-down","rank","loading","view","c-scale-to-original","date","edit","edit-outline","folder","folder-opened","folder-add","folder-remove","folder-delete","folder-checked","tickets","document-remove","document-delete","document-copy","document-checked","document","document-add","printer","paperclip","takeaway-box","search","monitor","attract","mobile","scissors","umbrella","headset","brush","mouse","coordinate","magic-stick","reading","data-line","data-board","pie-chart","data-analysis","collection-tag","film","suitcase","suitcase-1","receiving","collection","files","notebook-1","notebook-2","toilet-paper","office-building","school","table-lamp","house","no-smoking","smoking","shopping-cart-full","shopping-cart-1","shopping-cart-2","shopping-bag-1","shopping-bag-2","sold-out","sell","present","box","bank-card","money","coin","wallet","discount","price-tag","news","guide","male","female","thumb","cpu","link","connection","open","turn-off","set-up","chat-round","chat-line-round","chat-square","chat-dot-round","chat-dot-square","chat-line-square","message","postcard","position","turn-off-microphone","microphone","close-notification","bangzhu","time","odometer","crop","aim","switch-button","full-screen","copy-document","mic","stopwatch","medal-1","medal","trophy","trophy-1","first-aid-kit","discover","place","location","location-outline","location-information","add-location","delete-location","map-location","alarm-clock","timer","watch-1","watch","lock","unlock","key","service","mobile-phone","bicycle","truck","ship","basketball","football","soccer","baseball","wind-power","light-rain","lightning","heavy-rain","sunrise","sunrise-1","sunset","sunny","cloudy","partly-cloudy","cloudy-and-sunny","moon","moon-night","dish","dish-1","food","chicken","fork-spoon","knife-fork","burger","tableware","sugar","dessert","ice-cream","hot-water","water-cup","coffee-cup","cold-drink","goblet","goblet-full","goblet-square","goblet-square-full","refrigerator","grape","watermelon","cherry","apple","pear","orange","coffee","ice-tea","ice-drink","milk-tea","potato-strips","lollipop","ice-cream-square","ice-cream-round"]
\ No newline at end of file
diff --git a/src/utils/generator/js.js b/src/utils/generator/js.js
new file mode 100644
index 0000000..dc38bfe
--- /dev/null
+++ b/src/utils/generator/js.js
@@ -0,0 +1,370 @@
+import { titleCase } from '@/utils/index'
+import { trigger } from './config'
+// 文件大小设置
+const units = {
+ KB: '1024',
+ MB: '1024 / 1024',
+ GB: '1024 / 1024 / 1024',
+}
+/**
+ * @name: 生成js需要的数据
+ * @description: 生成js需要的数据
+ * @param {*} conf
+ * @param {*} type 弹窗或表单
+ * @return {*}
+ */
+export function makeUpJs(conf, type) {
+ conf = JSON.parse(JSON.stringify(conf))
+ const dataList = []
+ const ruleList = []
+ const optionsList = []
+ const propsList = []
+ const methodList = []
+ const uploadVarList = []
+
+ conf.fields.forEach((el) => {
+ buildAttributes(
+ el,
+ dataList,
+ ruleList,
+ optionsList,
+ methodList,
+ propsList,
+ uploadVarList
+ )
+ })
+
+ const script = buildexport(
+ conf,
+ type,
+ dataList.join('\n'),
+ ruleList.join('\n'),
+ optionsList.join('\n'),
+ uploadVarList.join('\n'),
+ propsList.join('\n'),
+ methodList.join('\n')
+ )
+
+ return script
+}
+/**
+ * @name: 生成参数
+ * @description: 生成参数,包括表单数据表单验证数据,多选选项数据,上传数据等
+ * @return {*}
+ */
+function buildAttributes(
+ el,
+ dataList,
+ ruleList,
+ optionsList,
+ methodList,
+ propsList,
+ uploadVarList
+){
+ buildData(el, dataList)
+ buildRules(el, ruleList)
+
+ if (el.options && el.options.length) {
+ buildOptions(el, optionsList)
+ if (el.dataType === 'dynamic') {
+ const model = `${el.vModel}Options`
+ const options = titleCase(model)
+ buildOptionMethod(`get${options}`, model, methodList)
+ }
+ }
+
+ if (el.props && el.props.props) {
+ buildProps(el, propsList)
+ }
+
+ if (el.action && el.tag === 'el-upload') {
+ uploadVarList.push(
+ `
+ // 上传请求路径
+ const ${el.vModel}Action = ref('${el.action}')
+ // 上传文件列表
+ const ${el.vModel}fileList = ref([])`
+ )
+ methodList.push(buildBeforeUpload(el))
+ if (!el['auto-upload']) {
+ methodList.push(buildSubmitUpload(el))
+ }
+ }
+
+ if (el.children) {
+ el.children.forEach((el2) => {
+ buildAttributes(
+ el2,
+ dataList,
+ ruleList,
+ optionsList,
+ methodList,
+ propsList,
+ uploadVarList
+ )
+ })
+ }
+}
+/**
+ * @name: 生成表单数据formData
+ * @description: 生成表单数据formData
+ * @param {*} conf
+ * @param {*} dataList 数据列表
+ * @return {*}
+ */
+function buildData(conf, dataList) {
+ if (conf.vModel === undefined) return
+ let defaultValue
+ if (typeof conf.defaultValue === 'string' && !conf.multiple) {
+ defaultValue = `'${conf.defaultValue}'`
+ } else {
+ defaultValue = `${JSON.stringify(conf.defaultValue)}`
+ }
+ dataList.push(`${conf.vModel}: ${defaultValue},`)
+}
+/**
+ * @name: 生成表单验证数据rule
+ * @description: 生成表单验证数据rule
+ * @param {*} conf
+ * @param {*} ruleList 验证数据列表
+ * @return {*}
+ */
+function buildRules(conf, ruleList) {
+ if (conf.vModel === undefined) return
+ const rules = []
+ if (trigger[conf.tag]) {
+ if (conf.required) {
+ const type = Array.isArray(conf.defaultValue) ? "type: 'array'," : ''
+ let message = Array.isArray(conf.defaultValue)
+ ? `请至少选择一个${conf.vModel}`
+ : conf.placeholder
+ if (message === undefined) message = `${conf.label}不能为空`
+ rules.push(
+ `{ required: true, ${type} message: '${message}', trigger: '${
+ trigger[conf.tag]
+ }' }`
+ )
+ }
+ if (conf.regList && Array.isArray(conf.regList)) {
+ conf.regList.forEach((item) => {
+ if (item.pattern) {
+ rules.push(
+ `{ pattern: new RegExp(${item.pattern}), message: '${
+ item.message
+ }', trigger: '${trigger[conf.tag]}' }`
+ )
+ }
+ })
+ }
+ ruleList.push(`${conf.vModel}: [${rules.join(',')}],`)
+ }
+}
+/**
+ * @name: 生成选项数据
+ * @description: 生成选项数据,单选多选下拉等
+ * @param {*} conf
+ * @param {*} optionsList 选项数据列表
+ * @return {*}
+ */
+function buildOptions(conf, optionsList) {
+ if (conf.vModel === undefined) return
+ if (conf.dataType === 'dynamic') {
+ conf.options = []
+ }
+ const str = `const ${conf.vModel}Options = ref(${JSON.stringify(conf.options)})`
+ optionsList.push(str)
+}
+/**
+ * @name: 生成方法
+ * @description: 生成方法
+ * @param {*} methodName 方法名
+ * @param {*} model
+ * @param {*} methodList 方法列表
+ * @return {*}
+ */
+function buildOptionMethod(methodName, model, methodList) {
+ const str = `function ${methodName}() {
+ // TODO 发起请求获取数据
+ ${model}.value
+ }`
+ methodList.push(str)
+}
+/**
+ * @name: 生成表单组件需要的props设置
+ * @description: 生成表单组件需要的props设置,如;级联组件
+ * @param {*} conf
+ * @param {*} propsList
+ * @return {*}
+ */
+function buildProps(conf, propsList) {
+ if (conf.dataType === 'dynamic') {
+ conf.valueKey !== 'value' && (conf.props.props.value = conf.valueKey)
+ conf.labelKey !== 'label' && (conf.props.props.label = conf.labelKey)
+ conf.childrenKey !== 'children' &&
+ (conf.props.props.children = conf.childrenKey)
+ }
+ const str = `
+ // props设置
+ const ${conf.vModel}Props = ref(${JSON.stringify(conf.props.props)})`
+ propsList.push(str)
+}
+/**
+ * @name: 生成上传组件的相关内容
+ * @description: 生成上传组件的相关内容
+ * @param {*} conf
+ * @return {*}
+ */
+function buildBeforeUpload(conf) {
+ const unitNum = units[conf.sizeUnit]
+ let rightSizeCode = ''
+ let acceptCode = ''
+ const returnList = []
+ if (conf.fileSize) {
+ rightSizeCode = `let isRightSize = file.size / ${unitNum} < ${conf.fileSize}
+ if(!isRightSize){
+ proxy.$modal.msgError('文件大小超过 ${conf.fileSize}${conf.sizeUnit}')
+ }`
+ returnList.push('isRightSize')
+ }
+ if (conf.accept) {
+ acceptCode = `let isAccept = new RegExp('${conf.accept}').test(file.type)
+ if(!isAccept){
+ proxy.$modal.msgError('应该选择${conf.accept}类型的文件')
+ }`
+ returnList.push('isAccept')
+ }
+ const str = `
+ /**
+ * @name: 上传之前的文件判断
+ * @description: 上传之前的文件判断,判断文件大小文件类型等
+ * @param {*} file
+ * @return {*}
+ */
+ function ${conf.vModel}BeforeUpload(file) {
+ ${rightSizeCode}
+ ${acceptCode}
+ return ${returnList.join('&&')}
+ }`
+ return returnList.length ? str : ''
+}
+/**
+ * @name: 生成提交表单方法
+ * @description: 生成提交表单方法
+ * @param {Object} conf vModel 表单ref
+ * @return {*}
+ */
+function buildSubmitUpload(conf) {
+ const str = `function submitUpload() {
+ this.$refs['${conf.vModel}'].submit()
+ }`
+ return str
+}
+/**
+ * @name: 组装js代码
+ * @description: 组装js代码方法
+ * @return {*}
+ */
+function buildexport(
+ conf,
+ type,
+ data,
+ rules,
+ selectOptions,
+ uploadVar,
+ props,
+ methods
+) {
+ let str = `
+ const { proxy } = getCurrentInstance()
+ const ${conf.formRef} = ref()
+ const data = reactive({
+ ${conf.formModel}: {
+ ${data}
+ },
+ ${conf.formRules}: {
+ ${rules}
+ }
+ })
+
+ const {${conf.formModel}, ${conf.formRules}} = toRefs(data)
+
+ ${selectOptions}
+
+ ${uploadVar}
+
+ ${props}
+
+ ${methods}
+ `
+
+ if(type === 'dialog') {
+ str += `
+ // 弹窗设置
+ const dialogVisible = defineModel()
+ // 弹窗确认回调
+ const emit = defineEmits(['confirm'])
+ /**
+ * @name: 弹窗打开后执行
+ * @description: 弹窗打开后执行方法
+ * @return {*}
+ */
+ function onOpen(){
+
+ }
+ /**
+ * @name: 弹窗关闭时执行
+ * @description: 弹窗关闭方法,重置表单
+ * @return {*}
+ */
+ function onClose(){
+ ${conf.formRef}.value.resetFields()
+ }
+ /**
+ * @name: 弹窗取消
+ * @description: 弹窗取消方法
+ * @return {*}
+ */
+ function close(){
+ dialogVisible.value = false
+ }
+ /**
+ * @name: 弹窗表单提交
+ * @description: 弹窗表单提交方法
+ * @return {*}
+ */
+ function handelConfirm(){
+ ${conf.formRef}.value.validate((valid) => {
+ if (!valid) return
+ // TODO 提交表单
+
+ close()
+ // 回调父级组件
+ emit('confirm')
+ })
+ }
+ `
+ } else {
+ str += `
+ /**
+ * @name: 表单提交
+ * @description: 表单提交方法
+ * @return {*}
+ */
+ function submitForm() {
+ ${conf.formRef}.value.validate((valid) => {
+ if (!valid) return
+ // TODO 提交表单
+ })
+ }
+ /**
+ * @name: 表单重置
+ * @description: 表单重置方法
+ * @return {*}
+ */
+ function resetForm() {
+ ${conf.formRef}.value.resetFields()
+ }
+ `
+ }
+ return str
+}
diff --git a/src/utils/generator/render.js b/src/utils/generator/render.js
new file mode 100644
index 0000000..d6d4414
--- /dev/null
+++ b/src/utils/generator/render.js
@@ -0,0 +1,156 @@
+import { defineComponent, h } from 'vue'
+import { makeMap } from '@/utils/index'
+
+const isAttr = makeMap(
+ 'accept,accept-charset,accesskey,action,align,alt,async,autocomplete,' +
+ 'autofocus,autoplay,autosave,bgcolor,border,buffered,challenge,charset,' +
+ 'checked,cite,class,code,codebase,color,cols,colspan,content,http-equiv,' +
+ 'name,contenteditable,contextmenu,controls,coords,data,datetime,default,' +
+ 'defer,dir,dirname,disabled,download,draggable,dropzone,enctype,method,for,' +
+ 'form,formaction,headers,height,hidden,high,href,hreflang,http-equiv,' +
+ 'icon,id,ismap,itemprop,keytype,kind,label,lang,language,list,loop,low,' +
+ 'manifest,max,maxlength,media,method,GET,POST,min,multiple,email,file,' +
+ 'muted,name,novalidate,open,optimum,pattern,ping,placeholder,poster,' +
+ 'preload,radiogroup,readonly,rel,required,reversed,rows,rowspan,sandbox,' +
+ 'scope,scoped,seamless,selected,shape,size,type,text,password,sizes,span,' +
+ 'spellcheck,src,srcdoc,srclang,srcset,start,step,style,summary,tabindex,' +
+ 'target,title,type,usemap,value,width,wrap' + 'prefix-icon'
+)
+const isNotProps = makeMap(
+ 'layout,prepend,regList,tag,document,changeTag,defaultValue'
+)
+
+function useVModel(props, emit) {
+ return {
+ modelValue: props.defaultValue,
+ 'onUpdate:modelValue': (val) => emit('update:modelValue', val),
+ }
+}
+const componentChild = {
+ 'el-button': {
+ default(h, conf, key) {
+ return conf[key]
+ },
+ },
+ 'el-select': {
+ options(h, conf, key) {
+ return conf.options.map(item => h(resolveComponent('el-option'), {
+ label: item.label,
+ value: item.value,
+ }))
+ }
+ },
+ 'el-radio-group': {
+ options(h, conf, key) {
+ return conf.optionType === 'button' ? conf.options.map(item => h(resolveComponent('el-checkbox-button'), {
+ label: item.value,
+ }, () => item.label)) : conf.options.map(item => h(resolveComponent('el-radio'), {
+ label: item.value,
+ border: conf.border,
+ }, () => item.label))
+ }
+ },
+ 'el-checkbox-group': {
+ options(h, conf, key) {
+ return conf.optionType === 'button' ? conf.options.map(item => h(resolveComponent('el-checkbox-button'), {
+ label: item.value,
+ }, () => item.label)) : conf.options.map(item => h(resolveComponent('el-checkbox'), {
+ label: item.value,
+ border: conf.border,
+ }, () => item.label))
+ }
+ },
+ 'el-upload': {
+ 'list-type': (h, conf, key) => {
+ const option = {}
+ // if (conf.showTip) {
+ // tip = h('div', {
+ // class: "el-upload__tip"
+ // }, () => '只能上传不超过' + conf.fileSize + conf.sizeUnit + '的' + conf.accept + '文件')
+ // }
+ if (conf['list-type'] === 'picture-card') {
+ return h(resolveComponent('el-icon'), option, () => h(resolveComponent('Plus')))
+ } else {
+ // option.size = "small"
+ option.type = "primary"
+ option.icon = "Upload"
+ return h(resolveComponent('el-button'), option, () => conf.buttonText)
+ }
+ },
+
+ }
+}
+const componentSlot = {
+ 'el-upload': {
+ 'tip': (h, conf, key) => {
+ if (conf.showTip) {
+ return () => h('div', {
+ class: "el-upload__tip"
+ }, '只能上传不超过' + conf.fileSize + conf.sizeUnit + '的' + conf.accept + '文件')
+ }
+ },
+ }
+}
+export default defineComponent({
+
+ // 使用 render 函数
+ render() {
+ const dataObject = {
+ attrs: {},
+ props: {},
+ on: {},
+ style: {}
+ }
+ const confClone = JSON.parse(JSON.stringify(this.conf))
+ const children = []
+ const slot = {}
+ const childObjs = componentChild[confClone.tag]
+ if (childObjs) {
+ Object.keys(childObjs).forEach(key => {
+ const childFunc = childObjs[key]
+ if (confClone[key]) {
+ children.push(childFunc(h, confClone, key))
+ }
+ })
+ }
+ const slotObjs = componentSlot[confClone.tag]
+ if (slotObjs) {
+ Object.keys(slotObjs).forEach(key => {
+ const childFunc = slotObjs[key]
+ if (confClone[key]) {
+ slot[key] = childFunc(h, confClone, key)
+ }
+ })
+ }
+ Object.keys(confClone).forEach(key => {
+ const val = confClone[key]
+ if (dataObject[key]) {
+ dataObject[key] = val
+ } else if (isAttr(key)) {
+ dataObject.attrs[key] = val
+ } else if (!isNotProps(key)) {
+ dataObject.props[key] = val
+ }
+ })
+ if(children.length > 0){
+ slot.default = () => children
+ }
+
+ return h(resolveComponent(this.conf.tag),
+ {
+ modelValue: this.$attrs.modelValue,
+ ...dataObject.props,
+ ...dataObject.attrs,
+ style: {
+ ...dataObject.style
+ },
+ }
+ , slot ?? null)
+ },
+ props: {
+ conf: {
+ type: Object,
+ required: true,
+ },
+ }
+})
\ No newline at end of file
diff --git a/src/utils/index.js b/src/utils/index.js
new file mode 100644
index 0000000..407d990
--- /dev/null
+++ b/src/utils/index.js
@@ -0,0 +1,407 @@
+import { parseTime } from './ruoyi'
+
+/**
+ * 表格时间格式化
+ */
+export function formatDate(cellValue) {
+ if (cellValue == null || cellValue == "") return "";
+ var date = new Date(cellValue)
+ var year = date.getFullYear()
+ var month = date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1
+ var day = date.getDate() < 10 ? '0' + date.getDate() : date.getDate()
+ var hours = date.getHours() < 10 ? '0' + date.getHours() : date.getHours()
+ var minutes = date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes()
+ var seconds = date.getSeconds() < 10 ? '0' + date.getSeconds() : date.getSeconds()
+ return year + '-' + month + '-' + day + ' ' + hours + ':' + minutes + ':' + seconds
+}
+
+/**
+ * @param {number} time
+ * @param {string} option
+ * @returns {string}
+ */
+export function formatTime(time, option) {
+ if (('' + time).length === 10) {
+ time = parseInt(time) * 1000
+ } else {
+ time = +time
+ }
+ const d = new Date(time)
+ const now = Date.now()
+
+ const diff = (now - d) / 1000
+
+ if (diff < 30) {
+ return '刚刚'
+ } else if (diff < 3600) {
+ // less 1 hour
+ return Math.ceil(diff / 60) + '分钟前'
+ } else if (diff < 3600 * 24) {
+ return Math.ceil(diff / 3600) + '小时前'
+ } else if (diff < 3600 * 24 * 2) {
+ return '1天前'
+ }
+ if (option) {
+ return parseTime(time, option)
+ } else {
+ return (
+ d.getMonth() +
+ 1 +
+ '月' +
+ d.getDate() +
+ '日' +
+ d.getHours() +
+ '时' +
+ d.getMinutes() +
+ '分'
+ )
+ }
+}
+
+/**
+ * @param {string} url
+ * @returns {Object}
+ */
+export function getQueryObject(url) {
+ url = url == null ? window.location.href : url
+ const search = url.substring(url.lastIndexOf('?') + 1)
+ const obj = {}
+ const reg = /([^?&=]+)=([^?&=]*)/g
+ search.replace(reg, (rs, $1, $2) => {
+ const name = decodeURIComponent($1)
+ let val = decodeURIComponent($2)
+ val = String(val)
+ obj[name] = val
+ return rs
+ })
+ return obj
+}
+
+/**
+ * @param {string} input value
+ * @returns {number} output value
+ */
+export function byteLength(str) {
+ // returns the byte length of an utf8 string
+ let s = str.length
+ for (var i = str.length - 1; i >= 0; i--) {
+ const code = str.charCodeAt(i)
+ if (code > 0x7f && code <= 0x7ff) s++
+ else if (code > 0x7ff && code <= 0xffff) s += 2
+ if (code >= 0xDC00 && code <= 0xDFFF) i--
+ }
+ return s
+}
+
+/**
+ * @param {Array} actual
+ * @returns {Array}
+ */
+export function cleanArray(actual) {
+ const newArray = []
+ for (let i = 0; i < actual.length; i++) {
+ if (actual[i]) {
+ newArray.push(actual[i])
+ }
+ }
+ return newArray
+}
+
+/**
+ * @param {Object} json
+ * @returns {Array}
+ */
+export function param(json) {
+ if (!json) return ''
+ return cleanArray(
+ Object.keys(json).map(key => {
+ if (json[key] === undefined) return ''
+ return encodeURIComponent(key) + '=' + encodeURIComponent(json[key])
+ })
+ ).join('&')
+}
+
+/**
+ * @param {string} url
+ * @returns {Object}
+ */
+export function param2Obj(url) {
+ const search = decodeURIComponent(url.split('?')[1]).replace(/\+/g, ' ')
+ if (!search) {
+ return {}
+ }
+ const obj = {}
+ const searchArr = search.split('&')
+ searchArr.forEach(v => {
+ const index = v.indexOf('=')
+ if (index !== -1) {
+ const name = v.substring(0, index)
+ const val = v.substring(index + 1, v.length)
+ obj[name] = val
+ }
+ })
+ return obj
+}
+
+/**
+ * @param {string} val
+ * @returns {string}
+ */
+export function html2Text(val) {
+ const div = document.createElement('div')
+ div.innerHTML = val
+ return div.textContent || div.innerText
+}
+
+/**
+ * Merges two objects, giving the last one precedence
+ * @param {Object} target
+ * @param {(Object|Array)} source
+ * @returns {Object}
+ */
+export function objectMerge(target, source) {
+ if (typeof target !== 'object') {
+ target = {}
+ }
+ if (Array.isArray(source)) {
+ return source.slice()
+ }
+ Object.keys(source).forEach(property => {
+ const sourceProperty = source[property]
+ if (typeof sourceProperty === 'object') {
+ target[property] = objectMerge(target[property], sourceProperty)
+ } else {
+ target[property] = sourceProperty
+ }
+ })
+ return target
+}
+
+/**
+ * @param {HTMLElement} element
+ * @param {string} className
+ */
+export function toggleClass(element, className) {
+ if (!element || !className) {
+ return
+ }
+ let classString = element.className
+ const nameIndex = classString.indexOf(className)
+ if (nameIndex === -1) {
+ classString += '' + className
+ } else {
+ classString =
+ classString.substr(0, nameIndex) +
+ classString.substr(nameIndex + className.length)
+ }
+ element.className = classString
+}
+
+/**
+ * @param {string} type
+ * @returns {Date}
+ */
+export function getTime(type) {
+ if (type === 'start') {
+ return new Date().getTime() - 3600 * 1000 * 24 * 90
+ } else {
+ return new Date(new Date().toDateString())
+ }
+}
+
+/**
+ * @param {Function} func
+ * @param {number} wait
+ * @param {boolean} immediate
+ * @return {*}
+ */
+export function debounce(func, wait, immediate) {
+ let timeout, args, context, timestamp, result
+
+ const later = function() {
+ // 据上一次触发时间间隔
+ const last = +new Date() - timestamp
+
+ // 上次被包装函数被调用时间间隔 last 小于设定时间间隔 wait
+ if (last < wait && last > 0) {
+ timeout = setTimeout(later, wait - last)
+ } else {
+ timeout = null
+ // 如果设定为immediate===true,因为开始边界已经调用过了此处无需调用
+ if (!immediate) {
+ result = func.apply(context, args)
+ if (!timeout) context = args = null
+ }
+ }
+ }
+
+ return function(...args) {
+ context = this
+ timestamp = +new Date()
+ const callNow = immediate && !timeout
+ // 如果延时不存在,重新设定延时
+ if (!timeout) timeout = setTimeout(later, wait)
+ if (callNow) {
+ result = func.apply(context, args)
+ context = args = null
+ }
+
+ return result
+ }
+}
+
+/**
+ * This is just a simple version of deep copy
+ * Has a lot of edge cases bug
+ * If you want to use a perfect deep copy, use lodash's _.cloneDeep
+ * @param {Object} source
+ * @returns {Object}
+ */
+export function deepClone(source) {
+ if (!source && typeof source !== 'object') {
+ throw new Error('error arguments', 'deepClone')
+ }
+ const targetObj = source.constructor === Array ? [] : {}
+ Object.keys(source).forEach(keys => {
+ if (source[keys] && typeof source[keys] === 'object') {
+ targetObj[keys] = deepClone(source[keys])
+ } else {
+ targetObj[keys] = source[keys]
+ }
+ })
+ return targetObj
+}
+
+/**
+ * @param {Array} arr
+ * @returns {Array}
+ */
+export function uniqueArr(arr) {
+ return Array.from(new Set(arr))
+}
+
+/**
+ * @returns {string}
+ */
+export function createUniqueString() {
+ const timestamp = +new Date() + ''
+ const randomNum = parseInt((1 + Math.random()) * 65536) + ''
+ return (+(randomNum + timestamp)).toString(32)
+}
+
+/**
+ * Check if an element has a class
+ * @param {HTMLElement} elm
+ * @param {string} cls
+ * @returns {boolean}
+ */
+export function hasClass(ele, cls) {
+ return !!ele.className.match(new RegExp('(\\s|^)' + cls + '(\\s|$)'))
+}
+
+/**
+ * Add class to element
+ * @param {HTMLElement} elm
+ * @param {string} cls
+ */
+export function addClass(ele, cls) {
+ if (!hasClass(ele, cls)) ele.className += ' ' + cls
+}
+
+/**
+ * Remove class from element
+ * @param {HTMLElement} elm
+ * @param {string} cls
+ */
+export function removeClass(ele, cls) {
+ if (hasClass(ele, cls)) {
+ const reg = new RegExp('(\\s|^)' + cls + '(\\s|$)')
+ ele.className = ele.className.replace(reg, ' ')
+ }
+}
+
+export function makeMap(str, expectsLowerCase) {
+ const map = Object.create(null)
+ const list = str.split(',')
+ for (let i = 0; i < list.length; i++) {
+ map[list[i]] = true
+ }
+ return expectsLowerCase
+ ? val => map[val.toLowerCase()]
+ : val => map[val]
+}
+
+export const exportDefault = 'export default '
+
+export const beautifierConf = {
+ html: {
+ indent_size: '2',
+ indent_char: ' ',
+ max_preserve_newlines: '-1',
+ preserve_newlines: false,
+ keep_array_indentation: false,
+ break_chained_methods: false,
+ indent_scripts: 'separate',
+ brace_style: 'end-expand',
+ space_before_conditional: true,
+ unescape_strings: false,
+ jslint_happy: false,
+ end_with_newline: true,
+ wrap_line_length: '110',
+ indent_inner_html: true,
+ comma_first: false,
+ e4x: true,
+ indent_empty_lines: true
+ },
+ js: {
+ indent_size: '2',
+ indent_char: ' ',
+ max_preserve_newlines: '-1',
+ preserve_newlines: false,
+ keep_array_indentation: false,
+ break_chained_methods: false,
+ indent_scripts: 'normal',
+ brace_style: 'end-expand',
+ space_before_conditional: true,
+ unescape_strings: false,
+ jslint_happy: true,
+ end_with_newline: true,
+ wrap_line_length: '110',
+ indent_inner_html: true,
+ comma_first: false,
+ e4x: true,
+ indent_empty_lines: true
+ }
+}
+
+// 首字母大小
+export function titleCase(str) {
+ return str.replace(/( |^)[a-z]/g, L => L.toUpperCase())
+}
+
+// 下划转驼峰
+export function camelCase(str) {
+ return str.replace(/_[a-z]/g, str1 => str1.substr(-1).toUpperCase())
+}
+
+export function isNumberStr(str) {
+ return /^[+-]?(0|([1-9]\d*))(\.\d+)?$/g.test(str)
+}
+
+export function isAudioByMimeType(mimeType) {
+ const audioMimeTypes = [
+ 'audio/mpeg',
+ 'audio/wav',
+ 'audio/x-wav',
+ 'audio/flac',
+ 'audio/aac',
+ 'audio/ogg',
+ 'audio/mp4',
+ 'audio/webm',
+ 'audio/x-m4a',
+ 'audio/amr',
+ 'audio/midi'
+ ];
+
+ return audioMimeTypes.includes(mimeType.toLowerCase());
+}
\ No newline at end of file
diff --git a/src/utils/jsencrypt.js b/src/utils/jsencrypt.js
new file mode 100644
index 0000000..78d9523
--- /dev/null
+++ b/src/utils/jsencrypt.js
@@ -0,0 +1,30 @@
+import JSEncrypt from 'jsencrypt/bin/jsencrypt.min'
+
+// 密钥对生成 http://web.chacuo.net/netrsakeypair
+
+const publicKey = 'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdH\n' +
+ 'nzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ=='
+
+const privateKey = 'MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAqhHyZfSsYourNxaY\n' +
+ '7Nt+PrgrxkiA50efORdI5U5lsW79MmFnusUA355oaSXcLhu5xxB38SMSyP2KvuKN\n' +
+ 'PuH3owIDAQABAkAfoiLyL+Z4lf4Myxk6xUDgLaWGximj20CUf+5BKKnlrK+Ed8gA\n' +
+ 'kM0HqoTt2UZwA5E2MzS4EI2gjfQhz5X28uqxAiEA3wNFxfrCZlSZHb0gn2zDpWow\n' +
+ 'cSxQAgiCstxGUoOqlW8CIQDDOerGKH5OmCJ4Z21v+F25WaHYPxCFMvwxpcw99Ecv\n' +
+ 'DQIgIdhDTIqD2jfYjPTY8Jj3EDGPbH2HHuffvflECt3Ek60CIQCFRlCkHpi7hthh\n' +
+ 'YhovyloRYsM+IS9h/0BzlEAuO0ktMQIgSPT3aFAgJYwKpqRYKlLDVcflZFCKY7u3\n' +
+ 'UP8iWi1Qw0Y='
+
+// 加密
+export function encrypt(txt) {
+ const encryptor = new JSEncrypt()
+ encryptor.setPublicKey(publicKey) // 设置公钥
+ return encryptor.encrypt(txt) // 对数据进行加密
+}
+
+// 解密
+export function decrypt(txt) {
+ const encryptor = new JSEncrypt()
+ encryptor.setPrivateKey(privateKey) // 设置私钥
+ return encryptor.decrypt(txt) // 对数据进行解密
+}
+
diff --git a/src/utils/permission.js b/src/utils/permission.js
new file mode 100644
index 0000000..93fee87
--- /dev/null
+++ b/src/utils/permission.js
@@ -0,0 +1,51 @@
+import useUserStore from '@/store/modules/user'
+
+/**
+ * 字符权限校验
+ * @param {Array} value 校验值
+ * @returns {Boolean}
+ */
+export function checkPermi(value) {
+ if (value && value instanceof Array && value.length > 0) {
+ const permissions = useUserStore().permissions
+ const permissionDatas = value
+ const all_permission = "*:*:*";
+
+ const hasPermission = permissions.some(permission => {
+ return all_permission === permission || permissionDatas.includes(permission)
+ })
+
+ if (!hasPermission) {
+ return false
+ }
+ return true
+ } else {
+ console.error(`need roles! Like checkPermi="['system:user:add','system:user:edit']"`)
+ return false
+ }
+}
+
+/**
+ * 角色权限校验
+ * @param {Array} value 校验值
+ * @returns {Boolean}
+ */
+export function checkRole(value) {
+ if (value && value instanceof Array && value.length > 0) {
+ const roles = useUserStore().roles
+ const permissionRoles = value
+ const super_admin = "admin";
+
+ const hasRole = roles.some(role => {
+ return super_admin === role || permissionRoles.includes(role)
+ })
+
+ if (!hasRole) {
+ return false
+ }
+ return true
+ } else {
+ console.error(`need roles! Like checkRole="['admin','editor']"`)
+ return false
+ }
+}
\ No newline at end of file
diff --git a/src/utils/request.js b/src/utils/request.js
new file mode 100644
index 0000000..2b10149
--- /dev/null
+++ b/src/utils/request.js
@@ -0,0 +1,152 @@
+import axios from 'axios'
+import { ElNotification , ElMessageBox, ElMessage, ElLoading } from 'element-plus'
+import { getToken } from '@/utils/auth'
+import errorCode from '@/utils/errorCode'
+import { tansParams, blobValidate } from '@/utils/ruoyi'
+import cache from '@/plugins/cache'
+import { saveAs } from 'file-saver'
+import useUserStore from '@/store/modules/user'
+
+let downloadLoadingInstance;
+// 是否显示重新登录
+export let isRelogin = { show: false };
+
+axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'
+// 创建axios实例
+const service = axios.create({
+ // axios中请求配置有baseURL选项,表示请求URL公共部分
+ baseURL: import.meta.env.VITE_APP_BASE_API,
+ // 超时
+ timeout: 100000
+})
+
+// request拦截器
+service.interceptors.request.use(config => {
+ // 是否需要设置 token
+ const isToken = (config.headers || {}).isToken === false
+ // 是否需要防止数据重复提交
+ const isRepeatSubmit = (config.headers || {}).repeatSubmit === false
+ if (getToken() && !isToken) {
+ config.headers['Authorization'] = 'Bearer ' + getToken() // 让每个请求携带自定义token 请根据实际情况自行修改
+ }
+ // get请求映射params参数
+ if (config.method === 'get' && config.params) {
+ let url = config.url + '?' + tansParams(config.params);
+ url = url.slice(0, -1);
+ config.params = {};
+ config.url = url;
+ }
+ if (!isRepeatSubmit && (config.method === 'post' || config.method === 'put')) {
+ const requestObj = {
+ url: config.url,
+ data: typeof config.data === 'object' ? JSON.stringify(config.data) : config.data,
+ time: new Date().getTime()
+ }
+ const requestSize = Object.keys(JSON.stringify(requestObj)).length; // 请求数据大小
+ const limitSize = 5 * 1024 * 1024; // 限制存放数据5M
+ if (requestSize >= limitSize) {
+ console.warn(`[${config.url}]: ` + '请求数据大小超出允许的5M限制,无法进行防重复提交验证。')
+ return config;
+ }
+ const sessionObj = cache.session.getJSON('sessionObj')
+ if (sessionObj === undefined || sessionObj === null || sessionObj === '') {
+ cache.session.setJSON('sessionObj', requestObj)
+ } else {
+ const s_url = sessionObj.url; // 请求地址
+ const s_data = sessionObj.data; // 请求数据
+ const s_time = sessionObj.time; // 请求时间
+ const interval = 500; // 间隔时间(ms),小于此时间视为重复提交
+ if (s_data === requestObj.data && requestObj.time - s_time < interval && s_url === requestObj.url) {
+ const message = '数据正在处理,请勿重复提交';
+ console.warn(`[${s_url}]: ` + message)
+ return Promise.reject(new Error(message))
+ } else {
+ cache.session.setJSON('sessionObj', requestObj)
+ }
+ }
+ }
+ return config
+}, error => {
+ console.log(error)
+ Promise.reject(error)
+})
+
+// 响应拦截器
+service.interceptors.response.use(res => {
+ // 未设置状态码则默认成功状态
+ const code = res.data.code || 200;
+ // 获取错误信息
+ const msg = errorCode[code] || res.data.msg || errorCode['default']
+ // 二进制数据则直接返回
+ if (res.request.responseType === 'blob' || res.request.responseType === 'arraybuffer') {
+ return res.data
+ }
+ if (code === 401) {
+ if (!isRelogin.show) {
+ isRelogin.show = true;
+ ElMessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', { confirmButtonText: '重新登录', cancelButtonText: '取消', type: 'warning' }).then(() => {
+ isRelogin.show = false;
+ useUserStore().logOut().then(() => {
+ location.href = '/index';
+ })
+ }).catch(() => {
+ isRelogin.show = false;
+ });
+ }
+ return Promise.reject('无效的会话,或者会话已过期,请重新登录。')
+ } else if (code === 500) {
+ ElMessage({ message: msg, type: 'error' })
+ return Promise.reject(new Error(msg))
+ } else if (code === 601) {
+ ElMessage({ message: msg, type: 'warning' })
+ return Promise.reject(new Error(msg))
+ } else if (code !== 200) {
+ ElNotification.error({ title: msg })
+ return Promise.reject('error')
+ } else {
+ return Promise.resolve(res.data)
+ }
+ },
+ error => {
+ console.log('err' + error)
+ let { message } = error;
+ if (message == "Network Error") {
+ message = "后端接口连接异常";
+ } else if (message.includes("timeout")) {
+ message = "系统接口请求超时";
+ } else if (message.includes("Request failed with status code")) {
+ message = "系统接口" + message.substr(message.length - 3) + "异常";
+ }
+ ElMessage({ message: message, type: 'error', duration: 5 * 1000 })
+ return Promise.reject(error)
+ }
+)
+
+// 通用下载方法
+export function download(url, params, filename, config) {
+ downloadLoadingInstance = ElLoading.service({ text: "正在下载数据,请稍候", background: "rgba(0, 0, 0, 0.7)", })
+ return service.post(url, params, {
+ transformRequest: [(params) => { return tansParams(params) }],
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
+ responseType: 'blob',
+ ...config
+ }).then(async (data) => {
+ const isBlob = blobValidate(data);
+ if (isBlob) {
+ const blob = new Blob([data])
+ saveAs(blob, filename)
+ } else {
+ const resText = await data.text();
+ const rspObj = JSON.parse(resText);
+ const errMsg = errorCode[rspObj.code] || rspObj.msg || errorCode['default']
+ ElMessage.error(errMsg);
+ }
+ downloadLoadingInstance.close();
+ }).catch((r) => {
+ console.error(r)
+ ElMessage.error('下载文件出现错误,请联系管理员!')
+ downloadLoadingInstance.close();
+ })
+}
+
+export default service
diff --git a/src/utils/requestController.js b/src/utils/requestController.js
new file mode 100644
index 0000000..b14a4cf
--- /dev/null
+++ b/src/utils/requestController.js
@@ -0,0 +1,94 @@
+export class ConcurrentRequestController {
+ constructor(maxConcurrency = 5) {
+ this.maxConcurrency = maxConcurrency;
+ this.activeCount = 0;
+ this.queue = [];
+ this.abortControllers = new Map();
+ this.isCancelled = false;
+ }
+
+ // 添加请求到控制器
+ addRequest(requestFn) {
+ if (this.isCancelled) {
+ return Promise.reject(new Error('请求控制器已被取消'));
+ }
+
+ const abortController = new AbortController();
+ const signal = abortController.signal;
+
+ return new Promise((resolve, reject) => {
+ const task = () => {
+ if (this.isCancelled) {
+ reject(new Error('请求控制器已被取消'));
+ return;
+ }
+ const taskId = Date.now() + '_' + Math.random().toString(36);
+ this.abortControllers.set(taskId, abortController);
+ this.activeCount++;
+ requestFn(signal)
+ .then(result => {
+ if (!this.isCancelled) {
+ resolve(result);
+ }
+ })
+ .catch(error => {
+ if (!this.isCancelled || error.name !== 'AbortError') {
+ reject(error);
+ }
+ })
+ .finally(() => {
+ this.activeCount--;
+ this.abortControllers.delete(taskId);
+ this._processQueue();
+ });
+ };
+
+ // 如果当前活跃请求数小于最大并发数,立即执行
+ if (this.activeCount < this.maxConcurrency) {
+ task();
+ } else {
+ // 否则加入队列等待
+ this.queue.push(task);
+ }
+ });
+ }
+
+ // 处理等待队列中的任务
+ _processQueue() {
+ if (this.queue.length > 0 && this.activeCount < this.maxConcurrency && !this.isCancelled) {
+ const nextTask = this.queue.shift();
+ nextTask();
+ }
+ }
+
+ // 取消所有请求(已开始和未开始的)
+ cancelAll() {
+ if (this.isCancelled) {
+ return
+ };
+
+ this.isCancelled = true;
+
+ // 取消所有正在进行的请求
+ this.abortControllers.forEach(controller => {
+ controller.abort();
+ });
+
+ // 清空等待队列
+ this.queue = [];
+ this.activeCount = 0;
+ this.isCancelled = false;
+ this.abortControllers.clear();
+ console.log('所有请求已取消');
+ }
+
+ // 获取当前状态
+ getStatus() {
+ return {
+ activeCount: this.activeCount,
+ queueLength: this.queue.length,
+ maxConcurrency: this.maxConcurrency,
+ isCancelled: this.isCancelled
+ };
+ }
+}
\ No newline at end of file
diff --git a/src/utils/ruoyi.js b/src/utils/ruoyi.js
new file mode 100644
index 0000000..4efca08
--- /dev/null
+++ b/src/utils/ruoyi.js
@@ -0,0 +1,246 @@
+
+
+/**
+ * 通用js方法封装处理
+ * Copyright (c) 2019 ruoyi
+ */
+
+// 日期格式化
+export function parseTime(time, pattern) {
+ if (arguments.length === 0 || !time) {
+ return null
+ }
+ const format = pattern || '{y}-{m}-{d} {h}:{i}:{s}'
+ let date
+ if (typeof time === 'object') {
+ date = time
+ } else {
+ if ((typeof time === 'string') && (/^[0-9]+$/.test(time))) {
+ time = parseInt(time)
+ } else if (typeof time === 'string') {
+ time = time.replace(new RegExp(/-/gm), '/').replace('T', ' ').replace(new RegExp(/\.[\d]{3}/gm), '');
+ }
+ if ((typeof time === 'number') && (time.toString().length === 10)) {
+ time = time * 1000
+ }
+ date = new Date(time)
+ }
+ const formatObj = {
+ y: date.getFullYear(),
+ m: date.getMonth() + 1,
+ d: date.getDate(),
+ h: date.getHours(),
+ i: date.getMinutes(),
+ s: date.getSeconds(),
+ a: date.getDay()
+ }
+ const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => {
+ let value = formatObj[key]
+ // Note: getDay() returns 0 on Sunday
+ if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value] }
+ if (result.length > 0 && value < 10) {
+ value = '0' + value
+ }
+ return value || 0
+ })
+ return time_str
+}
+
+// 表单重置
+export function resetForm(refName) {
+ if (this.$refs[refName]) {
+ this.$refs[refName].resetFields();
+ }
+}
+
+// 添加日期范围
+export function addDateRange(params, dateRange, propName) {
+ let search = params;
+ search.params = typeof (search.params) === 'object' && search.params !== null && !Array.isArray(search.params) ? search.params : {};
+ dateRange = Array.isArray(dateRange) ? dateRange : [];
+ if (typeof (propName) === 'undefined') {
+ search.params['beginTime'] = dateRange[0];
+ search.params['endTime'] = dateRange[1];
+ } else {
+ search.params['begin' + propName] = dateRange[0];
+ search.params['end' + propName] = dateRange[1];
+ }
+ return search;
+}
+
+// 回显数据字典
+export function selectDictLabel(datas, value) {
+ if (value === undefined) {
+ return "";
+ }
+ var actions = [];
+ Object.keys(datas).some((key) => {
+ if (datas[key].value == ('' + value)) {
+ actions.push(datas[key].label);
+ return true;
+ }
+ })
+ if (actions.length === 0) {
+ actions.push(value);
+ }
+ return actions.join('');
+}
+
+// 回显数据字典(字符串数组)
+export function selectDictLabels(datas, value, separator) {
+ if (value === undefined || value.length ===0) {
+ return "";
+ }
+ if (Array.isArray(value)) {
+ value = value.join(",");
+ }
+ var actions = [];
+ var currentSeparator = undefined === separator ? "," : separator;
+ var temp = value.split(currentSeparator);
+ Object.keys(value.split(currentSeparator)).some((val) => {
+ var match = false;
+ Object.keys(datas).some((key) => {
+ if (datas[key].value == ('' + temp[val])) {
+ actions.push(datas[key].label + currentSeparator);
+ match = true;
+ }
+ })
+ if (!match) {
+ actions.push(temp[val] + currentSeparator);
+ }
+ })
+ return actions.join('').substring(0, actions.join('').length - 1);
+}
+
+// 字符串格式化(%s )
+export function sprintf(str) {
+ var args = arguments, flag = true, i = 1;
+ str = str.replace(/%s/g, function () {
+ var arg = args[i++];
+ if (typeof arg === 'undefined') {
+ flag = false;
+ return '';
+ }
+ return arg;
+ });
+ return flag ? str : '';
+}
+
+// 转换字符串,undefined,null等转化为""
+export function parseStrEmpty(str) {
+ if (!str || str == "undefined" || str == "null") {
+ return "";
+ }
+ return str;
+}
+
+// 数据合并
+export function mergeRecursive(source, target) {
+ for (var p in target) {
+ try {
+ if (target[p].constructor == Object) {
+ source[p] = mergeRecursive(source[p], target[p]);
+ } else {
+ source[p] = target[p];
+ }
+ } catch (e) {
+ source[p] = target[p];
+ }
+ }
+ return source;
+};
+
+/**
+ * 构造树型结构数据
+ * @param {*} data 数据源
+ * @param {*} id id字段 默认 'id'
+ * @param {*} parentId 父节点字段 默认 'parentId'
+ * @param {*} children 孩子节点字段 默认 'children'
+ */
+export function handleTree(data, id, parentId, children) {
+ let config = {
+ id: id || 'id',
+ parentId: parentId || 'parentId',
+ childrenList: children || 'children'
+ };
+
+ var childrenListMap = {};
+ var nodeIds = {};
+ var tree = [];
+
+ for (let d of data) {
+ let parentId = d[config.parentId];
+ if (childrenListMap[parentId] == null) {
+ childrenListMap[parentId] = [];
+ }
+ nodeIds[d[config.id]] = d;
+ childrenListMap[parentId].push(d);
+ }
+
+ for (let d of data) {
+ let parentId = d[config.parentId];
+ if (nodeIds[parentId] == null) {
+ tree.push(d);
+ }
+ }
+
+ for (let t of tree) {
+ adaptToChildrenList(t);
+ }
+
+ function adaptToChildrenList(o) {
+ if (childrenListMap[o[config.id]] !== null) {
+ o[config.childrenList] = childrenListMap[o[config.id]];
+ }
+ if (o[config.childrenList]) {
+ for (let c of o[config.childrenList]) {
+ adaptToChildrenList(c);
+ }
+ }
+ }
+ return tree;
+}
+
+/**
+* 参数处理
+* @param {*} params 参数
+*/
+export function tansParams(params) {
+ let result = ''
+ for (const propName of Object.keys(params)) {
+ const value = params[propName];
+ var part = encodeURIComponent(propName) + "=";
+ if (value !== null && value !== "" && typeof (value) !== "undefined") {
+ if (typeof value === 'object') {
+ for (const key of Object.keys(value)) {
+ if (value[key] !== null && value[key] !== "" && typeof (value[key]) !== 'undefined') {
+ let params = propName + '[' + key + ']';
+ var subPart = encodeURIComponent(params) + "=";
+ result += subPart + encodeURIComponent(value[key]) + "&";
+ }
+ }
+ } else {
+ result += part + encodeURIComponent(value) + "&";
+ }
+ }
+ }
+ return result
+}
+
+
+// 返回项目路径
+export function getNormalPath(p) {
+ if (p.length === 0 || !p || p == 'undefined') {
+ return p
+ };
+ let res = p.replace('//', '/')
+ if (res[res.length - 1] === '/') {
+ return res.slice(0, res.length - 1)
+ }
+ return res;
+}
+
+// 验证是否为blob格式
+export function blobValidate(data) {
+ return data.type !== 'application/json'
+}
diff --git a/src/utils/scroll-to.js b/src/utils/scroll-to.js
new file mode 100644
index 0000000..c5d8e04
--- /dev/null
+++ b/src/utils/scroll-to.js
@@ -0,0 +1,58 @@
+Math.easeInOutQuad = function(t, b, c, d) {
+ t /= d / 2
+ if (t < 1) {
+ return c / 2 * t * t + b
+ }
+ t--
+ return -c / 2 * (t * (t - 2) - 1) + b
+}
+
+// requestAnimationFrame for Smart Animating http://goo.gl/sx5sts
+var requestAnimFrame = (function() {
+ return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || function(callback) { window.setTimeout(callback, 1000 / 60) }
+})()
+
+/**
+ * Because it's so fucking difficult to detect the scrolling element, just move them all
+ * @param {number} amount
+ */
+function move(amount) {
+ document.documentElement.scrollTop = amount
+ document.body.parentNode.scrollTop = amount
+ document.body.scrollTop = amount
+}
+
+function position() {
+ return document.documentElement.scrollTop || document.body.parentNode.scrollTop || document.body.scrollTop
+}
+
+/**
+ * @param {number} to
+ * @param {number} duration
+ * @param {Function} callback
+ */
+export function scrollTo(to, duration, callback) {
+ const start = position()
+ const change = to - start
+ const increment = 20
+ let currentTime = 0
+ duration = (typeof (duration) === 'undefined') ? 500 : duration
+ var animateScroll = function() {
+ // increment the time
+ currentTime += increment
+ // find the value with the quadratic in-out easing function
+ var val = Math.easeInOutQuad(currentTime, start, change, duration)
+ // move the document.body
+ move(val)
+ // do the animation unless its over
+ if (currentTime < duration) {
+ requestAnimFrame(animateScroll)
+ } else {
+ if (callback && typeof (callback) === 'function') {
+ // the animation is done so lets callback
+ callback()
+ }
+ }
+ }
+ animateScroll()
+}
diff --git a/src/utils/theme.js b/src/utils/theme.js
new file mode 100644
index 0000000..f4badc6
--- /dev/null
+++ b/src/utils/theme.js
@@ -0,0 +1,49 @@
+// 处理主题样式
+export function handleThemeStyle(theme) {
+ document.documentElement.style.setProperty('--el-color-primary', theme)
+ for (let i = 1; i <= 9; i++) {
+ document.documentElement.style.setProperty(`--el-color-primary-light-${i}`, `${getLightColor(theme, i / 10)}`)
+ }
+ for (let i = 1; i <= 9; i++) {
+ document.documentElement.style.setProperty(`--el-color-primary-dark-${i}`, `${getDarkColor(theme, i / 10)}`)
+ }
+}
+
+// hex颜色转rgb颜色
+export function hexToRgb(str) {
+ str = str.replace('#', '')
+ let hexs = str.match(/../g)
+ for (let i = 0; i < 3; i++) {
+ hexs[i] = parseInt(hexs[i], 16)
+ }
+ return hexs
+}
+
+// rgb颜色转Hex颜色
+export function rgbToHex(r, g, b) {
+ let hexs = [r.toString(16), g.toString(16), b.toString(16)]
+ for (let i = 0; i < 3; i++) {
+ if (hexs[i].length == 1) {
+ hexs[i] = `0${hexs[i]}`
+ }
+ }
+ return `#${hexs.join('')}`
+}
+
+// 变浅颜色值
+export function getLightColor(color, level) {
+ let rgb = hexToRgb(color)
+ for (let i = 0; i < 3; i++) {
+ rgb[i] = Math.floor((255 - rgb[i]) * level + rgb[i])
+ }
+ return rgbToHex(rgb[0], rgb[1], rgb[2])
+}
+
+// 变深颜色值
+export function getDarkColor(color, level) {
+ let rgb = hexToRgb(color)
+ for (let i = 0; i < 3; i++) {
+ rgb[i] = Math.floor(rgb[i] * (1 - level))
+ }
+ return rgbToHex(rgb[0], rgb[1], rgb[2])
+}
diff --git a/src/utils/validate.js b/src/utils/validate.js
new file mode 100644
index 0000000..6a4c0c5
--- /dev/null
+++ b/src/utils/validate.js
@@ -0,0 +1,114 @@
+/**
+ * 路径匹配器
+ * @param {string} pattern
+ * @param {string} path
+ * @returns {Boolean}
+ */
+export function isPathMatch(pattern, path) {
+ const regexPattern = pattern.replace(/\//g, '\\/').replace(/\*\*/g, '.*').replace(/\*/g, '[^\\/]*')
+ const regex = new RegExp(`^${regexPattern}$`)
+ return regex.test(path)
+}
+
+/**
+ * 判断value字符串是否为空
+ * @param {string} value
+ * @returns {Boolean}
+ */
+export function isEmpty(value) {
+ if (value == null || value == "" || value == undefined || value == "undefined") {
+ return true
+ }
+ return false
+}
+
+/**
+ * 判断url是否是http或https
+ * @param {string} url
+ * @returns {Boolean}
+ */
+export function isHttp(url) {
+ return url.indexOf('http://') !== -1 || url.indexOf('https://') !== -1
+}
+
+/**
+ * 判断path是否为外链
+ * @param {string} path
+ * @returns {Boolean}
+ */
+export function isExternal(path) {
+ return /^(https?:|mailto:|tel:)/.test(path)
+}
+
+/**
+ * @param {string} str
+ * @returns {Boolean}
+ */
+export function validUsername(str) {
+ const valid_map = ['admin', 'editor']
+ return valid_map.indexOf(str.trim()) >= 0
+}
+
+/**
+ * @param {string} url
+ * @returns {Boolean}
+ */
+export function validURL(url) {
+ const reg = /^(https?|ftp):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/
+ return reg.test(url)
+}
+
+/**
+ * @param {string} str
+ * @returns {Boolean}
+ */
+export function validLowerCase(str) {
+ const reg = /^[a-z]+$/
+ return reg.test(str)
+}
+
+/**
+ * @param {string} str
+ * @returns {Boolean}
+ */
+export function validUpperCase(str) {
+ const reg = /^[A-Z]+$/
+ return reg.test(str)
+}
+
+/**
+ * @param {string} str
+ * @returns {Boolean}
+ */
+export function validAlphabets(str) {
+ const reg = /^[A-Za-z]+$/
+ return reg.test(str)
+}
+
+/**
+ * @param {string} email
+ * @returns {Boolean}
+ */
+export function validEmail(email) {
+ const reg = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
+ return reg.test(email)
+}
+
+/**
+ * @param {string} str
+ * @returns {Boolean}
+ */
+export function isString(str) {
+ return typeof str === 'string' || str instanceof String
+}
+
+/**
+ * @param {Array} arg
+ * @returns {Boolean}
+ */
+export function isArray(arg) {
+ if (typeof Array.isArray === 'undefined') {
+ return Object.prototype.toString.call(arg) === '[object Array]'
+ }
+ return Array.isArray(arg)
+}
diff --git a/src/views/device/profile/index.vue b/src/views/device/profile/index.vue
new file mode 100644
index 0000000..34443c3
--- /dev/null
+++ b/src/views/device/profile/index.vue
@@ -0,0 +1,508 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 新增
+
+
+
+ 修改
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 修改
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ controlModuleList.find(item => item.value === scope.row.deviceModel)?.label }}
+
+
+
+
+
+
+ 修改
+
+ 删除
+
+
+
+
+
+
+
+
+
edit=false"
+ @open="getDeviceList">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/device/register/components/Camera/index.vue b/src/views/device/register/components/Camera/index.vue
new file mode 100644
index 0000000..466ccb9
--- /dev/null
+++ b/src/views/device/register/components/Camera/index.vue
@@ -0,0 +1,330 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/views/device/register/components/ControlCard/index.vue b/src/views/device/register/components/ControlCard/index.vue
new file mode 100644
index 0000000..1590fb4
--- /dev/null
+++ b/src/views/device/register/components/ControlCard/index.vue
@@ -0,0 +1,36 @@
+
+
+
+
+
{{ props.title }}
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/views/device/register/components/DexHand/LeftBottomHand.vue b/src/views/device/register/components/DexHand/LeftBottomHand.vue
new file mode 100644
index 0000000..27d3d37
--- /dev/null
+++ b/src/views/device/register/components/DexHand/LeftBottomHand.vue
@@ -0,0 +1,431 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/views/device/register/components/DexHand/LeftTopHand.vue b/src/views/device/register/components/DexHand/LeftTopHand.vue
new file mode 100644
index 0000000..5efda72
--- /dev/null
+++ b/src/views/device/register/components/DexHand/LeftTopHand.vue
@@ -0,0 +1,270 @@
+
+
+
+ {{ isCopied ? '完成' : '复制' }}
+
+
+
+ {{ slider.value }}
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/views/device/register/components/DexHand/RightBottomCharts.vue b/src/views/device/register/components/DexHand/RightBottomCharts.vue
new file mode 100644
index 0000000..bc42122
--- /dev/null
+++ b/src/views/device/register/components/DexHand/RightBottomCharts.vue
@@ -0,0 +1,229 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/views/device/register/components/DexHand/RightTopCharts.vue b/src/views/device/register/components/DexHand/RightTopCharts.vue
new file mode 100644
index 0000000..09cae93
--- /dev/null
+++ b/src/views/device/register/components/DexHand/RightTopCharts.vue
@@ -0,0 +1,162 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/views/device/register/components/DexHand/index.vue b/src/views/device/register/components/DexHand/index.vue
new file mode 100644
index 0000000..fd33381
--- /dev/null
+++ b/src/views/device/register/components/DexHand/index.vue
@@ -0,0 +1,158 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/views/device/register/components/Head/index.vue b/src/views/device/register/components/Head/index.vue
new file mode 100644
index 0000000..3142033
--- /dev/null
+++ b/src/views/device/register/components/Head/index.vue
@@ -0,0 +1,314 @@
+
+
+
人工头舵机控制
+
+
+
+
Yaw 预设位(左右转头)
+
+ 位 {{ index + 1 }}
+
+
+
应用当前 Yaw
+
+
+
+
+
+
+
+ {{ index + 1 }}
+
+
+
+
+
+
Pitch 预设位(上下点头)
+
+ 位 {{ index + 1 }}
+
+
+
应用当前 Pitch
+
+
+
+
+
+
+ 当前 Yaw:
+ {{ servo1.toFixed(2) }}
+
+
+ 当前 Pitch:
+ {{ servo2.toFixed(2) }}
+
+
立即下发舵机值
+
复位到中心 (0.50, 0.50)
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/views/device/register/components/LogicFlow/CustomGroup.js b/src/views/device/register/components/LogicFlow/CustomGroup.js
new file mode 100644
index 0000000..7cc34cf
--- /dev/null
+++ b/src/views/device/register/components/LogicFlow/CustomGroup.js
@@ -0,0 +1,103 @@
+
+import { GroupNodeModel, GroupNode } from '@logicflow/extension'
+import { recursiveFilter } from "@/utils/convertNodeRedToLogicFlow";
+
+class CustomGroupModel extends GroupNodeModel {
+ initNodeData(data) {
+ super.initNodeData(data);
+ this.children = new Set(data.children || [])
+ this.zIndex = 0;
+
+ this.width = data.w + 80; // 增加内边距
+ this.height = data.h + 80;
+ this.x = data.x;
+ this.y = data.y;
+
+ setTimeout(() => {
+ this.updateSize()
+ const nodes = this.graphModel.nodes;
+ const arr = recursiveFilter(nodes, this.id)
+ arr.forEach((item) => {
+ item.updateSize()
+ })
+ }, 0)
+ }
+
+ setAttributes() {
+ this.text.editable = false;
+ }
+
+ getTextStyle() {
+ const style = super.getTextStyle();
+ style.fill = this.properties.style?.color || '#333';
+ style.fontSize = 16;
+ return style;
+ }
+
+ updateSize() {
+ const children = []
+ this.children.forEach((id) => {
+ children.push(this.graphModel.getNodeModelById(id))
+ });
+ if (children.length === 0) return;
+
+ let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
+ children.forEach((node) => {
+ minX = Math.min(minX, node.x - node.width / 2);
+ maxX = Math.max(maxX, node.x + node.width / 2);
+ minY = Math.min(minY, node.y - node.height / 2);
+ maxY = Math.max(maxY, node.y + node.height / 2);
+ });
+
+ this.width = maxX - minX + 80; // 增加内边距
+ this.height = maxY - minY + 80;
+ this.x = (minX + maxX) / 2;
+ this.y = (minY + maxY) / 2;
+ this.setTextCoordinate(this.width, this.height, this.x, this.y)
+ }
+
+ setTextCoordinate(width, height, x, y) {
+ const strategies = {
+ "n": () => {
+ this.text.x = x
+ this.text.y = y - height / 2 + 20
+ },
+ "ne": () => {
+ this.text.x = x + width / 2 - 20
+ this.text.y = y - height / 2 + 20
+ },
+ "sw": () => {
+ this.text.x = x - width / 2 + 20
+ this.text.y = y + height / 2 - 20
+ },
+ "s": () => {
+ this.text.x = x
+ this.text.y = y + height / 2 - 20
+ },
+ "se": () => {
+ this.text.x = x + width / 2 - 20
+ this.text.y = y + height / 2 - 20
+ },
+ "default": () => {
+ this.text.x = x - width / 2 + 20
+ this.text.y = y - height / 2 + 20
+ }
+ };
+ const labelPosition = this.properties.style?.['label-position'] || 'default'
+ const action = strategies[labelPosition]
+ action();
+ }
+
+ removeChild(id) {
+ this.children.delete(id) // 现在可安全调用
+ }
+}
+
+// 导出方法注册
+export function registerCustomGroup(lf) {
+ lf.register({
+ type: "customGroup",
+ view: GroupNode,
+ model: CustomGroupModel
+ })
+}
diff --git a/src/views/device/register/components/LogicFlow/IPlayer/config.ts b/src/views/device/register/components/LogicFlow/IPlayer/config.ts
new file mode 100644
index 0000000..3048428
--- /dev/null
+++ b/src/views/device/register/components/LogicFlow/IPlayer/config.ts
@@ -0,0 +1,59 @@
+export const conf = {
+ /**
+ * 基础功能配置
+ * */
+ autoplay: false, // 是否自动播放,不自动播放,浏览器有限制规则
+ autoplayMuted: false, // 是否自动播放(静音播放)
+ videoInit: true, // 是否默认初始化video,默认初始化,默认true
+ playsinline: true, // 是否启用内联播放模式,仅移动端生效
+ defaultPlaybackRate: 1, // 默认播放速度(可选:0.5/0.75/1/1.5/2等)
+ volume: 0.72, // 播放音量(可选:0 ~ 1)
+ loop: false, // 是否循环播放,默认不循环播放
+ startTime: 0, // 点播模式下,初始起播时间
+ videoAttributes: {}, // video扩展属性,暂且不配置
+ lang: 'zh-cn', // 播放器初始显示语言,设置为中文
+ fluid: true, // 是否流式布局(宽高优先于流失布局,默认16:9)注掉上方宽高看效果
+ fitVideoSize: 'fixed', // 保持容器宽/高,不做适配,按照容器来
+ videoFillMode: 'auto', // 宽高不够自动底色填充(fill拉伸填充等...)
+ seekedStatus: 'play', // 跳转后继续播放
+ // 播放器进度条故事点信息数组
+ progressDot: [
+ {
+ id: 0, // 唯一标识,用于删除的时候索引
+ time: 30, // 展示的时间点,例子为在播放到10s钟的时候展示
+ text: '进度条信息提示...', // hover的时候展示文案,可以为空
+ duration: 1, // 展示时间跨度,单位为s
+ style: { // 指定样式
+ backgroundColor: 'white'
+ }
+ },
+ ],
+ thumbnail: null, // 进度条预览图配置,普通业务用不到
+ marginControls: false, // 是否开启画面和控制栏分离模式,不开启空间多一些
+ domEventType: 'default', // 响应的事件类型,不用指定,用默认的即可
+ /**
+ * 交互功能配置(一般使用默认即可)
+ * */
+ /**
+ * 插件配置,根据需求自选
+ * */
+ icons: {}, // 使用默认的icon图标
+ i18n: [], // 使用默认的中文
+ // 自定义一些颜色
+ commonStyle: {
+ progressColor: 'green', // 整个进度条颜色
+ playedColor: 'chocolate', // 已播放的进度条颜色
+ volumeColor: 'pink', // 音量大小竖向滑块颜色
+ },
+ controls: true, // 是否使用底部控制栏,默认使用
+ miniprogress: true, // 是否使用mini进度条(当底部控制栏隐藏时生效)
+ screenShot: false, // 关闭截图功能
+ rotate: true, // 是否使用视频旋转插件,默认不使用
+ download: true, // 是否使用下载按钮,一般不用,一般自定义控制
+ pip: true, // 使用使用画中画模式,默认不用
+ mini: true, // 是否使用小屏幕控件
+ cssFullscreen: true, // 是否使用网页样式全屏按钮开关
+ playbackRate: [0.5, 1, 1.5, 2, 3], //传入倍速可选数组
+ // playbackRate: true, //false,禁用倍速播放(即控制栏不显示)
+ keyShortcut: false, // 是否开启快捷键模式
+}
diff --git a/src/views/device/register/components/LogicFlow/IPlayer/index.vue b/src/views/device/register/components/LogicFlow/IPlayer/index.vue
new file mode 100644
index 0000000..55be899
--- /dev/null
+++ b/src/views/device/register/components/LogicFlow/IPlayer/index.vue
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+
+
diff --git a/src/views/device/register/components/LogicFlow/customFlow.vue b/src/views/device/register/components/LogicFlow/customFlow.vue
new file mode 100644
index 0000000..9ba9db2
--- /dev/null
+++ b/src/views/device/register/components/LogicFlow/customFlow.vue
@@ -0,0 +1,238 @@
+
+
+
+
节点类型: {{ props.properties.type }}
+
+
状态:{{ stateMap[props.properties.state] }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/device/register/components/LogicFlow/customFlowNode.js b/src/views/device/register/components/LogicFlow/customFlowNode.js
new file mode 100644
index 0000000..447800b
--- /dev/null
+++ b/src/views/device/register/components/LogicFlow/customFlowNode.js
@@ -0,0 +1,134 @@
+// 导入 HtmlNode 及其模型,为后续继承做准备
+import { HtmlNode, HtmlNodeModel } from '@logicflow/core';
+// 导入 Vue 相关方法,用于渲染组件
+import { createApp, h } from 'vue';
+import ElementPlus from 'element-plus'
+// 导入 Vue 组件
+import CustomFlow from './customFlow.vue';
+
+/**
+ * 定义一个 元素的 HTML 节点类,继承自 HtmlNode
+ * 该类负责在 HTML 中渲染 元素,并处理其交互逻辑
+ */
+class CustomFlowHtmlNode extends HtmlNode {
+ isMounted; // 标记组件是否已挂载
+ r; // 渲染函数
+ app; // Vue 应用实例
+
+ /**
+ * 构造函数
+ * @param props 传递给节点的属性,包括模型、图模型等
+ */
+ constructor(props) {
+ super(props);
+ this.isMounted = false;
+ // 创建 元素的渲染函数
+ this.r = h(CustomFlow, {
+ model: props.model,
+ graphModel: props.graphModel,
+ properties: {
+ ...props.model.getProperties(),
+ },
+ text: props.model.text.value,
+ });
+
+ // 创建 Vue 应用实例,并指定渲染函数
+ this.app = createApp({
+ render: () => this.r
+ });
+ }
+
+ /**
+ * 将 HTML 内容设置到指定的根元素上
+ * @param rootEl 根元素
+ */
+ setHtml(rootEl) {
+ if (!this.isMounted) {
+ this.isMounted = true;
+ const node = document.createElement('div');
+ node.style.width = '100%'
+ node.style.height = '100%'
+ rootEl.appendChild(node);
+ this.app.use(ElementPlus) // 关键:单独注册ElementPlus
+ this.app.mount(node);
+ } else {
+ this.r.component.props.properties = this.props.model.getProperties();
+ }
+ }
+
+ /**
+ * 获取节点文本内容
+ * 对于元素,返回 null,因为其内容由特定组件渲染
+ * @returns {null}
+ */
+ getText() {
+ return null;
+ }
+}
+
+/**
+ * 定义一个元素的 HTML 模型类,继承自 HtmlNodeModel
+ * 该类主要设置节点的属性和样式
+ */
+class CustomFlowHtmlModel extends HtmlNodeModel {
+ initNodeData(data) {
+ super.initNodeData(data);
+ // 仅允许从底部锚点连出
+ this.sourceRules.push({
+ message: '只能从输出锚点连线',
+ validate: (sourceNode, targetNode, sourceAnchor) =>
+ // sourceAnchor.name === 'right'
+ sourceAnchor.properties.connectionType === 'source'
+ });
+
+ // 仅允许连接到顶部锚点
+ this.targetRules.push({
+ message: '只能连接到输入锚点',
+ validate: (sourceNode, targetNode, targetAnchor) =>
+ // targetAnchor.name === 'left'
+ targetAnchor.properties.connectionType === 'target'
+ });
+ }
+
+ /**
+ * 设置节点属性
+ * 包括宽度、高度、文本编辑属性等
+ */
+ setAttributes() {
+ this.width = 200;
+ this.height = 100;
+ this.text.editable = false;
+ }
+
+ // 定义节点只有左右两个锚点. 锚点位置通过中心点和宽度算出来。
+ getDefaultAnchor() {
+ let _a = this, x = _a.x, y = _a.y, width = _a.width, height = _a.height;
+ return [
+ // { x: x, y: y - height / 2, id: "".concat(this.id, "_0") },
+ { x: x + width / 2, y: y, name: 'left', id: "".concat(this.id, "_1"), properties: { connectionType: 'target' } },
+ // { x: x, y: y + height / 2, id: "".concat(this.id, "_2") },
+ { x: x - width / 2, y: y, name: 'right', id: "".concat(this.id, "_3"), properties: { connectionType: 'source' } },
+ ];
+ }
+
+ /**
+ * 获取节点轮廓样式
+ * 覆盖父类方法,设置 stroke 属性为 none,以适应特定的视觉效果
+ * @returns {object} 节点轮廓样式
+ */
+ getOutlineStyle() {
+ const style = super.getOutlineStyle();
+ style.stroke = 'none';
+ style.hover.stroke = 'none';
+ return style;
+ }
+}
+
+// 导出方法注册
+export function registerCustomNode(lf) {
+ lf.register({
+ type: "CustomNode",
+ view: CustomFlowHtmlNode,
+ model: CustomFlowHtmlModel
+ })
+}
\ No newline at end of file
diff --git a/src/views/device/register/components/LogicFlow/index.vue b/src/views/device/register/components/LogicFlow/index.vue
new file mode 100644
index 0000000..facfd2a
--- /dev/null
+++ b/src/views/device/register/components/LogicFlow/index.vue
@@ -0,0 +1,294 @@
+
+
+
+
实例状态:{{ stateMap[instStatus]}}
+
lfRef.translateCenter()">居中
+
lfRef.fitView()">适应屏幕
+
停止
+
暂停
+
恢复
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/views/device/register/components/MechanicalArm/components/JointControl.vue b/src/views/device/register/components/MechanicalArm/components/JointControl.vue
new file mode 100644
index 0000000..f53b595
--- /dev/null
+++ b/src/views/device/register/components/MechanicalArm/components/JointControl.vue
@@ -0,0 +1,133 @@
+
+
+
+
+
+
+ {{ formatAngle(joint.value) }}
+ [{{ formatAngle(joint.min) }}, {{ formatAngle(joint.max) }}]
+
+
+
+
updateJointAngle(joint.name, value)"/>
+
+ {{ formatAngle(joint.min) }}
+ {{ formatAngle(joint.max) }}
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/views/device/register/components/MechanicalArm/components/UrdfViewer.vue b/src/views/device/register/components/MechanicalArm/components/UrdfViewer.vue
new file mode 100644
index 0000000..bdcc52b
--- /dev/null
+++ b/src/views/device/register/components/MechanicalArm/components/UrdfViewer.vue
@@ -0,0 +1,394 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/views/device/register/components/MechanicalArm/index.vue b/src/views/device/register/components/MechanicalArm/index.vue
new file mode 100644
index 0000000..558f317
--- /dev/null
+++ b/src/views/device/register/components/MechanicalArm/index.vue
@@ -0,0 +1,350 @@
+
+
+
+
+
+
+
目标
+
+
+
+
零位姿态
+
初始位置
+
+
+
X: {{ form.x.toFixed(6) }} m
+
Y: {{ form.y.toFixed(6) }} m
+
Z: {{ form.z.toFixed(6) }} m
+
RX: {{ form.rx.toFixed(6) }} deg
+
RY: {{ form.ry.toFixed(6) }} deg
+
RZ: {{ form.rz.toFixed(6) }} deg
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ pos.label }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/device/register/components/Microphone/index.vue b/src/views/device/register/components/Microphone/index.vue
new file mode 100644
index 0000000..fa76496
--- /dev/null
+++ b/src/views/device/register/components/Microphone/index.vue
@@ -0,0 +1,225 @@
+
+
+
远程麦克风
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/device/register/components/Motor/index.vue b/src/views/device/register/components/Motor/index.vue
new file mode 100644
index 0000000..8ecf371
--- /dev/null
+++ b/src/views/device/register/components/Motor/index.vue
@@ -0,0 +1,76 @@
+
+
+
+
+
+
+
+
+ 电机pic
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/views/device/register/components/Speaker/index.vue b/src/views/device/register/components/Speaker/index.vue
new file mode 100644
index 0000000..f29bf07
--- /dev/null
+++ b/src/views/device/register/components/Speaker/index.vue
@@ -0,0 +1,276 @@
+
+
+
远程扬声器
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ Math.round(volume) }}%
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/views/device/register/components/SpeechSynthesis/AudioPlayer.vue b/src/views/device/register/components/SpeechSynthesis/AudioPlayer.vue
new file mode 100644
index 0000000..be36efc
--- /dev/null
+++ b/src/views/device/register/components/SpeechSynthesis/AudioPlayer.vue
@@ -0,0 +1,108 @@
+
+
+
+
+
{{ formattedDuration }}"
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/device/register/components/SpeechSynthesis/chat.vue b/src/views/device/register/components/SpeechSynthesis/chat.vue
new file mode 100644
index 0000000..aaf1aca
--- /dev/null
+++ b/src/views/device/register/components/SpeechSynthesis/chat.vue
@@ -0,0 +1,699 @@
+
+
+
+
+
+
+ {{ queryText }}
+
+
+
+
+
{{ `我是${aiName}助手,很高兴见到你` }}
+
我可以帮你合成音色,请把你的任务交给我吧~
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/device/register/components/SpeechSynthesis/img/ai.png b/src/views/device/register/components/SpeechSynthesis/img/ai.png
new file mode 100644
index 0000000..79bbe45
Binary files /dev/null and b/src/views/device/register/components/SpeechSynthesis/img/ai.png differ
diff --git a/src/views/device/register/components/SpeechSynthesis/img/user.png b/src/views/device/register/components/SpeechSynthesis/img/user.png
new file mode 100644
index 0000000..481f7e7
Binary files /dev/null and b/src/views/device/register/components/SpeechSynthesis/img/user.png differ
diff --git a/src/views/device/register/components/SpeechSynthesis/index.vue b/src/views/device/register/components/SpeechSynthesis/index.vue
new file mode 100644
index 0000000..94b9003
--- /dev/null
+++ b/src/views/device/register/components/SpeechSynthesis/index.vue
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/device/register/components/SpeechSynthesis/soundRecording.vue b/src/views/device/register/components/SpeechSynthesis/soundRecording.vue
new file mode 100644
index 0000000..5b2a05b
--- /dev/null
+++ b/src/views/device/register/components/SpeechSynthesis/soundRecording.vue
@@ -0,0 +1,115 @@
+
+
+
+
+
+
+
+
diff --git a/src/views/device/register/components/TouchInteraction/ai.png b/src/views/device/register/components/TouchInteraction/ai.png
new file mode 100644
index 0000000..79bbe45
Binary files /dev/null and b/src/views/device/register/components/TouchInteraction/ai.png differ
diff --git a/src/views/device/register/components/TouchInteraction/index.vue b/src/views/device/register/components/TouchInteraction/index.vue
new file mode 100644
index 0000000..08788ae
--- /dev/null
+++ b/src/views/device/register/components/TouchInteraction/index.vue
@@ -0,0 +1,512 @@
+
+
+
触控交互
+
+
+
+
+
+
+
+
+ select file
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/device/register/components/TouchInteraction/zsj.png b/src/views/device/register/components/TouchInteraction/zsj.png
new file mode 100644
index 0000000..481f7e7
Binary files /dev/null and b/src/views/device/register/components/TouchInteraction/zsj.png differ
diff --git a/src/views/device/register/components/index.js b/src/views/device/register/components/index.js
new file mode 100644
index 0000000..93584d6
--- /dev/null
+++ b/src/views/device/register/components/index.js
@@ -0,0 +1,59 @@
+import {controlPageList} from "@/enum/index.js";
+import {ElMessage} from "element-plus";
+import router from "@/router";
+
+export const intoControlPage = (path, params) => {
+ if (!controlPageList.includes(path)) {
+ ElMessage.warning('该页面不在控制台页面中,无法打开');
+ return
+ }
+ // const fullPath = `${window.location.origin}${path}/${params}`;
+ // // 使用 window.open 在新标签页中打开
+ // window.open(fullPath, '_blank');
+ const fullPath = `${path}/${params}`; // 构造路由路径
+ router.push(fullPath); // 使用路由跳转
+}
+
+// 定义需要转换的属性列表
+const propertiesToConvert = [
+ "workModule",
+ "posStep",
+ "postureStep",
+ "jointStep",
+ "joint1",
+ "joint2",
+ "joint3",
+ "joint4",
+ "joint5",
+ "joint6",
+ "x",
+ "y",
+ "z",
+ "rx",
+ "ry",
+ "rz"
+];
+export const formatNumber = (data) => {
+ const res = JSON.parse(JSON.stringify(data));
+// 遍历属性列表并转换值
+ propertiesToConvert.forEach(property => {
+ if (data[property] !== null) {
+ res[property] = Number(data[property]);
+ }
+ });
+ return res
+}
+
+//对象中存在数字类型属性时,转换为字符串类型
+export const convertNumbersToStrings = (obj) => {
+ for (const key in obj) {
+ if (obj.hasOwnProperty(key)) {
+ const value = obj[key];
+ // 检查值是否不是 null 且是数字类型
+ if (value !== null && typeof value === 'number') {
+ obj[key] = String(value); // 转换为字符串
+ }
+ }
+ }
+ return obj;
+}
diff --git a/src/views/device/register/index.vue b/src/views/device/register/index.vue
new file mode 100644
index 0000000..7520614
--- /dev/null
+++ b/src/views/device/register/index.vue
@@ -0,0 +1,414 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 新增
+
+
+
+ 修改
+
+
+
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ controlModuleList.find(item => item.value === scope.row.deviceModel)?.label }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 修改
+
+ 示教
+
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/device/robot/ContextMenu.vue b/src/views/device/robot/ContextMenu.vue
new file mode 100644
index 0000000..ab47455
--- /dev/null
+++ b/src/views/device/robot/ContextMenu.vue
@@ -0,0 +1,89 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/views/device/robot/TechButtonWithLine.vue b/src/views/device/robot/TechButtonWithLine.vue
new file mode 100644
index 0000000..a2a3526
--- /dev/null
+++ b/src/views/device/robot/TechButtonWithLine.vue
@@ -0,0 +1,547 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/views/device/robot/index.vue b/src/views/device/robot/index.vue
new file mode 100644
index 0000000..058ce86
--- /dev/null
+++ b/src/views/device/robot/index.vue
@@ -0,0 +1,874 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ isTreeCollapsed ? '>' : '<' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/views/device/terminal/index.vue b/src/views/device/terminal/index.vue
new file mode 100644
index 0000000..b47886d
--- /dev/null
+++ b/src/views/device/terminal/index.vue
@@ -0,0 +1,316 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 新增
+
+
+
+ 修改
+
+
+
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 修改
+
+
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/error/401.vue b/src/views/error/401.vue
new file mode 100644
index 0000000..1ba3792
--- /dev/null
+++ b/src/views/error/401.vue
@@ -0,0 +1,82 @@
+
+
+
+ 返回
+
+
+
+
+ 401错误!
+
+ 您没有访问权限!
+ 对不起,您没有访问权限,请不要进行非法操作!您可以返回主页面
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/error/404.vue b/src/views/error/404.vue
new file mode 100644
index 0000000..f205303
--- /dev/null
+++ b/src/views/error/404.vue
@@ -0,0 +1,227 @@
+
+
+
+
+
+
+ 404错误!
+
+
+ {{ message }}
+
+
+ 对不起,您正在寻找的页面不存在。尝试检查URL的错误,然后按浏览器上的刷新按钮或尝试在我们的应用程序中找到其他内容。
+
+
+ 返回首页
+
+
+
+
+
+
+
+
+
diff --git a/src/views/flow/components/Aside.vue b/src/views/flow/components/Aside.vue
new file mode 100644
index 0000000..3d90731
--- /dev/null
+++ b/src/views/flow/components/Aside.vue
@@ -0,0 +1,76 @@
+
+
+
+
+
+
+
+
+ {{ node.name }}
+
+
{{ node.desc }}
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/views/flow/components/FormItemRecursive.vue b/src/views/flow/components/FormItemRecursive.vue
new file mode 100644
index 0000000..d466bb3
--- /dev/null
+++ b/src/views/flow/components/FormItemRecursive.vue
@@ -0,0 +1,221 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/views/flow/components/NodeState.vue b/src/views/flow/components/NodeState.vue
new file mode 100644
index 0000000..201e671
--- /dev/null
+++ b/src/views/flow/components/NodeState.vue
@@ -0,0 +1,176 @@
+
+
+
+
+
+
+
+
+
+
+
+
{{ getStateText() }}
+
+ {{ props.runtimes }}ms
+
+
+
+ {{ popoverVisible ? "隐藏结果" : "展示结果" }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/flow/components/NodeTitle.vue b/src/views/flow/components/NodeTitle.vue
new file mode 100644
index 0000000..73d1b68
--- /dev/null
+++ b/src/views/flow/components/NodeTitle.vue
@@ -0,0 +1,272 @@
+
+
+
+
+
+
+
+ {{ localName }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ props.nodeDesc }}
+
+
+
+
diff --git a/src/views/flow/components/OuterNode.vue b/src/views/flow/components/OuterNode.vue
new file mode 100644
index 0000000..1da9764
--- /dev/null
+++ b/src/views/flow/components/OuterNode.vue
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/views/flow/components/ParamsDrawer.vue b/src/views/flow/components/ParamsDrawer.vue
new file mode 100644
index 0000000..5502a5f
--- /dev/null
+++ b/src/views/flow/components/ParamsDrawer.vue
@@ -0,0 +1,1800 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/views/flow/components/TestRun.vue b/src/views/flow/components/TestRun.vue
new file mode 100644
index 0000000..9e3bbb6
--- /dev/null
+++ b/src/views/flow/components/TestRun.vue
@@ -0,0 +1,146 @@
+
+
+
+
+
+
+ {{ row.name }}
+ *
+
+
+
+
+ {{ scope.row.type }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 关闭
+ 确定
+
+
+
+
\ No newline at end of file
diff --git a/src/views/flow/config.js b/src/views/flow/config.js
new file mode 100644
index 0000000..68d89d4
--- /dev/null
+++ b/src/views/flow/config.js
@@ -0,0 +1,700 @@
+import { Group, SelectionSelect } from "@logicflow/extension";
+import { registerCommon } from "./nodes/common/index";
+
+import { registerFunction } from './nodes/function'
+
+import cameraSvg from './icon/camera.svg'
+import videoSvg from './icon/video.svg'
+import loopSvg from './icon/loop.svg'
+import stopLoopSvg from './icon/stopLoop.svg'
+import switchSvg from './icon/switch.svg'
+import startSvg from './icon/start.svg'
+import endSvg from './icon/end.svg'
+import microphoneSvg from './icon/microphone.svg'
+import sleepSvg from './icon/sleep.svg'
+import planSvg from './icon/plan.svg'
+import awakenSvg from './icon/awaken.svg'
+import dialogueSvg from './icon/dialogue.svg'
+import audioSvg from './icon/audio.svg'
+import touchSvg from './icon/touch.svg'
+import expressionSvg from './icon/expression.svg'
+
+import { v4 as randomUUID } from 'uuid'
+
+export const lfConfig = {
+ idGenerator: () => {
+ // 生成标准UUID并移除连字符
+ // return crypto.randomUUID().replace(/-/g, '');
+ return randomUUID().replace(/-/g, '')
+ },
+ background: {
+ backgroundColor: "#f6f8fa",
+ },
+ grid: false,
+ plugins: [Group, SelectionSelect],
+ animation: true,
+ adjustEdgeStartAndEnd: false,
+ // multipleSelectKey: "ctrl",
+ // disabledTools: ["multipleSelect"],
+ partial: true,
+ // group: {
+ // foldable: true, // 启用折叠功能
+ // foldSize: 30, // 折叠后显示的图标尺寸
+ // },
+ edgeType: "bezier",
+ zoomConfig: {
+ min: 0.05, // 最小缩放比例(支持更小值,如 0.01,但不建议过小)
+ max: 3, // 最大缩放比例(可自定义)
+ step: 0.1 // 每次滚轮缩放的步长(默认 0.1)
+ },
+ style: {
+ anchor: {
+ show: true, // 强制全局锚点显示
+ hoverOn: false,
+ visibility: "visible",
+ fill: "#FF5722", // 实心绿色
+ stroke: "#FF5722", // 边框同色
+ strokeWidth: 0, // 消除边框
+ r: 7, // 锚点半径
+ hover: { fill: "#FF5722" }, // 禁用悬停变色
+ },
+ anchorLine: {
+ stroke: "#FF5722",
+ strokeWidth: 2,
+ strokeDasharray: "3,2",
+ },
+ bezier: {
+ stroke: "#FF5722",
+ strokeWidth: 2,
+ arrow: false, // 关闭箭头显示
+ animation: true, // 开启动画效果
+ animationSpeed: 3,
+ },
+ },
+};
+
+// 注册自定义节点
+export const registerCustomizeNode = (lf) => {
+ registerCommon(lf)
+ registerFunction(lf)
+};
+
+const deviceOptions = () => {
+ return [{
+ value: 'cam1',
+ label: 'cam1'
+ }, {
+ value: 'cam2',
+ label: 'cam2'
+ }, {
+ value: 'cam3',
+ label: 'cam3'
+ }, {
+ value: 'cam4',
+ label: 'cam4'
+ }]
+}
+
+const audioOptions = () => {
+ return [{
+ value: 'spk1',
+ label: 'spk1'
+ }]
+}
+
+const httpMethodOptions = () => {
+ return [{
+ value: 'POST',
+ label: 'POST'
+ }, {
+ value: 'GET',
+ label: 'GET'
+ }, {
+ value: 'PUT',
+ label: 'PUT'
+ }, {
+ value: 'DELETE',
+ label: 'DELETE'
+ }]
+}
+
+const expressOptions = () => {
+ return [{
+ value: 1,
+ label: '高兴'
+ }, {
+ value: 2,
+ label: '惊讶'
+ }, {
+ value: 3,
+ label: '愤怒'
+ }, {
+ value: 4,
+ label: '悲伤'
+ }, {
+ value: 5,
+ label: '睡觉'
+ }, {
+ value: 6,
+ label: '打哈欠'
+ }]
+}
+
+const taskTypeOptions = () => {
+ return [{
+ value: 'voice',
+ label: '语音交互'
+ }, {
+ value: 'touch',
+ label: '触控交互'
+ }]
+}
+
+const voiceOptions = () => {
+ return [{
+ value: 'x4_xiaoyan',
+ label: 'x4_xiaoyan'
+ }]
+}
+
+export const collapseList = [
+ {
+ collapseTitle: "相机",
+ nodeList: [
+ {
+ icon: cameraSvg,
+ name: "开启相机",
+ type: "serviceNode",
+ desc: "在获取相机照片时,需要先启动相机",
+ action: 'CAMERA_START',
+ nodeType: 'EDGE',
+ nodeParams: [
+ { name: "deviceId", type: "input", input: "", componentType: 'select', selectOptions: deviceOptions(), disabled: true }
+ ],
+ outputParams: [],
+ outputType: 'json'
+ },
+ {
+ icon: cameraSvg,
+ name: "获取图片",
+ type: "serviceNode",
+ desc: "获取相机拍摄的照片",
+ action: 'CAMERA_GETRGBIMAGE',
+ nodeType: 'EDGE',
+ nodeParams: [
+ { name: "deviceId", type: "input", input: "", componentType: 'select', selectOptions: deviceOptions(), disabled: true }
+ ],
+ outputType: 'img',
+ outputParams: [{ name: 'imageUrl', type: 'array', desc: '图片地址数组', children: [], disabled: true }]
+ },
+ {
+ icon: cameraSvg,
+ name: "关闭相机",
+ type: "serviceNode",
+ desc: "获取相机照片后,关闭该相机",
+ action: 'CAMERA_STOP',
+ nodeType: 'EDGE',
+ nodeParams: [
+ { name: "deviceId", type: "input", input: "", componentType: 'select', selectOptions: deviceOptions(), disabled: true }
+ ],
+ outputParams: [],
+ outputType: 'json'
+ },
+ ],
+ },
+ {
+ collapseTitle: "录像",
+ nodeList: [
+ {
+ icon: videoSvg,
+ name: "开启录像",
+ type: "serviceNode",
+ desc: "开启摄像头,并开始录像",
+ action: 'CAMERA_RECORDING_START',
+ nodeType: 'EDGE',
+ nodeParams: [
+ { name: "deviceId", type: "input", input: "", componentType: 'select', selectOptions: deviceOptions(), disabled: true }
+ ],
+ outputParams: [],
+ outputType: 'json'
+ },
+ {
+ icon: videoSvg,
+ name: "结束录像",
+ type: "serviceNode",
+ desc: "停止录像,并关闭摄像头",
+ action: 'CAMERA_RECORDING_STOP',
+ nodeType: 'EDGE',
+ nodeParams: [
+ { name: "deviceId", type: "input", input: "", componentType: 'select', selectOptions: deviceOptions(), disabled: true }
+ ],
+ outputType: 'video',
+ outputParams: [{ name: 'videoUrl', type: 'array', children: [], desc: '视频播放地址数组', disabled: true}]
+ }
+ ],
+ },
+ {
+ collapseTitle: "功能",
+ nodeList: [
+ {
+ icon: loopSvg,
+ name: "循环",
+ type: "loop",
+ action: 'START_LOOP',
+ desc: "实现对列表对象循环执行一系列任务",
+ nodeParams: [{ name: "loopNum", type: "input", input: null, quote: "" }],
+ outputParams: [],
+ outputType: 'json'
+ },
+ {
+ icon: stopLoopSvg,
+ name: "结束循环",
+ type: "stopLoop",
+ action: 'STOP_LOOP',
+ desc: "用于立即终止当前所在的循环,跳出循环体",
+ },
+ // {
+ // icon: startSvg,
+ // name: "单次循环开始",
+ // type: "subStart",
+ // action: 'SUB_START',
+ // desc: "循环体内部工作流的开始节点,开始循环体内部的单次流程",
+ // },
+ // {
+ // icon: endSvg,
+ // name: "单次循环结束",
+ // type: "subEnd",
+ // action: 'SUB_END',
+ // desc: "循环体内部工作流的终止节点,结束循环体内部的单次流程",
+ // },
+ {
+ icon: switchSvg,
+ name: "分支",
+ type: "branch",
+ action: 'BRANCH',
+ desc: "连接多个下游分支,根据设定的条件按照顺序查找的方式来匹配运行的分支,如果匹配到某条件则只运行该条件对应的分支,否则继续匹配下一条件直至结束",
+ },
+ {
+ icon: sleepSvg,
+ name: "睡眠",
+ type: "sleep",
+ desc: "用于睡眠整个流程,表示延迟多少毫秒",
+ action: 'sleep',
+ nodeParams: [
+ { name: "delayMs", type: "input", componentType: 'number', input: 0, disabled: true }
+ ],
+ outputParams: [],
+ outputType: 'json'
+ },
+ {
+ icon: expressionSvg,
+ name: "HTTP请求",
+ type: "http",
+ desc: "HTTP请求",
+ action: 'HTTP',
+ nodeType: 'HTTP',
+ nodeParams: [
+ { name: "config", type: "input", input: "", children: [
+ { name: "url", type: "input", input: "", disabled: true, required: true },
+ { name: "method", type: "input", input: "POST", componentType: 'select', selectOptions: httpMethodOptions(), disabled: true },
+ { name: "timeout", type: "input", input: "10000", componentType: 'number', required: false, disabled: true }
+ ], disabled: true, required: true },
+ { name: "headers", type: "input", input: "", children: [
+ { name: "", type: "input", input: ""}
+ ], disabled: true, required: true },
+ { name: "params", type: "input", input: "", children: [
+ { name: "", type: "input", input: ""}
+ ], disabled: true, required: true },
+ { name: "body", type: "input", input: "", children: [
+ { name: 'bodyType', type: "input", input: "json", disabled: true },
+ { name: 'json', type: "input", input: '{}' },
+ { name: 'formData', type: "input", input: "", children: []}
+ ], required: true },
+ ],
+ outputType: 'json',
+ outputParams: [{ name: 'result', type: 'Object', children: [], desc: 'HTTP请求结果'}]
+ },
+ {
+ icon: expressionSvg,
+ name: "代码",
+ type: "code",
+ desc: "js代码",
+ action: 'CODE',
+ canAddFormItem: true,
+ nodeParams: [
+ { name: "input", type: "input", input: "", required: true },
+ { name: "code", type: "input", input: `
+// 方法定义不能修改
+function handler(params) {
+ // 返回值是一个可序列化成 json 的 dict 或 object
+ const result ={
+ type: 2,
+ message: params.input
+ }
+ return result
+}
+ `, required: true }
+ ],
+ outputType: 'json',
+ outputParams: [{ name: 'result', type: 'object', children: [
+ {name: 'type', type: 'number', desc: '占位属性'},
+ {name: 'message', type: 'string', desc: '占位属性'}
+ ], desc: 'js节点返回'}]
+ },
+ {
+ icon: microphoneSvg,
+ name: "获取当前循环对象",
+ type: "currentLoop",
+ desc: "获取当前循环对象",
+ action: 'GET_CURRENT_OBJECT',
+ nodeParams: [
+ { name: "array", type: "input", input: "" }
+ ],
+ outputType: 'json',
+ outputParams: [
+ { name: 'index', type: 'number', desc: '索引'},
+ { name: 'object', type: 'object', children: [
+ { name: 'test', type: 'string', desc: '占位属性' }
+ ], desc: '当前循环对象', }
+ ]
+ },
+ ],
+ },
+ {
+ collapseTitle: "机器人",
+ nodeList: [
+ {
+ icon: videoSvg,
+ name: "机械臂",
+ type: "serviceNode",
+ desc: "控制机器人的手臂",
+ action: 'ALM',
+ nodeParams: [
+ { name: "deviceId", type: "input", input: "", disabled: true }
+ ],
+ outputType: 'json'
+ },
+ ],
+ },
+ {
+ collapseTitle: "音频",
+ nodeList: [
+ {
+ icon: microphoneSvg,
+ name: "启动麦克风",
+ type: "serviceNode",
+ desc: "用于启动麦克风的节点",
+ action: 'MICROPHONE_START',
+ nodeType: 'EDGE',
+ nodeParams: [
+ { name: "deviceId", type: "input", input: "", componentType: 'select', selectOptions: audioOptions(), disabled: true }
+ ],
+ outputType: 'json'
+ },
+ {
+ icon: microphoneSvg,
+ name: "停止麦克风",
+ type: "serviceNode",
+ desc: "用于停止麦克风的节点",
+ action: 'MICROPHONE_STOP',
+ nodeType: 'EDGE',
+ nodeParams: [
+ { name: "deviceId", type: "input", input: "", componentType: 'select', selectOptions: audioOptions(), disabled: true }
+ ],
+ outputType: 'json'
+ },
+ {
+ icon: audioSvg,
+ name: "播放音频",
+ type: "serviceNode",
+ desc: "用于播放音频的节点",
+ action: 'SPEAKER_PLAYAUDIO',
+ nodeParams: [
+ { name: "deviceId", type: "input", input: "", componentType: 'select', selectOptions: audioOptions(), disabled: true }
+ ],
+ outputType: 'json'
+ },
+ ],
+ },
+ {
+ collapseTitle: "大模型",
+ nodeList: [
+ {
+ icon: microphoneSvg,
+ name: "获取触控坐标",
+ type: "serviceNode",
+ desc: "用于获取触控坐标的节点",
+ action: 'TOUCH_COORDINATES',
+ nodeType: "LLM",
+ nodeParams: [
+ { name: "targetFeature", type: "input", input: "运动模式", disabled: true },
+ { name: "manufacturer", type: "input", input: "xiaomi", disabled: true },
+ { name: "vehType", type: "input", input: "su7", disabled: true },
+ ],
+ outputType: 'json',
+ outputParams: [{ name: 'coordinates', type: 'object', children: [
+ { name: 'x', type: 'number', desc: 'x坐标' },
+ { name: 'y', type: 'number', desc: 'y坐标' }
+ ], desc: '坐标对象', }]
+ }, {
+ icon: dialogueSvg,
+ name: "tts语音合成",
+ type: "serviceNode",
+ desc: "将指令文本(text)处理合成语音,输出语音路径(audioPath)",
+ action: 'GENERATE_ADVANCED_AUDIO',
+ nodeType: "LLM",
+ outputType: 'json',
+ nodeParams: [{ name: 'text', type: "input", input: "", disabled: true }],
+ outputParams: [{ name: 'audioPath', type: 'string', desc: '语音路径', disabled: true}]
+ }, {
+ icon: dialogueSvg,
+ name: "意图识别",
+ type: "serviceNode",
+ desc: "意图识别,输出类型(type) 1: 语音交互 2: 触控交互 3: 闲聊 4: 知识问答",
+ action: 'INTENT_RECOGNITION',
+ nodeType: "LLM",
+ outputType: 'json',
+ nodeParams: [{ name: 'text', type: "input", input: "", disabled: true }],
+ outputParams: [{ name: 'type', type: 'string', desc: '1:语音交互 2:触控交互 3: 闲聊 4:知识问答', disabled: true}]
+ }, {
+ icon: dialogueSvg,
+ name: "商道智能体",
+ type: "sdAgent",
+ desc: "通过apiKey调用商道大模型",
+ action: 'AI_AGENT_PLATFORM',
+ nodeType: "LLM",
+ outputType: 'json',
+ nodeParams: [
+ { name: "config", type: "input", input: "", children: [
+ { name: 'apiKey', type: "input", input: "", disabled: true },
+ { name: 'text', type: "input", input: "", disabled: true },
+ ]},
+ { name: 'invokeTts', type: "input", input: false },
+ { name: 'tts', type: "input", input: "", children: [
+ { name: 'url', type: "input", input: "", disabled: true },
+ { name: 'text', type: "input", input: "", disabled: true },
+ { name: 'speed', type: "input", input: "", max: 100, disabled: true },
+ { name: 'voice', type: "input", input: "", componentType: 'select', selectOptions: voiceOptions(), disabled: true },
+ { name: 'volume', type: "input", input: "", componentType: 'number', max: 100, disabled: true }
+ ], disabled: true }
+ ],
+ outputParams: [{ name: 'result', type: 'string', desc: '商道大模型的返回文案', disabled: true}]
+ }, {
+ icon: dialogueSvg,
+ name: "tts语音播放",
+ type: "serviceNode",
+ desc: "调用tts将文案转换为语音播放",
+ action: 'AI_TTS',
+ nodeType: "LLM",
+ outputType: 'json',
+ nodeParams: [
+ { name: 'url', type: "input", input: "", disabled: true },
+ { name: 'text', type: "input", input: "", disabled: true },
+ { name: 'speed', type: "input", input: "", max: 100, disabled: true },
+ { name: 'voice', type: "input", input: "", componentType: 'select', selectOptions: voiceOptions(), disabled: true },
+ { name: 'volume', type: "input", input: "", componentType: 'number', max: 100, disabled: true }
+ ]
+ }, {
+ icon: dialogueSvg,
+ name: "获取当前页面名称",
+ type: "serviceNode",
+ desc: "获取当前页面名称",
+ action: 'GET_CURRENT_PAGE',
+ nodeType: "LLM",
+ outputType: 'json',
+ nodeParams: [
+ { name: 'url', type: "input", input: "", disabled: true },
+ ],
+ outputParams: [{ name: 'pageName', type: 'string', desc: '当前页面名称', disabled: true}]
+ }
+ ],
+ },
+ {
+ collapseTitle: "语音交互",
+ nodeList: [
+ {
+ icon: planSvg,
+ name: "方案处理",
+ type: "serviceNode",
+ desc: "语音交互-方案处理,输入方案id(schemeId),输出唤醒语料(wakeCorpus)和测试语料数组(testCorpus)",
+ action: 'VI_SCHEME',
+ nodeParams: [{ name: 'schemeId', type: "input", input: "", disabled: true }],
+ outputType: 'json',
+ outputParams: [
+ { name: 'wakeCorpus', type: 'object', desc: '唤醒语料', children: []},
+ { name: 'testCorpus', type: 'array', desc: '测试语料数组', children: []},
+ { name: 'sceneType', type: 'number', desc: '1:唤醒测试,2:单次对话测试,3:连续对话测试', disabled: true }
+ ]
+ },
+ {
+ icon: awakenSvg,
+ name: "唤醒语料",
+ type: "serviceNode",
+ desc: "唤醒语料处理,输入唤醒语料(wakeCorpus),输出语音路径(audioPath)",
+ action: 'VI_CORPUS_WAKE',
+ nodeType: "EDGE",
+ outputType: 'json',
+ nodeParams: [{ name: 'wakeCorpus', type: "input", input: "", disabled: true }],
+ outputParams: [{ name: 'audioPath', type: 'string', desc: '语音路径', disabled: true}]
+ },
+ {
+ icon: dialogueSvg,
+ name: "单次对话语料",
+ type: "serviceNode",
+ desc: "测试语料-单次对话语料处理,输入单次对话语料(testCorpus),输出语音路径(audioPath)",
+ action: 'VI_CORPUS_SINGLE',
+ nodeType: "EDGE",
+ outputType: 'json',
+ nodeParams: [{ name: 'testCorpus', type: "input", input: "", disabled: true }],
+ outputParams: [{ name: 'audioPath', type: 'string', desc: '语音路径', disabled: true}]
+ },
+ {
+ icon: dialogueSvg,
+ name: "连续对话语料",
+ type: "serviceNode",
+ desc: "测试语料-连续对话语料处理,输入连续对话语料(testCorpus),输出语音路径(audioPath)",
+ action: 'VI_CORPUS_CONTINUOUS',
+ nodeType: "EDGE",
+ outputType: 'json',
+ nodeParams: [{ name: 'testCorpus', type: "input", input: "", disabled: true }],
+ outputParams: [{ name: 'audioPath', type: 'string', desc: '语音路径', disabled: true}]
+ },
+ {
+ icon: audioSvg,
+ name: "播放语料",
+ type: "serviceNode",
+ desc: "用于播放语料的节点,需要输入语音路径(audioPath)和播放语音的设备id(deviceId)",
+ action: 'VI_PLAY_CORPUS',
+ nodeType: "EDGE",
+ outputType: 'json',
+ nodeParams: [
+ { name: 'audioPath', type: "input", input: "", disabled: true },
+ { name: 'deviceId', type: "input", input: "", disabled: true },
+ { name: 'bioId', type: "input", input: "", disabled: true, required: false }
+ ],
+ }
+ ],
+ },
+ {
+ collapseTitle: "触控交互",
+ nodeList: [
+ {
+ icon: touchSvg,
+ name: "触控",
+ type: "serviceNode",
+ desc: "去触控坐标的点位",
+ action: 'TOUCH',
+ nodeType: "EDGE",
+ nodeParams: [
+ { name: "deviceId", type: "input", input: "", disabled: true },
+ { name: "touchParams", type: "input", input: "", disabled: true }
+ ],
+ outputType: 'json',
+ },
+ {
+ icon: expressionSvg,
+ name: "开始说话",
+ type: "serviceNode",
+ desc: "控制机器人说话",
+ action: 'BIO_HEAD_SPEAK_START',
+ nodeType: "EDGE",
+ nodeParams: [
+ { name: "deviceId", type: "input", input: "", disabled: true }
+ ],
+ outputType: 'json',
+ },
+ {
+ icon: expressionSvg,
+ name: "停止说话",
+ type: "serviceNode",
+ desc: "停止机器人说话",
+ action: 'BIO_HEAD_SPEAK_STOP',
+ nodeType: "EDGE",
+ nodeParams: [
+ { name: "deviceId", type: "input", input: "", disabled: true }
+ ],
+ outputType: 'json',
+ },
+ {
+ icon: expressionSvg,
+ name: "表情",
+ type: "serviceNode",
+ desc: "控制机器人的面部表情",
+ action: 'BIO_HEAD_SPECIAL_EXPRESSION',
+ nodeType: "EDGE",
+ nodeParams: [
+ { name: "deviceId", type: "input", input: "", disabled: true },
+ { name: "expressKey", type: "input",componentType: 'select', selectOptions: expressOptions(), disabled: true },
+ ],
+ outputType: 'json',
+ },
+ {
+ icon: touchSvg,
+ name: "路径搜索",
+ type: "serviceNode",
+ desc: "路径搜索",
+ action: 'TI_PATH_SEARCH',
+ nodeType: "EDGE",
+ nodeParams: [
+ { name: "currentLocation", type: "input", input: "", disabled: true },
+ { name: "funcId", type: "input", input: "", disabled: true },
+ { name: "vehicleConfigId", type: "input", input: "", disabled: true },
+ ],
+ outputType: 'json',
+ outputParams: [
+ { name: 'nextLocation', type: 'string', desc: '下一路径位置', disabled: true},
+ { name: 'isEnd', type: 'string', desc: '是否结束', disabled: true},
+ { name: 'iconUrl', type: 'string', desc: '图标路径', disabled: true}
+ ]
+ },
+ {
+ icon: touchSvg,
+ name: "获取触控二维坐标",
+ type: "serviceNode",
+ desc: "获取触控二维坐标",
+ action: 'TI_TOUCH_COORDINATES',
+ nodeType: "EDGE",
+ nodeParams: [
+ { name: "imageUrl", type: "input", input: "", disabled: true },
+ { name: "iconUrl", type: "input", input: "", disabled: true }
+ ],
+ outputType: 'json',
+ outputParams: [
+ { name: 'touchCoordinates', type: 'string', desc: '触控二维坐标', disabled: true},
+ ]
+ }
+ ],
+ },
+ {
+ collapseTitle: "AI评估",
+ nodeList: [
+ {
+ icon: expressionSvg,
+ name: "AI评估预处理",
+ type: "serviceNode",
+ desc: "AI评估预处理",
+ action: 'AI_EVALUATION_PRE',
+ nodeType: "AE",
+ nodeParams: [
+ { name: "taskType", type: "input", input: "", componentType: 'select', selectOptions: taskTypeOptions(), disabled: true },
+ { name: "wakeCorpus", type: "input", input: "", disabled: true, required: false },
+ { name: "testCorpus", type: "input", input: "", disabled: true, required: false },
+ { name: "sceneType", type: "input", input: "", disabled: true, required: false },
+ ],
+ outputType: 'json',
+ },
+ {
+ icon: touchSvg,
+ name: "AI评估执行",
+ type: "serviceNode",
+ desc: "AI评估执行",
+ action: 'AI_EVALUATION_END',
+ nodeType: "AE",
+ nodeParams: [
+ { name: "videoUrl", type: "input", input: "", disabled: true }
+ ],
+ outputType: 'json',
+ }
+ ],
+ },
+]
diff --git a/src/views/flow/icon/audio.svg b/src/views/flow/icon/audio.svg
new file mode 100644
index 0000000..e71eb39
--- /dev/null
+++ b/src/views/flow/icon/audio.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/views/flow/icon/awaken.svg b/src/views/flow/icon/awaken.svg
new file mode 100644
index 0000000..26b7a98
--- /dev/null
+++ b/src/views/flow/icon/awaken.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/views/flow/icon/camera.svg b/src/views/flow/icon/camera.svg
new file mode 100644
index 0000000..5d21dd9
--- /dev/null
+++ b/src/views/flow/icon/camera.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/views/flow/icon/dialogue.svg b/src/views/flow/icon/dialogue.svg
new file mode 100644
index 0000000..b208067
--- /dev/null
+++ b/src/views/flow/icon/dialogue.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/views/flow/icon/end.svg b/src/views/flow/icon/end.svg
new file mode 100644
index 0000000..81dc559
--- /dev/null
+++ b/src/views/flow/icon/end.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/views/flow/icon/expression.svg b/src/views/flow/icon/expression.svg
new file mode 100644
index 0000000..ae84256
--- /dev/null
+++ b/src/views/flow/icon/expression.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/views/flow/icon/group.svg b/src/views/flow/icon/group.svg
new file mode 100644
index 0000000..d2f250d
--- /dev/null
+++ b/src/views/flow/icon/group.svg
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/src/views/flow/icon/loop.svg b/src/views/flow/icon/loop.svg
new file mode 100644
index 0000000..00e5311
--- /dev/null
+++ b/src/views/flow/icon/loop.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/views/flow/icon/microphone.svg b/src/views/flow/icon/microphone.svg
new file mode 100644
index 0000000..d576328
--- /dev/null
+++ b/src/views/flow/icon/microphone.svg
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/src/views/flow/icon/plan.svg b/src/views/flow/icon/plan.svg
new file mode 100644
index 0000000..d166240
--- /dev/null
+++ b/src/views/flow/icon/plan.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/views/flow/icon/sleep.svg b/src/views/flow/icon/sleep.svg
new file mode 100644
index 0000000..75639ab
--- /dev/null
+++ b/src/views/flow/icon/sleep.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/views/flow/icon/start.svg b/src/views/flow/icon/start.svg
new file mode 100644
index 0000000..6e6db22
--- /dev/null
+++ b/src/views/flow/icon/start.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/views/flow/icon/stopLoop.svg b/src/views/flow/icon/stopLoop.svg
new file mode 100644
index 0000000..2cf73c6
--- /dev/null
+++ b/src/views/flow/icon/stopLoop.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/views/flow/icon/switch.svg b/src/views/flow/icon/switch.svg
new file mode 100644
index 0000000..58c1870
--- /dev/null
+++ b/src/views/flow/icon/switch.svg
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/views/flow/icon/touch.svg b/src/views/flow/icon/touch.svg
new file mode 100644
index 0000000..44a14a5
--- /dev/null
+++ b/src/views/flow/icon/touch.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/views/flow/icon/video.svg b/src/views/flow/icon/video.svg
new file mode 100644
index 0000000..400695e
--- /dev/null
+++ b/src/views/flow/icon/video.svg
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/src/views/flow/index.vue b/src/views/flow/index.vue
new file mode 100644
index 0000000..403709d
--- /dev/null
+++ b/src/views/flow/index.vue
@@ -0,0 +1,939 @@
+
+
+
+
+
+
{{ flowInfoData.name }}
+
{{ stateMap[flowState] }}
+
+
+
+
+ {{ totalRunningTime }}ms
+
+ 清除上一次运行结果
+ 分组
+ 试运行
+ 暂停
+ 恢复
+ 终止
+
+
+
+ 发布
+
+
发布
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/flow/nodes/common/endNode.js b/src/views/flow/nodes/common/endNode.js
new file mode 100644
index 0000000..5533405
--- /dev/null
+++ b/src/views/flow/nodes/common/endNode.js
@@ -0,0 +1,196 @@
+// 导入 HtmlNode 及其模型,为后续继承做准备
+import { HtmlNode, HtmlNodeModel } from "@logicflow/core";
+// 导入 Vue 相关方法,用于渲染组件
+import { createApp, h, nextTick } from "vue";
+import ElementPlus from "element-plus";
+// 导入 Vue 组件
+import EndNode from "./endNode.vue";
+import JsonViewer from 'vue3-json-viewer'
+
+/**
+ * 定义一个 元素的 HTML 节点类,继承自 HtmlNode
+ * 该类负责在 HTML 中渲染 元素,并处理其交互逻辑
+ */
+class EndNodeHtmlNode extends HtmlNode {
+ resizeObserver = null;
+ isMounted; // 标记组件是否已挂载
+ r; // 渲染函数
+ app; // Vue 应用实例
+ container = null;
+
+ /**
+ * 构造函数
+ * @param props 传递给节点的属性,包括模型、图模型等
+ */
+ constructor(props) {
+ super(props);
+ this.initVueApp(props);
+ }
+
+ initVueApp(props) {
+ this.isMounted = false;
+
+ this.hideAnchor = false;
+ this.autoExpand = true; // 防止锚点被折叠
+ this.anchorsPreset = "default"; // 重置锚点预设
+ // 创建 元素的渲染函数
+ this.r = h(EndNode, {
+ model: props.model,
+ graphModel: props.graphModel,
+ properties: {
+ ...props.model.getProperties(),
+ },
+ text: props.model.text.value,
+ onContentChange: this.handleContentChange.bind(this),
+ onBindRef: this.handleComponentInstance.bind(this),
+ });
+
+ // 创建 Vue 应用实例,并指定渲染函数
+ this.app = createApp({
+ render: () => this.r,
+ });
+ }
+
+ /**
+ * 将 HTML 内容设置到指定的根元素上
+ * @param rootEl 根元素
+ */
+ async setHtml(rootEl) {
+ if (!this.isMounted) {
+ this.isMounted = true;
+ this.container = document.createElement("div");
+ this.container.style.display = "inline-block"; // 关键:确保容器自适应内容
+
+ rootEl.appendChild(this.container);
+ this.app.use(ElementPlus); // 关键:单独注册ElementPlus
+ this.app.use(JsonViewer)
+ this.app.mount(this.container);
+ await nextTick();
+ this.setupSizeObserver();
+ } else {
+ this.r.component.props.properties = this.props.model.getProperties();
+ }
+ }
+
+ /**
+ * 获取节点文本内容
+ * 对于元素,返回 null,因为其内容由特定组件渲染
+ * @returns {null}
+ */
+ getText() {
+ return null;
+ }
+
+ handleComponentInstance(data) {
+ // 将组件实例保存到节点模型
+ this.props.model.setComponentInstance(data);
+ }
+
+ handleContentChange() {
+ // 内容变化时强制更新尺寸
+ this.updateNodeSize();
+ }
+
+ // 渲染完成后获取实际尺寸
+ updateNodeSize() {
+ if (this.container) {
+ const { SCALE_X, SCALE_Y } = this.props.graphModel.transformModel;
+ const node = this.container.querySelector(".node__container");
+ const rect = node.getBoundingClientRect();
+ this.props.model.updateSize(rect.width / SCALE_X, rect.height / SCALE_Y);
+ }
+ }
+
+ setupSizeObserver() {
+ // 首次渲染立即检测
+ requestAnimationFrame(() => {
+ this.updateNodeSize();
+ // 持续监听变化
+ this.resizeObserver = new ResizeObserver(() => {
+ this.updateNodeSize();
+ });
+ this.resizeObserver.observe(this.container);
+ });
+ }
+
+ // 组件卸载时移除监听
+ onDestroy() {
+ this.resizeObserver?.disconnect();
+ }
+}
+
+/**
+ * 定义一个元素的 HTML 模型类,继承自 HtmlNodeModel
+ * 该类主要设置节点的属性和样式
+ */
+class EndNodeHtmlModel extends HtmlNodeModel {
+ initNodeData(data) {
+ super.initNodeData(data);
+ }
+
+ // 保存组件实例引用
+ setComponentInstance(instance) {
+ this.componentInstance = instance;
+ }
+
+ // 验证表单
+ async validateForm() {
+ return { valid: true, message: "无验证方法" };
+ }
+
+ /**
+ * 设置节点属性
+ * 包括宽度、高度、文本编辑属性等
+ */
+ setAttributes() {
+ // 初始设置为0,后续动态更新
+ this.width = 0;
+ this.height = 0;
+ this.text.editable = false;
+ }
+
+ updateSize(width, height) {
+ this.width = width + 24;
+ this.height = height + 50;
+ this.initNodeData(this); // 触发节点重绘
+ }
+
+ // 定义节点只有左右两个锚点. 锚点位置通过中心点和宽度算出来。
+ getDefaultAnchor() {
+ let _a = this,
+ x = _a.x,
+ y = _a.y,
+ width = _a.width,
+ height = _a.height;
+ return [
+ {
+ x: x - width / 2,
+ y: y - 25,
+ name: "left",
+ id: "".concat(this.id, "_3"),
+ properties: { connectionType: "target" },
+ },
+ ];
+ }
+
+ /**
+ * 获取节点轮廓样式
+ * 覆盖父类方法,设置 stroke 属性为 none,以适应特定的视觉效果
+ * @returns {object} 节点轮廓样式
+ */
+ getOutlineStyle() {
+ const style = super.getOutlineStyle();
+ style.stroke = "none";
+ style.hover.stroke = "none";
+ return style;
+ }
+}
+
+// 导出方法注册
+export function registerEndNode(lf) {
+ lf.register({
+ type: "end",
+ view: EndNodeHtmlNode,
+ model: EndNodeHtmlModel,
+ });
+}
diff --git a/src/views/flow/nodes/common/endNode.vue b/src/views/flow/nodes/common/endNode.vue
new file mode 100644
index 0000000..f8bb74d
--- /dev/null
+++ b/src/views/flow/nodes/common/endNode.vue
@@ -0,0 +1,246 @@
+
+
+
+
+
+
+
end
+
+
工作流的终止节点,结束整个流程
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/views/flow/nodes/common/index.js b/src/views/flow/nodes/common/index.js
new file mode 100644
index 0000000..3a704ee
--- /dev/null
+++ b/src/views/flow/nodes/common/index.js
@@ -0,0 +1,11 @@
+import { registerStartNode } from "./startNode";
+import { registerEndNode } from "./endNode";
+import { registerSelectAreaNode } from './selectAreaNode.js'
+import { registerServiceNode } from './serviceNode.js'
+
+export const registerCommon = (lf) => {
+ registerStartNode(lf)
+ registerEndNode(lf)
+ registerSelectAreaNode(lf)
+ registerServiceNode(lf)
+}
diff --git a/src/views/flow/nodes/common/selectAreaNode.js b/src/views/flow/nodes/common/selectAreaNode.js
new file mode 100644
index 0000000..747edb2
--- /dev/null
+++ b/src/views/flow/nodes/common/selectAreaNode.js
@@ -0,0 +1,102 @@
+import { GroupNodeModel } from '@logicflow/extension';
+// 导入 Vue 相关方法,用于渲染组件
+import { createApp, h, nextTick } from "vue";
+import { HtmlNode, h as svgH } from "@logicflow/core";
+import ElementPlus from "element-plus";
+// @ts-ignore
+import SelectAreaNode from './selectAreaNode.vue';
+
+class SelectAreaModel extends GroupNodeModel {
+
+ initNodeData(data) {
+ super.initNodeData(data);
+ this.children = new Set(data.children || [])
+ this.zIndex = 0;
+ this.isGroup = true;
+ this.resizable = false;
+ }
+
+ getDefaultAnchor() {
+ return []
+ }
+
+ isAllowAppendIn() {
+ return false
+ }
+}
+
+class SelectAreaView extends HtmlNode {
+ resizeObserver = null;
+ isMounted; // 标记组件是否已挂载
+ r; // 渲染函数
+ app; // Vue 应用实例
+ container = null;
+
+ static reusePool = new Map(); // 节点复用池
+
+ /**
+ * 构造函数
+ * @param props 传递给节点的属性,包括模型、图模型等
+ */
+ constructor(props) {
+ super(props);
+ this.initVueApp(props);
+ }
+
+ initVueApp(props) {
+ this.isMounted = false;
+
+ this.hideAnchor = false;
+ this.autoExpand = true; // 防止锚点被折叠
+ this.anchorsPreset = "default"; // 重置锚点预设
+ // 创建 元素的渲染函数
+ this.r = h(SelectAreaNode, {
+ model: props.model,
+ graphModel: props.graphModel,
+ properties: {
+ ...props.model.getProperties(),
+ }
+ });
+
+ // 创建 Vue 应用实例,并指定渲染函数
+ this.app = createApp({
+ render: () => this.r,
+ });
+ }
+
+ /**
+ * 将 HTML 内容设置到指定的根元素上
+ * @param rootEl 根元素
+ */
+ async setHtml(rootEl) {
+ const nodeId = this.props.model.id;
+ if (SelectAreaView.reusePool.has(nodeId)) {
+ rootEl.appendChild(SelectAreaView.reusePool.get(nodeId));
+ return;
+ }
+
+ if (!this.isMounted) {
+ this.isMounted = true;
+ this.container = document.createElement("div");
+ this.container.style.width = "100%"; // 关键:确保容器自适应内容
+ this.container.style.height = "100%"; // 关键:确保容器自适应内容
+
+ rootEl.appendChild(this.container);
+ this.app.use(ElementPlus); // 关键:单独注册ElementPlus
+ this.app.mount(this.container);
+ SelectAreaView.reusePool.set(nodeId, this.container);
+ } else {
+ this.r.component.props.properties = this.props.model.getProperties();
+ }
+ }
+}
+
+
+// 导出方法注册
+export function registerSelectAreaNode(lf) {
+ lf.register({
+ type: "selectArea",
+ view: SelectAreaView,
+ model: SelectAreaModel,
+ });
+}
\ No newline at end of file
diff --git a/src/views/flow/nodes/common/selectAreaNode.vue b/src/views/flow/nodes/common/selectAreaNode.vue
new file mode 100644
index 0000000..58bf34a
--- /dev/null
+++ b/src/views/flow/nodes/common/selectAreaNode.vue
@@ -0,0 +1,59 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/src/views/flow/nodes/common/serviceNode.js b/src/views/flow/nodes/common/serviceNode.js
new file mode 100644
index 0000000..529dbb8
--- /dev/null
+++ b/src/views/flow/nodes/common/serviceNode.js
@@ -0,0 +1,213 @@
+// 导入 HtmlNode 及其模型,为后续继承做准备
+import { HtmlNode, HtmlNodeModel } from "@logicflow/core";
+// 导入 Vue 相关方法,用于渲染组件
+import { createApp, h, nextTick } from "vue";
+import ElementPlus from "element-plus";
+// 导入 Vue 组件
+// @ts-ignore
+import ServiceNode from "./serviceNode.vue";
+import OuterNode from "../../components/OuterNode.vue";
+import JsonViewer from 'vue3-json-viewer'
+
+/**
+ * 定义一个 元素的 HTML 节点类,继承自 HtmlNode
+ * 该类负责在 HTML 中渲染 元素,并处理其交互逻辑
+ */
+class ServiceNodeHtmlNode extends HtmlNode {
+ resizeObserver = null;
+ isMounted; // 标记组件是否已挂载
+ r; // 渲染函数
+ app; // Vue 应用实例
+ container = null;
+
+ static reusePool = new Map(); // 节点复用池
+
+ /**
+ * 构造函数
+ * @param props 传递给节点的属性,包括模型、图模型等
+ */
+ constructor(props) {
+ super(props);
+ this.initVueApp(props);
+ }
+
+ initVueApp(props) {
+ this.isMounted = false;
+
+ this.hideAnchor = false;
+ this.autoExpand = true; // 防止锚点被折叠
+ this.anchorsPreset = "default"; // 重置锚点预设
+ // 创建 元素的渲染函数
+ this.r = h(OuterNode, {
+ model: props.model,
+ component: ServiceNode,
+ properties: {
+ ...props.model.getProperties(),
+ },
+ onContentChange: this.handleContentChange.bind(this),
+ onBindRef: this.handleComponentInstance.bind(this)
+ });
+
+ // 创建 Vue 应用实例,并指定渲染函数
+ this.app = createApp({
+ render: () => this.r,
+ });
+ }
+
+ /**
+ * 将 HTML 内容设置到指定的根元素上
+ * @param rootEl 根元素
+ */
+ async setHtml(rootEl) {
+ const nodeId = this.props.model.id;
+ if (ServiceNodeHtmlNode.reusePool.has(nodeId)) {
+ rootEl.appendChild(ServiceNodeHtmlNode.reusePool.get(nodeId));
+ return;
+ }
+
+ if (!this.isMounted) {
+ this.isMounted = true;
+ this.container = document.createElement("div");
+ this.container.style.display = "inline-block"; // 关键:确保容器自适应内容
+
+ rootEl.appendChild(this.container);
+ this.app.use(ElementPlus);
+ this.app.use(JsonViewer);
+
+ this.app.mount(this.container);
+ await nextTick();
+ this.setupSizeObserver();
+ ServiceNodeHtmlNode.reusePool.set(nodeId, this.container);
+ } else {
+ this.r.component.props.properties = this.props.model.getProperties();
+ }
+ }
+
+ /**
+ * 获取节点文本内容
+ * 对于元素,返回 null,因为其内容由特定组件渲染
+ * @returns {null}
+ */
+ getText() {
+ return null;
+ }
+
+ handleComponentInstance(data) {
+ // 将组件实例保存到节点模型
+ this.props.model.setComponentInstance(data)
+ }
+
+ handleContentChange() {
+ // 内容变化时强制更新尺寸
+ this.updateNodeSize();
+ }
+
+ // 渲染完成后获取实际尺寸
+ updateNodeSize() {
+ if (this.container) {
+ const { SCALE_X, SCALE_Y } = this.props.graphModel.transformModel;
+ const node = this.container.querySelector(".node__container");
+ const rect = node.getBoundingClientRect();
+ this.props.model.updateSize(rect.width / SCALE_X, rect.height / SCALE_Y);
+ }
+ }
+
+ setupSizeObserver() {
+ // 首次渲染立即检测
+ requestAnimationFrame(() => {
+ this.updateNodeSize();
+ // 持续监听变化
+ this.resizeObserver = new ResizeObserver(() => {
+ this.updateNodeSize();
+ });
+ this.resizeObserver.observe(this.container);
+ });
+ }
+
+ // 组件卸载时移除监听
+ onDestroy() {
+ this.resizeObserver?.disconnect();
+ }
+}
+
+/**
+ * 定义一个元素的 HTML 模型类,继承自 HtmlNodeModel
+ * 该类主要设置节点的属性和样式
+ */
+class ServiceNodeHtmlModel extends HtmlNodeModel {
+ initNodeData(data) {
+ super.initNodeData(data);
+ }
+
+ // 保存组件实例引用
+ setComponentInstance(instance) {
+ this.componentInstance = instance;
+ }
+
+ closePopover() {
+ this.componentInstance.closePopover()
+ }
+
+ /**
+ * 设置节点属性
+ * 包括宽度、高度、文本编辑属性等
+ */
+ setAttributes() {
+ // 初始设置为0,后续动态更新
+ this.width = 0;
+ this.height = 0;
+ this.text.editable = false;
+ }
+
+ updateSize(width, height) {
+ this.width = width + 24;
+ this.height = height + 50;
+ this.initNodeData(this); // 触发节点重绘
+ }
+
+ // 定义节点只有左右两个锚点. 锚点位置通过中心点和宽度算出来。
+ getDefaultAnchor() {
+ let _a = this,
+ x = _a.x,
+ y = _a.y,
+ width = _a.width,
+ height = _a.height;
+ return [
+ {
+ x: x + width / 2 + 4,
+ y: y,
+ name: "right",
+ id: "".concat(this.id, "_1"),
+ properties: { connectionType: "source" },
+ },
+ {
+ x: x - width / 2,
+ y: y,
+ name: "left",
+ id: "".concat(this.id, "_3"),
+ properties: { connectionType: "target" },
+ },
+ ];
+ }
+
+ /**
+ * 获取节点轮廓样式
+ * 覆盖父类方法,设置 stroke 属性为 none,以适应特定的视觉效果
+ * @returns {object} 节点轮廓样式
+ */
+ getOutlineStyle() {
+ const style = super.getOutlineStyle();
+ style.stroke = "none";
+ style.hover.stroke = "none";
+ return style;
+ }
+}
+
+// 导出方法注册
+export function registerServiceNode(lf) {
+ lf.register({
+ type: "serviceNode",
+ view: ServiceNodeHtmlNode,
+ model: ServiceNodeHtmlModel,
+ });
+}
diff --git a/src/views/flow/nodes/common/serviceNode.vue b/src/views/flow/nodes/common/serviceNode.vue
new file mode 100644
index 0000000..6af53a7
--- /dev/null
+++ b/src/views/flow/nodes/common/serviceNode.vue
@@ -0,0 +1,144 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ errorInfoData }}
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/flow/nodes/common/startNode.js b/src/views/flow/nodes/common/startNode.js
new file mode 100644
index 0000000..c898983
--- /dev/null
+++ b/src/views/flow/nodes/common/startNode.js
@@ -0,0 +1,204 @@
+// 导入 HtmlNode 及其模型,为后续继承做准备
+import { HtmlNode, HtmlNodeModel } from '@logicflow/core';
+// 导入 Vue 相关方法,用于渲染组件
+import { createApp, h, nextTick } from 'vue';
+import ElementPlus from 'element-plus'
+// 导入 Vue 组件
+import StartNode from './startNode.vue';
+import OuterNode from "../../components/OuterNode.vue";
+import JsonViewer from 'vue3-json-viewer'
+
+/**
+ * 定义一个 元素的 HTML 节点类,继承自 HtmlNode
+ * 该类负责在 HTML 中渲染 元素,并处理其交互逻辑
+ */
+class StartNodeHtmlNode extends HtmlNode {
+ resizeObserver = null;
+ isMounted; // 标记组件是否已挂载
+ r; // 渲染函数
+ app; // Vue 应用实例
+ container = null;
+
+ static reusePool = new Map(); // 节点复用池
+
+ /**
+ * 构造函数
+ * @param props 传递给节点的属性,包括模型、图模型等
+ */
+ constructor(props) {
+ super(props);
+ this.initVueApp(props)
+ }
+
+ initVueApp(props) {
+ this.isMounted = false;
+
+ this.hideAnchor = false;
+ this.autoExpand = true; // 防止锚点被折叠
+ this.anchorsPreset = "default"; // 重置锚点预设
+ // 创建 元素的渲染函数
+ this.r = h(OuterNode, {
+ model: props.model,
+ component: StartNode,
+ properties: {
+ ...props.model.getProperties()
+ },
+ onContentChange: this.handleContentChange.bind(this),
+ onBindRef: this.handleComponentInstance.bind(this)
+ });
+
+ // 创建 Vue 应用实例,并指定渲染函数
+ this.app = createApp({
+ render: () => this.r
+ });
+ }
+
+ /**
+ * 将 HTML 内容设置到指定的根元素上
+ * @param rootEl 根元素
+ */
+ async setHtml(rootEl) {
+ const nodeId = this.props.model.id;
+ if (StartNodeHtmlNode.reusePool.has(nodeId)) {
+ rootEl.appendChild(StartNodeHtmlNode.reusePool.get(nodeId));
+ return;
+ }
+
+ if (!this.isMounted) {
+ this.isMounted = true;
+ this.container = document.createElement('div');
+ this.container.style.display = 'inline-block'; // 关键:确保容器自适应内容
+
+ rootEl.appendChild(this.container);
+ this.app.use(ElementPlus) // 关键:单独注册ElementPlus
+ this.app.use(JsonViewer)
+ this.app.mount(this.container);
+ await nextTick();
+ this.setupSizeObserver();
+ StartNodeHtmlNode.reusePool.set(nodeId, this.container);
+ } else {
+ this.r.component.props.properties = this.props.model.getProperties();
+ }
+ }
+
+ /**
+ * 获取节点文本内容
+ * 对于元素,返回 null,因为其内容由特定组件渲染
+ * @returns {null}
+ */
+ getText() {
+ return null;
+ }
+
+ handleComponentInstance(data) {
+ // 将组件实例保存到节点模型
+ this.props.model.setComponentInstance(data)
+ }
+
+ handleContentChange() {
+ // 内容变化时强制更新尺寸
+ this.updateNodeSize();
+ }
+
+ // 渲染完成后获取实际尺寸
+ updateNodeSize () {
+ if (this.container) {
+ const { SCALE_X, SCALE_Y } = this.props.graphModel.transformModel;
+ const node = this.container.querySelector('.node__box')
+ const rect = node.getBoundingClientRect();
+ this.props.model.updateSize(rect.width / SCALE_X, rect.height / SCALE_Y);
+ }
+ }
+
+ setupSizeObserver() {
+ // 首次渲染立即检测
+ requestAnimationFrame(() => {
+ this.updateNodeSize();
+ // 持续监听变化
+ this.resizeObserver = new ResizeObserver(() => {
+ this.updateNodeSize();
+ });
+ this.resizeObserver.observe(this.container);
+ });
+ }
+
+ // 组件卸载时移除监听
+ onDestroy() {
+ this.resizeObserver?.disconnect();
+ }
+}
+
+/**
+ * 定义一个元素的 HTML 模型类,继承自 HtmlNodeModel
+ * 该类主要设置节点的属性和样式
+ */
+class StartNodeHtmlModel extends HtmlNodeModel {
+ initNodeData(data) {
+ super.initNodeData(data);
+ }
+
+ // 保存组件实例引用
+ setComponentInstance(instance) {
+ this.componentInstance = instance;
+ }
+
+ closePopover() {
+ this.componentInstance.closePopover()
+ }
+
+ /**
+ * 设置节点属性
+ * 包括宽度、高度、文本编辑属性等
+ */
+ setAttributes() {
+ // 初始设置为0,后续动态更新
+ this.width = 0;
+ this.height = 0;
+ this.text.editable = false;
+ this.deletable = false; // 关键代码:禁止删除节点
+ }
+
+ updateSize(width, height) {
+ this.width = width + 24;
+ this.height = height + 50;
+ this.initNodeData(this); // 触发节点重绘
+ }
+
+ // 定义节点只有左右两个锚点. 锚点位置通过中心点和宽度算出来。
+ getDefaultAnchor() {
+ let _a = this, x = _a.x, y = _a.y, width = _a.width, height = _a.height;
+ return [
+ { x: x + width / 2 - 24, y: y - 25, name: 'right', id: "".concat(this.id, "_1"), properties: { connectionType: 'source' }, onlyAsSource: true },
+ ];
+ }
+
+ /**
+ * 获取节点轮廓样式
+ * 覆盖父类方法,设置 stroke 属性为 none,以适应特定的视觉效果
+ * @returns {object} 节点轮廓样式
+ */
+ getOutlineStyle() {
+ const style = super.getOutlineStyle();
+ style.stroke = 'none';
+ style.hover.stroke = 'none';
+ return style;
+ }
+}
+
+// 导出方法注册
+export function registerStartNode(lf) {
+ lf.register({
+ type: "start",
+ view: StartNodeHtmlNode,
+ model: StartNodeHtmlModel,
+ effect: ['status'],
+ events: {
+ remove: (event) => {
+ // 阻止默认的删除事件
+ event.preventDefault();
+ // 可以在此处添加自定义的删除逻辑,例如弹出提示框
+ alert('此节点不可删除');
+ },
+ }
+ })
+}
\ No newline at end of file
diff --git a/src/views/flow/nodes/common/startNode.vue b/src/views/flow/nodes/common/startNode.vue
new file mode 100644
index 0000000..ba2bef8
--- /dev/null
+++ b/src/views/flow/nodes/common/startNode.vue
@@ -0,0 +1,157 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Start
+
+
+
工作流的起始节点,用于设定启动工作流需要的信息
+
+
+
+
+
+
+
+
+
diff --git a/src/views/flow/nodes/function/codeNode.js b/src/views/flow/nodes/function/codeNode.js
new file mode 100644
index 0000000..1610a2e
--- /dev/null
+++ b/src/views/flow/nodes/function/codeNode.js
@@ -0,0 +1,213 @@
+// 导入 HtmlNode 及其模型,为后续继承做准备
+import { HtmlNode, HtmlNodeModel } from "@logicflow/core";
+// 导入 Vue 相关方法,用于渲染组件
+import { createApp, h, nextTick } from "vue";
+import ElementPlus from "element-plus";
+// 导入 Vue 组件
+// @ts-ignore
+import CodeNode from "./codeNode.vue";
+import OuterNode from "../../components/OuterNode.vue";
+import JsonViewer from 'vue3-json-viewer'
+
+/**
+ * 定义一个 元素的 HTML 节点类,继承自 HtmlNode
+ * 该类负责在 HTML 中渲染 元素,并处理其交互逻辑
+ */
+class CodeNodeHtmlNode extends HtmlNode {
+ resizeObserver = null;
+ isMounted; // 标记组件是否已挂载
+ r; // 渲染函数
+ app; // Vue 应用实例
+ container = null;
+
+ static reusePool = new Map(); // 节点复用池
+
+ /**
+ * 构造函数
+ * @param props 传递给节点的属性,包括模型、图模型等
+ */
+ constructor(props) {
+ super(props);
+ this.initVueApp(props);
+ }
+
+ initVueApp(props) {
+ this.isMounted = false;
+
+ this.hideAnchor = false;
+ this.autoExpand = true; // 防止锚点被折叠
+ this.anchorsPreset = "default"; // 重置锚点预设
+ // 创建 元素的渲染函数
+ this.r = h(OuterNode, {
+ model: props.model,
+ component: CodeNode,
+ properties: {
+ ...props.model.getProperties(),
+ },
+ onContentChange: this.handleContentChange.bind(this),
+ onBindRef: this.handleComponentInstance.bind(this)
+ });
+
+ // 创建 Vue 应用实例,并指定渲染函数
+ this.app = createApp({
+ render: () => this.r,
+ });
+ }
+
+ /**
+ * 将 HTML 内容设置到指定的根元素上
+ * @param rootEl 根元素
+ */
+ async setHtml(rootEl) {
+ const nodeId = this.props.model.id;
+ if (CodeNodeHtmlNode.reusePool.has(nodeId)) {
+ rootEl.appendChild(CodeNodeHtmlNode.reusePool.get(nodeId));
+ return;
+ }
+
+ if (!this.isMounted) {
+ this.isMounted = true;
+ this.container = document.createElement("div");
+ this.container.style.display = "inline-block"; // 关键:确保容器自适应内容
+
+ rootEl.appendChild(this.container);
+ this.app.use(ElementPlus);
+ this.app.use(JsonViewer);
+
+ this.app.mount(this.container);
+ await nextTick();
+ this.setupSizeObserver();
+ CodeNodeHtmlNode.reusePool.set(nodeId, this.container);
+ } else {
+ this.r.component.props.properties = this.props.model.getProperties();
+ }
+ }
+
+ /**
+ * 获取节点文本内容
+ * 对于元素,返回 null,因为其内容由特定组件渲染
+ * @returns {null}
+ */
+ getText() {
+ return null;
+ }
+
+ handleComponentInstance(data) {
+ // 将组件实例保存到节点模型
+ this.props.model.setComponentInstance(data)
+ }
+
+ handleContentChange() {
+ // 内容变化时强制更新尺寸
+ this.updateNodeSize();
+ }
+
+ // 渲染完成后获取实际尺寸
+ updateNodeSize() {
+ if (this.container) {
+ const { SCALE_X, SCALE_Y } = this.props.graphModel.transformModel;
+ const node = this.container.querySelector(".node__container");
+ const rect = node.getBoundingClientRect();
+ this.props.model.updateSize(rect.width / SCALE_X, rect.height / SCALE_Y);
+ }
+ }
+
+ setupSizeObserver() {
+ // 首次渲染立即检测
+ requestAnimationFrame(() => {
+ this.updateNodeSize();
+ // 持续监听变化
+ this.resizeObserver = new ResizeObserver(() => {
+ this.updateNodeSize();
+ });
+ this.resizeObserver.observe(this.container);
+ });
+ }
+
+ // 组件卸载时移除监听
+ onDestroy() {
+ this.resizeObserver?.disconnect();
+ }
+}
+
+/**
+ * 定义一个元素的 HTML 模型类,继承自 HtmlNodeModel
+ * 该类主要设置节点的属性和样式
+ */
+class CodeNodeHtmlModel extends HtmlNodeModel {
+ initNodeData(data) {
+ super.initNodeData(data);
+ }
+
+ // 保存组件实例引用
+ setComponentInstance(instance) {
+ this.componentInstance = instance;
+ }
+
+ closePopover() {
+ this.componentInstance.closePopover()
+ }
+
+ /**
+ * 设置节点属性
+ * 包括宽度、高度、文本编辑属性等
+ */
+ setAttributes() {
+ // 初始设置为0,后续动态更新
+ this.width = 0;
+ this.height = 0;
+ this.text.editable = false;
+ }
+
+ updateSize(width, height) {
+ this.width = width + 24;
+ this.height = height + 50;
+ this.initNodeData(this); // 触发节点重绘
+ }
+
+ // 定义节点只有左右两个锚点. 锚点位置通过中心点和宽度算出来。
+ getDefaultAnchor() {
+ let _a = this,
+ x = _a.x,
+ y = _a.y,
+ width = _a.width,
+ height = _a.height;
+ return [
+ {
+ x: x + width / 2 + 4,
+ y: y,
+ name: "right",
+ id: "".concat(this.id, "_1"),
+ properties: { connectionType: "source" },
+ },
+ {
+ x: x - width / 2,
+ y: y,
+ name: "left",
+ id: "".concat(this.id, "_3"),
+ properties: { connectionType: "target" },
+ },
+ ];
+ }
+
+ /**
+ * 获取节点轮廓样式
+ * 覆盖父类方法,设置 stroke 属性为 none,以适应特定的视觉效果
+ * @returns {object} 节点轮廓样式
+ */
+ getOutlineStyle() {
+ const style = super.getOutlineStyle();
+ style.stroke = "none";
+ style.hover.stroke = "none";
+ return style;
+ }
+}
+
+// 导出方法注册
+export function registerCodeNode(lf) {
+ lf.register({
+ type: "code",
+ view: CodeNodeHtmlNode,
+ model: CodeNodeHtmlModel,
+ });
+}
diff --git a/src/views/flow/nodes/function/codeNode.vue b/src/views/flow/nodes/function/codeNode.vue
new file mode 100644
index 0000000..32a2469
--- /dev/null
+++ b/src/views/flow/nodes/function/codeNode.vue
@@ -0,0 +1,139 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ errorInfoData }}
+
+
+
+
+
+
+
+
+
diff --git a/src/views/flow/nodes/function/currentLoop.vue b/src/views/flow/nodes/function/currentLoop.vue
new file mode 100644
index 0000000..7703d20
--- /dev/null
+++ b/src/views/flow/nodes/function/currentLoop.vue
@@ -0,0 +1,256 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/flow/nodes/function/currentLoopNode.js b/src/views/flow/nodes/function/currentLoopNode.js
new file mode 100644
index 0000000..cd434f8
--- /dev/null
+++ b/src/views/flow/nodes/function/currentLoopNode.js
@@ -0,0 +1,219 @@
+// 导入 HtmlNode 及其模型,为后续继承做准备
+import { HtmlNode, HtmlNodeModel } from "@logicflow/core";
+// 导入 Vue 相关方法,用于渲染组件
+import { createApp, h, nextTick } from "vue";
+import ElementPlus from "element-plus";
+// 导入 Vue 组件
+// @ts-ignore
+import CurrentLoop from "./currentLoop.vue";
+import OuterNode from "../../components/OuterNode.vue";
+import JsonViewer from 'vue3-json-viewer'
+
+/**
+ * 定义一个 元素的 HTML 节点类,继承自 HtmlNode
+ * 该类负责在 HTML 中渲染 元素,并处理其交互逻辑
+ */
+class CurrentLoopNodeHtmlNode extends HtmlNode {
+ resizeObserver = null;
+ isMounted; // 标记组件是否已挂载
+ r; // 渲染函数
+ app; // Vue 应用实例
+ container = null;
+
+ static reusePool = new Map(); // 节点复用池
+
+ /**
+ * 构造函数
+ * @param props 传递给节点的属性,包括模型、图模型等
+ */
+ constructor(props) {
+ super(props);
+ this.initVueApp(props);
+ }
+
+ initVueApp(props) {
+ this.isMounted = false;
+
+ this.hideAnchor = false;
+ this.autoExpand = true; // 防止锚点被折叠
+ this.anchorsPreset = "default"; // 重置锚点预设
+ // 创建 元素的渲染函数
+ this.r = h(OuterNode, {
+ model: props.model,
+ component: CurrentLoop,
+ properties: {
+ ...props.model.getProperties(),
+ },
+ onContentChange: this.handleContentChange.bind(this),
+ onBindRef: this.handleComponentInstance.bind(this)
+ });
+
+ // 创建 Vue 应用实例,并指定渲染函数
+ this.app = createApp({
+ render: () => this.r,
+ });
+ }
+
+ /**
+ * 将 HTML 内容设置到指定的根元素上
+ * @param rootEl 根元素
+ */
+ async setHtml(rootEl) {
+ const nodeId = this.props.model.id;
+ if (CurrentLoopNodeHtmlNode.reusePool.has(nodeId)) {
+ rootEl.appendChild(CurrentLoopNodeHtmlNode.reusePool.get(nodeId));
+ return;
+ }
+
+ if (!this.isMounted) {
+ this.isMounted = true;
+ this.container = document.createElement("div");
+ this.container.style.display = "inline-block"; // 关键:确保容器自适应内容
+
+ rootEl.appendChild(this.container);
+ this.app.use(ElementPlus);
+ this.app.use(JsonViewer);
+
+ this.app.mount(this.container);
+ await nextTick();
+ this.setupSizeObserver();
+ CurrentLoopNodeHtmlNode.reusePool.set(nodeId, this.container);
+ } else {
+ this.r.component.props.properties = this.props.model.getProperties();
+ }
+ }
+
+ /**
+ * 获取节点文本内容
+ * 对于元素,返回 null,因为其内容由特定组件渲染
+ * @returns {null}
+ */
+ getText() {
+ return null;
+ }
+
+ handleComponentInstance(data) {
+ // 将组件实例保存到节点模型
+ this.props.model.setComponentInstance(data)
+ }
+
+ handleContentChange() {
+ // 内容变化时强制更新尺寸
+ this.updateNodeSize();
+ }
+
+ // 渲染完成后获取实际尺寸
+ updateNodeSize() {
+ if (this.container) {
+ const { SCALE_X, SCALE_Y } = this.props.graphModel.transformModel;
+ const node = this.container.querySelector(".node__container");
+ const rect = node.getBoundingClientRect();
+ this.props.model.updateSize(rect.width / SCALE_X, rect.height / SCALE_Y);
+ }
+ }
+
+ setupSizeObserver() {
+ // 首次渲染立即检测
+ requestAnimationFrame(() => {
+ this.updateNodeSize();
+ // 持续监听变化
+ this.resizeObserver = new ResizeObserver(() => {
+ this.updateNodeSize();
+ });
+ this.resizeObserver.observe(this.container);
+ });
+ }
+
+ // 组件卸载时移除监听
+ onDestroy() {
+ this.resizeObserver?.disconnect();
+ }
+}
+
+/**
+ * 定义一个元素的 HTML 模型类,继承自 HtmlNodeModel
+ * 该类主要设置节点的属性和样式
+ */
+class CurrentLoopNodeHtmlModel extends HtmlNodeModel {
+ initNodeData(data) {
+ super.initNodeData(data);
+ }
+
+ // 保存组件实例引用
+ setComponentInstance(instance) {
+ this.componentInstance = instance;
+ }
+
+ // 验证表单
+ async validateForm() {
+ const result = this.componentInstance.validateForm()
+ return result
+ }
+
+ closePopover() {
+ this.componentInstance.closePopover()
+ }
+
+ /**
+ * 设置节点属性
+ * 包括宽度、高度、文本编辑属性等
+ */
+ setAttributes() {
+ // 初始设置为0,后续动态更新
+ this.width = 0;
+ this.height = 0;
+ this.text.editable = false;
+ }
+
+ updateSize(width, height) {
+ this.width = width + 24;
+ this.height = height + 50;
+ this.initNodeData(this); // 触发节点重绘
+ }
+
+ // 定义节点只有左右两个锚点. 锚点位置通过中心点和宽度算出来。
+ getDefaultAnchor() {
+ let _a = this,
+ x = _a.x,
+ y = _a.y,
+ width = _a.width,
+ height = _a.height;
+ return [
+ {
+ x: x + width / 2 + 4,
+ y: y,
+ name: "right",
+ id: "".concat(this.id, "_1"),
+ properties: { connectionType: "source" },
+ },
+ {
+ x: x - width / 2,
+ y: y,
+ name: "left",
+ id: "".concat(this.id, "_3"),
+ properties: { connectionType: "target" },
+ },
+ ];
+ }
+
+ /**
+ * 获取节点轮廓样式
+ * 覆盖父类方法,设置 stroke 属性为 none,以适应特定的视觉效果
+ * @returns {object} 节点轮廓样式
+ */
+ getOutlineStyle() {
+ const style = super.getOutlineStyle();
+ style.stroke = "none";
+ style.hover.stroke = "none";
+ return style;
+ }
+}
+
+// 导出方法注册
+export function registerCurrentLoopNode(lf) {
+ lf.register({
+ type: "currentLoop",
+ view: CurrentLoopNodeHtmlNode,
+ model: CurrentLoopNodeHtmlModel
+ });
+}
diff --git a/src/views/flow/nodes/function/httpNode.js b/src/views/flow/nodes/function/httpNode.js
new file mode 100644
index 0000000..3b5758f
--- /dev/null
+++ b/src/views/flow/nodes/function/httpNode.js
@@ -0,0 +1,213 @@
+// 导入 HtmlNode 及其模型,为后续继承做准备
+import { HtmlNode, HtmlNodeModel } from "@logicflow/core";
+// 导入 Vue 相关方法,用于渲染组件
+import { createApp, h, nextTick } from "vue";
+import ElementPlus from "element-plus";
+// 导入 Vue 组件
+// @ts-ignore
+import HttpNode from "./httpNode.vue";
+import OuterNode from "../../components/OuterNode.vue";
+import JsonViewer from 'vue3-json-viewer'
+
+/**
+ * 定义一个 元素的 HTML 节点类,继承自 HtmlNode
+ * 该类负责在 HTML 中渲染 元素,并处理其交互逻辑
+ */
+class HttpNodeHtmlNode extends HtmlNode {
+ resizeObserver = null;
+ isMounted; // 标记组件是否已挂载
+ r; // 渲染函数
+ app; // Vue 应用实例
+ container = null;
+
+ static reusePool = new Map(); // 节点复用池
+
+ /**
+ * 构造函数
+ * @param props 传递给节点的属性,包括模型、图模型等
+ */
+ constructor(props) {
+ super(props);
+ this.initVueApp(props);
+ }
+
+ initVueApp(props) {
+ this.isMounted = false;
+
+ this.hideAnchor = false;
+ this.autoExpand = true; // 防止锚点被折叠
+ this.anchorsPreset = "default"; // 重置锚点预设
+ // 创建 元素的渲染函数
+ this.r = h(OuterNode, {
+ model: props.model,
+ component: HttpNode,
+ properties: {
+ ...props.model.getProperties(),
+ },
+ onContentChange: this.handleContentChange.bind(this),
+ onBindRef: this.handleComponentInstance.bind(this)
+ });
+
+ // 创建 Vue 应用实例,并指定渲染函数
+ this.app = createApp({
+ render: () => this.r,
+ });
+ }
+
+ /**
+ * 将 HTML 内容设置到指定的根元素上
+ * @param rootEl 根元素
+ */
+ async setHtml(rootEl) {
+ const nodeId = this.props.model.id;
+ if (HttpNodeHtmlNode.reusePool.has(nodeId)) {
+ rootEl.appendChild(HttpNodeHtmlNode.reusePool.get(nodeId));
+ return;
+ }
+
+ if (!this.isMounted) {
+ this.isMounted = true;
+ this.container = document.createElement("div");
+ this.container.style.display = "inline-block"; // 关键:确保容器自适应内容
+
+ rootEl.appendChild(this.container);
+ this.app.use(ElementPlus);
+ this.app.use(JsonViewer);
+
+ this.app.mount(this.container);
+ await nextTick();
+ this.setupSizeObserver();
+ HttpNodeHtmlNode.reusePool.set(nodeId, this.container);
+ } else {
+ this.r.component.props.properties = this.props.model.getProperties();
+ }
+ }
+
+ /**
+ * 获取节点文本内容
+ * 对于元素,返回 null,因为其内容由特定组件渲染
+ * @returns {null}
+ */
+ getText() {
+ return null;
+ }
+
+ handleComponentInstance(data) {
+ // 将组件实例保存到节点模型
+ this.props.model.setComponentInstance(data)
+ }
+
+ handleContentChange() {
+ // 内容变化时强制更新尺寸
+ this.updateNodeSize();
+ }
+
+ // 渲染完成后获取实际尺寸
+ updateNodeSize() {
+ if (this.container) {
+ const { SCALE_X, SCALE_Y } = this.props.graphModel.transformModel;
+ const node = this.container.querySelector(".node__container");
+ const rect = node.getBoundingClientRect();
+ this.props.model.updateSize(rect.width / SCALE_X, rect.height / SCALE_Y);
+ }
+ }
+
+ setupSizeObserver() {
+ // 首次渲染立即检测
+ requestAnimationFrame(() => {
+ this.updateNodeSize();
+ // 持续监听变化
+ this.resizeObserver = new ResizeObserver(() => {
+ this.updateNodeSize();
+ });
+ this.resizeObserver.observe(this.container);
+ });
+ }
+
+ // 组件卸载时移除监听
+ onDestroy() {
+ this.resizeObserver?.disconnect();
+ }
+}
+
+/**
+ * 定义一个元素的 HTML 模型类,继承自 HtmlNodeModel
+ * 该类主要设置节点的属性和样式
+ */
+class HttpNodeHtmlModel extends HtmlNodeModel {
+ initNodeData(data) {
+ super.initNodeData(data);
+ }
+
+ // 保存组件实例引用
+ setComponentInstance(instance) {
+ this.componentInstance = instance;
+ }
+
+ closePopover() {
+ this.componentInstance.closePopover()
+ }
+
+ /**
+ * 设置节点属性
+ * 包括宽度、高度、文本编辑属性等
+ */
+ setAttributes() {
+ // 初始设置为0,后续动态更新
+ this.width = 0;
+ this.height = 0;
+ this.text.editable = false;
+ }
+
+ updateSize(width, height) {
+ this.width = width + 24;
+ this.height = height + 50;
+ this.initNodeData(this); // 触发节点重绘
+ }
+
+ // 定义节点只有左右两个锚点. 锚点位置通过中心点和宽度算出来。
+ getDefaultAnchor() {
+ let _a = this,
+ x = _a.x,
+ y = _a.y,
+ width = _a.width,
+ height = _a.height;
+ return [
+ {
+ x: x + width / 2 + 4,
+ y: y,
+ name: "right",
+ id: "".concat(this.id, "_1"),
+ properties: { connectionType: "source" },
+ },
+ {
+ x: x - width / 2,
+ y: y,
+ name: "left",
+ id: "".concat(this.id, "_3"),
+ properties: { connectionType: "target" },
+ },
+ ];
+ }
+
+ /**
+ * 获取节点轮廓样式
+ * 覆盖父类方法,设置 stroke 属性为 none,以适应特定的视觉效果
+ * @returns {object} 节点轮廓样式
+ */
+ getOutlineStyle() {
+ const style = super.getOutlineStyle();
+ style.stroke = "none";
+ style.hover.stroke = "none";
+ return style;
+ }
+}
+
+// 导出方法注册
+export function registerHttpNode(lf) {
+ lf.register({
+ type: "http",
+ view: HttpNodeHtmlNode,
+ model: HttpNodeHtmlModel,
+ });
+}
diff --git a/src/views/flow/nodes/function/httpNode.vue b/src/views/flow/nodes/function/httpNode.vue
new file mode 100644
index 0000000..96823ae
--- /dev/null
+++ b/src/views/flow/nodes/function/httpNode.vue
@@ -0,0 +1,141 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ errorInfoData }}
+
+
+
+
+
+
+
+
+
diff --git a/src/views/flow/nodes/function/index.js b/src/views/flow/nodes/function/index.js
new file mode 100644
index 0000000..3de78bd
--- /dev/null
+++ b/src/views/flow/nodes/function/index.js
@@ -0,0 +1,23 @@
+import { registerLoopBodyNode } from './loop'
+import { registerStopLoopNode } from './stopLoop'
+import { registerSwitch } from './switch'
+import { registerSubEndNode } from './subEnd'
+import { registerSubStartNode } from './subStart'
+import { registerSleepNode } from './sleep'
+import { registerHttpNode } from './httpNode'
+import { registerCodeNode } from './codeNode'
+import { registerCurrentLoopNode } from './currentLoopNode'
+import { registerSdAgentNode } from './sdAgent'
+
+export const registerFunction = (lf) => {
+ registerLoopBodyNode(lf)
+ registerStopLoopNode(lf)
+ registerSwitch(lf)
+ registerSubEndNode(lf)
+ registerSubStartNode(lf)
+ registerSleepNode(lf)
+ registerHttpNode(lf)
+ registerCodeNode(lf)
+ registerCurrentLoopNode(lf)
+ registerSdAgentNode(lf)
+}
\ No newline at end of file
diff --git a/src/views/flow/nodes/function/loop.js b/src/views/flow/nodes/function/loop.js
new file mode 100644
index 0000000..84069ba
--- /dev/null
+++ b/src/views/flow/nodes/function/loop.js
@@ -0,0 +1,286 @@
+import { GroupNodeModel } from '@logicflow/extension';
+// 导入 Vue 相关方法,用于渲染组件
+import { createApp, h, nextTick } from "vue";
+import { HtmlNode, h as svgH } from "@logicflow/core";
+import ElementPlus from "element-plus";
+import Loop from './loop.vue';
+
+class loopBodyModel extends GroupNodeModel {
+
+ initNodeData(data) {
+ super.initNodeData(data);
+ this.children = new Set(data.children || [])
+ this.zIndex = 0;
+ this.isGroup = true;
+ this.resizable = false;
+ }
+
+ // 保存组件实例引用
+ setComponentInstance(instance) {
+ this.componentInstance = instance;
+ }
+
+ // 递归获取所有有效子节点(包含多层嵌套 children)
+ getAllValidChildren = (nodeIds, graphModel) => {
+ const validNodes = [];
+ // 遍历当前层节点 ID
+ Array.from(nodeIds).forEach((id) => {
+ const node = graphModel.getNodeModelById(id);
+ if (node) {
+ // 当前节点有效,加入结果
+ validNodes.push(node);
+ // 如果节点还有 children,递归获取下级节点
+ if (node.children && node.children.size > 0) {
+ const childNodes = this.getAllValidChildren(node.children, graphModel);
+ validNodes.push(...childNodes);
+ }
+ } else {
+ // 无效 ID,从集合中删除
+ nodeIds.delete(id);
+ }
+ });
+ return validNodes;
+ };
+
+ // 计算子节点的包围盒(包含所有子节点的最小矩形区域)
+ getChildrenBBox() {
+ const children = this.getAllValidChildren(this.children, this.graphModel)
+ console.log('children', children)
+ // Array.from(this.children).forEach((id) => {
+ // const node = this.graphModel.getNodeModelById(id)
+ // if (node) {
+ // children.push(this.graphModel.getNodeModelById(id))
+ // } else {
+ // this.children.delete(id)
+ // }
+ // });
+
+ let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
+ nextTick(() => {
+ children.forEach((node) => {
+ let _width = node.type === 'loop' ? 500 : 320
+ let _height = node.type === 'loop' ? 300 : 200
+ let width = node.width === 0 ? _width : node.width
+ let height = node.height === 0 ? _height : node.height
+ minX = Math.min(minX, node.x - width / 2);
+ maxX = Math.max(maxX, node.x + width / 2);
+ minY = Math.min(minY, node.y - height / 2);
+ maxY = Math.max(maxY, node.y + height / 2);
+ });
+ this.width = maxX - minX + 180; // 增加内边距
+ this.height = maxY - minY + 300;
+ this.x = (minX + maxX) / 2;
+ this.y = (minY + maxY) / 2;
+ const properties = lf.getProperties(this.id)
+ this.setProperties({
+ ...properties,
+ width: this.width,
+ height: this.height
+ })
+ this.getDefaultAnchor()
+ })
+ }
+
+ updateSize() {
+ const children = Array.from(this.children);
+ if (children.length > 0) {
+ this.getChildrenBBox();
+ }
+ }
+
+ // 添加子节点时立即更新大小
+ addChild(childId) {
+ super.addChild(childId);
+ this.updateSize();
+ }
+
+ // 移除子节点时更新大小
+ removeChild(childId) {
+ return false
+ }
+
+ // 定义节点只有左右两个锚点. 锚点位置通过中心点和宽度算出来。
+ getDefaultAnchor() {
+ let _a = this,
+ x = _a.x,
+ y = _a.y,
+ width = _a.width,
+ height = _a.height;
+ return [
+ {
+ x: x + width / 2 ,
+ y: y + height / 2,
+ name: "right",
+ id: "".concat(this.id, "_1"),
+ properties: { connectionType: "source" },
+ },
+ {
+ x: x - width / 2,
+ y: y + height / 2,
+ name: "left",
+ id: "".concat(this.id, "_3"),
+ properties: { connectionType: "target" },
+ },
+ ];
+ }
+
+ isAllowAppendIn() {
+ return false;
+ }
+}
+
+class loopBodyView extends HtmlNode {
+ resizeObserver = null;
+ isMounted; // 标记组件是否已挂载
+ r; // 渲染函数
+ app; // Vue 应用实例
+ container = null;
+
+ static reusePool = new Map(); // 节点复用池
+
+ /**
+ * 构造函数
+ * @param props 传递给节点的属性,包括模型、图模型等
+ */
+ constructor(props) {
+ super(props);
+ this.initVueApp(props);
+ }
+
+ initVueApp(props) {
+ this.isMounted = false;
+
+ this.hideAnchor = false;
+ this.autoExpand = true; // 防止锚点被折叠
+ this.anchorsPreset = "default"; // 重置锚点预设
+ // 创建 元素的渲染函数
+ this.r = h(Loop, {
+ model: props.model,
+ graphModel: props.graphModel,
+ properties: {
+ ...props.model.getProperties(),
+ },
+ onAddToGroup: this.addChild.bind(this),
+ onContentChange: this.handleContentChange.bind(this),
+ onBindRef: this.handleComponentInstance.bind(this)
+ });
+
+ // 创建 Vue 应用实例,并指定渲染函数
+ this.app = createApp({
+ render: () => this.r,
+ });
+ }
+
+ addChild(data) {
+ this.addToGroup(data);
+ }
+
+ handleComponentInstance(data) {
+ // 将组件实例保存到节点模型
+ this.props.model.setComponentInstance(data)
+ }
+
+ handleContentChange() {
+ // 内容变化时强制更新尺寸
+ this.props.model.updateSize();
+ }
+
+ // 关键修改:重写getShape方法
+ getShape() {
+ const { model } = this.props;
+ const { x, y, width, height } = model;
+
+ // 创建自定义HTML内容
+ const html = super.getShape();
+ // 创建SVG组,包含HTML内容和锚点
+ return svgH('g', {}, [
+ // 绘制组节点背景
+ svgH('rect', {
+ x: x - width / 2,
+ y: y - height / 2,
+ width,
+ height,
+ stroke: '#88f',
+ strokeWidth: 0,
+ strokeDasharray: '5,5',
+ fill: 'rgba(136, 136, 255, 0.1)',
+ rx: 4,
+ ry: 4
+ }),
+
+ // 添加HTML内容
+ html,
+
+ // 渲染锚点
+ this.getAnchorShapes()
+ ]);
+ }
+
+ // 创建锚点元素
+ getAnchorShapes() {
+ const { model } = this.props;
+ const anchors = model.anchors || [];
+
+ const anchorShapes = anchors.map((offset, index) => {
+ const {x, y } = offset
+
+ return svgH('circle', {
+ cx: x,
+ cy: y,
+ r: 7,
+ hover: { fill: "#FF5722" }, // 禁用悬停变色
+ fill: '#FF5722',
+ stroke: '#FF5722',
+ strokeWidth: 0,
+ className: 'lf-node-anchor',
+ cursor: 'crosshair',
+ 'data-anchor-id': `anchor_${index}`
+ });
+ });
+
+ return svgH('g', {}, anchorShapes);
+ }
+
+ /**
+ * 将 HTML 内容设置到指定的根元素上
+ * @param rootEl 根元素
+ */
+ async setHtml(rootEl) {
+ const nodeId = this.props.model.id;
+ if (loopBodyView.reusePool.has(nodeId)) {
+ rootEl.appendChild(loopBodyView.reusePool.get(nodeId));
+ return;
+ }
+
+ if (!this.isMounted) {
+ this.isMounted = true;
+ this.container = document.createElement("div");
+ // this.container.style.display = "inline-block"; // 关键:确保容器自适应内容
+ this.container.style.width = "100%"; // 关键:确保容器自适应内容
+ this.container.style.height = "100%"; // 关键:确保容器自适应内容
+
+ rootEl.appendChild(this.container);
+ this.app.use(ElementPlus); // 关键:单独注册ElementPlus
+ this.app.mount(this.container);
+ loopBodyView.reusePool.set(nodeId, this.container);
+ } else {
+ this.r.component.props.properties = this.props.model.getProperties();
+ }
+ }
+
+ addToGroup(childId) {
+ const groupModel = lf.getNodeModelById(this.props.model.id);
+ groupModel.addChild(childId);
+ groupModel.updateSize()
+ }
+}
+
+
+// 导出方法注册
+export function registerLoopBodyNode(lf) {
+ lf.register({
+ type: "loop",
+ view: loopBodyView,
+ model: loopBodyModel,
+ });
+}
\ No newline at end of file
diff --git a/src/views/flow/nodes/function/loop.vue b/src/views/flow/nodes/function/loop.vue
new file mode 100644
index 0000000..58dd3d5
--- /dev/null
+++ b/src/views/flow/nodes/function/loop.vue
@@ -0,0 +1,116 @@
+
+
+
+
+
diff --git a/src/views/flow/nodes/function/sdAgent.js b/src/views/flow/nodes/function/sdAgent.js
new file mode 100644
index 0000000..53e6749
--- /dev/null
+++ b/src/views/flow/nodes/function/sdAgent.js
@@ -0,0 +1,213 @@
+// 导入 HtmlNode 及其模型,为后续继承做准备
+import { HtmlNode, HtmlNodeModel } from "@logicflow/core";
+// 导入 Vue 相关方法,用于渲染组件
+import { createApp, h, nextTick } from "vue";
+import ElementPlus from "element-plus";
+// 导入 Vue 组件
+// @ts-ignore
+import SdAgent from "./sdAgent.vue";
+import OuterNode from "../../components/OuterNode.vue";
+import JsonViewer from 'vue3-json-viewer'
+
+/**
+ * 定义一个 元素的 HTML 节点类,继承自 HtmlNode
+ * 该类负责在 HTML 中渲染 元素,并处理其交互逻辑
+ */
+class SdAgentNodeHtmlNode extends HtmlNode {
+ resizeObserver = null;
+ isMounted; // 标记组件是否已挂载
+ r; // 渲染函数
+ app; // Vue 应用实例
+ container = null;
+
+ static reusePool = new Map(); // 节点复用池
+
+ /**
+ * 构造函数
+ * @param props 传递给节点的属性,包括模型、图模型等
+ */
+ constructor(props) {
+ super(props);
+ this.initVueApp(props);
+ }
+
+ initVueApp(props) {
+ this.isMounted = false;
+
+ this.hideAnchor = false;
+ this.autoExpand = true; // 防止锚点被折叠
+ this.anchorsPreset = "default"; // 重置锚点预设
+ // 创建 元素的渲染函数
+ this.r = h(OuterNode, {
+ model: props.model,
+ component: SdAgent,
+ properties: {
+ ...props.model.getProperties(),
+ },
+ onContentChange: this.handleContentChange.bind(this),
+ onBindRef: this.handleComponentInstance.bind(this)
+ });
+
+ // 创建 Vue 应用实例,并指定渲染函数
+ this.app = createApp({
+ render: () => this.r,
+ });
+ }
+
+ /**
+ * 将 HTML 内容设置到指定的根元素上
+ * @param rootEl 根元素
+ */
+ async setHtml(rootEl) {
+ const nodeId = this.props.model.id;
+ if (SdAgentNodeHtmlNode.reusePool.has(nodeId)) {
+ rootEl.appendChild(SdAgentNodeHtmlNode.reusePool.get(nodeId));
+ return;
+ }
+
+ if (!this.isMounted) {
+ this.isMounted = true;
+ this.container = document.createElement("div");
+ this.container.style.display = "inline-block"; // 关键:确保容器自适应内容
+
+ rootEl.appendChild(this.container);
+ this.app.use(ElementPlus);
+ this.app.use(JsonViewer);
+
+ this.app.mount(this.container);
+ await nextTick();
+ this.setupSizeObserver();
+ SdAgentNodeHtmlNode.reusePool.set(nodeId, this.container);
+ } else {
+ this.r.component.props.properties = this.props.model.getProperties();
+ }
+ }
+
+ /**
+ * 获取节点文本内容
+ * 对于元素,返回 null,因为其内容由特定组件渲染
+ * @returns {null}
+ */
+ getText() {
+ return null;
+ }
+
+ handleComponentInstance(data) {
+ // 将组件实例保存到节点模型
+ this.props.model.setComponentInstance(data)
+ }
+
+ handleContentChange() {
+ // 内容变化时强制更新尺寸
+ this.updateNodeSize();
+ }
+
+ // 渲染完成后获取实际尺寸
+ updateNodeSize() {
+ if (this.container) {
+ const { SCALE_X, SCALE_Y } = this.props.graphModel.transformModel;
+ const node = this.container.querySelector(".node__container");
+ const rect = node.getBoundingClientRect();
+ this.props.model.updateSize(rect.width / SCALE_X, rect.height / SCALE_Y);
+ }
+ }
+
+ setupSizeObserver() {
+ // 首次渲染立即检测
+ requestAnimationFrame(() => {
+ this.updateNodeSize();
+ // 持续监听变化
+ this.resizeObserver = new ResizeObserver(() => {
+ this.updateNodeSize();
+ });
+ this.resizeObserver.observe(this.container);
+ });
+ }
+
+ // 组件卸载时移除监听
+ onDestroy() {
+ this.resizeObserver?.disconnect();
+ }
+}
+
+/**
+ * 定义一个元素的 HTML 模型类,继承自 HtmlNodeModel
+ * 该类主要设置节点的属性和样式
+ */
+class SdAgentNodeHtmlModel extends HtmlNodeModel {
+ initNodeData(data) {
+ super.initNodeData(data);
+ }
+
+ // 保存组件实例引用
+ setComponentInstance(instance) {
+ this.componentInstance = instance;
+ }
+
+ closePopover() {
+ this.componentInstance.closePopover()
+ }
+
+ /**
+ * 设置节点属性
+ * 包括宽度、高度、文本编辑属性等
+ */
+ setAttributes() {
+ // 初始设置为0,后续动态更新
+ this.width = 0;
+ this.height = 0;
+ this.text.editable = false;
+ }
+
+ updateSize(width, height) {
+ this.width = width + 24;
+ this.height = height + 50;
+ this.initNodeData(this); // 触发节点重绘
+ }
+
+ // 定义节点只有左右两个锚点. 锚点位置通过中心点和宽度算出来。
+ getDefaultAnchor() {
+ let _a = this,
+ x = _a.x,
+ y = _a.y,
+ width = _a.width,
+ height = _a.height;
+ return [
+ {
+ x: x + width / 2 + 4,
+ y: y,
+ name: "right",
+ id: "".concat(this.id, "_1"),
+ properties: { connectionType: "source" },
+ },
+ {
+ x: x - width / 2,
+ y: y,
+ name: "left",
+ id: "".concat(this.id, "_3"),
+ properties: { connectionType: "target" },
+ },
+ ];
+ }
+
+ /**
+ * 获取节点轮廓样式
+ * 覆盖父类方法,设置 stroke 属性为 none,以适应特定的视觉效果
+ * @returns {object} 节点轮廓样式
+ */
+ getOutlineStyle() {
+ const style = super.getOutlineStyle();
+ style.stroke = "none";
+ style.hover.stroke = "none";
+ return style;
+ }
+}
+
+// 导出方法注册
+export function registerSdAgentNode(lf) {
+ lf.register({
+ type: "sdAgent",
+ view: SdAgentNodeHtmlNode,
+ model: SdAgentNodeHtmlModel
+ });
+}
diff --git a/src/views/flow/nodes/function/sdAgent.vue b/src/views/flow/nodes/function/sdAgent.vue
new file mode 100644
index 0000000..c3e98c1
--- /dev/null
+++ b/src/views/flow/nodes/function/sdAgent.vue
@@ -0,0 +1,124 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/flow/nodes/function/sleep.js b/src/views/flow/nodes/function/sleep.js
new file mode 100644
index 0000000..ed1aef6
--- /dev/null
+++ b/src/views/flow/nodes/function/sleep.js
@@ -0,0 +1,213 @@
+// 导入 HtmlNode 及其模型,为后续继承做准备
+import { HtmlNode, HtmlNodeModel } from "@logicflow/core";
+// 导入 Vue 相关方法,用于渲染组件
+import { createApp, h, nextTick } from "vue";
+import ElementPlus from "element-plus";
+// 导入 Vue 组件
+// @ts-ignore
+import Sleep from "./sleep.vue";
+import OuterNode from "../../components/OuterNode.vue";
+import JsonViewer from 'vue3-json-viewer'
+
+/**
+ * 定义一个 元素的 HTML 节点类,继承自 HtmlNode
+ * 该类负责在 HTML 中渲染 元素,并处理其交互逻辑
+ */
+class SleepNodeHtmlNode extends HtmlNode {
+ resizeObserver = null;
+ isMounted; // 标记组件是否已挂载
+ r; // 渲染函数
+ app; // Vue 应用实例
+ container = null;
+
+ static reusePool = new Map(); // 节点复用池
+
+ /**
+ * 构造函数
+ * @param props 传递给节点的属性,包括模型、图模型等
+ */
+ constructor(props) {
+ super(props);
+ this.initVueApp(props);
+ }
+
+ initVueApp(props) {
+ this.isMounted = false;
+
+ this.hideAnchor = false;
+ this.autoExpand = true; // 防止锚点被折叠
+ this.anchorsPreset = "default"; // 重置锚点预设
+ // 创建 元素的渲染函数
+ this.r = h(OuterNode, {
+ model: props.model,
+ component: Sleep,
+ properties: {
+ ...props.model.getProperties(),
+ },
+ onContentChange: this.handleContentChange.bind(this),
+ onBindRef: this.handleComponentInstance.bind(this)
+ });
+
+ // 创建 Vue 应用实例,并指定渲染函数
+ this.app = createApp({
+ render: () => this.r,
+ });
+ }
+
+ /**
+ * 将 HTML 内容设置到指定的根元素上
+ * @param rootEl 根元素
+ */
+ async setHtml(rootEl) {
+ const nodeId = this.props.model.id;
+ if (SleepNodeHtmlNode.reusePool.has(nodeId)) {
+ rootEl.appendChild(SleepNodeHtmlNode.reusePool.get(nodeId));
+ return;
+ }
+
+ if (!this.isMounted) {
+ this.isMounted = true;
+ this.container = document.createElement("div");
+ this.container.style.display = "inline-block"; // 关键:确保容器自适应内容
+
+ rootEl.appendChild(this.container);
+ this.app.use(ElementPlus);
+ this.app.use(JsonViewer);
+
+ this.app.mount(this.container);
+ await nextTick();
+ this.setupSizeObserver();
+ SleepNodeHtmlNode.reusePool.set(nodeId, this.container);
+ } else {
+ this.r.component.props.properties = this.props.model.getProperties();
+ }
+ }
+
+ /**
+ * 获取节点文本内容
+ * 对于元素,返回 null,因为其内容由特定组件渲染
+ * @returns {null}
+ */
+ getText() {
+ return null;
+ }
+
+ handleComponentInstance(data) {
+ // 将组件实例保存到节点模型
+ this.props.model.setComponentInstance(data)
+ }
+
+ handleContentChange() {
+ // 内容变化时强制更新尺寸
+ this.updateNodeSize();
+ }
+
+ // 渲染完成后获取实际尺寸
+ updateNodeSize() {
+ if (this.container) {
+ const { SCALE_X, SCALE_Y } = this.props.graphModel.transformModel;
+ const node = this.container.querySelector(".node__container");
+ const rect = node.getBoundingClientRect();
+ this.props.model.updateSize(rect.width / SCALE_X, rect.height / SCALE_Y);
+ }
+ }
+
+ setupSizeObserver() {
+ // 首次渲染立即检测
+ requestAnimationFrame(() => {
+ this.updateNodeSize();
+ // 持续监听变化
+ this.resizeObserver = new ResizeObserver(() => {
+ this.updateNodeSize();
+ });
+ this.resizeObserver.observe(this.container);
+ });
+ }
+
+ // 组件卸载时移除监听
+ onDestroy() {
+ this.resizeObserver?.disconnect();
+ }
+}
+
+/**
+ * 定义一个元素的 HTML 模型类,继承自 HtmlNodeModel
+ * 该类主要设置节点的属性和样式
+ */
+class SleepNodeHtmlModel extends HtmlNodeModel {
+ initNodeData(data) {
+ super.initNodeData(data);
+ }
+
+ // 保存组件实例引用
+ setComponentInstance(instance) {
+ this.componentInstance = instance;
+ }
+
+ closePopover() {
+ this.componentInstance.closePopover()
+ }
+
+ /**
+ * 设置节点属性
+ * 包括宽度、高度、文本编辑属性等
+ */
+ setAttributes() {
+ // 初始设置为0,后续动态更新
+ this.width = 0;
+ this.height = 0;
+ this.text.editable = false;
+ }
+
+ updateSize(width, height) {
+ this.width = width + 24;
+ this.height = height + 50;
+ this.initNodeData(this); // 触发节点重绘
+ }
+
+ // 定义节点只有左右两个锚点. 锚点位置通过中心点和宽度算出来。
+ getDefaultAnchor() {
+ let _a = this,
+ x = _a.x,
+ y = _a.y,
+ width = _a.width,
+ height = _a.height;
+ return [
+ {
+ x: x + width / 2 + 4,
+ y: y,
+ name: "right",
+ id: "".concat(this.id, "_1"),
+ properties: { connectionType: "source" },
+ },
+ {
+ x: x - width / 2,
+ y: y,
+ name: "left",
+ id: "".concat(this.id, "_3"),
+ properties: { connectionType: "target" },
+ },
+ ];
+ }
+
+ /**
+ * 获取节点轮廓样式
+ * 覆盖父类方法,设置 stroke 属性为 none,以适应特定的视觉效果
+ * @returns {object} 节点轮廓样式
+ */
+ getOutlineStyle() {
+ const style = super.getOutlineStyle();
+ style.stroke = "none";
+ style.hover.stroke = "none";
+ return style;
+ }
+}
+
+// 导出方法注册
+export function registerSleepNode(lf) {
+ lf.register({
+ type: "sleep",
+ view: SleepNodeHtmlNode,
+ model: SleepNodeHtmlModel
+ });
+}
diff --git a/src/views/flow/nodes/function/sleep.vue b/src/views/flow/nodes/function/sleep.vue
new file mode 100644
index 0000000..fe66088
--- /dev/null
+++ b/src/views/flow/nodes/function/sleep.vue
@@ -0,0 +1,125 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/flow/nodes/function/stopLoop.js b/src/views/flow/nodes/function/stopLoop.js
new file mode 100644
index 0000000..5246a3f
--- /dev/null
+++ b/src/views/flow/nodes/function/stopLoop.js
@@ -0,0 +1,191 @@
+// 导入 HtmlNode 及其模型,为后续继承做准备
+import { HtmlNode, HtmlNodeModel } from "@logicflow/core";
+// 导入 Vue 相关方法,用于渲染组件
+import { createApp, h, nextTick } from "vue";
+import ElementPlus from "element-plus";
+// 导入 Vue 组件
+import StopLoop from "./stopLoop.vue";
+import JsonViewer from 'vue3-json-viewer'
+
+/**
+ * 定义一个 元素的 HTML 节点类,继承自 HtmlNode
+ * 该类负责在 HTML 中渲染 元素,并处理其交互逻辑
+ */
+class StopLoopHtmlNode extends HtmlNode {
+ resizeObserver = null;
+ isMounted; // 标记组件是否已挂载
+ r; // 渲染函数
+ app; // Vue 应用实例
+ container = null;
+
+ /**
+ * 构造函数
+ * @param props 传递给节点的属性,包括模型、图模型等
+ */
+ constructor(props) {
+ super(props);
+ this.initVueApp(props);
+ }
+
+ initVueApp(props) {
+ this.isMounted = false;
+
+ this.hideAnchor = false;
+ this.autoExpand = true; // 防止锚点被折叠
+ this.anchorsPreset = "default"; // 重置锚点预设
+ // 创建 元素的渲染函数
+ this.r = h(StopLoop, {
+ model: props.model,
+ graphModel: props.graphModel,
+ properties: {
+ ...props.model.getProperties(),
+ },
+ text: props.model.text.value,
+ onContentChange: this.handleContentChange.bind(this),
+ onBindRef: this.handleComponentInstance.bind(this),
+ });
+
+ // 创建 Vue 应用实例,并指定渲染函数
+ this.app = createApp({
+ render: () => this.r,
+ });
+ }
+
+ /**
+ * 将 HTML 内容设置到指定的根元素上
+ * @param rootEl 根元素
+ */
+ async setHtml(rootEl) {
+ if (!this.isMounted) {
+ this.isMounted = true;
+ this.container = document.createElement("div");
+ this.container.style.display = "inline-block"; // 关键:确保容器自适应内容
+
+ rootEl.appendChild(this.container);
+ this.app.use(ElementPlus); // 关键:单独注册ElementPlus
+ this.app.use(JsonViewer)
+ this.app.mount(this.container);
+ await nextTick();
+ this.setupSizeObserver();
+ } else {
+ this.r.component.props.properties = this.props.model.getProperties();
+ }
+ }
+
+ /**
+ * 获取节点文本内容
+ * 对于元素,返回 null,因为其内容由特定组件渲染
+ * @returns {null}
+ */
+ getText() {
+ return null;
+ }
+
+ handleComponentInstance(data) {
+ // 将组件实例保存到节点模型
+ this.props.model.setComponentInstance(data);
+ }
+
+ handleContentChange() {
+ // 内容变化时强制更新尺寸
+ this.updateNodeSize();
+ }
+
+ // 渲染完成后获取实际尺寸
+ updateNodeSize() {
+ if (this.container) {
+ const { SCALE_X, SCALE_Y } = this.props.graphModel.transformModel;
+ const node = this.container.querySelector(".node__container");
+ const rect = node.getBoundingClientRect();
+ this.props.model.updateSize(rect.width / SCALE_X, rect.height / SCALE_Y);
+ }
+ }
+
+ setupSizeObserver() {
+ // 首次渲染立即检测
+ requestAnimationFrame(() => {
+ this.updateNodeSize();
+ // 持续监听变化
+ this.resizeObserver = new ResizeObserver(() => {
+ this.updateNodeSize();
+ });
+ this.resizeObserver.observe(this.container);
+ });
+ }
+
+ // 组件卸载时移除监听
+ onDestroy() {
+ this.resizeObserver?.disconnect();
+ }
+}
+
+/**
+ * 定义一个元素的 HTML 模型类,继承自 HtmlNodeModel
+ * 该类主要设置节点的属性和样式
+ */
+class StopLoopHtmlModel extends HtmlNodeModel {
+ initNodeData(data) {
+ super.initNodeData(data);
+ }
+
+ // 保存组件实例引用
+ setComponentInstance(instance) {
+ this.componentInstance = instance;
+ }
+
+ /**
+ * 设置节点属性
+ * 包括宽度、高度、文本编辑属性等
+ */
+ setAttributes() {
+ // 初始设置为0,后续动态更新
+ this.width = 0;
+ this.height = 0;
+ this.text.editable = false;
+ }
+
+ updateSize(width, height) {
+ this.width = width + 24;
+ this.height = height + 50;
+ this.initNodeData(this); // 触发节点重绘
+ }
+
+ // 定义节点只有左右两个锚点. 锚点位置通过中心点和宽度算出来。
+ getDefaultAnchor() {
+ let _a = this,
+ x = _a.x,
+ y = _a.y,
+ width = _a.width,
+ height = _a.height;
+ return [
+ {
+ x: x - width / 2,
+ y: y - 25,
+ name: "left",
+ id: "".concat(this.id, "_3"),
+ properties: { connectionType: "target" },
+ },
+ ];
+ }
+
+ /**
+ * 获取节点轮廓样式
+ * 覆盖父类方法,设置 stroke 属性为 none,以适应特定的视觉效果
+ * @returns {object} 节点轮廓样式
+ */
+ getOutlineStyle() {
+ const style = super.getOutlineStyle();
+ style.stroke = "none";
+ style.hover.stroke = "none";
+ return style;
+ }
+}
+
+// 导出方法注册
+export function registerStopLoopNode(lf) {
+ lf.register({
+ type: "stopLoop",
+ view: StopLoopHtmlNode,
+ model: StopLoopHtmlModel,
+ });
+}
diff --git a/src/views/flow/nodes/function/stopLoop.vue b/src/views/flow/nodes/function/stopLoop.vue
new file mode 100644
index 0000000..e6ec746
--- /dev/null
+++ b/src/views/flow/nodes/function/stopLoop.vue
@@ -0,0 +1,197 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/views/flow/nodes/function/subEnd.js b/src/views/flow/nodes/function/subEnd.js
new file mode 100644
index 0000000..64f826a
--- /dev/null
+++ b/src/views/flow/nodes/function/subEnd.js
@@ -0,0 +1,186 @@
+// 导入 HtmlNode 及其模型,为后续继承做准备
+import { HtmlNode, HtmlNodeModel } from "@logicflow/core";
+// 导入 Vue 相关方法,用于渲染组件
+import { createApp, h, nextTick } from "vue";
+import ElementPlus from "element-plus";
+// 导入 Vue 组件
+import SubEndNode from "./subEnd.vue";
+
+/**
+ * 定义一个 元素的 HTML 节点类,继承自 HtmlNode
+ * 该类负责在 HTML 中渲染 元素,并处理其交互逻辑
+ */
+class SubEndNodeHtmlNode extends HtmlNode {
+ resizeObserver = null;
+ isMounted; // 标记组件是否已挂载
+ r; // 渲染函数
+ app; // Vue 应用实例
+ container = null;
+
+ /**
+ * 构造函数
+ * @param props 传递给节点的属性,包括模型、图模型等
+ */
+ constructor(props) {
+ super(props);
+ this.initVueApp(props);
+ }
+
+ initVueApp(props) {
+ this.isMounted = false;
+
+ this.hideAnchor = false;
+ this.autoExpand = true; // 防止锚点被折叠
+ this.anchorsPreset = "default"; // 重置锚点预设
+ // 创建 元素的渲染函数
+ this.r = h(SubEndNode, {
+ model: props.model,
+ graphModel: props.graphModel,
+ properties: {
+ ...props.model.getProperties(),
+ }
+ });
+
+ // 创建 Vue 应用实例,并指定渲染函数
+ this.app = createApp({
+ render: () => this.r,
+ });
+ }
+
+ /**
+ * 将 HTML 内容设置到指定的根元素上
+ * @param rootEl 根元素
+ */
+ async setHtml(rootEl) {
+ if (!this.isMounted) {
+ this.isMounted = true;
+ this.container = document.createElement("div");
+ this.container.style.display = "inline-block"; // 关键:确保容器自适应内容
+
+ rootEl.appendChild(this.container);
+ this.app.use(ElementPlus); // 关键:单独注册ElementPlus
+ this.app.mount(this.container);
+ await nextTick();
+ this.setupSizeObserver();
+ } else {
+ this.r.component.props.properties = this.props.model.getProperties();
+ }
+ }
+
+ /**
+ * 获取节点文本内容
+ * 对于元素,返回 null,因为其内容由特定组件渲染
+ * @returns {null}
+ */
+ getText() {
+ return null;
+ }
+
+ handleComponentInstance(data) {
+ // 将组件实例保存到节点模型
+ this.props.model.setComponentInstance(data);
+ }
+
+ handleContentChange() {
+ // 内容变化时强制更新尺寸
+ this.updateNodeSize();
+ }
+
+ // 渲染完成后获取实际尺寸
+ updateNodeSize() {
+ if (this.container) {
+ const { SCALE_X, SCALE_Y } = this.props.graphModel.transformModel;
+ const node = this.container.querySelector(".node__container");
+ const rect = node.getBoundingClientRect();
+ this.props.model.updateSize(rect.width / SCALE_X, rect.height / SCALE_Y);
+ }
+ }
+
+ setupSizeObserver() {
+ // 首次渲染立即检测
+ requestAnimationFrame(() => {
+ this.updateNodeSize();
+ // 持续监听变化
+ this.resizeObserver = new ResizeObserver(() => {
+ this.updateNodeSize();
+ });
+ this.resizeObserver.observe(this.container);
+ });
+ }
+
+ // 组件卸载时移除监听
+ onDestroy() {
+ this.resizeObserver?.disconnect();
+ }
+}
+
+/**
+ * 定义一个元素的 HTML 模型类,继承自 HtmlNodeModel
+ * 该类主要设置节点的属性和样式
+ */
+class SubEndNodeHtmlModel extends HtmlNodeModel {
+ initNodeData(data) {
+ super.initNodeData(data);
+ }
+
+ // 保存组件实例引用
+ setComponentInstance(instance) {
+ this.componentInstance = instance;
+ }
+
+ /**
+ * 设置节点属性
+ * 包括宽度、高度、文本编辑属性等
+ */
+ setAttributes() {
+ // 初始设置为0,后续动态更新
+ this.width = 0;
+ this.height = 0;
+ this.text.editable = false;
+ }
+
+ updateSize(width, height) {
+ this.width = width + 24;
+ this.height = height + 50;
+ this.initNodeData(this); // 触发节点重绘
+ }
+
+ // 定义节点只有左右两个锚点. 锚点位置通过中心点和宽度算出来。
+ getDefaultAnchor() {
+ let _a = this,
+ x = _a.x,
+ y = _a.y,
+ width = _a.width,
+ height = _a.height;
+ return [
+ {
+ x: x - width / 2,
+ y: y - 25,
+ name: "left",
+ id: "".concat(this.id, "_3"),
+ properties: { connectionType: "target" },
+ },
+ ];
+ }
+
+ /**
+ * 获取节点轮廓样式
+ * 覆盖父类方法,设置 stroke 属性为 none,以适应特定的视觉效果
+ * @returns {object} 节点轮廓样式
+ */
+ getOutlineStyle() {
+ const style = super.getOutlineStyle();
+ style.stroke = "none";
+ style.hover.stroke = "none";
+ return style;
+ }
+}
+
+// 导出方法注册
+export function registerSubEndNode(lf) {
+ lf.register({
+ type: "subEnd",
+ view: SubEndNodeHtmlNode,
+ model: SubEndNodeHtmlModel,
+ });
+}
diff --git a/src/views/flow/nodes/function/subEnd.vue b/src/views/flow/nodes/function/subEnd.vue
new file mode 100644
index 0000000..714b9ee
--- /dev/null
+++ b/src/views/flow/nodes/function/subEnd.vue
@@ -0,0 +1,145 @@
+
+
+
+
+
+
+
单次循环结束
+
+
循环体内部工作流的终止节点,结束循环体内部的单次流程
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/views/flow/nodes/function/subStart.js b/src/views/flow/nodes/function/subStart.js
new file mode 100644
index 0000000..2948cf2
--- /dev/null
+++ b/src/views/flow/nodes/function/subStart.js
@@ -0,0 +1,185 @@
+// 导入 HtmlNode 及其模型,为后续继承做准备
+import { HtmlNode, HtmlNodeModel } from "@logicflow/core";
+// 导入 Vue 相关方法,用于渲染组件
+import { createApp, h, nextTick } from "vue";
+import ElementPlus from "element-plus";
+// 导入 Vue 组件
+import SubStartNode from "./subStart.vue";
+
+/**
+ * 定义一个 元素的 HTML 节点类,继承自 HtmlNode
+ * 该类负责在 HTML 中渲染 元素,并处理其交互逻辑
+ */
+class SubStartNodeHtmlNode extends HtmlNode {
+ resizeObserver = null;
+ isMounted; // 标记组件是否已挂载
+ r; // 渲染函数
+ app; // Vue 应用实例
+ container = null;
+
+ /**
+ * 构造函数
+ * @param props 传递给节点的属性,包括模型、图模型等
+ */
+ constructor(props) {
+ super(props);
+ this.initVueApp(props);
+ }
+
+ initVueApp(props) {
+ this.isMounted = false;
+
+ this.hideAnchor = false;
+ this.autoExpand = true; // 防止锚点被折叠
+ this.anchorsPreset = "default"; // 重置锚点预设
+ // 创建 元素的渲染函数
+ this.r = h(SubStartNode, {
+ model: props.model,
+ graphModel: props.graphModel,
+ properties: {
+ ...props.model.getProperties(),
+ }
+ });
+
+ // 创建 Vue 应用实例,并指定渲染函数
+ this.app = createApp({
+ render: () => this.r,
+ });
+ }
+
+ /**
+ * 将 HTML 内容设置到指定的根元素上
+ * @param rootEl 根元素
+ */
+ async setHtml(rootEl) {
+ if (!this.isMounted) {
+ this.isMounted = true;
+ this.container = document.createElement("div");
+ this.container.style.display = "inline-block"; // 关键:确保容器自适应内容
+
+ rootEl.appendChild(this.container);
+ this.app.use(ElementPlus); // 关键:单独注册ElementPlus
+ this.app.mount(this.container);
+ await nextTick();
+ this.setupSizeObserver();
+ } else {
+ this.r.component.props.properties = this.props.model.getProperties();
+ }
+ }
+
+ /**
+ * 获取节点文本内容
+ * 对于元素,返回 null,因为其内容由特定组件渲染
+ * @returns {null}
+ */
+ getText() {
+ return null;
+ }
+
+ handleComponentInstance(data) {
+ // 将组件实例保存到节点模型
+ this.props.model.setComponentInstance(data);
+ }
+
+ handleContentChange() {
+ // 内容变化时强制更新尺寸
+ this.updateNodeSize();
+ }
+
+ // 渲染完成后获取实际尺寸
+ updateNodeSize() {
+ if (this.container) {
+ const { SCALE_X, SCALE_Y } = this.props.graphModel.transformModel;
+ const node = this.container.querySelector(".node__container");
+ const rect = node.getBoundingClientRect();
+ this.props.model.updateSize(rect.width / SCALE_X, rect.height / SCALE_Y);
+ }
+ }
+
+ setupSizeObserver() {
+ // 首次渲染立即检测
+ requestAnimationFrame(() => {
+ this.updateNodeSize();
+ // 持续监听变化
+ this.resizeObserver = new ResizeObserver(() => {
+ this.updateNodeSize();
+ });
+ this.resizeObserver.observe(this.container);
+ });
+ }
+
+ // 组件卸载时移除监听
+ onDestroy() {
+ this.resizeObserver?.disconnect();
+ }
+}
+
+/**
+ * 定义一个元素的 HTML 模型类,继承自 HtmlNodeModel
+ * 该类主要设置节点的属性和样式
+ */
+class SubStartNodeHtmlModel extends HtmlNodeModel {
+ initNodeData(data) {
+ super.initNodeData(data);
+ }
+
+ // 保存组件实例引用
+ setComponentInstance(instance) {
+ this.componentInstance = instance;
+ }
+
+ /**
+ * 设置节点属性
+ * 包括宽度、高度、文本编辑属性等
+ */
+ setAttributes() {
+ // 初始设置为0,后续动态更新
+ this.width = 0;
+ this.height = 0;
+ this.text.editable = false;
+ }
+
+ updateSize(width, height) {
+ this.width = width + 24;
+ this.height = height + 50;
+ this.initNodeData(this); // 触发节点重绘
+ }
+
+ // 定义节点只有左右两个锚点. 锚点位置通过中心点和宽度算出来。
+ getDefaultAnchor() {
+ let _a = this,
+ x = _a.x,
+ y = _a.y,
+ width = _a.width,
+ height = _a.height;
+ return [
+ {
+ x: x + width / 2 - 24,
+ y: y - 25,
+ name: "right",
+ id: "".concat(this.id, "_1")
+ },
+ ];
+ }
+
+ /**
+ * 获取节点轮廓样式
+ * 覆盖父类方法,设置 stroke 属性为 none,以适应特定的视觉效果
+ * @returns {object} 节点轮廓样式
+ */
+ getOutlineStyle() {
+ const style = super.getOutlineStyle();
+ style.stroke = "none";
+ style.hover.stroke = "none";
+ return style;
+ }
+}
+
+// 导出方法注册
+export function registerSubStartNode(lf) {
+ lf.register({
+ type: "subStart",
+ view: SubStartNodeHtmlNode,
+ model: SubStartNodeHtmlModel,
+ });
+}
diff --git a/src/views/flow/nodes/function/subStart.vue b/src/views/flow/nodes/function/subStart.vue
new file mode 100644
index 0000000..4389046
--- /dev/null
+++ b/src/views/flow/nodes/function/subStart.vue
@@ -0,0 +1,106 @@
+
+
+
+
+
+
+
单次循环开始
+
+
循环体内部工作流的开始节点,开始循环体内部的单次流程
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/views/flow/nodes/function/switch.js b/src/views/flow/nodes/function/switch.js
new file mode 100644
index 0000000..2b08a86
--- /dev/null
+++ b/src/views/flow/nodes/function/switch.js
@@ -0,0 +1,318 @@
+// 导入 HtmlNode 及其模型,为后续继承做准备
+import { HtmlNode, HtmlNodeModel, h as svgH } from "@logicflow/core";
+// 导入 Vue 相关方法,用于渲染组件
+import { createApp, h, nextTick } from "vue";
+import ElementPlus from "element-plus";
+// 导入 Vue 组件
+import Switch from "./switch.vue";
+import JsonViewer from 'vue3-json-viewer'
+
+/**
+ * 定义一个 元素的 HTML 节点类,继承自 HtmlNode
+ * 该类负责在 HTML 中渲染 元素,并处理其交互逻辑
+ */
+class SwitchHtmlNode extends HtmlNode {
+ resizeObserver = null;
+ isMounted; // 标记组件是否已挂载
+ r; // 渲染函数
+ app; // Vue 应用实例
+ container = null;
+
+ static reusePool = new Map(); // 节点复用池
+
+ /**
+ * 构造函数
+ * @param props 传递给节点的属性,包括模型、图模型等
+ */
+ constructor(props) {
+ super(props);
+ this.initVueApp(props);
+ }
+
+ initVueApp(props) {
+ this.isMounted = false;
+
+ this.hideAnchor = false;
+ this.autoExpand = true; // 防止锚点被折叠
+ this.anchorsPreset = "default"; // 重置锚点预设
+ // 创建 元素的渲染函数
+ this.r = h(Switch, {
+ model: props.model,
+ graphModel: props.graphModel,
+ properties: {
+ ...props.model.getProperties(),
+ },
+ text: props.model.text.value,
+ onContentChange: this.handleContentChange.bind(this),
+ onBindRef: this.handleComponentInstance.bind(this),
+ // 添加锚点相关回调
+ onAddAnchor: this.handleAddAnchor.bind(this),
+ onRemoveAnchor: this.handleRemoveAnchor.bind(this),
+ onChangeAnchor: this.handleChangeAnchor.bind(this)
+ });
+
+ // 创建 Vue 应用实例,并指定渲染函数
+ this.app = createApp({
+ render: () => this.r,
+ });
+ }
+
+ /**
+ * 将 HTML 内容设置到指定的根元素上
+ * @param rootEl 根元素
+ */
+ async setHtml(rootEl) {
+ const nodeId = this.props.model.id;
+ if (SwitchHtmlNode.reusePool.has(nodeId)) {
+ rootEl.appendChild(SwitchHtmlNode.reusePool.get(nodeId));
+ return;
+ }
+
+ if (!this.isMounted) {
+ this.isMounted = true;
+ this.container = document.createElement("div");
+ this.container.style.display = "inline-block"; // 关键:确保容器自适应内容
+
+ rootEl.appendChild(this.container);
+ this.app.use(ElementPlus);
+ this.app.use(JsonViewer);
+
+ this.app.mount(this.container);
+ await nextTick();
+ this.setupSizeObserver();
+ SwitchHtmlNode.reusePool.set(nodeId, this.container);
+ } else {
+ this.r.component.props.properties = this.props.model.getProperties();
+ }
+
+
+ }
+
+ /**
+ * 获取节点文本内容
+ * 对于元素,返回 null,因为其内容由特定组件渲染
+ * @returns {null}
+ */
+ getText() {
+ return null;
+ }
+
+ handleComponentInstance(data) {
+ // 将组件实例保存到节点模型
+ this.props.model.setComponentInstance(data)
+ }
+
+ handleContentChange() {
+ // 内容变化时强制更新尺寸
+ this.updateNodeSize();
+ }
+
+ // 处理添加锚点
+ handleAddAnchor(height, anchorId) {
+ const { model } = this.props
+ model.addAnchor(height, anchorId)
+ }
+
+ // 处理删除锚点
+ handleRemoveAnchor(anchorId) {
+ this.props.model.removeAnchor(anchorId);
+ }
+
+ handleChangeAnchor(height, anchorId) {
+ const { model } = this.props
+ model.changeAnchorHeight(height, anchorId)
+ }
+
+ // 渲染完成后获取实际尺寸
+ updateNodeSize() {
+ if (this.container) {
+ const { SCALE_X, SCALE_Y } = this.props.graphModel.transformModel;
+ const node = this.container.querySelector(".node__container");
+ const rect = node.getBoundingClientRect();
+ this.props.model.updateSize(rect.width / SCALE_X, rect.height / SCALE_Y);
+ }
+ }
+
+ setupSizeObserver() {
+ // 首次渲染立即检测
+ requestAnimationFrame(() => {
+ this.updateNodeSize();
+ // 持续监听变化
+ this.resizeObserver = new ResizeObserver(() => {
+ this.updateNodeSize();
+ });
+ this.resizeObserver.observe(this.container);
+ });
+ }
+
+ // 组件卸载时移除监听
+ onDestroy() {
+ this.resizeObserver?.disconnect();
+ }
+}
+
+/**
+ * 定义一个元素的 HTML 模型类,继承自 HtmlNodeModel
+ * 该类主要设置节点的属性和样式
+ */
+class SwitchHtmlModel extends HtmlNodeModel {
+
+ initNodeData(data) {
+ super.initNodeData(data);
+ }
+
+ // 返回动态锚点
+ addAnchor(height, anchorId) {
+ const properties = this.getProperties()
+ const anchor = properties?.anchor || []
+ anchor.push({
+ id: anchorId,
+ name: 'right',
+ x: this.x,
+ y: this.y,
+ height
+ })
+ this.setProperties({
+ ...properties,
+ anchor
+ })
+ this.getDefaultAnchor()
+ this.initNodeData(this)
+ }
+
+ removeAnchor(anchorId) {
+ const properties = this.getProperties()
+ const anchor = properties?.anchor || []
+ let nweAnchor = anchor.filter(item => item.id !== anchorId);
+ this.setProperties({
+ ...properties,
+ anchor: nweAnchor
+ })
+ }
+
+ changeAnchorHeight(height, anchorId) {
+ const properties = this.getProperties()
+ const anchor = properties?.anchor || []
+ anchor.forEach(item => {
+ if (item.id === anchorId) {
+ item.height = height
+ }
+ })
+ this.setProperties({
+ ...properties,
+ anchor
+ })
+ this.getDefaultAnchor()
+ this.initNodeData(this)
+ }
+
+ // 保存组件实例引用
+ setComponentInstance(instance) {
+ this.componentInstance = instance;
+ }
+
+ closePopover() {
+ this.componentInstance.closePopover()
+ }
+
+ // 验证表单
+ async validateForm() {
+ if (this.componentInstance && this.componentInstance.validate) {
+ try {
+ const result = await this.componentInstance.validate();
+ if (result) {
+ return { valid: true, message: "验证通过" };
+ } else {
+ return { valid: false, message: "验证失败" };
+ }
+ } catch {
+ return { valid: false, message: "验证失败" };
+ }
+ }
+ return { valid: true, message: "无验证方法" };
+ }
+
+ /**
+ * 设置节点属性
+ * 包括宽度、高度、文本编辑属性等
+ */
+ setAttributes() {
+ // 初始设置为0,后续动态更新
+ this.width = 0;
+ this.height = 0;
+ this.text.editable = false;
+ }
+
+ updateSize(width, height) {
+ this.width = width + 24;
+ this.height = height + 50;
+ // const properties = lf.getProperties(this.id)
+ // this.setProperties({
+ // ...properties,
+ // width: this.width,
+ // height: this.height
+ // })
+ this.initNodeData(this); // 触发节点重绘
+ }
+
+ // 定义节点只有左右两个锚点. 锚点位置通过中心点和宽度算出来。
+ getDefaultAnchor() {
+ let _a = this,
+ x = _a.x,
+ y = _a.y,
+ width = _a.width,
+ height = _a.height;
+ const properties = this.getProperties()
+ const anchor = properties?.anchor || []
+ // const { SCALE_Y } = this.graphModel.transformModel;
+ const ids = []
+ anchor.forEach(item => {
+ ids.push(item.id)
+ if (item.id !== "".concat(this.id, "_else")) {
+ item.x = x + width / 2 - 40
+ // item.y = (y - height / 2 + item.height + 60) / SCALE_Y
+ item.y = y - height / 2 + item.height + 60
+ } else {
+ item.x = x + width / 2 - 40
+ item.y = y + height / 2 - 80
+ }
+ })
+
+ return [
+ // {
+ // x: x + width / 2 - 40,
+ // y: y + height / 2 - 80,
+ // name: "right",
+ // id: "".concat(this.id, "_else")
+ // },
+ {
+ x: x - width / 2,
+ y: y - 25,
+ name: "left",
+ id: "".concat(this.id, "_entry")
+ },
+ ...anchor
+ ];
+ }
+
+ /**
+ * 获取节点轮廓样式
+ * 覆盖父类方法,设置 stroke 属性为 none,以适应特定的视觉效果
+ * @returns {object} 节点轮廓样式
+ */
+ getOutlineStyle() {
+ const style = super.getOutlineStyle();
+ style.stroke = "none";
+ style.hover.stroke = "none";
+ return style;
+ }
+}
+
+// 导出方法注册
+export function registerSwitch(lf) {
+ lf.register({
+ type: "branch",
+ view: SwitchHtmlNode,
+ model: SwitchHtmlModel,
+ });
+}
diff --git a/src/views/flow/nodes/function/switch.vue b/src/views/flow/nodes/function/switch.vue
new file mode 100644
index 0000000..9e63ab3
--- /dev/null
+++ b/src/views/flow/nodes/function/switch.vue
@@ -0,0 +1,642 @@
+
+
+
+
+
+
+
+
+
+
+ {{ errorInfoData }}
+
+
+
+
+
+
+
+
+
+
+
{{ index === 0 ? "If" : "Else If" }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/home/components/BarChart.vue b/src/views/home/components/BarChart.vue
new file mode 100644
index 0000000..cb08904
--- /dev/null
+++ b/src/views/home/components/BarChart.vue
@@ -0,0 +1,117 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/views/home/components/LineChart.vue b/src/views/home/components/LineChart.vue
new file mode 100644
index 0000000..99ad96c
--- /dev/null
+++ b/src/views/home/components/LineChart.vue
@@ -0,0 +1,118 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/views/home/index.vue b/src/views/home/index.vue
new file mode 100644
index 0000000..3995412
--- /dev/null
+++ b/src/views/home/index.vue
@@ -0,0 +1,307 @@
+
+
+
+
+
图标
+
+
测试任务总数
+
1248
+
较昨日 +128 ↑
+
+
+
+
图标
+
+
今日执行任务
+
86
+
较昨日 +16 ↑
+
+
+
+
图标
+
+
在线设备数
+
128
+
较昨日 +8 ↑
+
+
+
+
图标
+
+
异常告警数
+
23
+
较昨日 -5 ↓
+
+
+
+
图标
+
+
测试通过率
+
96.37%
+
较昨日 +1.28% ↑
+
+
+
+
图标
+
+
本周执行时长
+
268.5h
+
较昨日 +23.6h ↑
+
+
+
+
+
+
+
+
+
+
+
待处理事项
+
+
待审核用例
+
待处理异常
+
待分配任务
+
待生成报告
+
+
+
+
告警信息
+
+
+
+
{{ statusList[alarm.status] }}
+
{{ alarm.context }}
+
+
{{ alarm.time }}
+
+
+
+
+
+
+
最近测试任务
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ parseTime(scope.row.createTime) }}
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/views/index.vue b/src/views/index.vue
new file mode 100644
index 0000000..60f2acb
--- /dev/null
+++ b/src/views/index.vue
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/inspection/alarm/index.vue b/src/views/inspection/alarm/index.vue
new file mode 100644
index 0000000..b6a0d09
--- /dev/null
+++ b/src/views/inspection/alarm/index.vue
@@ -0,0 +1,3 @@
+
+ 告警记录
+
\ No newline at end of file
diff --git a/src/views/inspection/cockpit/canvasUtils.js b/src/views/inspection/cockpit/canvasUtils.js
new file mode 100644
index 0000000..99c4aae
--- /dev/null
+++ b/src/views/inspection/cockpit/canvasUtils.js
@@ -0,0 +1,619 @@
+import { useInspectionStore } from "@/store/modules/inspection";
+
+const inspectionStore = useInspectionStore();
+
+
+/**
+ * 6位十六进制颜色字符串转为 CSS rgba 字符串
+ * @param {string} hex - 如 "#ff6e40"
+ * @param {number} alpha - 透明度 0~1
+ * @returns {string} CSS rgba 字符串
+ */
+function hexToCssRgba(hexString, alpha) {
+ const r = parseInt(hexString.slice(1, 3), 16); // 提取 R 分量
+ const g = parseInt(hexString.slice(3, 5), 16); // 提取 G 分量
+ const b = parseInt(hexString.slice(5, 7), 16); // 提取 B 分量
+ return `rgba(${r},${g},${b},${alpha})`;
+}
+
+/**
+ * 将世界坐标转换为 Canvas 屏幕像素坐标
+ * 注意:世界坐标 Y 轴向上,Canvas Y 轴向下,因此需要翻转
+ * @param {number} worldX - 世界坐标 X(米)
+ * @param {number} worldY - 世界坐标 Y(米)
+ * @returns {{x: number, y: number}} Canvas 上的屏幕像素坐标
+ */
+export const worldToScreen = (worldX, worldY, dataStore) => {
+ const store = dataStore || inspectionStore;
+ const camera = store.camera
+ const screenX = (worldX - camera.centerX) * camera.pixelsPerMeter + store.canvasWidthPx / 2; // 水平偏移 + 居中
+ const screenY = -(worldY - camera.centerY) * camera.pixelsPerMeter + store.canvasHeightPx / 2; // 垂直翻转 + 居中
+ return { x: screenX, y: screenY };
+};
+
+/**
+ * 将 Canvas 屏幕像素坐标反算为世界坐标
+ * 与 worldToScreen 互为逆运算
+ * @param {number} screenX - Canvas 像素 X
+ * @param {number} screenY - Canvas 像素 Y
+ * @returns {{x: number, y: number}} 世界坐标(米)
+ */
+export const screenToWorld = (screenX, screenY, dataStore) => {
+ const store = dataStore || inspectionStore;
+ const camera = store.camera;
+ const worldX = (screenX - store.canvasWidthPx / 2) / camera.pixelsPerMeter + camera.centerX; // 水平反算
+ const worldY = -(screenY - store.canvasHeightPx / 2) / camera.pixelsPerMeter + camera.centerY; // 垂直反算(带翻转)
+ return { x: worldX, y: worldY };
+};
+
+
+/**
+ * 0xAARRGGBB 整数颜色解码为 {r, g, b, a} 对象
+ * @param {number} argbInt - 32位 ARGB 整数,如 0xCCFF0040
+ * @returns {{r:number, g:number, b:number, a:number}|null} RGBA 分量 (0~255),无效输入返回 null
+ */
+export const decodeArgbInteger = (argbInt) => {
+ if (typeof argbInt !== 'number') return null; // 非数字类型直接返回
+ return {
+ a: (argbInt >>> 24) & 0xFF, // 高8位:Alpha 透明度
+ r: (argbInt >>> 16) & 0xFF, // 次高8位:Red
+ g: (argbInt >>> 8) & 0xFF, // 次低8位:Green
+ b: argbInt & 0xFF, // 低8位:Blue
+ };
+}
+
+/**
+ * 将 decodeArgbInteger 的结果转为 CSS rgba() 字符串
+ * @param {{r,g,b,a}} colorObj - RGBA 分量对象
+ * @param {number} [alphaOverride] - 可选,覆盖 alpha 值(0~1)
+ * @returns {string|null} CSS rgba 字符串,如 "rgba(255,0,64,0.3)"
+ */
+export const toCssRgba = (colorObj, alphaOverride) => {
+ if (!colorObj) return null; // 空对象返回 null
+ const alpha = alphaOverride !== undefined // 如果提供了覆盖值
+ ? alphaOverride // 使用覆盖值
+ : colorObj.a / 255; // 否则将 0~255 映射到 0~1
+ return `rgba(${colorObj.r},${colorObj.g},${colorObj.b},${alpha})`;
+}
+
+/**
+ * 栅格底图绘制函数
+ * 将预渲染的离屏 Canvas 图像绘制到主 Canvas 上,并根据当前地图范围进行缩放和平移
+ * @param {*} canvasCtx
+ * @param {*} map
+ * @param {*} gridOffscreen
+ */
+export const drawGrid = (canvasCtx, map, dataStore) => {
+ const store = dataStore || inspectionStore;
+ const topLeftScreen = worldToScreen(map.originX, map.maxY, store); // 图像左上角(世界坐标→屏幕)
+ const bottomRightScreen = worldToScreen(map.maxX, map.originY, store); // 图像右下角(世界坐标→屏幕)
+ canvasCtx.imageSmoothingEnabled = false; // 关闭抗锯齿(保持像素清晰)
+ canvasCtx.drawImage(
+ store.gridOffscreen, // 离屏 Canvas 图像源
+ topLeftScreen.x, topLeftScreen.y, // 目标左上角
+ bottomRightScreen.x - topLeftScreen.x, // 目标宽度
+ bottomRightScreen.y - topLeftScreen.y // 目标高度
+ );
+}
+
+/**
+ * 网格辅助线(每1米一条)
+ * @param {*} canvasCtx
+ * @param {*} canvasWidthPx
+ * @param {*} canvasHeightPx
+ */
+export const drawGridlines = (canvasCtx, dataStore) => {
+ // 计算当前可视范围的世界坐标
+ const store = dataStore || inspectionStore;
+ const viewTopLeft = screenToWorld(0, 0, store); // 视口左上角世界坐标
+ const viewBottomRight = screenToWorld(store.canvasWidthPx, store.canvasHeightPx, store); // 视口右下角世界坐标
+ const viewMinX = Math.floor(Math.min(viewTopLeft.x, viewBottomRight.x)); // 可视X最小整数
+ const viewMaxX = Math.ceil(Math.max(viewTopLeft.x, viewBottomRight.x)); // 可视X最大整数
+ const viewMinY = Math.floor(Math.min(viewTopLeft.y, viewBottomRight.y)); // 可视Y最小整数
+ const viewMaxY = Math.ceil(Math.max(viewTopLeft.y, viewBottomRight.y)); // 可视Y最大整数
+
+ // 绘制1米间隔的浅色网格线
+ canvasCtx.strokeStyle = 'rgba(30,50,80,0.25)'; // 淡蓝灰色
+ canvasCtx.lineWidth = 0.5; // 细线
+ canvasCtx.beginPath();
+ for (let worldX = viewMinX; worldX <= viewMaxX; worldX++) { // 逐列绘制垂直线
+ const screenPos = worldToScreen(worldX, 0, store);
+ canvasCtx.moveTo(screenPos.x, 0); // 从画布顶部
+ canvasCtx.lineTo(screenPos.x, store.canvasHeightPx); // 到画布底部
+ }
+ for (let worldY = viewMinY; worldY <= viewMaxY; worldY++) { // 逐行绘制水平线
+ const screenPos = worldToScreen(0, worldY, store);
+ canvasCtx.moveTo(0, screenPos.y); // 从画布左边
+ canvasCtx.lineTo(store.canvasWidthPx, screenPos.y); // 到画布右边
+ }
+ canvasCtx.stroke();
+}
+
+/**
+ * 绘制高级区域列表
+ * @param {*} canvasCtx
+ * @param {*} map
+ * @param {*} dataStore
+ */
+export const drawAdvancedAreaList = (canvasCtx, map, dataStore) => {
+ const store = dataStore || inspectionStore;
+ for (let areaIndex = 0; areaIndex < map.advancedAreas.length; areaIndex++) {
+ const area = map.advancedAreas[areaIndex]; // 当前区域
+ const vertices = area.posGroup || []; // 多边形顶点数组
+ if (vertices.length < 3) continue; // 少于3个顶点无法构成多边形,跳过
+
+ // ── 颜色:优先使用 attribute 中的 ARGB 整数,否则用默认品红色 ──
+ let fillColorCss = 'rgba(224,64,251,0.12)'; // 默认填充(品红半透明)
+ let strokeColorCss = 'rgba(224,64,251,0.5)'; // 默认描边(品红较不透明)
+ if (area.attribute) {
+ const brushColor = decodeArgbInteger(area.attribute.colorBrush); // 解码填充色
+ const penColor = decodeArgbInteger(area.attribute.colorPen); // 解码描边色
+ if (brushColor) {
+ // 使用原始 alpha 值的一半作为填充透明度,最小 0.06
+ fillColorCss = toCssRgba(brushColor, Math.max(0.06, (brushColor.a / 255) * 0.5));
+ }
+ if (penColor) {
+ strokeColorCss = toCssRgba(penColor, 0.6); // 描边60%不透明
+ }
+ }
+
+ // ── 绘制多边形 ──
+ canvasCtx.fillStyle = fillColorCss; // 设置填充色
+ canvasCtx.strokeStyle = strokeColorCss; // 设置描边色
+ canvasCtx.lineWidth = 1.2; // 描边宽度
+ canvasCtx.beginPath();
+ const firstVertexScreen = worldToScreen(vertices[0].x, vertices[0].y, store); // 第一个顶点的屏幕位置
+ canvasCtx.moveTo(firstVertexScreen.x, firstVertexScreen.y); // 移动到起点
+ for (let i = 1; i < vertices.length; i++) { // 依次连接后续顶点
+ const vertexScreen = worldToScreen(vertices[i].x, vertices[i].y, store);
+ canvasCtx.lineTo(vertexScreen.x, vertexScreen.y);
+ }
+ canvasCtx.closePath(); // 闭合路径
+ canvasCtx.fill(); // 填充
+ canvasCtx.stroke(); // 描边
+
+ // ── 在多边形质心处绘制区域名称标签 ──
+ let centroidWorldX = 0, centroidWorldY = 0; // 质心世界坐标
+ for (const v of vertices) { centroidWorldX += v.x; centroidWorldY += v.y; } // 累加顶点坐标
+ centroidWorldX /= vertices.length; // 求平均得到质心 X
+ centroidWorldY /= vertices.length; // 求平均得到质心 Y
+ const centroidScreen = worldToScreen(centroidWorldX, centroidWorldY, store);
+ canvasCtx.font = '500 8px "JetBrains Mono"'; // 标签字体
+ canvasCtx.fillStyle = strokeColorCss; // 与描边同色
+ canvasCtx.textAlign = 'center'; // 居中对齐
+ canvasCtx.fillText(area.instanceName || '', centroidScreen.x, centroidScreen.y + 3); // 绘制文字
+ }
+};
+
+/**
+ * 绘制高级线列表
+ * @param {*} canvasCtx
+ * @param {*} map
+ */
+export const drawAdvancedLineList = (canvasCtx, map, dataStore) => {
+ const store = dataStore || inspectionStore;
+ for (let advLineIndex = 0; advLineIndex < map.advancedLines.length; advLineIndex++) {
+ const advLine = map.advancedLines[advLineIndex]; // 当前高级线
+ const lineData = advLine.line; // 内部的 line 对象
+ if (!lineData || !lineData.startPos || !lineData.endPos) continue; // 缺少端点则跳过
+
+ const startScreen = worldToScreen(lineData.startPos.x, lineData.startPos.y, store);
+ const endScreen = worldToScreen(lineData.endPos.x, lineData.endPos.y, store);
+
+ // ── 根据 className 决定颜色和线型 ──
+ let lineColorHex = '#ff6e40'; // 默认橙色
+ let lineDashPattern = []; // 默认实线(无虚线)
+ const classNameLower = (advLine.className || '').toLowerCase();
+ if (classNameLower.includes('forbidden')) { // 禁行线 → 红色虚线
+ lineColorHex = '#ff1744';
+ lineDashPattern = [8, 4]; // 虚线模式:8px实 4px空
+ } else if (classNameLower.includes('virtual')) { // 虚拟线 → 橙色虚线
+ lineColorHex = '#ffab40';
+ lineDashPattern = [4, 4];
+ }
+
+ // 绘制高级线
+ canvasCtx.save();
+ canvasCtx.shadowColor = lineColorHex; // 辉光色与线条同色
+ canvasCtx.shadowBlur = 4; // 辉光半径
+ canvasCtx.strokeStyle = lineColorHex; // 线条颜色
+ canvasCtx.lineWidth = 2.5; // 线宽(比基础线粗)
+ canvasCtx.setLineDash(lineDashPattern); // 设置虚线模式
+ canvasCtx.globalAlpha = 0.8; // 80%不透明
+ canvasCtx.beginPath();
+ canvasCtx.moveTo(startScreen.x, startScreen.y);
+ canvasCtx.lineTo(endScreen.x, endScreen.y);
+ canvasCtx.stroke();
+ canvasCtx.setLineDash([]); // 恢复实线
+ canvasCtx.restore();
+
+ // 在中点绘制名称标签
+ // const midpointScreenX = (startScreen.x + endScreen.x) / 2;
+ // const midpointScreenY = (startScreen.y + endScreen.y) / 2;
+ // canvasCtx.font = '500 8px "JetBrains Mono"';
+ // canvasCtx.fillStyle = hexToCssRgba(lineColorHex, 0.55); // 55%不透明的文字
+ // canvasCtx.textAlign = 'center';
+ // canvasCtx.fillText(advLine.instanceName || advLine.className || '', midpointScreenX, midpointScreenY - 6);
+ }
+};
+
+/**
+ * 绘制基础直线列表
+ * @param {*} canvasCtx
+ * @param {*} map
+ * @param {*} dataStore
+ */
+export const drawNormalLines = (canvasCtx, map, dataStore) => {
+ const store = dataStore || inspectionStore;
+ for (let lineIndex = 0; lineIndex < map.normalLines.length; lineIndex++) {
+ const line = map.normalLines[lineIndex]; // 当前直线
+ const startScreen = worldToScreen(line.startPos.x, line.startPos.y, store); // 起点屏幕位置
+ const endScreen = worldToScreen(line.endPos.x, line.endPos.y, store); // 终点屏幕位置
+
+ // 绘制发光直线
+ canvasCtx.save();
+ canvasCtx.shadowColor = '#00e5ff'; // 阴影颜色(产生辉光)
+ canvasCtx.shadowBlur = 5; // 辉光扩散半径
+ canvasCtx.strokeStyle = '#00e5ff'; // 线条颜色:青色
+ canvasCtx.lineWidth = 2; // 线宽
+ canvasCtx.globalAlpha = 0.8; // 80%不透明
+ canvasCtx.beginPath();
+ canvasCtx.moveTo(startScreen.x, startScreen.y); // 从起点
+ canvasCtx.lineTo(endScreen.x, endScreen.y); // 到终点
+ canvasCtx.stroke();
+ canvasCtx.restore();
+
+ // 在中点绘制方向箭头
+ const midpointX = (startScreen.x + endScreen.x) / 2; // 中点X
+ const midpointY = (startScreen.y + endScreen.y) / 2; // 中点Y
+ const arrowAngle = Math.atan2(endScreen.y - startScreen.y, endScreen.x - startScreen.x); // 线段方向角
+ const arrowLength = 5; // 箭头长度
+ canvasCtx.fillStyle = 'rgba(0,229,255,0.6)'; // 箭头填充色
+ canvasCtx.beginPath();
+ canvasCtx.moveTo(midpointX + Math.cos(arrowAngle) * arrowLength, midpointY + Math.sin(arrowAngle) * arrowLength); // 箭头尖端
+ canvasCtx.lineTo(midpointX + Math.cos(arrowAngle + 2.5) * 4, midpointY + Math.sin(arrowAngle + 2.5) * 4); // 左翼
+ canvasCtx.lineTo(midpointX + Math.cos(arrowAngle - 2.5) * 4, midpointY + Math.sin(arrowAngle - 2.5) * 4); // 右翼
+ canvasCtx.closePath();
+ canvasCtx.fill();
+ }
+};
+
+/**
+ * 绘制高级曲线列表
+ * @param {*} canvasCtx
+ * @param {*} map
+ * @param {*} dataStore
+ */
+export const drawAdvancedCurveList = (canvasCtx, map, dataStore) => {
+ const store = dataStore || inspectionStore;
+ for (let curveIndex = 0; curveIndex < map.advancedCurves.length; curveIndex++) {
+ const curve = map.advancedCurves[curveIndex]; // 当前曲线
+
+ // 提取起点和终点的 pos(注意:startPos/endPos 是 {instanceName, pos:{x,y}} 结构)
+ const startPoint = curve.startPos?.pos; // 起点世界坐标
+ const endPoint = curve.endPos?.pos; // 终点世界坐标
+ const controlPoint1 = curve.controlPos1; // 第一控制点(直接 {x,y})
+ const controlPoint2 = curve.controlPos2; // 第二控制点(直接 {x,y})
+ if (!startPoint || !endPoint) continue; // 缺少端点则跳过
+
+ // 转换为屏幕坐标
+ const startScreen = worldToScreen(startPoint.x, startPoint.y, store);
+ const endScreen = worldToScreen(endPoint.x, endPoint.y, store);
+ const cp1Screen = controlPoint1 ? worldToScreen(controlPoint1.x, controlPoint1.y, store) : null; // 可能不存在
+ const cp2Screen = controlPoint2 ? worldToScreen(controlPoint2.x, controlPoint2.y, store) : null;
+
+ // ── 绘制贝塞尔曲线(虚线)──
+ canvasCtx.save();
+ canvasCtx.shadowColor = '#76ff03'; // 酸绿色辉光
+ canvasCtx.shadowBlur = 5;
+ canvasCtx.strokeStyle = '#76ff03'; // 酸绿色线条
+ canvasCtx.lineWidth = 2;
+ canvasCtx.setLineDash([8, 5]); // 虚线模式
+ canvasCtx.globalAlpha = 0.85;
+ canvasCtx.beginPath();
+ canvasCtx.moveTo(startScreen.x, startScreen.y); // 起点
+ if (cp1Screen && cp2Screen) {
+ // 有2个控制点 → 三次贝塞尔 (cubic bezier)
+ canvasCtx.bezierCurveTo(cp1Screen.x, cp1Screen.y, cp2Screen.x, cp2Screen.y, endScreen.x, endScreen.y);
+ } else if (cp1Screen) {
+ // 仅1个控制点 → 二次贝塞尔 (quadratic bezier)
+ canvasCtx.quadraticCurveTo(cp1Screen.x, cp1Screen.y, endScreen.x, endScreen.y);
+ } else {
+ // 无控制点 → 直线
+ canvasCtx.lineTo(endScreen.x, endScreen.y);
+ }
+ canvasCtx.stroke();
+ canvasCtx.setLineDash([]); // 恢复实线
+ canvasCtx.restore();
+
+ // ── 在 t=0.5 处绘制方向箭头 ──
+ if (cp1Screen && cp2Screen) {
+ const t = 0.5; // 参数 t = 0.5(曲线中点)
+ const oneMinusT = 0.5; // 1-t = 0.5
+ // 三次贝塞尔公式计算中点坐标
+ const arrowX = oneMinusT ** 3 * startScreen.x + 3 * oneMinusT ** 2 * t * cp1Screen.x + 3 * oneMinusT * t ** 2 * cp2Screen.x + t ** 3 * endScreen.x;
+ const arrowY = oneMinusT ** 3 * startScreen.y + 3 * oneMinusT ** 2 * t * cp1Screen.y + 3 * oneMinusT * t ** 2 * cp2Screen.y + t ** 3 * endScreen.y;
+ // 三次贝塞尔的一阶导数(切线方向)
+ const tangentX = 3 * oneMinusT ** 2 * (cp1Screen.x - startScreen.x) + 6 * oneMinusT * t * (cp2Screen.x - cp1Screen.x) + 3 * t ** 2 * (endScreen.x - cp2Screen.x);
+ const tangentY = 3 * oneMinusT ** 2 * (cp1Screen.y - startScreen.y) + 6 * oneMinusT * t * (cp2Screen.y - cp1Screen.y) + 3 * t ** 2 * (endScreen.y - cp2Screen.y);
+ const tangentAngle = Math.atan2(tangentY, tangentX); // 切线角度
+
+ // 绘制箭头
+ canvasCtx.fillStyle = 'rgba(118,255,3,0.7)';
+ canvasCtx.beginPath();
+ canvasCtx.moveTo(arrowX + Math.cos(tangentAngle) * 5, arrowY + Math.sin(tangentAngle) * 5);
+ canvasCtx.lineTo(arrowX + Math.cos(tangentAngle + 2.5) * 4, arrowY + Math.sin(tangentAngle + 2.5) * 4);
+ canvasCtx.lineTo(arrowX + Math.cos(tangentAngle - 2.5) * 4, arrowY + Math.sin(tangentAngle - 2.5) * 4);
+ canvasCtx.closePath();
+ canvasCtx.fill();
+
+ // 绘制控制点小圆(辅助调试用)
+ canvasCtx.fillStyle = 'rgba(118,255,3,0.25)'; // 浅绿色半透明
+ for (const controlScreen of [cp1Screen, cp2Screen]) {
+ canvasCtx.beginPath();
+ canvasCtx.arc(controlScreen.x, controlScreen.y, 2.5, 0, Math.PI * 2); // 半径2.5px的小圆
+ canvasCtx.fill();
+ }
+ }
+
+ // ── 曲线名称标签 ──
+ // canvasCtx.font = '500 7px "JetBrains Mono"';
+ // canvasCtx.fillStyle = 'rgba(118,255,3,0.5)';
+ // canvasCtx.textAlign = 'center';
+ // const labelScreenY = Math.min(startScreen.y, endScreen.y) - 8; // 标签放在最高点上方
+ // canvasCtx.fillText(curve.instanceName || '', (startScreen.x + endScreen.x) / 2, labelScreenY);
+ }
+};
+
+/**
+ * 绘制高级点列表
+ * @param {*} canvasCtx
+ * @param {*} map
+ * @param {*} dataStore
+ */
+export const drawAdvancedPointList = (canvasCtx, map, dataStore) => {
+ const store = dataStore || inspectionStore;
+ for (let pointIndex = 0; pointIndex < map.advancedPoints.length; pointIndex++) {
+ const advPoint = map.advancedPoints[pointIndex]; // 当前高级点
+ const pointScreen = worldToScreen(advPoint.pos.x, advPoint.pos.y, store); // 屏幕位置
+
+ // 判断是否被鼠标悬停
+ const isHovered = store.hoveredElement &&
+ store.hoveredElement.kind === 'point' &&
+ store.hoveredElement.index === pointIndex;
+
+ // ── 根据 className 决定颜色 ──
+ let pointColorHex = '#ffd600'; // 默认琥珀色
+ const classNameLower = (advPoint.className || '').toLowerCase();
+ if (classNameLower.includes('landmark') || classNameLower.includes('land')) {
+ pointColorHex = '#ffd600'; // LandMark → 琥珀色
+ } else if (classNameLower.includes('charge')) {
+ pointColorHex = '#22c55e'; // 充电点 → 绿色
+ } else if (classNameLower.includes('load')) {
+ pointColorHex = '#3b82f6'; // 装载点 → 蓝色
+ } else if (classNameLower.includes('work') || classNameLower.includes('process')) {
+ pointColorHex = '#a855f7'; // 工位 → 紫色
+ } else if (classNameLower.includes('station')) {
+ pointColorHex = '#00e5ff'; // 站点 → 青色
+ }
+
+ // ── 绘制菱形标记 ──
+ const diamondRadius = isHovered ? 8 : 5; // 悬停时放大
+ const pulseAlpha = isHovered ? Math.sin(store.animationTimeSeconds * 5) * 0.2 + 0.8 : 1; // 悬停时脉冲闪烁
+
+ canvasCtx.save();
+ canvasCtx.globalAlpha = pulseAlpha; // 应用透明度
+ canvasCtx.shadowColor = pointColorHex; // 辉光颜色
+ canvasCtx.shadowBlur = isHovered ? 18 : 8; // 悬停时增强辉光
+
+ // 绘制旋转45度的正方形(菱形)
+ canvasCtx.fillStyle = pointColorHex;
+ canvasCtx.beginPath();
+ canvasCtx.moveTo(pointScreen.x, pointScreen.y - diamondRadius); // 上顶点
+ canvasCtx.lineTo(pointScreen.x + diamondRadius * 0.7, pointScreen.y); // 右顶点
+ canvasCtx.lineTo(pointScreen.x, pointScreen.y + diamondRadius); // 下顶点
+ canvasCtx.lineTo(pointScreen.x - diamondRadius * 0.7, pointScreen.y); // 左顶点
+ canvasCtx.closePath();
+ canvasCtx.fill();
+
+ // 中心白色小圆点(指示精确位置)
+ canvasCtx.shadowBlur = 0;
+ canvasCtx.fillStyle = '#fff';
+ canvasCtx.globalAlpha = 0.9;
+ canvasCtx.beginPath();
+ canvasCtx.arc(pointScreen.x, pointScreen.y, 1.8, 0, Math.PI * 2); // 半径1.8px
+ canvasCtx.fill();
+ canvasCtx.restore();
+
+ // ── 方向箭头(基于 dir 字段,弧度)──
+ if (advPoint.dir !== undefined && advPoint.dir !== null) {
+ // dir 可能是字符串(smap 样例中为 "-1.5759999999999958")或数字
+ const dirRadians = typeof advPoint.dir === 'string' ? parseFloat(advPoint.dir) : advPoint.dir;
+ if (!isNaN(dirRadians)) { // 有效方向角
+ const arrowLengthPx = 12; // 箭头长度(屏幕像素)
+ canvasCtx.strokeStyle = hexToCssRgba(pointColorHex, 0.5); // 半透明的线条
+ canvasCtx.lineWidth = 1;
+ canvasCtx.beginPath();
+ canvasCtx.moveTo(pointScreen.x, pointScreen.y); // 从点中心出发
+ // 注意:世界坐标 Y 向上,但 atan2 使用的 dir 是标准数学角,屏幕 Y 翻转后取负
+ canvasCtx.lineTo(
+ pointScreen.x + Math.cos(dirRadians) * arrowLengthPx,
+ pointScreen.y - Math.sin(dirRadians) * arrowLengthPx // Y 翻转
+ );
+ canvasCtx.stroke();
+ }
+ }
+
+ // ── 名称标签 ──
+ canvasCtx.font = '500 7px "JetBrains Mono"';
+ canvasCtx.fillStyle = hexToCssRgba(pointColorHex, 0.7);
+ canvasCtx.textAlign = 'center';
+ canvasCtx.fillText(advPoint.instanceName || '', pointScreen.x, pointScreen.y - diamondRadius - 4);
+ }
+}
+
+/**
+ * 绘制地图边界
+ * @param {*} canvasCtx
+ * @param {*} map
+ * @param {*} dataStore
+ */
+export const drawMapBoundary = (canvasCtx, map, dataStore) => {
+ const store = dataStore || inspectionStore;
+ const boundaryTopLeft = worldToScreen(map.originX, map.maxY, store); // 边界左上角屏幕位置
+ const boundaryBottomRight = worldToScreen(map.maxX, map.originY, store); // 边界右下角屏幕位置
+ canvasCtx.strokeStyle = 'rgba(0,229,160,0.1)'; // 极淡的青绿色
+ canvasCtx.lineWidth = 1;
+ canvasCtx.setLineDash([5, 4]); // 虚线
+ canvasCtx.strokeRect(
+ boundaryTopLeft.x, boundaryTopLeft.y,
+ boundaryBottomRight.x - boundaryTopLeft.x,
+ boundaryBottomRight.y - boundaryTopLeft.y
+ );
+ canvasCtx.setLineDash([]); // 恢复实线
+};
+
+/**
+ * 从 normalPosList 构建占据栅格图像
+ *
+ * 原理:normalPosList 中每个点标记一个自由通行格子。
+ * 未出现在列表中的格子 = 障碍物(墙壁)
+ * 墙壁格子如果紧邻自由格子,则标记为"墙壁边缘",渲染高亮
+ *
+ * @param {Object} parsedMap - parseSmapJson 的输出
+ * @param {boolean} showEdgeGlow - 是否计算墙壁边缘高亮效果
+ * @returns {HTMLCanvasElement} 离屏 Canvas 元素,包含渲染好的栅格图像
+ */
+export const buildOccupancyGridImage = (parsedMap, showEdgeGlow) => {
+ const gridW = parsedMap.gridWidth; // 栅格宽度(像素)
+ const gridH = parsedMap.gridHeight; // 栅格高度(像素)
+ const originX = parsedMap.originX; // 世界坐标原点 X
+ const originY = parsedMap.originY; // 世界坐标原点 Y
+ const resolution = parsedMap.resolution; // 分辨率
+
+ // ── 第一步:遍历 normalPosList,标记自由空间 ──
+ const occupancyGrid = new Uint8Array(gridW * gridH); // 0=墙壁, 1=自由空间
+ for (const freeSpacePoint of parsedMap.normalPositions) {
+ // 将世界坐标转换为栅格像素坐标
+ const gridColX = Math.floor((freeSpacePoint.x - originX) / resolution); // 列号(从左到右)
+ const pixelRowFromBottom = Math.floor((freeSpacePoint.y - originY) / resolution); // 行号(从下到上,世界坐标系)
+ const imageRowY = gridH - 1 - pixelRowFromBottom; // 翻转为图像行号(从上到下)
+ // 安全边界检查后标记为自由空间
+ if (gridColX >= 0 && gridColX < gridW && imageRowY >= 0 && imageRowY < gridH) {
+ occupancyGrid[imageRowY * gridW + gridColX] = 1; // 标记该格为自由
+ }
+ }
+
+ // ── 第二步:遍历每个格子,按类型着色 ──
+ const offscreenCanvas = document.createElement('canvas'); // 创建离屏 Canvas
+ offscreenCanvas.width = gridW; // 设置尺寸
+ offscreenCanvas.height = gridH;
+ const offscreenCtx = offscreenCanvas.getContext('2d'); // 获取上下文
+ const imageData = offscreenCtx.createImageData(gridW, gridH);// 创建 ImageData
+ const pixelData = imageData.data; // 像素数据数组 (RGBA 扁平排列)
+
+ // 定义三种颜色
+ const FREE_SPACE_R = 10, FREE_SPACE_G = 16, FREE_SPACE_B = 30; // 自由空间:深蓝黑
+ const WALL_INTERIOR_R = 22, WALL_INTERIOR_G = 36, WALL_INTERIOR_B = 64; // 墙壁内部:深蓝
+ const WALL_EDGE_R = 45, WALL_EDGE_G = 130, WALL_EDGE_B = 255; // 墙壁边缘:亮蓝
+
+ for (let rowY = 0; rowY < gridH; rowY++) { // 逐行扫描
+ for (let colX = 0; colX < gridW; colX++) { // 逐列扫描
+ const gridIndex = rowY * gridW + colX; // 一维数组索引
+ const pixelOffset = gridIndex << 2; // RGBA 偏移(×4)
+
+ if (occupancyGrid[gridIndex] === 1) {
+ // ── 自由空间 ──
+ pixelData[pixelOffset] = FREE_SPACE_R; // R
+ pixelData[pixelOffset + 1] = FREE_SPACE_G; // G
+ pixelData[pixelOffset + 2] = FREE_SPACE_B; // B
+ pixelData[pixelOffset + 3] = 255; // A(不透明)
+ } else {
+ // ── 障物(墙壁)── 判断是否为边缘
+ let isAdjacentToFreeSpace = false; // 是否紧邻自由空间
+ if (showEdgeGlow) { // 仅在启用边缘效果时检测
+ if (rowY > 0 && occupancyGrid[(rowY - 1) * gridW + colX] === 1) isAdjacentToFreeSpace = true; // 上方
+ else if (rowY < gridH - 1 && occupancyGrid[(rowY + 1) * gridW + colX] === 1) isAdjacentToFreeSpace = true; // 下方
+ else if (colX > 0 && occupancyGrid[rowY * gridW + colX - 1] === 1) isAdjacentToFreeSpace = true; // 左方
+ else if (colX < gridW - 1 && occupancyGrid[rowY * gridW + colX + 1] === 1) isAdjacentToFreeSpace = true; // 右方
+ }
+ if (isAdjacentToFreeSpace) {
+ // 墙壁边缘:亮蓝色
+ pixelData[pixelOffset] = WALL_EDGE_R;
+ pixelData[pixelOffset + 1] = WALL_EDGE_G;
+ pixelData[pixelOffset + 2] = WALL_EDGE_B;
+ pixelData[pixelOffset + 3] = 255;
+ } else {
+ // 墙壁内部:深蓝色
+ pixelData[pixelOffset] = WALL_INTERIOR_R;
+ pixelData[pixelOffset + 1] = WALL_INTERIOR_G;
+ pixelData[pixelOffset + 2] = WALL_INTERIOR_B;
+ pixelData[pixelOffset + 3] = 255;
+ }
+ }
+ }
+ }
+
+ // 将 ImageData 绘制到离屏 Canvas 上
+ offscreenCtx.putImageData(imageData, 0, 0);
+ return offscreenCanvas;
+}
+
+export class MobileRobot {
+ constructor(id, name, x, y, angleRad = 0, color= "#00D4FF", imageUrl = null) {
+ this.id = id;
+ this.name = name;
+ this.x = x; // 世界坐标X (米)
+ this.y = y; // 世界坐标Y (米)
+ this.angle = angleRad; // 朝向弧度
+ this.imageUrl = imageUrl || '/bot.svg';
+ // 预加载图片 (可选)
+ this.imgElement = null;
+ if (this.imageUrl) {
+ this.init(color, this.imageUrl)
+ }
+ }
+
+ async init(color, imageUrl) {
+ const response = await fetch(imageUrl);
+ let svgText = await response.text();
+ // 替换颜色
+ svgText = svgText.replace(/currentColor/g, color);
+ svgText = svgText.replace(/stroke="[^"]*"/g, `stroke="${color}"`);
+ // 创建Blob URL
+ const blob = new Blob([svgText], { type: 'image/svg+xml' });
+ const url = URL.createObjectURL(blob);
+ this.imgElement = new Image();
+ this.imgElement.src = url;
+ }
+ // 命中检测: 检查世界坐标是否在机器人区域内 (半径0.35米)
+ hitTest(worldX, worldY, pixelsPerMeter){
+ const dx = worldX - this.x;
+ const dy = worldY - this.y;
+ const distance = Math.hypot(dx, dy);
+ const hitRadius = 0.35; // 米
+ return distance < hitRadius;
+ }
+
+ // 绘制到canvas上下文 (屏幕坐标)
+ draw(ctx, screenX, screenY, scale = 1) {
+ const size = 18; // 绘制大小px
+ if (this.imgElement && this.imgElement.complete) {
+ ctx.save();
+ ctx.translate(screenX, screenY);
+ ctx.rotate(-this.angle); // canvas Y向下, 角度翻转适配
+ ctx.drawImage(this.imgElement, -size / 2, -size / 2, size, size);
+ ctx.restore();
+ }
+ // 名字标签
+ ctx.font = '12px "JetBrains Mono"';
+ ctx.fillStyle = '#a0c0f0';
+ ctx.shadowBlur = 0;
+ ctx.fillText(this.name, screenX - 30, screenY - 20);
+ }
+
+ // 更新位置
+ setPosition(x, y) { this.x = x; this.y = y; }
+ setAngle(rad) { this.angle = rad; }
+}
+
diff --git a/src/views/inspection/cockpit/components/Header.vue b/src/views/inspection/cockpit/components/Header.vue
new file mode 100644
index 0000000..7c1da4a
--- /dev/null
+++ b/src/views/inspection/cockpit/components/Header.vue
@@ -0,0 +1,161 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/src/views/inspection/cockpit/components/ManualTakeover.vue b/src/views/inspection/cockpit/components/ManualTakeover.vue
new file mode 100644
index 0000000..247f8a1
--- /dev/null
+++ b/src/views/inspection/cockpit/components/ManualTakeover.vue
@@ -0,0 +1,674 @@
+
+
+
+
+
+
+
控制模式
+
+
+
+ 自动巡检
+
+
+
+ 人工接管
+
+
+
+
+
+ 底盘
+ 机械臂
+ 相机
+
+
人工控制面板
+
+
+
+ 方向控制器
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
移动速度
+
{{ speed }}m/s
+
+
+
+
+
+ 末端控制
+ 关节控制
+ 姿态控制
+
+
+
+
+
+
+
+
+ {{ joint.name }}
+
+ {{ (joint.value * 180 / Math.PI).toFixed(1) }}°
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/views/inspection/cockpit/components/MapCanvas.vue b/src/views/inspection/cockpit/components/MapCanvas.vue
new file mode 100644
index 0000000..a237691
--- /dev/null
+++ b/src/views/inspection/cockpit/components/MapCanvas.vue
@@ -0,0 +1,457 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/views/inspection/cockpit/components/MapOverview.vue b/src/views/inspection/cockpit/components/MapOverview.vue
new file mode 100644
index 0000000..8f418de
--- /dev/null
+++ b/src/views/inspection/cockpit/components/MapOverview.vue
@@ -0,0 +1,403 @@
+
+
+
+
+
机器人状态
+
+
+
+
{{ item.robotName }}
+
{{ robotStatus[item.status] }}
+
+
+
+
编号
+
{{ item.robotCode }}
+
+
+
当前任务
+
{{ getRunningTaskByRobotId(item.id) }}
+
+
+
电量
+
{{ item.batteryLevel }}%
+
+
+
IP端口
+
{{ `${item.ipAddress}: ${item.port}` }}
+
+
+
+
+
+
+
当前任务
+
+
{{ item.taskName }}
+
+
{{ taskStatus[item.status] }}
+
{{ item.remark }}
+
+
+
+
+
+
+
+
+
+
告警记录
+
+
#01 检测到异常噪声
+
14:28:33 — 厂区A 东侧走廊
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/views/inspection/cockpit/components/Monitor.vue b/src/views/inspection/cockpit/components/Monitor.vue
new file mode 100644
index 0000000..d404e90
--- /dev/null
+++ b/src/views/inspection/cockpit/components/Monitor.vue
@@ -0,0 +1,120 @@
+
+
+
+
+
选择机器人
+
+
+
+
+
+
+
+
机器人#0{{ index + 1}}
+
厂区A-在线
+
+
+
+
+
+
监控
+
+
+
+
+
\ No newline at end of file
diff --git a/src/views/inspection/cockpit/components/UrdfView.vue b/src/views/inspection/cockpit/components/UrdfView.vue
new file mode 100644
index 0000000..2a23c77
--- /dev/null
+++ b/src/views/inspection/cockpit/components/UrdfView.vue
@@ -0,0 +1,330 @@
+
+
+
+
+
+
+
+
+
正在加载机器人模型...
+
{{ loadingMessage }}
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/views/inspection/cockpit/index.vue b/src/views/inspection/cockpit/index.vue
new file mode 100644
index 0000000..f90095e
--- /dev/null
+++ b/src/views/inspection/cockpit/index.vue
@@ -0,0 +1,47 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/src/views/inspection/map/index.vue b/src/views/inspection/map/index.vue
new file mode 100644
index 0000000..2576ca7
--- /dev/null
+++ b/src/views/inspection/map/index.vue
@@ -0,0 +1,580 @@
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+ 重置
+
+
+
+
+
+ 新增
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 修改
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/views/inspection/point/index.vue b/src/views/inspection/point/index.vue
new file mode 100644
index 0000000..d4982c1
--- /dev/null
+++ b/src/views/inspection/point/index.vue
@@ -0,0 +1,399 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 新增
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 修改
+
+ 配置
+
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/inspection/robot/index.vue b/src/views/inspection/robot/index.vue
new file mode 100644
index 0000000..8c63a68
--- /dev/null
+++ b/src/views/inspection/robot/index.vue
@@ -0,0 +1,335 @@
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+ 重置
+
+
+
+
+
+ 新增
+
+
+
+
+
+
+
+
+
+
+
+ {{ scope.row.robotType == 1 ? '讲解机器人' : '巡检机器人' }}
+
+
+
+
+
+
+
+
+ 修改
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ connectionStatus ? '连接成功' : '测试连接' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/views/inspection/runTask/index.vue b/src/views/inspection/runTask/index.vue
new file mode 100644
index 0000000..7da4001
--- /dev/null
+++ b/src/views/inspection/runTask/index.vue
@@ -0,0 +1,626 @@
+
+
+
+
+
+
+
+
{{ task.taskName }}
+
+
+
+
+
+
+
+
机器人:{{ task.robotName }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ taskStatusMap[scope.row.status] || '未知状态' }}
+
+
+
+
+
+
+ {{ scope.row.taskType === '1' ? '巡检任务' : '讲解任务' }}
+
+
+
+
+
+
+ {{ formatDate(scope.row.startTime) }}
+
+
+
+
+ {{ formatDate(scope.row.endTime) }}
+
+
+
+
+
+ 日志
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/views/inspection/runTask/task-log.vue b/src/views/inspection/runTask/task-log.vue
new file mode 100644
index 0000000..97553d1
--- /dev/null
+++ b/src/views/inspection/runTask/task-log.vue
@@ -0,0 +1,73 @@
+
+
+
+
{{ formatDate(log.logTime) }}
+
{{ log.logContent }}
+
+
+
+
+
\ No newline at end of file
diff --git a/src/views/inspection/runTask/task-steps.vue b/src/views/inspection/runTask/task-steps.vue
new file mode 100644
index 0000000..f1d7ea3
--- /dev/null
+++ b/src/views/inspection/runTask/task-steps.vue
@@ -0,0 +1,88 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/views/inspection/task/index.vue b/src/views/inspection/task/index.vue
new file mode 100644
index 0000000..b70b0e9
--- /dev/null
+++ b/src/views/inspection/task/index.vue
@@ -0,0 +1,385 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 新增
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ scope.row.taskType === '1' ? '巡检任务' : '讲解任务' }}
+
+
+
+
+
+
+
+
+
+ 修改
+
+ 绑定点位
+
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
待选择点位
+
+
+
+
+
+
+
+ {{ element.waypointName }}
+
+
+
+
+
+
+
+
点位排序
+
+
+
+ {{ index + 1 }} 、{{ element.waypointName }}
+
+
+
+
+
+
+
+
1、将待选择点位拖拽至点位排序中
+
2、点位排序中可拖拽自定义其顺序
+
+
+
+
+
+
+
+
+
diff --git a/src/views/intelligenceTest/car/index.vue b/src/views/intelligenceTest/car/index.vue
new file mode 100644
index 0000000..a4ae1a2
--- /dev/null
+++ b/src/views/intelligenceTest/car/index.vue
@@ -0,0 +1,304 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 新增
+
+
+ 修改
+
+
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 修改
+
+
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/intelligenceTest/mobilePhone/index.vue b/src/views/intelligenceTest/mobilePhone/index.vue
new file mode 100644
index 0000000..9b7acac
--- /dev/null
+++ b/src/views/intelligenceTest/mobilePhone/index.vue
@@ -0,0 +1,321 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 新增
+
+
+ 修改
+
+
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 修改
+
+
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/intelligenceTest/runningTask/index.vue b/src/views/intelligenceTest/runningTask/index.vue
new file mode 100644
index 0000000..0a23ee3
--- /dev/null
+++ b/src/views/intelligenceTest/runningTask/index.vue
@@ -0,0 +1,647 @@
+
+
+
+
+
+
+
+
{{ task.taskName }}
+
+
+
+
+
+
+
+
手机:{{ task.phoneName }}
+
车辆:{{ task.vehicleName }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ taskStatusMap[scope.row.status] || '未知状态' }}
+
+
+
+
+
+
+ {{ scope.row.taskType === '1' ? '巡检任务' : '讲解任务' }}
+
+
+
+
+
+
+ {{ formatDate(scope.row.startTime) }}
+
+
+
+
+ {{ formatDate(scope.row.endTime) }}
+
+
+
+
+
+ 日志
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/views/intelligenceTest/runningTask/task-log.vue b/src/views/intelligenceTest/runningTask/task-log.vue
new file mode 100644
index 0000000..28b562b
--- /dev/null
+++ b/src/views/intelligenceTest/runningTask/task-log.vue
@@ -0,0 +1,71 @@
+
+
+
+
{{ formatDate(log.logTime) }}
+
{{ log.logContent }}
+
+
+
+
+
\ No newline at end of file
diff --git a/src/views/intelligenceTest/runningTask/task-steps.vue b/src/views/intelligenceTest/runningTask/task-steps.vue
new file mode 100644
index 0000000..3aa7440
--- /dev/null
+++ b/src/views/intelligenceTest/runningTask/task-steps.vue
@@ -0,0 +1,88 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/views/intelligenceTest/taskManage/index.vue b/src/views/intelligenceTest/taskManage/index.vue
new file mode 100644
index 0000000..57687c2
--- /dev/null
+++ b/src/views/intelligenceTest/taskManage/index.vue
@@ -0,0 +1,357 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 新增
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ scope.row.taskType === '1' ? '巡检任务' : '讲解任务' }}
+
+
+
+
+
+
+
+
+ 修改
+
+ 绑定用例
+
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
待选择用例
+
+
+
+
+
+
+
+ {{ element.caseName }}
+
+
+
+
+
+
+
+
用例排序
+
+
+
+ {{ index + 1 }} 、{{ element.caseName }}
+
+
+
+
+
+
+
+
1、将待选择用例拖拽至用例排序中
+
2、用例排序中可拖拽自定义其顺序
+
+
+
+
+
+
+
+
+
diff --git a/src/views/intelligenceTest/testCase/index.vue b/src/views/intelligenceTest/testCase/index.vue
new file mode 100644
index 0000000..ef81792
--- /dev/null
+++ b/src/views/intelligenceTest/testCase/index.vue
@@ -0,0 +1,330 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 新增
+
+
+ 修改
+
+
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 修改
+
+ 配置
+
+
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/login.vue b/src/views/login.vue
new file mode 100644
index 0000000..16a8f28
--- /dev/null
+++ b/src/views/login.vue
@@ -0,0 +1,236 @@
+
+
+
+ 招商车研AI测评机器人模拟测试平台
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 记住密码
+
+
+ 登 录
+ 登 录 中...
+
+
+ 立即注册
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/monitor/cache/index.vue b/src/views/monitor/cache/index.vue
new file mode 100644
index 0000000..f2a53a9
--- /dev/null
+++ b/src/views/monitor/cache/index.vue
@@ -0,0 +1,132 @@
+
+
+
+
+
+ 基本信息
+
+
+
+
+ Redis版本
+ {{ cache.info.redis_version }}
+ 运行模式
+ {{ cache.info.redis_mode == "standalone" ? "单机" : "集群" }}
+ 端口
+ {{ cache.info.tcp_port }}
+ 客户端数
+ {{ cache.info.connected_clients }}
+
+
+ 运行时间(天)
+ {{ cache.info.uptime_in_days }}
+ 使用内存
+ {{ cache.info.used_memory_human }}
+ 使用CPU
+ {{ parseFloat(cache.info.used_cpu_user_children).toFixed(2) }}
+ 内存配置
+ {{ cache.info.maxmemory_human }}
+
+
+ AOF是否开启
+ {{ cache.info.aof_enabled == "0" ? "否" : "是" }}
+ RDB是否成功
+ {{ cache.info.rdb_last_bgsave_status }}
+ Key数量
+ {{ cache.dbSize }}
+ 网络入口/出口
+ {{ cache.info.instantaneous_input_kbps }}kps/{{cache.info.instantaneous_output_kbps}}kps
+
+
+
+
+
+
+
+
+
+ 命令统计
+
+
+
+
+
+
+ 内存信息
+
+
+
+
+
+
+
+
diff --git a/src/views/monitor/cache/list.vue b/src/views/monitor/cache/list.vue
new file mode 100644
index 0000000..1696f3a
--- /dev/null
+++ b/src/views/monitor/cache/list.vue
@@ -0,0 +1,246 @@
+
+
+
+
+
+
+ 缓存列表
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 键名列表
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 缓存内容
+ 清理全部
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/monitor/druid/index.vue b/src/views/monitor/druid/index.vue
new file mode 100644
index 0000000..13736ec
--- /dev/null
+++ b/src/views/monitor/druid/index.vue
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
diff --git a/src/views/monitor/job/index.vue b/src/views/monitor/job/index.vue
new file mode 100644
index 0000000..d40828d
--- /dev/null
+++ b/src/views/monitor/job/index.vue
@@ -0,0 +1,501 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+ 重置
+
+
+
+
+
+ 新增
+
+
+ 修改
+
+
+ 删除
+
+
+ 导出
+
+
+ 日志
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 调用方法
+
+
+
+ Bean调用示例:ryTask.ryParams('ry')
+ Class类调用示例:com.ruoyi.quartz.task.RyTask.ryParams('ry')
+ 参数说明:支持字符串,布尔类型,长整型,浮点型,整型
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 生成表达式
+
+
+
+
+
+
+
+
+
+ {{ dict.label }}
+
+
+
+
+
+
+ 立即执行
+ 执行一次
+ 放弃执行
+
+
+
+
+
+
+ 允许
+ 禁止
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ form.jobId }}
+ {{ form.jobName }}
+
+
+ {{ jobGroupFormat(form) }}
+ {{ form.createTime }}
+
+
+ {{ form.cronExpression }}
+
+
+ {{ parseTime(form.nextValidTime) }}
+
+
+ {{ form.invokeTarget }}
+
+
+
+ 正常
+ 暂停
+
+
+
+
+ 允许
+ 禁止
+
+
+
+
+ 默认策略
+ 立即执行
+ 执行一次
+ 放弃执行
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/monitor/job/log.vue b/src/views/monitor/job/log.vue
new file mode 100644
index 0000000..6823883
--- /dev/null
+++ b/src/views/monitor/job/log.vue
@@ -0,0 +1,283 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+ 重置
+
+
+
+
+
+ 删除
+
+
+ 清空
+
+
+ 导出
+
+
+ 关闭
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ parseTime(scope.row.createTime) }}
+
+
+
+
+ 详细
+
+
+
+
+
+
+
+
+
+
+
+ {{ form.jobLogId }}
+ {{ form.jobName }}
+
+
+ {{ form.jobGroup }}
+ {{ form.createTime }}
+
+
+ {{ form.invokeTarget }}
+
+
+ {{ form.jobMessage }}
+
+
+
+ 正常
+ 失败
+
+
+
+ {{ form.exceptionInfo }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/monitor/logininfor/index.vue b/src/views/monitor/logininfor/index.vue
new file mode 100644
index 0000000..09c5642
--- /dev/null
+++ b/src/views/monitor/logininfor/index.vue
@@ -0,0 +1,233 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+ 重置
+
+
+
+
+
+ 删除
+
+
+ 清空
+
+
+ 解锁
+
+
+ 导出
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ parseTime(scope.row.loginTime) }}
+
+
+
+
+
+
+
+
+
diff --git a/src/views/monitor/online/index.vue b/src/views/monitor/online/index.vue
new file mode 100644
index 0000000..9481959
--- /dev/null
+++ b/src/views/monitor/online/index.vue
@@ -0,0 +1,109 @@
+
+
+
+
+
+
+
+
+
+
+ 搜索
+ 重置
+
+
+
+
+
+ {{ (pageNum - 1) * pageSize + scope.$index + 1 }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ parseTime(scope.row.loginTime) }}
+
+
+
+
+ 强退
+
+
+
+
+
+
+
+
+
diff --git a/src/views/monitor/operlog/index.vue b/src/views/monitor/operlog/index.vue
new file mode 100644
index 0000000..3f2ea94
--- /dev/null
+++ b/src/views/monitor/operlog/index.vue
@@ -0,0 +1,310 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+ 重置
+
+
+
+
+
+ 删除
+
+
+ 清空
+
+
+ 导出
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ parseTime(scope.row.operTime) }}
+
+
+
+
+ {{ scope.row.costTime }}毫秒
+
+
+
+
+ 详细
+
+
+
+
+
+
+
+
+
+
+
+ {{ form.title }} / {{ typeFormat(form) }}
+ {{ form.operName }} / {{ form.operIp }} / {{ form.operLocation }}
+
+
+ {{ form.operUrl }}
+ {{ form.requestMethod }}
+
+
+ {{ form.method }}
+
+
+ {{ form.operParam }}
+
+
+ {{ form.jsonResult }}
+
+
+
+ 正常
+ 失败
+
+
+
+ {{ form.costTime }}毫秒
+
+
+ {{ parseTime(form.operTime) }}
+
+
+ {{ form.errorMsg }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/monitor/server/index.vue b/src/views/monitor/server/index.vue
new file mode 100644
index 0000000..a1178b1
--- /dev/null
+++ b/src/views/monitor/server/index.vue
@@ -0,0 +1,187 @@
+
+
+
+
+
+ CPU
+
+
+
+
+ 属性
+ 值
+
+
+
+
+ 核心数
+ {{ server.cpu.cpuNum }}
+
+
+ 用户使用率
+ {{ server.cpu.used }}%
+
+
+ 系统使用率
+ {{ server.cpu.sys }}%
+
+
+ 当前空闲率
+ {{ server.cpu.free }}%
+
+
+
+
+
+
+
+
+
+ 内存
+
+
+
+
+ 属性
+ 内存
+ JVM
+
+
+
+
+ 总内存
+ {{ server.mem.total }}G
+ {{ server.jvm.total }}M
+
+
+ 已用内存
+ {{ server.mem.used}}G
+ {{ server.jvm.used}}M
+
+
+ 剩余内存
+ {{ server.mem.free }}G
+ {{ server.jvm.free }}M
+
+
+ 使用率
+ {{ server.mem.usage }}%
+ {{ server.jvm.usage }}%
+
+
+
+
+
+
+
+
+
+ 服务器信息
+
+
+
+
+ 服务器名称
+ {{ server.sys.computerName }}
+ 操作系统
+ {{ server.sys.osName }}
+
+
+ 服务器IP
+ {{ server.sys.computerIp }}
+ 系统架构
+ {{ server.sys.osArch }}
+
+
+
+
+
+
+
+
+
+ Java虚拟机信息
+
+
+
+
+ Java名称
+ {{ server.jvm.name }}
+ Java版本
+ {{ server.jvm.version }}
+
+
+ 启动时间
+ {{ server.jvm.startTime }}
+ 运行时长
+ {{ server.jvm.runTime }}
+
+
+ 安装路径
+ {{ server.jvm.home }}
+
+
+ 项目路径
+ {{ server.sys.userDir }}
+
+
+ 运行参数
+ {{ server.jvm.inputArgs }}
+
+
+
+
+
+
+
+
+
+ 磁盘状态
+
+
+
+
+ 盘符路径
+ 文件系统
+ 盘符类型
+ 总大小
+ 可用大小
+ 已用大小
+ 已用百分比
+
+
+
+
+ {{ sysFile.dirName }}
+ {{ sysFile.sysTypeName }}
+ {{ sysFile.typeName }}
+ {{ sysFile.total }}
+ {{ sysFile.free }}
+ {{ sysFile.used }}
+ {{ sysFile.usage }}%
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/redirect/index.vue b/src/views/redirect/index.vue
new file mode 100644
index 0000000..15401f8
--- /dev/null
+++ b/src/views/redirect/index.vue
@@ -0,0 +1,14 @@
+
+
+
+
+
diff --git a/src/views/register.vue b/src/views/register.vue
new file mode 100644
index 0000000..97e51d0
--- /dev/null
+++ b/src/views/register.vue
@@ -0,0 +1,219 @@
+
+
+
+ 智能座舱测评AI机器人
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 注 册
+ 注 册 中...
+
+
+ 使用已有账户登录
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/system/config/index.vue b/src/views/system/config/index.vue
new file mode 100644
index 0000000..2945fea
--- /dev/null
+++ b/src/views/system/config/index.vue
@@ -0,0 +1,316 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+ 重置
+
+
+
+
+
+ 新增
+
+
+ 修改
+
+
+ 删除
+
+
+ 导出
+
+
+ 刷新缓存
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ parseTime(scope.row.createTime) }}
+
+
+
+
+ 修改
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ dict.label }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/system/dept/index.vue b/src/views/system/dept/index.vue
new file mode 100644
index 0000000..b356179
--- /dev/null
+++ b/src/views/system/dept/index.vue
@@ -0,0 +1,283 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+ 重置
+
+
+
+
+
+ 新增
+
+
+ 展开/折叠
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ parseTime(scope.row.createTime) }}
+
+
+
+
+ 修改
+ 新增
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ dict.label }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/system/dict/data.vue b/src/views/system/dict/data.vue
new file mode 100644
index 0000000..9dc8eaf
--- /dev/null
+++ b/src/views/system/dict/data.vue
@@ -0,0 +1,362 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+ 重置
+
+
+
+
+
+ 新增
+
+
+ 修改
+
+
+ 删除
+
+
+ 导出
+
+
+ 关闭
+
+
+
+
+
+
+
+
+
+ {{ scope.row.dictLabel }}
+ {{ scope.row.dictLabel }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ parseTime(scope.row.createTime) }}
+
+
+
+
+ 修改
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ dict.label }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/system/dict/index.vue b/src/views/system/dict/index.vue
new file mode 100644
index 0000000..e40cb43
--- /dev/null
+++ b/src/views/system/dict/index.vue
@@ -0,0 +1,420 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+ 重置
+
+
+
+
+
+ 新增
+
+
+ 修改
+
+
+ 删除
+
+
+ 导出
+
+
+ 刷新缓存
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ scope.row.dictType }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ parseTime(scope.row.createTime) }}
+
+
+
+
+ 修改
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ dict.label }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/system/file/index.vue b/src/views/system/file/index.vue
new file mode 100644
index 0000000..8fa3319
--- /dev/null
+++ b/src/views/system/file/index.vue
@@ -0,0 +1,350 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 新增
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ fileType.find(item => item.value === scope.row.fileType)["label"] }}
+
+
+
+
+ {{ parseTime(scope.row.createTime, '{y}-{m}-{d} {h}:{i}:{s}') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 支持格式:json/ xlsx/ txt / png / jpg / jpeg / mp3 / mp4
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/system/group/index.vue b/src/views/system/group/index.vue
new file mode 100644
index 0000000..925ef7d
--- /dev/null
+++ b/src/views/system/group/index.vue
@@ -0,0 +1,289 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 新增
+
+
+
+ 修改
+
+
+
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ sys_group_type?.find(item => Number(item.value) === scope.row.type)["label"] }}
+
+
+
+
+
+
+
+ 修改
+
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/system/menu/index.vue b/src/views/system/menu/index.vue
new file mode 100644
index 0000000..b65b3a8
--- /dev/null
+++ b/src/views/system/menu/index.vue
@@ -0,0 +1,461 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+ 重置
+
+
+
+
+
+ 新增
+
+
+ 展开/折叠
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ parseTime(scope.row.createTime) }}
+
+
+
+
+ 修改
+ 新增
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 目录
+ 菜单
+ 按钮
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 路由名称
+
+
+
+
+
+
+
+
+
+
+
+ 是否外链
+
+
+
+ 是
+ 否
+
+
+
+
+
+
+
+
+
+
+ 路由地址
+
+
+
+
+
+
+
+
+
+
+
+
+ 组件路径
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 权限字符
+
+
+
+
+
+
+
+
+
+
+
+
+ 路由参数
+
+
+
+
+
+
+
+
+
+
+
+ 是否缓存
+
+
+
+ 缓存
+ 不缓存
+
+
+
+
+
+
+
+
+
+
+ 显示状态
+
+
+
+ {{ dict.label }}
+
+
+
+
+
+
+
+
+
+
+ 菜单状态
+
+
+
+ {{ dict.label }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/system/notice/index.vue b/src/views/system/notice/index.vue
new file mode 100644
index 0000000..b009d7c
--- /dev/null
+++ b/src/views/system/notice/index.vue
@@ -0,0 +1,292 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+ 重置
+
+
+
+
+
+ 新增
+
+
+ 修改
+
+
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ parseTime(scope.row.createTime, '{y}-{m}-{d}') }}
+
+
+
+
+ 修改
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ dict.label }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/system/post/index.vue b/src/views/system/post/index.vue
new file mode 100644
index 0000000..045ed41
--- /dev/null
+++ b/src/views/system/post/index.vue
@@ -0,0 +1,287 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+ 重置
+
+
+
+
+
+ 新增
+
+
+ 修改
+
+
+ 删除
+
+
+ 导出
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ parseTime(scope.row.createTime) }}
+
+
+
+
+ 修改
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ dict.label }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/system/role/authUser.vue b/src/views/system/role/authUser.vue
new file mode 100644
index 0000000..5bc3404
--- /dev/null
+++ b/src/views/system/role/authUser.vue
@@ -0,0 +1,179 @@
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+ 重置
+
+
+
+
+
+ 添加用户
+
+
+ 批量取消授权
+
+
+ 关闭
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ parseTime(scope.row.createTime) }}
+
+
+
+
+ 取消授权
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/system/role/index.vue b/src/views/system/role/index.vue
new file mode 100644
index 0000000..3af1b4e
--- /dev/null
+++ b/src/views/system/role/index.vue
@@ -0,0 +1,593 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+ 重置
+
+
+
+
+ 新增
+
+
+ 修改
+
+
+ 删除
+
+
+ 导出
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ parseTime(scope.row.createTime) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 权限字符
+
+
+
+
+
+
+
+
+
+ {{ dict.label }}
+
+
+
+ 展开/折叠
+ 全选/全不选
+ 父子联动
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 展开/折叠
+ 全选/全不选
+ 父子联动
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/system/role/selectUser.vue b/src/views/system/role/selectUser.vue
new file mode 100644
index 0000000..6e61b4c
--- /dev/null
+++ b/src/views/system/role/selectUser.vue
@@ -0,0 +1,144 @@
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+ 重置
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ parseTime(scope.row.createTime) }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/system/user/authRole.vue b/src/views/system/user/authRole.vue
new file mode 100644
index 0000000..7555cac
--- /dev/null
+++ b/src/views/system/user/authRole.vue
@@ -0,0 +1,116 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ (pageNum - 1) * pageSize + scope.$index + 1 }}
+
+
+
+
+
+
+
+
+ {{ parseTime(scope.row.createTime) }}
+
+
+
+
+
+
+
+
+ 提交
+ 返回
+
+
+
+
+
+
diff --git a/src/views/system/user/index.vue b/src/views/system/user/index.vue
new file mode 100644
index 0000000..d221a84
--- /dev/null
+++ b/src/views/system/user/index.vue
@@ -0,0 +1,547 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+ 重置
+
+
+
+
+
+ 新增
+
+
+ 修改
+
+
+ 删除
+
+
+ 导入
+
+
+ 导出
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ parseTime(scope.row.createTime) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ dict.label }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 将文件拖到此处,或点击上传
+
+
+
+ 是否更新已经存在的用户数据
+
+
仅允许导入xls、xlsx格式文件。
+
下载模板
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/system/user/profile/index.vue b/src/views/system/user/profile/index.vue
new file mode 100644
index 0000000..5851f05
--- /dev/null
+++ b/src/views/system/user/profile/index.vue
@@ -0,0 +1,87 @@
+
+
+
+
+
+
+
+ 个人信息
+
+
+
+
+
+
+
+
+ 用户名称
+ {{ state.user.userName }}
+
+
+ 手机号码
+ {{ state.user.phonenumber }}
+
+
+ 用户邮箱
+ {{ state.user.email }}
+
+
+ 所属部门
+ {{ state.user.dept.deptName }} / {{ state.postGroup }}
+
+
+ 所属角色
+ {{ state.roleGroup }}
+
+
+ 创建日期
+ {{ state.user.createTime }}
+
+
+
+
+
+
+
+
+
+ 基本资料
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/system/user/profile/resetPwd.vue b/src/views/system/user/profile/resetPwd.vue
new file mode 100644
index 0000000..b484755
--- /dev/null
+++ b/src/views/system/user/profile/resetPwd.vue
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 保存
+ 关闭
+
+
+
+
+
diff --git a/src/views/system/user/profile/userAvatar.vue b/src/views/system/user/profile/userAvatar.vue
new file mode 100644
index 0000000..3dbc40c
--- /dev/null
+++ b/src/views/system/user/profile/userAvatar.vue
@@ -0,0 +1,180 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 选择
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 提 交
+
+
+
+
+
+
+
+
+
diff --git a/src/views/system/user/profile/userInfo.vue b/src/views/system/user/profile/userInfo.vue
new file mode 100644
index 0000000..2737fe1
--- /dev/null
+++ b/src/views/system/user/profile/userInfo.vue
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 男
+ 女
+
+
+
+ 保存
+ 关闭
+
+
+
+
+
diff --git a/src/views/system/vehOper/index.vue b/src/views/system/vehOper/index.vue
new file mode 100644
index 0000000..0a9b389
--- /dev/null
+++ b/src/views/system/vehOper/index.vue
@@ -0,0 +1,349 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 新增
+
+
+
+ 修改
+
+
+
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 修改
+
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 格式化
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/test/configs/Run.vue b/src/views/test/configs/Run.vue
new file mode 100644
index 0000000..3040369
--- /dev/null
+++ b/src/views/test/configs/Run.vue
@@ -0,0 +1,172 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ row.name }}
+ *
+
+
+
+
+
+ {{ row.type }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 关闭
+ 确定
+
+
+
+
diff --git a/src/views/test/configs/index.vue b/src/views/test/configs/index.vue
new file mode 100644
index 0000000..f70efe7
--- /dev/null
+++ b/src/views/test/configs/index.vue
@@ -0,0 +1,696 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 新增
+
+
+
+ 修改
+
+
+
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 修改
+
+ 编排
+
+ 执行
+
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
待检测项
+
+
+
+
+
+
+ {{ element.name }}
+
+
+
+
+
+
+
+
流程编排
+
+
+
+ {{ index + 1 }} 、{{ element.name }}
+
+
+
+
+
+
+
+
1、将待检测项拖拽至流程编排中
+
2、流程编排中可拖拽自定义其顺序
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/test/detect/index.vue b/src/views/test/detect/index.vue
new file mode 100644
index 0000000..836d697
--- /dev/null
+++ b/src/views/test/detect/index.vue
@@ -0,0 +1,477 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 新增
+
+
+
+ 修改
+
+
+
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{
+ groupList?.find((item) => item.value === scope.row.groupId)[
+ "label"
+ ]
+ }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 修改
+
+ 配置
+
+
+
+
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/test/log/index.vue b/src/views/test/log/index.vue
new file mode 100644
index 0000000..9af113e
--- /dev/null
+++ b/src/views/test/log/index.vue
@@ -0,0 +1,450 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ parseTime(scope.row.createTime) }}
+
+
+
+
+
+
+
+
+
+
+ 日志
+
+ 流程可视化
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{
+ item.logLevel
+ }}
+ {{
+ item.nodeType
+ }}
+ {{
+ item.message
+ }}
+
+ {{ JSON.parse(item.params) }}
+
+
+
+
+
+
+
+
+
+
+
{{ item.name }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/test/params/index.vue b/src/views/test/params/index.vue
new file mode 100644
index 0000000..1375f04
--- /dev/null
+++ b/src/views/test/params/index.vue
@@ -0,0 +1,431 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 新增
+
+
+
+ 修改
+
+
+
+ 删除
+
+
+
+ 导出
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 修改
+
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ form.sourceValue=null">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 是
+ 否
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/tool/build/CodeTypeDialog.vue b/src/views/tool/build/CodeTypeDialog.vue
new file mode 100644
index 0000000..b322c69
--- /dev/null
+++ b/src/views/tool/build/CodeTypeDialog.vue
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+ {{ item.label }}
+
+
+
+
+
+
+
+
+
+ 取消
+ 确定
+
+
+
+
+
diff --git a/src/views/tool/build/DraggableItem.vue b/src/views/tool/build/DraggableItem.vue
new file mode 100644
index 0000000..d345ae5
--- /dev/null
+++ b/src/views/tool/build/DraggableItem.vue
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+ {{ element.componentName }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/tool/build/IconsDialog.vue b/src/views/tool/build/IconsDialog.vue
new file mode 100644
index 0000000..98d9c13
--- /dev/null
+++ b/src/views/tool/build/IconsDialog.vue
@@ -0,0 +1,115 @@
+
+
+
+
+
diff --git a/src/views/tool/build/RightPanel.vue b/src/views/tool/build/RightPanel.vue
new file mode 100644
index 0000000..48e3087
--- /dev/null
+++ b/src/views/tool/build/RightPanel.vue
@@ -0,0 +1,918 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.label }}
+
+
+
+
+
+
+
+
+ {{ activeData.componentName }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 选择
+
+
+
+
+
+
+
+
+ 选择
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 默认
+
+
+ 右侧
+
+
+
+
+
+
+ 个字符
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text
+
+
+ picture
+
+
+ picture-card
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 选项
+
+
+
+
+
+
+
+ 添加选项
+
+
+
+
+
+
+ 选项
+
+
+
+ 动态数据
+
+
+ 静态数据
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 添加父级
+
+
+
+
+
+
+
+
+ 默认
+
+
+ 按钮
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 较大
+
+
+ 默认
+
+
+ 较小
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 布局结构树
+
+
+
+
+ {{ node.label }}
+
+
+
+
+
+
+ 正则校验
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 添加规则
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 较大
+
+
+ 默认
+
+
+ 较小
+
+
+
+
+
+
+ 左对齐
+
+
+ 右对齐
+
+
+ 顶部对齐
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/tool/build/TreeNodeDialog.vue b/src/views/tool/build/TreeNodeDialog.vue
new file mode 100644
index 0000000..372d3af
--- /dev/null
+++ b/src/views/tool/build/TreeNodeDialog.vue
@@ -0,0 +1,93 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/tool/build/index.vue b/src/views/tool/build/index.vue
new file mode 100644
index 0000000..e630740
--- /dev/null
+++ b/src/views/tool/build/index.vue
@@ -0,0 +1,653 @@
+
+
+
+
+
+
Form Generator
+
+
+
+
+
+ 输入型组件
+
+
+
+
+
+
+ {{ element.label }}
+
+
+
+
+
+ 选择型组件
+
+
+
+
+
+
+ {{ element.label }}
+
+
+
+
+
+ 布局型组件
+
+
+
+
+
+
+ {{ element.label }}
+
+
+
+
+
+
+
+
+
+
+ 导出vue文件
+
+
+ 复制代码
+
+
+ 清空
+
+
+
+
+
+
+
+
+
+
+
+ 从左侧拖入或点选组件进行表单设计
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/tool/gen/basicInfoForm.vue b/src/views/tool/gen/basicInfoForm.vue
new file mode 100644
index 0000000..39c8515
--- /dev/null
+++ b/src/views/tool/gen/basicInfoForm.vue
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/tool/gen/createTable.vue b/src/views/tool/gen/createTable.vue
new file mode 100644
index 0000000..00fdfdb
--- /dev/null
+++ b/src/views/tool/gen/createTable.vue
@@ -0,0 +1,46 @@
+
+
+
+ 创建表语句(支持多个建表语句):
+
+
+
+
+
+
+
+
diff --git a/src/views/tool/gen/editTable.vue b/src/views/tool/gen/editTable.vue
new file mode 100644
index 0000000..5d22780
--- /dev/null
+++ b/src/views/tool/gen/editTable.vue
@@ -0,0 +1,200 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ dict.dictName }}
+ {{ dict.dictType }}
+
+
+
+
+
+
+
+
+
+
+
+
+ 提交
+ 返回
+
+
+
+
+
+
diff --git a/src/views/tool/gen/genInfoForm.vue b/src/views/tool/gen/genInfoForm.vue
new file mode 100644
index 0000000..e5189c5
--- /dev/null
+++ b/src/views/tool/gen/genInfoForm.vue
@@ -0,0 +1,306 @@
+
+
+
+
+
+ 生成模板
+
+
+
+
+
+
+
+
+
+
+ 前端类型
+
+
+
+
+
+
+
+
+
+
+ 生成包路径
+
+
+
+
+
+
+
+
+
+
+
+ 生成模块名
+
+
+
+
+
+
+
+
+
+
+
+ 生成业务名
+
+
+
+
+
+
+
+
+
+
+
+ 生成功能名
+
+
+
+
+
+
+
+
+
+
+
+ 生成代码方式
+
+
+
+
+ zip压缩包
+ 自定义路径
+
+
+
+
+
+
+ 上级菜单
+
+
+
+
+
+
+
+
+
+
+
+ 自定义路径
+
+
+
+
+
+
+
+
+ 最近路径快速选择
+
+
+
+
+ 恢复默认的生成基础路径
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 树编码字段
+
+
+
+
+
+
+
+
+
+
+
+
+ 树父编码字段
+
+
+
+
+
+
+
+
+
+
+
+
+ 树名称字段
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 关联子表的表名
+
+
+
+
+
+
+
+
+
+
+
+
+ 子表关联的外键名
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/tool/gen/importTable.vue b/src/views/tool/gen/importTable.vue
new file mode 100644
index 0000000..d0115c4
--- /dev/null
+++ b/src/views/tool/gen/importTable.vue
@@ -0,0 +1,126 @@
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+ 重置
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/tool/gen/index.vue b/src/views/tool/gen/index.vue
new file mode 100644
index 0000000..e6bf638
--- /dev/null
+++ b/src/views/tool/gen/index.vue
@@ -0,0 +1,310 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+ 重置
+
+
+
+
+
+ 生成
+
+
+ 创建
+
+
+ 导入
+
+
+ 修改
+
+
+ 删除
+
+
+
+
+
+
+
+
+ {{(queryParams.pageNum - 1) * queryParams.pageSize + scope.$index + 1}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 复制
+ {{ value }}
+
+
+
+
+
+
+
+
+
diff --git a/src/views/tool/swagger/index.vue b/src/views/tool/swagger/index.vue
new file mode 100644
index 0000000..a44fa71
--- /dev/null
+++ b/src/views/tool/swagger/index.vue
@@ -0,0 +1,9 @@
+
+
+
+
+
diff --git a/vite.config.js b/vite.config.js
new file mode 100644
index 0000000..11f4957
--- /dev/null
+++ b/vite.config.js
@@ -0,0 +1,93 @@
+import {defineConfig, loadEnv, normalizePath} from 'vite'
+import path from 'path'
+import createVitePlugins from './vite/plugins'
+
+// https://vitejs.dev/config/
+export default defineConfig(({mode, command}) => {
+ const env = loadEnv(mode, process.cwd())
+ const {VITE_APP_ENV, VITE_API_URL} = env
+ const variablePath = normalizePath(path.resolve('./src/style/variable.scss'));
+ return {
+ // 部署生产环境和开发环境下的URL。
+ // 默认情况下,vite 会假设你的应用是被部署在一个域名的根路径上
+ // 例如 https://www.ruoyi.vip/。如果应用被部署在一个子路径上,你就需要用这个选项指定这个子路径。例如,如果你的应用被部署在 https://www.ruoyi.vip/admin/,则设置 baseUrl 为 /admin/。
+ base: VITE_APP_ENV === 'production' ? '/' : '/',
+ plugins: createVitePlugins(env, command === 'build'),
+ assetsInclude: ['**/*.urdf', '**/*.xacro'], // 将 .urdf 文件视为静态资源
+ optimizeDeps: {
+ exclude: [
+ // 排除 Monaco Editor 相关的依赖
+ 'monaco-editor',
+ 'monaco-editor/esm/vs/base/worker/workerMain.js',
+ 'monaco-editor/esm/vs/editor/editor.main.js',
+ // 排除 TypeScript worker
+ 'ts.worker.js',
+ 'typescript',
+ ],
+ include: [
+ // 确保其他依赖被优化
+ 'monaco-editor/esm/vs/language/typescript/monaco.contribution',
+ ],
+ },
+ resolve: {
+ // https://cn.vitejs.dev/config/#resolve-alias
+ alias: {
+ // 设置路径
+ '~': path.resolve(__dirname, './'),
+ // 设置别名
+ '@': path.resolve(__dirname, './src')
+ },
+ // https://cn.vitejs.dev/config/#resolve-extensions
+ extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue']
+ },
+ // vite 相关配置
+ server: {
+ port: 13080,
+ host: true,
+ open: true,
+ proxy: {
+ // https://cn.vitejs.dev/config/#server-proxy
+ '/dev-api': {
+ //李小龙 http://192.168.0.201:13080
+ // dev http://10.148.20.34:13080
+ target: VITE_API_URL,
+ // target: command === 'build' ? VITE_API_URL : 'http://192.168.0.201:13080',
+ changeOrigin: true,
+ rewrite: (p) => p.replace(/^\/dev-api/, '')
+ }
+ }
+ },
+ //fix:error:stdin>:7356:1: warning: "@charset" must be the first rule in the file
+ css: {
+ postcss: {
+ plugins: [
+ {
+ postcssPlugin: 'internal:charset-removal',
+ AtRule: {
+ charset: (atRule) => {
+ if (atRule.name === 'charset') {
+ atRule.remove();
+ }
+ }
+ }
+ }
+ ]
+ },
+ preprocessorOptions: {
+ scss: {
+ additionalData: `@import "${variablePath}";`
+ },
+ },
+ },
+ build: {
+ rollupOptions: {
+ // 确保 Monaco Editor 的 worker 文件被正确处理
+ output: {
+ manualChunks: {
+ monaco: ['monaco-editor'],
+ },
+ },
+ },
+ },
+ }
+})
diff --git a/vite/plugins/auto-import.js b/vite/plugins/auto-import.js
new file mode 100644
index 0000000..a5d3576
--- /dev/null
+++ b/vite/plugins/auto-import.js
@@ -0,0 +1,12 @@
+import autoImport from 'unplugin-auto-import/vite'
+
+export default function createAutoImport() {
+ return autoImport({
+ imports: [
+ 'vue',
+ 'vue-router',
+ 'pinia'
+ ],
+ dts: false
+ })
+}
diff --git a/vite/plugins/compression.js b/vite/plugins/compression.js
new file mode 100644
index 0000000..e90aaec
--- /dev/null
+++ b/vite/plugins/compression.js
@@ -0,0 +1,28 @@
+import compression from 'vite-plugin-compression'
+
+export default function createCompression(env) {
+ const { VITE_BUILD_COMPRESS } = env
+ const plugin = []
+ if (VITE_BUILD_COMPRESS) {
+ const compressList = VITE_BUILD_COMPRESS.split(',')
+ if (compressList.includes('gzip')) {
+ // http://doc.ruoyi.vip/ruoyi-vue/other/faq.html#使用gzip解压缩静态文件
+ plugin.push(
+ compression({
+ ext: '.gz',
+ deleteOriginFile: false
+ })
+ )
+ }
+ if (compressList.includes('brotli')) {
+ plugin.push(
+ compression({
+ ext: '.br',
+ algorithm: 'brotliCompress',
+ deleteOriginFile: false
+ })
+ )
+ }
+ }
+ return plugin
+}
diff --git a/vite/plugins/index.js b/vite/plugins/index.js
new file mode 100644
index 0000000..dbad7e0
--- /dev/null
+++ b/vite/plugins/index.js
@@ -0,0 +1,26 @@
+import vue from '@vitejs/plugin-vue'
+
+import createAutoImport from './auto-import'
+import createSvgIcon from './svg-icon'
+import createCompression from './compression'
+import createSetupExtend from './setup-extend'
+
+import monacoPluginModule from 'vite-plugin-monaco-editor'
+
+// 2. 提取真正的插件函数(核心修正)
+const monacoEditorPlugin = monacoPluginModule.default || monacoPluginModule
+export default function createVitePlugins(viteEnv, isBuild = false) {
+ const vitePlugins = [
+ vue(),
+ // 配置 Monaco 插件
+ monacoEditorPlugin({
+ // 按需引入语言(减小打包体积),默认全部引入
+ languages: ['javascript', 'html', 'css', 'json', 'python', 'java']
+ })
+ ]
+ vitePlugins.push(createAutoImport())
+ vitePlugins.push(createSetupExtend())
+ vitePlugins.push(createSvgIcon(isBuild))
+ isBuild && vitePlugins.push(...createCompression(viteEnv))
+ return vitePlugins
+}
diff --git a/vite/plugins/setup-extend.js b/vite/plugins/setup-extend.js
new file mode 100644
index 0000000..ed8342e
--- /dev/null
+++ b/vite/plugins/setup-extend.js
@@ -0,0 +1,5 @@
+import setupExtend from 'unplugin-vue-setup-extend-plus/vite'
+
+export default function createSetupExtend() {
+ return setupExtend({})
+}
diff --git a/vite/plugins/svg-icon.js b/vite/plugins/svg-icon.js
new file mode 100644
index 0000000..30a4140
--- /dev/null
+++ b/vite/plugins/svg-icon.js
@@ -0,0 +1,10 @@
+import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
+import path from 'path'
+
+export default function createSvgIcon(isBuild) {
+ return createSvgIconsPlugin({
+ iconDirs: [path.resolve(process.cwd(), 'src/assets/icons/svg')],
+ symbolId: 'icon-[dir]-[name]',
+ svgoOptions: isBuild
+ })
+}