commit b6b368abc2d8882704f1ec17d132ca87636f95ec Author: zhanghao <774378400@qq.com> Date: Thu Jun 18 14:49:21 2026 +0800 first commit diff --git a/.env.development b/.env.development new file mode 100644 index 0000000..aca0341 --- /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.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.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/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/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/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 @@ + + + + + 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 @@ + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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/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 @@ + + + + + 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 @@ + + + + + + + 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/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..cf65917 --- /dev/null +++ b/src/router/index.js @@ -0,0 +1,316 @@ +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, + }, + { + path: "/device", + component: Layout, + children: [ + { + path: "mechanical_arm/:id", + component: () => + import("@/views/device/register/components/MechanicalArm/index.vue"), + name: "机械臂示教", + meta: { title: "机械臂详情" }, + hidden: true, + }, + { + path: "camera/:id", + component: () => + import("@/views/device/register/components/Camera/index.vue"), + name: "摄像头示教", + meta: { title: "摄像头详情" }, + hidden: true, + }, + { + path: "head/:id", + component: () => + import("@/views/device/register/components/Head/index.vue"), + name: "人工头示教", + meta: { title: "人工头详情" }, + hidden: true, + }, + { + path: "speaker/:id", + component: () => + import("@/views/device/register/components/Speaker/index.vue"), + name: "扬声器示教", + meta: { title: "扬声器详情" }, + hidden: true, + }, + { + path: "hand/:id", + component: () => + import("@/views/device/register/components/DexHand/index.vue"), + name: "灵巧手示教", + meta: { title: "灵巧手详情" }, + hidden: true, + },{ + path: "microphone/:id", + component: () => + import("@/views/device/register/components/Microphone/index.vue"), + name: "麦克风示教", + meta: { title: "麦克风详情" }, + hidden: true, + } + ], + }, + { + //语音合成 + path: "/speech_synthesis", + component: () => + import("@/views/device/register/components/SpeechSynthesis/index.vue"), + meta: { title: "语音合成", noLogin: true }, + hidden: true, + }, + { + //触摸交互 + path: "/touch_interaction", + component: () => + import("@/views/device/register/components/TouchInteraction/index.vue"), + meta: { title: "触摸交互", noLogin: true }, + hidden: true, + }, + { + //cockpit + path: "/inspection/cockpit", + component: () => + import("@/views/inspection/cockpit/index.vue"), + meta: { title: "logicFlow", noLogin: true }, + hidden: true, + }, + { + //logicFlow + path: "/logic_flow", + component: () => + import("@/views/device/register/components/LogicFlow/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: "/Motor/:id", + component: () => + import("@/views/device/register/components/Motor/index.vue"), + 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: "/vi/plan", + component: Layout, + hidden: true, + permissions: ["vi:testProject:list"], + children: [ + { + path: "index/:id", + component: () => import("@/views/vi/testProject/plan/index"), + name: "Plan", + meta: { title: "方案管理", activeMenu: "/vi/testProject" }, + }, + ], + }, + { + path: "/is/plan", + component: Layout, + hidden: true, + permissions: ["is:project:list"], + children: [ + { + path: "index/:id", + component: () => import("@/views/is/project/plan/index"), + name: "ProjectPlan", + meta: { title: "项目方案", activeMenu: "/is/plan" }, + }, + ], + }, + { + path: "/is/evaluate", + component: Layout, + hidden: true, + permissions: ["is:project:list"], + children: [ + { + path: "index/:id", + component: () => import("@/views/is/project/evaluate/index"), + name: "isProjectEvaluate", + meta: { title: "触控评价", activeMenu: "/is/evaluate" }, + }, + ], + }, + { + path: "/vi/evaluate", + component: Layout, + hidden: true, + permissions: ["is:project:list"], + children: [ + { + path: "index/:id", + component: () => import("@/views/vi/testProject/evaluate/index"), + name: "viProjectEvaluate", + meta: { title: "语音评价", activeMenu: "/vi/evaluate" }, + }, + ], + }, + { + path: "/monitor/job-log", + component: Layout, + hidden: true, + permissions: ["monitor:job:list"], + children: [ + { + path: "index/:jobId(\\d+)", + component: () => import("@/views/monitor/job/log"), + name: "JobLog", + meta: { title: "调度日志", activeMenu: "/monitor/job" }, + }, + ], + }, + { + 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/auth.js b/src/utils/auth.js new file mode 100644 index 0000000..08a43d6 --- /dev/null +++ b/src/utils/auth.js @@ -0,0 +1,15 @@ +import Cookies from 'js-cookie' + +const TokenKey = 'Admin-Token' + +export function getToken() { + return Cookies.get(TokenKey) +} + +export function setToken(token) { + return Cookies.set(TokenKey, token) +} + +export function removeToken() { + return Cookies.remove(TokenKey) +} diff --git a/src/utils/convertNodeRedToLogicFlow.js b/src/utils/convertNodeRedToLogicFlow.js new file mode 100644 index 0000000..b79ffbd --- /dev/null +++ b/src/utils/convertNodeRedToLogicFlow.js @@ -0,0 +1,105 @@ + +import { getFlowById } from '@/api/device/register' + +const filterNodeRedData = async (id) => { + 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 `` +} + +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-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-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-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-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-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-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-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-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-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-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-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-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-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}` + } +} + +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(``) + } + if (conf.append) { + children.push(``) + } + 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}}`) + } + 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}}`) + } + 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/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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + \ 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 @@ + + + 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 @@ + + + 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 @@ + + \ 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 @@ + + + + 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 @@ + + + + + + + \ 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 @@ + + + + + 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 @@ + + + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + diff --git a/src/views/index.vue b/src/views/index.vue new file mode 100644 index 0000000..60a9e14 --- /dev/null +++ b/src/views/index.vue @@ -0,0 +1,40 @@ + + + + + + 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..1e8ae57 --- /dev/null +++ b/src/views/inspection/cockpit/components/ManualTakeover.vue @@ -0,0 +1,643 @@ + + + + \ 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..06b9aa1 --- /dev/null +++ b/src/views/inspection/cockpit/components/MapCanvas.vue @@ -0,0 +1,456 @@ + + + + + \ 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 @@ + + + + + \ 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 @@ + + + \ 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..b411dde --- /dev/null +++ b/src/views/inspection/cockpit/components/UrdfView.vue @@ -0,0 +1,588 @@ + + + + + \ 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 @@ + + + + \ 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 @@ + + + \ 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 @@ + + + \ 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 @@ + + + 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 @@ + + + + + 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 @@ + + + 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 @@ + + + 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 @@ + + + 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 @@ + + + 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 @@ + + + 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 @@ + + + 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 @@ + + + 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 @@ + + + + + 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 @@ + + + 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 @@ + + + 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 @@ + + + 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 @@ + + + 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 @@ + + + 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 @@ + + + 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 @@ + + + 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 @@ + + + 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 @@ + + + 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 @@ + + + + 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 @@ + + + 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 @@ + + + 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 @@ + + + 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 @@ + + + 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 @@ + + + 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 @@ + + 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 @@ + + + + + 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 @@ + + + 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 @@ + + + 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 @@ + + + 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 @@ + + + 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 @@ + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + 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 @@ + + + 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 @@ + + + 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..c0a436e --- /dev/null +++ b/vite.config.js @@ -0,0 +1,92 @@ +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'), + 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 + }) +}