commit 5fedd87f49c30757a144a1a67d3983774daadb9f Author: zhanghao <774378400@qq.com> Date: Tue Aug 25 10:15:37 2026 +0800 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..855ca54 --- /dev/null +++ b/.gitignore @@ -0,0 +1,28 @@ +.DS_Store +node_modules/ +dist/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +**/*.log +**/*.tgz + +tests/**/coverage/ +tests/e2e/reports +selenium-debug.log + +# Editor directories and files +.idea +.vscode +*.suo +*.ntvs* +*.njsproj +*.sln +*.local + + +package-lock.json +yarn.lock + +.history + 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..38dceb9 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# @cmvr/iot-common + +CMVR IoT applications share their application shell, permissions, common components and reusable business pages through this package. + +Each host must call `configureCmvrCommon` before mounting Vue. API endpoints, authentication cookies and browser storage remain isolated per application. diff --git a/package.json b/package.json new file mode 100644 index 0000000..dfb4ef1 --- /dev/null +++ b/package.json @@ -0,0 +1,77 @@ +{ + "name": "@cmvr/iot-common", + "version": "1.0.0", + "description": "CMVR IoT shared frontend runtime and business modules", + "author": { + "name": "zhanghao", + "email": "774378400@qq.com" + }, + "type": "module", + "license": "UNLICENSED", + "private": false, + "main": "./dist/index.js", + + "module": "./dist/index.js", + "exports": { + ".": "./dist/index.js", + "./style.css": "./dist/style.css" + }, + "files": [ + "dist", + "README.md" + ], + "scripts": { + "build": "vite build", + "prepublishOnly": "pnpm build" + }, + "publishConfig": { + "registry": "http://192.168.28.10:4873/", + "access": "restricted" + }, + "peerDependencies": { + "@element-plus/icons-vue": "^2.3.2", + "element-plus": "^2.13.1", + "pinia": "^3.0.4", + "vue": "^3.5.26", + "vue-router": "^4.6.4" + }, + "dependencies": { + "@vue-flow/background": "^1.3.2", + "@vue-flow/controls": "^1.1.3", + "@vue-flow/core": "^1.48.2", + "@vueup/vue-quill": "1.2.0", + "@vueuse/core": "14.1.0", + "ace-builds": "^1.43.0", + "axios": "1.13.2", + "clipboard": "2.0.11", + "dayjs": "^1.11.13", + "echarts": "5.6.0", + "file-saver": "2.0.5", + "fuse.js": "7.1.0", + "js-beautify": "1.15.4", + "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", + "sortablejs": "^1.15.6", + "splitpanes": "3.1.5", + "uuid": "^11.1.0", + "vue-cropper": "1.1.1", + "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.2.4", + "sass-embedded": "1.97.2", + "unplugin-auto-import": "0.18.6", + "unplugin-vue-setup-extend-plus": "1.0.1", + "vite": "6.4.1" + }, + "packageManager": "pnpm@10.24.0" +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..cb7d55f --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,3681 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@element-plus/icons-vue': + specifier: ^2.3.2 + version: 2.3.2(vue@3.5.41) + '@vue-flow/background': + specifier: ^1.3.2 + version: 1.3.2(@vue-flow/core@1.48.2(vue@3.5.41))(vue@3.5.41) + '@vue-flow/controls': + specifier: ^1.1.3 + version: 1.1.3(@vue-flow/core@1.48.2(vue@3.5.41))(vue@3.5.41) + '@vue-flow/core': + specifier: ^1.48.2 + version: 1.48.2(vue@3.5.41) + '@vueup/vue-quill': + specifier: 1.2.0 + version: 1.2.0(vue@3.5.41) + '@vueuse/core': + specifier: 14.1.0 + version: 14.1.0(vue@3.5.41) + ace-builds: + specifier: ^1.43.0 + version: 1.44.0 + axios: + specifier: 1.13.2 + version: 1.13.2 + clipboard: + specifier: 2.0.11 + version: 2.0.11 + dayjs: + specifier: ^1.11.13 + version: 1.11.23 + echarts: + specifier: 5.6.0 + version: 5.6.0 + element-plus: + specifier: ^2.13.1 + version: 2.14.5(vue@3.5.41) + file-saver: + specifier: 2.0.5 + version: 2.0.5 + fuse.js: + specifier: 7.1.0 + version: 7.1.0 + js-beautify: + specifier: 1.15.4 + version: 1.15.4 + js-cookie: + specifier: 3.0.5 + version: 3.0.5 + jsencrypt: + specifier: 3.3.2 + version: 3.3.2 + json-editor-vue: + specifier: ^0.18.1 + version: 0.18.1(vue@3.5.41) + lodash: + specifier: ^4.17.21 + version: 4.18.1 + mitt: + specifier: ^3.0.1 + version: 3.0.1 + monaco-editor: + specifier: ^0.55.1 + version: 0.55.1 + nprogress: + specifier: 0.2.0 + version: 0.2.0 + pako: + specifier: ^2.1.0 + version: 2.2.0 + pinia: + specifier: ^3.0.4 + version: 3.0.4(vue@3.5.41) + sortablejs: + specifier: ^1.15.6 + version: 1.15.7 + splitpanes: + specifier: 3.1.5 + version: 3.1.5 + uuid: + specifier: ^11.1.0 + version: 11.1.1 + vue: + specifier: ^3.5.26 + version: 3.5.41 + vue-cropper: + specifier: 1.1.1 + version: 1.1.1 + vue-router: + specifier: ^4.6.4 + version: 4.6.4(vue@3.5.41) + vue3-ace-editor: + specifier: ^2.2.4 + version: 2.2.4(ace-builds@1.44.0)(vue@3.5.41) + vue3-json-viewer: + specifier: 2.3.0 + version: 2.3.0(vue@3.5.41) + vuedraggable: + specifier: 4.1.0 + version: 4.1.0(vue@3.5.41) + xgplayer: + specifier: ^3.0.22 + version: 3.0.26(core-js@3.50.0) + devDependencies: + '@vitejs/plugin-vue': + specifier: 5.2.4 + version: 5.2.4(vite@6.4.1(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.41) + sass-embedded: + specifier: 1.97.2 + version: 1.97.2 + unplugin-auto-import: + specifier: 0.18.6 + version: 0.18.6(@vueuse/core@14.1.0(vue@3.5.41))(rollup@4.62.5) + unplugin-vue-setup-extend-plus: + specifier: 1.0.1 + version: 1.0.1 + vite: + specifier: 6.4.1 + version: 6.4.1(sass-embedded@1.97.2)(sass@1.97.2) + +packages: + + '@antfu/utils@0.7.10': + resolution: {integrity: sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==} + + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.8': + resolution: {integrity: sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/types@7.29.8': + resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==} + engines: {node: '>=6.9.0'} + + '@bufbuild/protobuf@2.14.0': + resolution: {integrity: sha512-C3UGsiCwSprE2NKIIFA3hCDlpXTMCAXRZuEVp88L1GY36Y41+rYL5fryE+nOFhp4p4JPQvdV8PQ4DWgHgeTE+w==} + + '@codemirror/autocomplete@6.20.3': + resolution: {integrity: sha512-tlosUqb+3BbxCxZdu4tKeRghPFC+QM7q4X5YhKV2eCmPG+1r2F3f4AaSz5sCrFqUtX4Jh20VFTKecl16MgiV9g==} + + '@codemirror/commands@6.11.0': + resolution: {integrity: sha512-/K4Rl5BN0OtTiPWmJCdqODu38XnDMsDxKY5rgrPnCkutPTJf2wVbkoixLfealF5Kwse/s8P8M5jAiURiwSwnFA==} + + '@codemirror/lang-json@6.0.2': + resolution: {integrity: sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==} + + '@codemirror/language@6.12.4': + resolution: {integrity: sha512-1q4PaT+o6PbgpkJt4Q8Fv5XJxTy4FUZ4MWETtyiDw3J0Pyr9E2vqcKL+k9wcvjNTIsauxvE7OfmWj3FRPHQ76A==} + + '@codemirror/lint@6.9.7': + resolution: {integrity: sha512-28/+iWLYxKxsvGYhSYL7zaCZqLz5+FFFDq9tVsvGv9kv8RY4fFAchJ5WX9M3YrrRlTIsECjsXPqeNgnSmNP2dg==} + + '@codemirror/search@6.7.1': + resolution: {integrity: sha512-uMe5UO6PamJtSHrXhhHOzSX3ReWtiJrva6GnPMwSOrZtiExb5X5eExhr2OUZQVvdxPsKpY3Ro2mFbQadpPWmHA==} + + '@codemirror/state@6.7.1': + resolution: {integrity: sha512-9QzNDgE4EYDnAHfrTlR2lwiPciiOymLtwKK+8yHQzCc7GXhAP9xdEbEJFy2IWB1j9UGUl9BsgMmTo/ImA02T7A==} + + '@codemirror/view@6.43.9': + resolution: {integrity: sha512-sTuUzTpPMFebRhg6dawChoKKgndIwfjmJgKVxBefPElcU2NwQ6AFroupk0SFqEerQyZOGRfDNnSN8Dw/lMAsXw==} + + '@ctrl/tinycolor@4.2.0': + resolution: {integrity: sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A==} + engines: {node: '>=14'} + + '@element-plus/icons-vue@2.3.2': + resolution: {integrity: sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==} + peerDependencies: + vue: ^3.2.0 + + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@floating-ui/core@1.8.0': + resolution: {integrity: sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==} + + '@floating-ui/dom@1.8.0': + resolution: {integrity: sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==} + + '@floating-ui/utils@0.2.12': + resolution: {integrity: sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==} + + '@fortawesome/fontawesome-common-types@7.3.1': + resolution: {integrity: sha512-k0C0sdHmZtAo6dRDtd1Z/qcpyHbL0CKsjV8seMY/21xGhY5Wsv0XRmiI/xEEH4y2c9b1+jvgNs/3EqhV27yUEA==} + engines: {node: '>=6'} + + '@fortawesome/free-regular-svg-icons@7.3.1': + resolution: {integrity: sha512-q1EsmL7Q8DDnkRBUjSvrxbq7c9oVwwVjCn/xa5apKmdp65YSgzUg2y0Ltnd5aDbT6GdAQQdXql5Ha90ArqIReQ==} + engines: {node: '>=6'} + + '@fortawesome/free-solid-svg-icons@7.3.1': + resolution: {integrity: sha512-v0BLa0eqg7ubvVWeNSHVBs8fWH/GJicERZoJaxJ3FE/lj67VSqzoMg9pzfZVOfLMX10y0pGwQAuxoRVVH2patg==} + engines: {node: '>=6'} + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@jsep-plugin/assignment@1.3.0': + resolution: {integrity: sha512-VVgV+CXrhbMI3aSusQyclHkenWSAm95WaiKrMxRFam3JSUiIaQjoMIw2sEs/OX4XifnqeQUN4DYbJjlA8EfktQ==} + engines: {node: '>= 10.16.0'} + peerDependencies: + jsep: ^0.4.0||^1.0.0 + + '@jsep-plugin/regex@1.0.4': + resolution: {integrity: sha512-q7qL4Mgjs1vByCaTnDFcBnV9HS7GVPJX5vyVoCgZHNSC9rjwIlmbXG5sUuorR5ndfHAIlJ8pVStxvjXHbNvtUg==} + engines: {node: '>= 10.16.0'} + peerDependencies: + jsep: ^0.4.0||^1.0.0 + + '@jsonquerylang/jsonquery@5.1.1': + resolution: {integrity: sha512-Fj4SoA6Ku09EF+t7OEI8QLipA2A+fJCdEOwnDWG84o5jXMRjkcN5NCMH7kFZb5fP62xz914XV5LBOiDdiUXObg==} + hasBin: true + + '@lezer/common@1.5.2': + resolution: {integrity: sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==} + + '@lezer/highlight@1.2.3': + resolution: {integrity: sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==} + + '@lezer/json@1.0.3': + resolution: {integrity: sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==} + + '@lezer/lr@1.4.10': + resolution: {integrity: sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==} + + '@marijn/find-cluster-break@1.0.4': + resolution: {integrity: sha512-Wy0V7+SGUjnF9/TkiM1hKVDPj7jKXduPNboMVtHTA8dySMURWqfg/JZ9E2Sq8JgSJmkl7k7Qe9FLeMSrSraWmQ==} + + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + resolution: {integrity: sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==} + engines: {node: ^22.20 || ^24.12 || >=25} + cpu: [x64] + os: [linux] + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@one-ini/wasm@0.1.1': + resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==} + + '@parcel/watcher-android-arm64@2.6.0': + resolution: {integrity: sha512-trgpLSCKRC/huFjXX/Smh+0sWe4+YtKfktIToiMl59ghz7z+qkH6kMvNnUbLyRs9N11t8l4svSCs1+5B3rOAhA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [android] + + '@parcel/watcher-darwin-arm64@2.6.0': + resolution: {integrity: sha512-Y3QV0gl7Q1zbfueunkWIERICbEojQFCgpyG7YqOGNFLsckXyI1xu9mAIUpKY9QBYzBtSkN8dBPwd3yiAO9ovMw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [darwin] + + '@parcel/watcher-darwin-x64@2.6.0': + resolution: {integrity: sha512-Ohv6OpzhUfKYD7Beb8kDvG0jbIxORCYY1JRdZnaBtnjjkJxgD7ZVL0nw2sCYd0yTMKTvz3nnTnOF3cDifK+kvw==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [darwin] + + '@parcel/watcher-freebsd-x64@2.6.0': + resolution: {integrity: sha512-5HmXvDgs8VK+74jF9y9/2FE3/OnlcKmc56tjmSrEuZjpSZOGL+fvAu+HKJBdPs9uwoP2hE6TlSUpXZ/C5jUFmQ==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [freebsd] + + '@parcel/watcher-linux-arm-glibc@2.6.0': + resolution: {integrity: sha512-Ps/hui3A+vMbjdqlqAowK2ZL8+BO8dBjxeWXj6npTBs3jx4wWmbPpaLuqwrQrSqIVMCnpWo238bJ1U37GhQOYg==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm-musl@2.6.0': + resolution: {integrity: sha512-9c6AUHgHoG+IY88MRIHupztQiQnrbqHYQjkM2btA+Bf/wQnQMuiD0Wfk1EVv3TlNT3x41uU71rn6E4xh/+zvkw==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm64-glibc@2.6.0': + resolution: {integrity: sha512-yHRqS2owEXe6Hic9z6Mh1ECsCd+ODVOGvZDyciqRd21+v+o+DnXMOrw50DSpIG2sb8GPEaPPmfeCAWKPJdq46g==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-arm64-musl@2.6.0': + resolution: {integrity: sha512-WhB2e/V7rqdHHWZusBSPuy5Ei8S6lSz6FE5TKKQz5h3a0O+C+mhY7vxU9b/stqvMb8beLnPY82ZrFTLKs+SrKA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-x64-glibc@2.6.0': + resolution: {integrity: sha512-ulGE6x6Oz6iAwg75T8YQSoguBWasniIbX+QWpaYPcCnDOpdWX3k+4xbEYPZVLxOuoJI+svJJPD3sEj8G7lrQ3A==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-linux-x64-musl@2.6.0': + resolution: {integrity: sha512-tkBYKt7YQrjIJWYDnto2YgO8MRkjlMTSNoRHzsXinBqbLdeOM3L32wPZJvIZxqaLMfSlS/4sUjH/6STVP/XDLw==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-win32-arm64@2.6.0': + resolution: {integrity: sha512-gIZAP23jaHjGWasY/TY6yL7NHFClf0Ga7FN+iINvk+KN94rhm94lYZhFsbYFNcA04/onvGD9kKmiJLJB2HbNwQ==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [win32] + + '@parcel/watcher-win32-x64@2.6.0': + resolution: {integrity: sha512-cA+/pXV2YkfxlIcXOQ5fSWqAzzPyD78/x5qbK/I0vUkrlYHA8TIz+MXjAbGouguKVSI4bOmkTSJ1/poVSsgt+A==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [win32] + + '@parcel/watcher@2.6.0': + resolution: {integrity: sha512-7FNeNl8NCE7aINx7WXiKQrPYZWC/hvrTsmk6zmxbI7LTXE7hVek/n8AfVgpe2y82zl3w0HvCHN0bVKMBoJcC0w==} + engines: {node: '>= 10.0.0'} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@replit/codemirror-indentation-markers@6.5.3': + resolution: {integrity: sha512-hL5Sfvw3C1vgg7GolLe/uxX5T3tmgOA3ZzqlMv47zjU1ON51pzNWiVbS22oh6crYhtVhv8b3gdXwoYp++2ilHw==} + peerDependencies: + '@codemirror/language': ^6.0.0 + '@codemirror/state': ^6.0.0 + '@codemirror/view': ^6.0.0 + + '@rollup/pluginutils@5.4.0': + resolution: {integrity: sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/rollup-android-arm-eabi@4.62.5': + resolution: {integrity: sha512-jfkGfTwhQpsiSckPF8r9bU3pn3vyd72NlWaO+TgEO6WPSDnUhXzrNYCHBMOYj0ACaUgjm6eERLF+XV9a6RstoA==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.62.5': + resolution: {integrity: sha512-oGVqyQlxnrz9/ty89oHpU857VUHEl5/Xu4R2lS+aivCTrNnSsbiENzTnNaBsjxH0CNWGPhzHArOLFwo+oKXveA==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.62.5': + resolution: {integrity: sha512-bW7B8xMEq8n99Q3ieEcPRGuphurdZAaFzQc9Efyyw3FL6DZO6pMy9xhdN+kBoD7Sy05xNXSr4OyPPnpkYriS/A==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.62.5': + resolution: {integrity: sha512-YSwBS86QeHOGlrxJ1PSOIZSkzRL/JmKeunhc+lV6M1a6En8QuVCD/T/qIA0J4Gd2Y86RIOBYrLcOUtqGh9+/1w==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.62.5': + resolution: {integrity: sha512-2fST8lILgl7cKbme/1KDdPCmbXbG+gqoV3bHp19L0ypX/3akYMBVdOunPleRCwonoLnXOZ/0F+Mt/v8POFmfcQ==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.62.5': + resolution: {integrity: sha512-cpIxQCP9J+EVad0a6LO1kY3ZGODlk80VlI+2I96B8xMcdHZ4pLVhfQ49JFpYqjPF91FFkQWftf57YlDcTiw9yQ==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.62.5': + resolution: {integrity: sha512-r9fGh3eFs3e/udWh5ZjXQtxiYK/xoFxQaYR/cELxac/Udkl5Th+IsFm0CX3Kl9hmUH/we7EoMpjJgeQNnE0+IA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.62.5': + resolution: {integrity: sha512-xdvFdp7OM6KLJviJT2g/YuRSUjnZgGHk4RNgwIbN7X6cPugOucV60DdHXWzsBVCUdrGb6qSXnJQrrAKMmQuj3Q==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.62.5': + resolution: {integrity: sha512-rRqILAndyzHzP7T9NFQrq+4HFWNhqkqkKur7eiBpfLmz01PO0JKx5Vchu3YllE4YXI/Ftgq/szrDWg5GJ0mI8g==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.62.5': + resolution: {integrity: sha512-Gf4X3qVMucayUvux6aXXPgXovocSFUC0rrffDuPI/S2nHhNMhjcZxsrAFYCOF350PRreW1XwzFj3CT/3bKsWCw==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.62.5': + resolution: {integrity: sha512-+s5qA0TNM0qm8PK/a5gt/1Hpx+NV08uSuCncvhziIlQzT6AEV2fnUQo7eBtFTFO0nA9scauvoR2HusfXmQnO4w==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loong64-musl@4.62.5': + resolution: {integrity: sha512-ybb6QvWwWJCbBWqERpc8K3pYVGIrXlG8MEQ8IIuJY6Y9KdHQxoFoNyfkAOtKn1VHu3KuLidXvwrvGR1mEjeWCw==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.62.5': + resolution: {integrity: sha512-nZb1DtnOyhCmYvsC8A2CwOkopVg+IS1+fPUa7rMOAXtNw5+lLCLLPqd6XAiNrGtoQKsbvIBOwsHnBH/3wnb4HQ==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-ppc64-musl@4.62.5': + resolution: {integrity: sha512-yMbj63Sp89ryrXLWyz+sy+fYD2HpOnMCLGbe4Oa1smclFSUukdtD/BgdiHaAetJNb74URD8U4hM+qG5KVzMEkg==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.62.5': + resolution: {integrity: sha512-mhoan3OJw2kYV/e1jtIdmvUZgyBFeA6zGWsOswmR0Tg19TQbowZuR+JMLID6spbbBN7Zee2ejrgmy3+FxGrIdA==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.62.5': + resolution: {integrity: sha512-5ZTLmjWbb1VZdjuyhe83K/8QO0/h11midQCBP+X5OYn32ra7eOBoM0ZqtaY4nkgNsYgmdVhMYPoyVPTjUpHf3w==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.62.5': + resolution: {integrity: sha512-m53kG+br6PGxOTmgBEM2DHSDs9RVjsyEbUwjJPJGTFm1grWOG8EKJggDCTb60unD4Tjby8fi7/m9XfkEWasVWg==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.62.5': + resolution: {integrity: sha512-6RHPJR1g/uvdYU8uXBnfq3nlqyZCP82Fr6NHgfGoaIeSh0YEqnX/x6uA9MmJJbnSH7swqX4F+CkGdUF+6doiQA==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.62.5': + resolution: {integrity: sha512-xs+OXQtEXgpXT0DmA5+U3qnRZHdCST/5HRQxS8wSPZTUZN/EMWeHuSIod32LQklTBZBV9DyfncKBQ8n5V3eFdw==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openbsd-x64@4.62.5': + resolution: {integrity: sha512-e7hD+sl3s+mcLQDZ8pbudBVsdG6r5yN4w3LqG2TJ8sQHDpblWj5lrJs/3m01Cvlxbt4x13zu5thLjgypgtkYzw==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.62.5': + resolution: {integrity: sha512-GiyJaCf+WpMub/17aPcKk27QMl5W6f+KhdPTjlFOn5akH5Wa/DCM9Stdx5cDfmasyKB08MqpVQ1uJE2RkkpbXg==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.62.5': + resolution: {integrity: sha512-+OQ8U2DdoEfXl8T4Fb18AjmEwbXMerKDKCL8yCPAYhKCEEKoul7rkbeGCBFCbAlaGaa7pmtRTpkAJM2LE/i5FA==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.62.5': + resolution: {integrity: sha512-KanvAZrPKbDBFwrgiU9yEVpQoox9QPV1WZOXX7HudJQY+eSlu82CtWxDU8WtuRRvtN5EGkLczkd6Y6DTcvm9wA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.62.5': + resolution: {integrity: sha512-1aC3UEWTtRl3RK3VpDJ/Tqk1XI4SLTmXIthAq6wRWo8XiSXJNd+VprJM4/1P4+i6HIaFEFlVi9sTTziniD2tOQ==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.62.5': + resolution: {integrity: sha512-/gDJaRs4gl0NPIwqCz+6PkpmhhjRAD2j6P4rSNHBzUkO3naEx2mIU0pRle1vUNRQ7mE/+8OOeXLTv/J56FKiQg==} + cpu: [x64] + os: [win32] + + '@sphinxxxx/color-conversion@2.2.2': + resolution: {integrity: sha512-XExJS3cLqgrmNBIP3bBw6+1oQ1ksGjFh0+oClDKFYpCCqx/hlqwWO5KO/S63fzUo67SxI9dMrF0y5T/Ey7h8Zw==} + + '@sveltejs/acorn-typescript@1.0.13': + resolution: {integrity: sha512-wgKggnhZVL9Bfx1OaKKTrYY9BFRk6C8UAkQNUcIv1+llzYrIqy+RZm5HPKzn0NpEBvTVhTqB4kQyllZywsRBRQ==} + peerDependencies: + acorn: ^8.9.0 + + '@sxzz/popperjs-es@2.11.8': + resolution: {integrity: sha512-wOwESXvvED3S8xBmcPWHs2dUuzrE4XiZeFu7e1hROIJkm02a49N120pmOXxY33sBb6hArItm5W5tcg1cBtV+HQ==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/lodash-es@4.17.12': + resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==} + + '@types/lodash@4.17.25': + resolution: {integrity: sha512-+K1NIO8I+F9/wNulfVvu23QYd0Pe9/OCqRrim4NoYIf1VoEDL90Ve4ClzpyqBLc7NpGGWRvYNCKZ1BE/Jpf8dQ==} + + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + + '@types/web-bluetooth@0.0.20': + resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==} + + '@types/web-bluetooth@0.0.21': + resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==} + + '@vitejs/plugin-vue@5.2.4': + resolution: {integrity: sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==} + engines: {node: ^18.0.0 || >=20.0.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 + vue: ^3.2.25 + + '@vue-flow/background@1.3.2': + resolution: {integrity: sha512-eJPhDcLj1wEo45bBoqTXw1uhl0yK2RaQGnEINqvvBsAFKh/camHJd5NPmOdS1w+M9lggc9igUewxaEd3iCQX2w==} + peerDependencies: + '@vue-flow/core': ^1.23.0 + vue: ^3.3.0 + + '@vue-flow/controls@1.1.3': + resolution: {integrity: sha512-XCf+G+jCvaWURdFlZmOjifZGw3XMhN5hHlfMGkWh9xot+9nH9gdTZtn+ldIJKtarg3B21iyHU8JjKDhYcB6JMw==} + peerDependencies: + '@vue-flow/core': ^1.23.0 + vue: ^3.3.0 + + '@vue-flow/core@1.48.2': + resolution: {integrity: sha512-raxhgKWE+G/mcEvXJjGFUDYW9rAI3GOtiHR3ZkNpwBWuIaCC1EYiBmKGwJOoNzVFgwO7COgErnK7i08i287AFA==} + peerDependencies: + vue: ^3.3.0 + + '@vue/compiler-core@3.5.41': + resolution: {integrity: sha512-q0Xtv/F9w2YO/7htQhtiL+Ev2WCJbe5N2hc+XfgyKkEKqWpSxknmT8QOuGdEKNdjPq0c3F7rNpFkTo3Kfrm7pg==} + + '@vue/compiler-dom@3.5.41': + resolution: {integrity: sha512-oKacVfNglLvGjnS6BXOlGL7EyG2h8X03pqXCjzotRZUaXGjbrTJUnVAQjrCqUnS+lyu31nwQjZY/d817GmCnfw==} + + '@vue/compiler-sfc@3.5.41': + resolution: {integrity: sha512-XJhip7R2wy6vX3knCxdZN4KracFaZUef58s1KYewqluedHIJaPIVfXoYT7MF1F8nCvv6k8bWWxDC8opMkg1VTQ==} + + '@vue/compiler-ssr@3.5.41': + resolution: {integrity: sha512-U3v5OejKEGqOI0Wy0+Sz7hGuIFZHA4LSXzrNM3IMIeDyJEBBfTpX26n3SDgToRpP2bLc9FfI2j/kSgcJ8Emq5A==} + + '@vue/devtools-api@6.6.4': + resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==} + + '@vue/devtools-api@7.7.10': + resolution: {integrity: sha512-KxtEpUOOpFz/qOGRrAwA36QF7DqIA+FXgCYit9mk9wjbaZt0sXOFz81ElOZtKA4HbWHUdwNjZHBFsFFyp5BZiA==} + + '@vue/devtools-kit@7.7.10': + resolution: {integrity: sha512-3WNi2Kq4tbpVbmhml7RiphmAt0279oh3fKNeWMQIrltfX8Q91b4i5PL8DtyNKdwmcsGrV4fg+erwWOmD05CLIw==} + + '@vue/devtools-shared@7.7.10': + resolution: {integrity: sha512-wOPslzB8vTvpxwdaOcR2qAbwmuSP0L+rhpoC6Cf56V3Jip+HWb7PQQXOUPgBNQARpXsbQX/+mvi8kKucmBGRwQ==} + + '@vue/reactivity@3.5.41': + resolution: {integrity: sha512-rznsqKM0np0x18EjzF8x88MpEhdNsffbvFbckLL5+oUKz1BxAImEmO7J1ArRYSyo6aQaVoBDp7jEkT91OOxydA==} + + '@vue/runtime-core@3.5.41': + resolution: {integrity: sha512-Vcry58hiAKwGen9Z1jUZE0feFsNArPCMOImYI8el48A9Idf6DuQYD0U05zZIF2Iad1hGhPSvcbBbAOhNr55fhg==} + + '@vue/runtime-dom@3.5.41': + resolution: {integrity: sha512-3vVBahVBS9+U6cmXBLyb8nE6/yYo4J/CGI9eVFs3KiMc0YHuudwKyShTD65jtJy/L9PUUxNAFu4cj4LiJ0UFbw==} + + '@vue/server-renderer@3.5.41': + resolution: {integrity: sha512-n6hx/pNFfbD6SuyeuMVkvqox8bwf/ET9JlA/kAz/imw8sw++wkqKe2mHX5KutjPpbKE4Z56yTHszoOjGMI9igQ==} + + '@vue/shared@3.5.41': + resolution: {integrity: sha512-IOnwSCma8j+9xJT6b8H0dEYidC80NsYmNMlZxRsukYcSoGaDBohog5hDxzeUXdFeGWFA++vWvxqOmrr96VlqMA==} + + '@vueup/vue-quill@1.2.0': + resolution: {integrity: sha512-kd5QPSHMDpycklojPXno2Kw2JSiKMYduKYQckTm1RJoVDA557MnyUXgcuuDpry4HY/Rny9nGNcK+m3AHk94wag==} + peerDependencies: + vue: ^3.2.41 + + '@vueuse/core@10.11.1': + resolution: {integrity: sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==} + + '@vueuse/core@14.1.0': + resolution: {integrity: sha512-rgBinKs07hAYyPF834mDTigH7BtPqvZ3Pryuzt1SD/lg5wEcWqvwzXXYGEDb2/cP0Sj5zSvHl3WkmMELr5kfWw==} + peerDependencies: + vue: ^3.5.0 + + '@vueuse/core@14.4.0': + resolution: {integrity: sha512-X4WHz1HlCzCBoYXesUkifzzWBAcZgXG8Fi5iNPQg/epdzOB3gu8Fawj3hvuwYR1nGcXGnvxwYYcUC/71++svtQ==} + peerDependencies: + vue: ^3.5.0 + + '@vueuse/metadata@10.11.1': + resolution: {integrity: sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==} + + '@vueuse/metadata@14.1.0': + resolution: {integrity: sha512-7hK4g015rWn2PhKcZ99NyT+ZD9sbwm7SGvp7k+k+rKGWnLjS/oQozoIZzWfCewSUeBmnJkIb+CNr7Zc/EyRnnA==} + + '@vueuse/metadata@14.4.0': + resolution: {integrity: sha512-swx/255R6JyHZFJhx845iz5CRWDZdCfvkZOpACWc5+c5WHcG24mv8gUT1WIdFQaHt6dq79rvILd9QnCWiyVm9g==} + + '@vueuse/shared@10.11.1': + resolution: {integrity: sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==} + + '@vueuse/shared@14.1.0': + resolution: {integrity: sha512-EcKxtYvn6gx1F8z9J5/rsg3+lTQnvOruQd8fUecW99DCK04BkWD7z5KQ/wTAx+DazyoEE9dJt/zV8OIEQbM6kw==} + peerDependencies: + vue: ^3.5.0 + + '@vueuse/shared@14.4.0': + resolution: {integrity: sha512-JRgY90Sz8DDtPMsaDflvPMp9xYk69JZAmbuDvAquUVXKr2gEjqtzGNTTthLfckH0BzBqvnu31gb4a8TGLRe79g==} + peerDependencies: + vue: ^3.5.0 + + abbrev@2.0.0: + resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + ace-builds@1.44.0: + resolution: {integrity: sha512-PFNMSYqFdEUkul2Ntud0HvA09AgY+F1ag0UYdpMH60wNI/qOA8cB8tlTgoALMEwIdUPJK2CjrIQ7OnbiSS/ugQ==} + + acorn@8.18.0: + resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.3.0: + resolution: {integrity: sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + aria-query@5.3.1: + resolution: {integrity: sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==} + engines: {node: '>= 0.4'} + + async-validator@4.2.5: + resolution: {integrity: sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + axios@1.13.2: + resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==} + + axobject-query@4.1.0: + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + birpc@2.9.0: + resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==} + + brace-expansion@2.1.4: + resolution: {integrity: sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + buffer-builder@0.2.0: + resolution: {integrity: sha512-7VPMEPuYznPSoR21NE1zvd2Xna6c/CloiZCfcMXR1Jny6PjX0N4Nsa38zcBFo/FMK+BlA+FLKbJCQ0i2yxp+Xg==} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bind@1.0.9: + resolution: {integrity: sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + clipboard@2.0.11: + resolution: {integrity: sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==} + + clone@2.1.2: + resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==} + engines: {node: '>=0.8'} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + codemirror-wrapped-line-indent@1.0.9: + resolution: {integrity: sha512-oc976hHLt35u6Ojbhub+IWOxEpapZSqYieLEdGhsgFZ4rtYQtdb5KjxzgjCCyVe3t0yk+a6hmaIOEsjU/tZRxQ==} + peerDependencies: + '@codemirror/language': ^6.9.0 + '@codemirror/state': ^6.2.1 + '@codemirror/view': ^6.17.1 + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + colorjs.io@0.5.2: + resolution: {integrity: sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + commander@10.0.1: + resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} + engines: {node: '>=14'} + + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + + confbox@0.2.4: + resolution: {integrity: sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==} + + config-chain@1.1.13: + resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} + + copy-anything@4.1.0: + resolution: {integrity: sha512-ufbM3smX/Jbnpk5wcQjzd1MgBpzmqfNETUAyZNrGwU9foRlyHoGzMMBBCRzEhQLBjZfFDE1W2ufPXX2vdWkV8Q==} + engines: {node: '>=18'} + + core-js@3.50.0: + resolution: {integrity: sha512-BRWgOLKkFeCgRudR6zrs8p9XJZcE14grzKMMssoYrk6krtuEZ7MTKPIY5RzOnqsEKIR9kst7wNzphttraT+Yqw==} + + crelt@1.0.7: + resolution: {integrity: sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-dispatch@3.0.1: + resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==} + engines: {node: '>=12'} + + d3-drag@3.0.0: + resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==} + engines: {node: '>=12'} + + d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-selection@3.0.0: + resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==} + engines: {node: '>=12'} + + d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + + d3-transition@3.0.1: + resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==} + engines: {node: '>=12'} + peerDependencies: + d3-selection: 2 - 3 + + d3-zoom@3.0.0: + resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==} + engines: {node: '>=12'} + + d@1.0.2: + resolution: {integrity: sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==} + engines: {node: '>=0.12'} + + danmu.js@1.2.1: + resolution: {integrity: sha512-evDEImUBo94c846fC92K//Dzll8jXnZ3zKmYlQHwMzmvw6IW2IyjWL3Ew2SqEAzuqauFnDkwJEgZauu3uW/p1Q==} + + dayjs@1.11.23: + resolution: {integrity: sha512-QDTCU0M0MxR3hQfnlDJfwekQiaanm1ubOD231u73WBckQ/fsamwRLiE2GBz6D3a/xF1NgfiDLJjXBa1hYOYTtQ==} + + deep-equal@1.1.2: + resolution: {integrity: sha512-5tdhKF6DbU7iIzrIOa1AOUt39ZRm13cmL1cGEh//aqR8x9+tNfbywRf0n5FD/18OKMdo7DNEtrX2t22ZAkI+eg==} + engines: {node: '>= 0.4'} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + delegate@3.2.0: + resolution: {integrity: sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + devalue@5.9.1: + resolution: {integrity: sha512-+17vil3EVQRzvtDJSFuTWEb8XJRvXqAiV3qZyQWD398QeXUa6CxsUyMdD1fxzEhUrd4FojitFz7lhIHBTlV4fw==} + + diff-sequences@29.6.3: + resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + dompurify@3.2.7: + resolution: {integrity: sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + echarts@5.6.0: + resolution: {integrity: sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==} + + editorconfig@1.0.7: + resolution: {integrity: sha512-e0GOtq/aTQhVdNyDU9e02+wz9oDDM+SIOQxWME2QRjzRX5yyLAuHDE+0aE8vHb9XRC8XD37eO2u57+F09JqFhw==} + engines: {node: '>=14'} + hasBin: true + + element-plus@2.14.5: + resolution: {integrity: sha512-bghYy/S+qg87enHPXELirhEdDqsVAUGcGpbGIeG8dz0kwpIkGz7gYsifulBshXX74iRtHib85XWQj0uSH2A1Yg==} + peerDependencies: + vue: ^3.3.7 + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.2: + resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + es5-ext@0.10.64: + resolution: {integrity: sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==} + engines: {node: '>=0.10'} + + es6-iterator@2.0.3: + resolution: {integrity: sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==} + + es6-symbol@3.1.4: + resolution: {integrity: sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==} + engines: {node: '>=0.12'} + + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} + hasBin: true + + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + + esm-env@1.2.2: + resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==} + + esniff@2.0.1: + resolution: {integrity: sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==} + engines: {node: '>=0.10'} + + esrap@2.3.6: + resolution: {integrity: sha512-yc0OC12UjPqLoc+fe+v5GNs4TOjAigUw3sTikfC+xeBPGUw7gDRz3DtYaqEhxyMVJojcSWJw7jT0QWR+CbuE/A==} + peerDependencies: + '@typescript-eslint/types': ^8.2.0 + peerDependenciesMeta: + '@typescript-eslint/types': + optional: true + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + event-emitter@0.3.5: + resolution: {integrity: sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==} + + eventemitter3@2.0.3: + resolution: {integrity: sha512-jLN68Dx5kyFHaePoXWPsCGW5qdyZQtLYHkxkg02/Mz6g0kYpDx4FyP6XfArhQdlOC4b8Mv+EMxPo/8La7Tzghg==} + + eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + + exsolve@1.1.1: + resolution: {integrity: sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g==} + + ext@1.7.0: + resolution: {integrity: sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==} + + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-diff@1.1.2: + resolution: {integrity: sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig==} + + fast-diff@1.2.0: + resolution: {integrity: sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-uri@3.1.6: + resolution: {integrity: sha512-7Ical1vFEMr0onbVzEDIreM22I4khW+fzyQPwvAFWBp1iwdshSZRsL4jjRvPG9JP1uiqMHRto+YU6R2/CzDz5Q==} + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-saver@2.0.5: + resolution: {integrity: sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + follow-redirects@1.16.0: + resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + form-data@4.0.6: + resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==} + engines: {node: '>= 6'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + + fuse.js@7.1.0: + resolution: {integrity: sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ==} + engines: {node: '>=10'} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob@10.5.0: + resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + hasBin: true + + good-listener@1.2.2: + resolution: {integrity: sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw==} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + + hookable@5.5.3: + resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + + immutable-json-patch@6.0.3: + resolution: {integrity: sha512-qFLRzQLjteiTGy5tRJcYqvuu0r/GdQ1fE27IyNBrJ/k7yGSO0xFr6QX62TtmIqSvjQF3afcNFUHsGPYW7ydaHQ==} + + immutable@5.1.9: + resolution: {integrity: sha512-m8nVez3rwrgmWxtLMt1ZYXB2Lv7OKYn/disyxAlSDYAlKSlFoPPfIAmAM/M5xqL4m4C/wAPw7S2/CNaUii1Hxg==} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + is-arguments@1.2.0: + resolution: {integrity: sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==} + engines: {node: '>= 0.4'} + + is-date-object@1.1.0: + resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} + engines: {node: '>= 0.4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-reference@3.0.3: + resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} + + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + jmespath@0.16.0: + resolution: {integrity: sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==} + engines: {node: '>= 0.6.0'} + + js-beautify@1.15.4: + resolution: {integrity: sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==} + engines: {node: '>=14'} + hasBin: true + + js-cookie@3.0.5: + resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==} + engines: {node: '>=14'} + + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + + jsencrypt@3.3.2: + resolution: {integrity: sha512-arQR1R1ESGdAxY7ZheWr12wCaF2yF47v5qpB76TtV64H1pyGudk9Hvw8Y9tb/FiTIaaTRUyaSnm5T/Y53Ghm/A==} + + jsep@1.4.0: + resolution: {integrity: sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==} + engines: {node: '>= 10.16.0'} + + json-editor-vue@0.18.1: + resolution: {integrity: sha512-SQCtNngo/ScFjXC7KUOqLOeeLgvl+xwWbxfNelqIOHC6uLilQl7AlWzNJyrDqo+RWnc53nT0OngXki5uTx9SJg==} + peerDependencies: + '@vue/composition-api': '>=1' + vue: 2||3 + peerDependenciesMeta: + '@vue/composition-api': + optional: true + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-source-map@0.6.1: + resolution: {integrity: sha512-1QoztHPsMQqhDq0hlXY5ZqcEdUzxQEIxgFkKl4WUp2pgShObl+9ovi4kRh2TfvAfxAoHOJ9vIMEqk3k4iex7tg==} + + jsonpath-plus@10.4.0: + resolution: {integrity: sha512-T92WWatJXmhBbKsgH/0hl+jxjdXrifi5IKeMY02DWggRxX0UElcbVzPlmgLTbvsPeW1PasQ6xE2Q75stkhGbsA==} + engines: {node: '>=18.0.0'} + hasBin: true + + jsonrepair@3.15.0: + resolution: {integrity: sha512-wy8OTjwsJwQRnQJkKnMJJ9vcytRdBPAgIF/Hy6+s1dAj42BHMKiyL8JzEieIl3JY7idt8eyHwBWTO8mh/+mtwA==} + hasBin: true + + local-pkg@0.5.1: + resolution: {integrity: sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==} + engines: {node: '>=14'} + + local-pkg@1.2.1: + resolution: {integrity: sha512-++gUqRDEvcnN6Zhqrr+y/CkVEHhlrR96vZn3nZZPYzMcBUyBtTKzB9NadClFIsIVSsu+3i9tfk/erqy9kAmt7Q==} + engines: {node: '>=14'} + + locate-character@3.0.0: + resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} + + lodash-es@4.18.1: + resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==} + + lodash-unified@1.0.3: + resolution: {integrity: sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==} + peerDependencies: + '@types/lodash-es': '*' + lodash: '*' + lodash-es: '*' + + lodash.clonedeep@4.5.0: + resolution: {integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==} + + lodash.isequal@4.5.0: + resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} + deprecated: This package is deprecated. Use require('node:util').isDeepStrictEqual instead. + + lodash@4.18.1: + resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + magic-string@0.26.7: + resolution: {integrity: sha512-hX9XH3ziStPoPhJxLq1syWuZMxbDvGNbVchfrdCtanC7D13888bMFow61x8axrx+GfHLtVeAx2kxL7tTGRl+Ow==} + engines: {node: '>=12'} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + marked@14.0.0: + resolution: {integrity: sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==} + engines: {node: '>= 18'} + hasBin: true + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + memoize-one@6.0.0: + resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + minimatch@9.0.9: + resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} + engines: {node: '>=16 || 14 >=14.17'} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + + mitt@3.0.1: + resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + + mlly@1.8.2: + resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} + + monaco-editor@0.55.1: + resolution: {integrity: sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==} + + nanoid@3.3.18: + resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare-lite@1.4.0: + resolution: {integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==} + + next-tick@1.1.0: + resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==} + + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + + nopt@7.2.1: + resolution: {integrity: sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + hasBin: true + + normalize-wheel-es@1.2.0: + resolution: {integrity: sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==} + + nprogress@0.2.0: + resolution: {integrity: sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==} + + object-is@1.1.6: + resolution: {integrity: sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + pako@2.2.0: + resolution: {integrity: sha512-zJq6RP/5q+TO2OpFV3FHzlPnFjmkb7Nc99a5SNjJE+uu/PkpChs+NIZSSzbBoD+6kjiISXjfYdwj1ZRQ81dz/w==} + + parchment@1.1.4: + resolution: {integrity: sha512-J5FBQt/pM2inLzg4hEWmzQx/8h8D0CiDxaG3vyp9rKrQRSDgBlhjdP5jQGgosEajXPSQouXGHOmVdgo7QmJuOg==} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + perfect-debounce@1.0.0: + resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + + picomatch@4.0.5: + resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} + engines: {node: '>=12'} + + pinia@3.0.4: + resolution: {integrity: sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==} + peerDependencies: + typescript: '>=4.5.0' + vue: ^3.5.11 + peerDependenciesMeta: + typescript: + optional: true + + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + + pkg-types@2.3.1: + resolution: {integrity: sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==} + + postcss@8.5.26: + resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==} + engines: {node: ^10 || ^12 || >=14} + + proto-list@1.2.4: + resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} + + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + + quansync@0.2.11: + resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + quill-delta@3.6.3: + resolution: {integrity: sha512-wdIGBlcX13tCHOXGMVnnTVFtGRLoP0imqxM696fIPwIf5ODIYUHIvHbZcyvGlZFiFhK5XzDC2lpjbxRhnM05Tg==} + engines: {node: '>=0.10'} + + quill-delta@4.2.2: + resolution: {integrity: sha512-qjbn82b/yJzOjstBgkhtBjN2TNK+ZHP/BgUQO+j6bRhWQQdmj2lH6hXG7+nwwLF41Xgn//7/83lxs9n2BkTtTg==} + + quill@1.3.7: + resolution: {integrity: sha512-hG/DVzh/TiknWtE6QmWAF/pxoZKYxfe3J/d/+ShUWkDvvkZQVTPeVmUJVu1uE6DDooC4fWTiCLh84ul89oNz5g==} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + regexp.prototype.flags@1.5.4: + resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} + engines: {node: '>= 0.4'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resize-observer-polyfill@1.5.1: + resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + + rollup@4.62.5: + resolution: {integrity: sha512-/tqMfgP7GPA3PHhCmuiS4vIjrSVhHLgY++i+dhbG462euyAj7FpM4D9uq1X3BgjlqRdpcOrYhcQtfiQLNc8tqw==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + rxjs@7.8.2: + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + + sass-embedded-all-unknown@1.97.2: + resolution: {integrity: sha512-Fj75+vOIDv1T/dGDwEpQ5hgjXxa2SmMeShPa8yrh2sUz1U44bbmY4YSWPCdg8wb7LnwiY21B2KRFM+HF42yO4g==} + cpu: ['!arm', '!arm64', '!riscv64', '!x64'] + + sass-embedded-android-arm64@1.97.2: + resolution: {integrity: sha512-pF6I+R5uThrscd3lo9B3DyNTPyGFsopycdx0tDAESN6s+dBbiRgNgE4Zlpv50GsLocj/lDLCZaabeTpL3ubhYA==} + engines: {node: '>=14.0.0'} + cpu: [arm64] + os: [android] + + sass-embedded-android-arm@1.97.2: + resolution: {integrity: sha512-BPT9m19ttY0QVHYYXRa6bmqmS3Fa2EHByNUEtSVcbm5PkIk1ntmYkG9fn5SJpIMbNmFDGwHx+pfcZMmkldhnRg==} + engines: {node: '>=14.0.0'} + cpu: [arm] + os: [android] + + sass-embedded-android-riscv64@1.97.2: + resolution: {integrity: sha512-fprI8ZTJdz+STgARhg8zReI2QhhGIT9G8nS7H21kc3IkqPRzhfaemSxEtCqZyvDbXPcgYiDLV7AGIReHCuATog==} + engines: {node: '>=14.0.0'} + cpu: [riscv64] + os: [android] + + sass-embedded-android-x64@1.97.2: + resolution: {integrity: sha512-RswwSjURZxupsukEmNt2t6RGvuvIw3IAD5sDq1Pc65JFvWFY3eHqCmH0lG0oXqMg6KJcF0eOxHOp2RfmIm2+4w==} + engines: {node: '>=14.0.0'} + cpu: [x64] + os: [android] + + sass-embedded-darwin-arm64@1.97.2: + resolution: {integrity: sha512-xcsZNnU1XZh21RE/71OOwNqPVcGBU0qT9A4k4QirdA34+ts9cDIaR6W6lgHOBR/Bnnu6w6hXJR4Xth7oFrefPA==} + engines: {node: '>=14.0.0'} + cpu: [arm64] + os: [darwin] + + sass-embedded-darwin-x64@1.97.2: + resolution: {integrity: sha512-T/9DTMpychm6+H4slHCAsYJRJ6eM+9H9idKlBPliPrP4T8JdC2Cs+ZOsYqrObj6eOtAD0fGf+KgyNhnW3xVafA==} + engines: {node: '>=14.0.0'} + cpu: [x64] + os: [darwin] + + sass-embedded-linux-arm64@1.97.2: + resolution: {integrity: sha512-Wh+nQaFer9tyE5xBPv5murSUZE/+kIcg8MyL5uqww6be9Iq+UmZpcJM7LUk+q8klQ9LfTmoDSNFA74uBqxD6IA==} + engines: {node: '>=14.0.0'} + cpu: [arm64] + os: [linux] + + sass-embedded-linux-arm@1.97.2: + resolution: {integrity: sha512-yDRe1yifGHl6kibkDlRIJ2ZzAU03KJ1AIvsAh4dsIDgK5jx83bxZLV1ZDUv7a8KK/iV/80LZnxnu/92zp99cXQ==} + engines: {node: '>=14.0.0'} + cpu: [arm] + os: [linux] + + sass-embedded-linux-musl-arm64@1.97.2: + resolution: {integrity: sha512-NfUqZSjHwnHvpSa7nyNxbWfL5obDjNBqhHUYmqbHUcmqBpFfHIQsUPgXME9DKn1yBlBc3mWnzMxRoucdYTzd2Q==} + engines: {node: '>=14.0.0'} + cpu: [arm64] + os: [linux] + + sass-embedded-linux-musl-arm@1.97.2: + resolution: {integrity: sha512-GIO6xfAtahJAWItvsXZ3MD1HM6s8cKtV1/HL088aUpKJaw/2XjTCveiOO2AdgMpLNztmq9DZ1lx5X5JjqhS45g==} + engines: {node: '>=14.0.0'} + cpu: [arm] + os: [linux] + + sass-embedded-linux-musl-riscv64@1.97.2: + resolution: {integrity: sha512-qtM4dJ5gLfvyTZ3QencfNbsTEShIWImSEpkThz+Y2nsCMbcMP7/jYOA03UWgPfEOKSehQQ7EIau7ncbFNoDNPQ==} + engines: {node: '>=14.0.0'} + cpu: [riscv64] + os: [linux] + + sass-embedded-linux-musl-x64@1.97.2: + resolution: {integrity: sha512-ZAxYOdmexcnxGnzdsDjYmNe3jGj+XW3/pF/n7e7r8y+5c6D2CQRrCUdapLgaqPt1edOPQIlQEZF8q5j6ng21yw==} + engines: {node: '>=14.0.0'} + cpu: [x64] + os: [linux] + + sass-embedded-linux-riscv64@1.97.2: + resolution: {integrity: sha512-reVwa9ZFEAOChXpDyNB3nNHHyAkPMD+FTctQKECqKiVJnIzv2EaFF6/t0wzyvPgBKeatA8jszAIeOkkOzbYVkQ==} + engines: {node: '>=14.0.0'} + cpu: [riscv64] + os: [linux] + + sass-embedded-linux-x64@1.97.2: + resolution: {integrity: sha512-bvAdZQsX3jDBv6m4emaU2OMTpN0KndzTAMgJZZrKUgiC0qxBmBqbJG06Oj/lOCoXGCxAvUOheVYpezRTF+Feog==} + engines: {node: '>=14.0.0'} + cpu: [x64] + os: [linux] + + sass-embedded-unknown-all@1.97.2: + resolution: {integrity: sha512-86tcYwohjPgSZtgeU9K4LikrKBJNf8ZW/vfsFbdzsRlvc73IykiqanufwQi5qIul0YHuu9lZtDWyWxM2dH/Rsg==} + os: ['!android', '!darwin', '!linux', '!win32'] + + sass-embedded-win32-arm64@1.97.2: + resolution: {integrity: sha512-Cv28q8qNjAjZfqfzTrQvKf4JjsZ6EOQ5FxyHUQQeNzm73R86nd/8ozDa1Vmn79Hq0kwM15OCM9epanDuTG1ksA==} + engines: {node: '>=14.0.0'} + cpu: [arm64] + os: [win32] + + sass-embedded-win32-x64@1.97.2: + resolution: {integrity: sha512-DVxLxkeDCGIYeyHLAvWW3yy9sy5Ruk5p472QWiyfyyG1G1ASAR8fgfIY5pT0vE6Rv+VAKVLwF3WTspUYu7S1/Q==} + engines: {node: '>=14.0.0'} + cpu: [x64] + os: [win32] + + sass-embedded@1.97.2: + resolution: {integrity: sha512-lKJcskySwAtJ4QRirKrikrWMFa2niAuaGenY2ElHjd55IwHUiur5IdKu6R1hEmGYMs4Qm+6rlRW0RvuAkmcryg==} + engines: {node: '>=16.0.0'} + hasBin: true + + sass@1.97.2: + resolution: {integrity: sha512-y5LWb0IlbO4e97Zr7c3mlpabcbBtS+ieiZ9iwDooShpFKWXf62zz5pEPdwrLYm+Bxn1fnbwFGzHuCLSA9tBmrw==} + engines: {node: '>=14.0.0'} + hasBin: true + + scule@1.3.0: + resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==} + + select@1.1.2: + resolution: {integrity: sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA==} + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + sortablejs@1.14.0: + resolution: {integrity: sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w==} + + sortablejs@1.15.7: + resolution: {integrity: sha512-Kk8wLQPlS+yi1ZEf48a4+fzHa4yxjC30M/Sr2AnQu+f/MPwvvX9XjZ6OWejiz8crBsLwSq8GHqaxaET7u6ux0A==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + sourcemap-codec@1.4.8: + resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} + deprecated: Please use @jridgewell/sourcemap-codec instead + + speakingurl@14.0.1: + resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==} + engines: {node: '>=0.10.0'} + + splitpanes@3.1.5: + resolution: {integrity: sha512-r3Mq2ITFQ5a2VXLOy4/Sb2Ptp7OfEO8YIbhVJqJXoFc9hc5nTXXkCvtVDjIGbvC0vdE7tse+xTM9BMjsszP6bw==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + + strip-literal@2.1.1: + resolution: {integrity: sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==} + + style-mod@4.1.3: + resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==} + + superjson@2.2.6: + resolution: {integrity: sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==} + engines: {node: '>=16'} + + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + + svelte@5.56.10: + resolution: {integrity: sha512-Lcxbj8I/KAbpY+VjtY4ENQBV0dDCipfGAhqb51XQZ67CIQqXgsv/8dPkbILaj4Fb6/b6JAEM/PIVbILXgDQy2g==} + engines: {node: '>=18'} + + sync-child-process@1.0.2: + resolution: {integrity: sha512-8lD+t2KrrScJ/7KXCSyfhT3/hRq78rC0wBFqNJXv3mZyn6hW2ypM05JmlSvtqRbeq6jqA94oHbxAr2vYsJ8vDA==} + engines: {node: '>=16.0.0'} + + sync-message-port@1.2.0: + resolution: {integrity: sha512-gAQ9qrUN/UCypHtGFbbe7Rc/f9bzO88IwrG8TDo/aMKAApKyD6E3W4Cm0EfhfBb6Z6SKt59tTCTfD+n1xmAvMg==} + engines: {node: '>=16.0.0'} + + tiny-emitter@2.1.0: + resolution: {integrity: sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + tslib@2.3.0: + resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + type@2.7.3: + resolution: {integrity: sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==} + + ufo@1.6.4: + resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==} + + unimport@3.14.6: + resolution: {integrity: sha512-CYvbDaTT04Rh8bmD8jz3WPmHYZRG/NnvYVzwD6V1YAlvvKROlAeNDUBhkBGzNav2RKaeuXvlWYaa1V4Lfi/O0g==} + + unplugin-auto-import@0.18.6: + resolution: {integrity: sha512-LMFzX5DtkTj/3wZuyG5bgKBoJ7WSgzqSGJ8ppDRdlvPh45mx6t6w3OcbExQi53n3xF5MYkNGPNR/HYOL95KL2A==} + engines: {node: '>=14'} + peerDependencies: + '@nuxt/kit': ^3.2.2 + '@vueuse/core': '*' + peerDependenciesMeta: + '@nuxt/kit': + optional: true + '@vueuse/core': + optional: true + + unplugin-vue-setup-extend-plus@1.0.1: + resolution: {integrity: sha512-mW2IzkyJITyspAV/LEdnEyE1CJip9jB5fCeaVv7Q6X0oJyDrOxXoB+jyet0q5pRJNjErbjQx950/8NPTvbqLTQ==} + + unplugin@1.16.1: + resolution: {integrity: sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w==} + engines: {node: '>=14.0.0'} + + uuid@11.1.1: + resolution: {integrity: sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==} + hasBin: true + + vanilla-jsoneditor@3.13.0: + resolution: {integrity: sha512-u9C+D8Ocgr39Gnsfu7USjilfKssqbDqZehMdE0qbwIFnZFdNwkU4iKeEvJxvYbmlVi/sMLxMTrU14PhtXVyiZg==} + + vanilla-picker@2.12.3: + resolution: {integrity: sha512-qVkT1E7yMbUsB2mmJNFmaXMWE2hF8ffqzMMwe9zdAikd8u2VfnsVY2HQcOUi2F38bgbxzlJBEdS1UUhOXdF9GQ==} + + varint@6.0.0: + resolution: {integrity: sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==} + + vite@6.4.1: + resolution: {integrity: sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + jiti: '>=1.21.0' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vue-component-type-helpers@3.3.11: + resolution: {integrity: sha512-LwcxzeliO9fkQcpJG0PoX8X5kmAhKmH9wkpDLxNabwzkQ9Zeib2YVHwFV4pcWmMLfXVfjr/dSV+DaJ3cIPgSNA==} + + vue-cropper@1.1.1: + resolution: {integrity: sha512-WsqKMpaBf9Osi1LQlE/5AKdD0nHWOy1asLXocaG8NomOWO07jiZi968+/PbMmnD0QbPJOumDQaGuGa13qys85A==} + + vue-demi@0.14.10: + resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==} + engines: {node: '>=12'} + hasBin: true + peerDependencies: + '@vue/composition-api': ^1.0.0-rc.1 + vue: ^3.0.0-0 || ^2.6.0 + peerDependenciesMeta: + '@vue/composition-api': + optional: true + + vue-router@4.6.4: + resolution: {integrity: sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==} + peerDependencies: + vue: ^3.5.0 + + vue3-ace-editor@2.2.4: + resolution: {integrity: sha512-FZkEyfpbH068BwjhMyNROxfEI8135Sc+x8ouxkMdCNkuj/Tuw83VP/gStFQqZHqljyX9/VfMTCdTqtOnJZGN8g==} + peerDependencies: + ace-builds: '*' + vue: ^3 + + vue3-json-viewer@2.3.0: + resolution: {integrity: sha512-n9Rd19nS2ETzc5/9h/eWeUrbxgIjugIlb4UtDtfjrfnNCJrSfBzPBh/ZNPi/7NbX7lqv/ZKP1UTNFXfmEX258A==} + peerDependencies: + vue: ^3.2.0 + + vue@3.5.41: + resolution: {integrity: sha512-2laE0p+aK+/AOPG/XL/WepOs/GlK755LJ1XECi9kDUrz1FKNw8rb2Xzlw9JS1rqEV55nb0ttsKxVlTCcd+R5cg==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + vuedraggable@4.1.0: + resolution: {integrity: sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==} + peerDependencies: + vue: ^3.0.1 + + w3c-keyname@2.2.8: + resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} + + webpack-virtual-modules@0.6.2: + resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + xgplayer-subtitles@3.0.26: + resolution: {integrity: sha512-HiHlw3E8emEQCv0xFYHdmNUFgyzyWGBnSNmyI/fkuuvRBb+4IbsCfxg6PF3r1TNCfzOuNzA/OaMbtXBSH8n/Bw==} + peerDependencies: + core-js: '>=3.12.1' + + xgplayer@3.0.26: + resolution: {integrity: sha512-F/urLGIrxCa1DI+wtkPyT0TcRS6Wejbo+OZKOTlSVvg0+SGyN3TfSXddNcu6LL876ZTWzlEwhS4C5nKSUV5FZw==} + peerDependencies: + core-js: '>=3.12.1' + + zimmerframe@1.1.4: + resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==} + + zrender@5.6.1: + resolution: {integrity: sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==} + +snapshots: + + '@antfu/utils@0.7.10': {} + + '@babel/helper-string-parser@7.29.7': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/parser@7.29.8': + dependencies: + '@babel/types': 7.29.8 + + '@babel/types@7.29.8': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + + '@bufbuild/protobuf@2.14.0': {} + + '@codemirror/autocomplete@6.20.3': + dependencies: + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.9 + '@lezer/common': 1.5.2 + + '@codemirror/commands@6.11.0': + dependencies: + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.9 + '@lezer/common': 1.5.2 + + '@codemirror/lang-json@6.0.2': + dependencies: + '@codemirror/language': 6.12.4 + '@lezer/json': 1.0.3 + + '@codemirror/language@6.12.4': + dependencies: + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.9 + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + style-mod: 4.1.3 + + '@codemirror/lint@6.9.7': + dependencies: + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.9 + crelt: 1.0.7 + + '@codemirror/search@6.7.1': + dependencies: + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.9 + crelt: 1.0.7 + + '@codemirror/state@6.7.1': + dependencies: + '@marijn/find-cluster-break': 1.0.4 + + '@codemirror/view@6.43.9': + dependencies: + '@codemirror/state': 6.7.1 + crelt: 1.0.7 + style-mod: 4.1.3 + w3c-keyname: 2.2.8 + + '@ctrl/tinycolor@4.2.0': {} + + '@element-plus/icons-vue@2.3.2(vue@3.5.41)': + dependencies: + vue: 3.5.41 + + '@esbuild/aix-ppc64@0.25.12': + optional: true + + '@esbuild/android-arm64@0.25.12': + optional: true + + '@esbuild/android-arm@0.25.12': + optional: true + + '@esbuild/android-x64@0.25.12': + optional: true + + '@esbuild/darwin-arm64@0.25.12': + optional: true + + '@esbuild/darwin-x64@0.25.12': + optional: true + + '@esbuild/freebsd-arm64@0.25.12': + optional: true + + '@esbuild/freebsd-x64@0.25.12': + optional: true + + '@esbuild/linux-arm64@0.25.12': + optional: true + + '@esbuild/linux-arm@0.25.12': + optional: true + + '@esbuild/linux-ia32@0.25.12': + optional: true + + '@esbuild/linux-loong64@0.25.12': + optional: true + + '@esbuild/linux-mips64el@0.25.12': + optional: true + + '@esbuild/linux-ppc64@0.25.12': + optional: true + + '@esbuild/linux-riscv64@0.25.12': + optional: true + + '@esbuild/linux-s390x@0.25.12': + optional: true + + '@esbuild/linux-x64@0.25.12': + optional: true + + '@esbuild/netbsd-arm64@0.25.12': + optional: true + + '@esbuild/netbsd-x64@0.25.12': + optional: true + + '@esbuild/openbsd-arm64@0.25.12': + optional: true + + '@esbuild/openbsd-x64@0.25.12': + optional: true + + '@esbuild/openharmony-arm64@0.25.12': + optional: true + + '@esbuild/sunos-x64@0.25.12': + optional: true + + '@esbuild/win32-arm64@0.25.12': + optional: true + + '@esbuild/win32-ia32@0.25.12': + optional: true + + '@esbuild/win32-x64@0.25.12': + optional: true + + '@floating-ui/core@1.8.0': + dependencies: + '@floating-ui/utils': 0.2.12 + + '@floating-ui/dom@1.8.0': + dependencies: + '@floating-ui/core': 1.8.0 + '@floating-ui/utils': 0.2.12 + + '@floating-ui/utils@0.2.12': {} + + '@fortawesome/fontawesome-common-types@7.3.1': {} + + '@fortawesome/free-regular-svg-icons@7.3.1': + dependencies: + '@fortawesome/fontawesome-common-types': 7.3.1 + + '@fortawesome/free-solid-svg-icons@7.3.1': + dependencies: + '@fortawesome/fontawesome-common-types': 7.3.1 + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.2.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@jsep-plugin/assignment@1.3.0(jsep@1.4.0)': + dependencies: + jsep: 1.4.0 + + '@jsep-plugin/regex@1.0.4(jsep@1.4.0)': + dependencies: + jsep: 1.4.0 + + '@jsonquerylang/jsonquery@5.1.1': {} + + '@lezer/common@1.5.2': {} + + '@lezer/highlight@1.2.3': + dependencies: + '@lezer/common': 1.5.2 + + '@lezer/json@1.0.3': + dependencies: + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + + '@lezer/lr@1.4.10': + dependencies: + '@lezer/common': 1.5.2 + + '@marijn/find-cluster-break@1.0.4': {} + + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + optional: true + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + + '@one-ini/wasm@0.1.1': {} + + '@parcel/watcher-android-arm64@2.6.0': + optional: true + + '@parcel/watcher-darwin-arm64@2.6.0': + optional: true + + '@parcel/watcher-darwin-x64@2.6.0': + optional: true + + '@parcel/watcher-freebsd-x64@2.6.0': + optional: true + + '@parcel/watcher-linux-arm-glibc@2.6.0': + optional: true + + '@parcel/watcher-linux-arm-musl@2.6.0': + optional: true + + '@parcel/watcher-linux-arm64-glibc@2.6.0': + optional: true + + '@parcel/watcher-linux-arm64-musl@2.6.0': + optional: true + + '@parcel/watcher-linux-x64-glibc@2.6.0': + optional: true + + '@parcel/watcher-linux-x64-musl@2.6.0': + optional: true + + '@parcel/watcher-win32-arm64@2.6.0': + optional: true + + '@parcel/watcher-win32-x64@2.6.0': + optional: true + + '@parcel/watcher@2.6.0': + dependencies: + detect-libc: 2.1.2 + is-glob: 4.0.3 + node-addon-api: 7.1.1 + picomatch: 4.0.5 + optionalDependencies: + '@parcel/watcher-android-arm64': 2.6.0 + '@parcel/watcher-darwin-arm64': 2.6.0 + '@parcel/watcher-darwin-x64': 2.6.0 + '@parcel/watcher-freebsd-x64': 2.6.0 + '@parcel/watcher-linux-arm-glibc': 2.6.0 + '@parcel/watcher-linux-arm-musl': 2.6.0 + '@parcel/watcher-linux-arm64-glibc': 2.6.0 + '@parcel/watcher-linux-arm64-musl': 2.6.0 + '@parcel/watcher-linux-x64-glibc': 2.6.0 + '@parcel/watcher-linux-x64-musl': 2.6.0 + '@parcel/watcher-win32-arm64': 2.6.0 + '@parcel/watcher-win32-x64': 2.6.0 + optional: true + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@replit/codemirror-indentation-markers@6.5.3(@codemirror/language@6.12.4)(@codemirror/state@6.7.1)(@codemirror/view@6.43.9)': + dependencies: + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.9 + + '@rollup/pluginutils@5.4.0(rollup@4.62.5)': + dependencies: + '@types/estree': 1.0.9 + estree-walker: 2.0.2 + picomatch: 4.0.5 + optionalDependencies: + rollup: 4.62.5 + + '@rollup/rollup-android-arm-eabi@4.62.5': + optional: true + + '@rollup/rollup-android-arm64@4.62.5': + optional: true + + '@rollup/rollup-darwin-arm64@4.62.5': + optional: true + + '@rollup/rollup-darwin-x64@4.62.5': + optional: true + + '@rollup/rollup-freebsd-arm64@4.62.5': + optional: true + + '@rollup/rollup-freebsd-x64@4.62.5': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.62.5': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.62.5': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.62.5': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.62.5': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.62.5': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.62.5': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.62.5': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.62.5': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.62.5': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.62.5': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.62.5': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.62.5': + optional: true + + '@rollup/rollup-linux-x64-musl@4.62.5': + optional: true + + '@rollup/rollup-openbsd-x64@4.62.5': + optional: true + + '@rollup/rollup-openharmony-arm64@4.62.5': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.62.5': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.62.5': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.62.5': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.62.5': + optional: true + + '@sphinxxxx/color-conversion@2.2.2': {} + + '@sveltejs/acorn-typescript@1.0.13(acorn@8.18.0)': + dependencies: + acorn: 8.18.0 + + '@sxzz/popperjs-es@2.11.8': {} + + '@types/estree@1.0.9': {} + + '@types/lodash-es@4.17.12': + dependencies: + '@types/lodash': 4.17.25 + + '@types/lodash@4.17.25': {} + + '@types/trusted-types@2.0.7': {} + + '@types/web-bluetooth@0.0.20': {} + + '@types/web-bluetooth@0.0.21': {} + + '@vitejs/plugin-vue@5.2.4(vite@6.4.1(sass-embedded@1.97.2)(sass@1.97.2))(vue@3.5.41)': + dependencies: + vite: 6.4.1(sass-embedded@1.97.2)(sass@1.97.2) + vue: 3.5.41 + + '@vue-flow/background@1.3.2(@vue-flow/core@1.48.2(vue@3.5.41))(vue@3.5.41)': + dependencies: + '@vue-flow/core': 1.48.2(vue@3.5.41) + vue: 3.5.41 + + '@vue-flow/controls@1.1.3(@vue-flow/core@1.48.2(vue@3.5.41))(vue@3.5.41)': + dependencies: + '@vue-flow/core': 1.48.2(vue@3.5.41) + vue: 3.5.41 + + '@vue-flow/core@1.48.2(vue@3.5.41)': + dependencies: + '@vueuse/core': 10.11.1(vue@3.5.41) + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-zoom: 3.0.0 + vue: 3.5.41 + transitivePeerDependencies: + - '@vue/composition-api' + + '@vue/compiler-core@3.5.41': + dependencies: + '@babel/parser': 7.29.8 + '@vue/shared': 3.5.41 + entities: 7.0.1 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.41': + dependencies: + '@vue/compiler-core': 3.5.41 + '@vue/shared': 3.5.41 + + '@vue/compiler-sfc@3.5.41': + dependencies: + '@babel/parser': 7.29.8 + '@vue/compiler-core': 3.5.41 + '@vue/compiler-dom': 3.5.41 + '@vue/compiler-ssr': 3.5.41 + '@vue/shared': 3.5.41 + estree-walker: 2.0.2 + magic-string: 0.30.21 + postcss: 8.5.26 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.5.41': + dependencies: + '@vue/compiler-dom': 3.5.41 + '@vue/shared': 3.5.41 + + '@vue/devtools-api@6.6.4': {} + + '@vue/devtools-api@7.7.10': + dependencies: + '@vue/devtools-kit': 7.7.10 + + '@vue/devtools-kit@7.7.10': + dependencies: + '@vue/devtools-shared': 7.7.10 + birpc: 2.9.0 + hookable: 5.5.3 + mitt: 3.0.1 + perfect-debounce: 1.0.0 + speakingurl: 14.0.1 + superjson: 2.2.6 + + '@vue/devtools-shared@7.7.10': + dependencies: + rfdc: 1.4.1 + + '@vue/reactivity@3.5.41': + dependencies: + '@vue/shared': 3.5.41 + + '@vue/runtime-core@3.5.41': + dependencies: + '@vue/reactivity': 3.5.41 + '@vue/shared': 3.5.41 + + '@vue/runtime-dom@3.5.41': + dependencies: + '@vue/reactivity': 3.5.41 + '@vue/runtime-core': 3.5.41 + '@vue/shared': 3.5.41 + csstype: 3.2.3 + + '@vue/server-renderer@3.5.41': + dependencies: + '@vue/compiler-ssr': 3.5.41 + '@vue/runtime-dom': 3.5.41 + '@vue/shared': 3.5.41 + + '@vue/shared@3.5.41': {} + + '@vueup/vue-quill@1.2.0(vue@3.5.41)': + dependencies: + quill: 1.3.7 + quill-delta: 4.2.2 + vue: 3.5.41 + + '@vueuse/core@10.11.1(vue@3.5.41)': + dependencies: + '@types/web-bluetooth': 0.0.20 + '@vueuse/metadata': 10.11.1 + '@vueuse/shared': 10.11.1(vue@3.5.41) + vue-demi: 0.14.10(vue@3.5.41) + transitivePeerDependencies: + - '@vue/composition-api' + - vue + + '@vueuse/core@14.1.0(vue@3.5.41)': + dependencies: + '@types/web-bluetooth': 0.0.21 + '@vueuse/metadata': 14.1.0 + '@vueuse/shared': 14.1.0(vue@3.5.41) + vue: 3.5.41 + + '@vueuse/core@14.4.0(vue@3.5.41)': + dependencies: + '@types/web-bluetooth': 0.0.21 + '@vueuse/metadata': 14.4.0 + '@vueuse/shared': 14.4.0(vue@3.5.41) + vue: 3.5.41 + + '@vueuse/metadata@10.11.1': {} + + '@vueuse/metadata@14.1.0': {} + + '@vueuse/metadata@14.4.0': {} + + '@vueuse/shared@10.11.1(vue@3.5.41)': + dependencies: + vue-demi: 0.14.10(vue@3.5.41) + transitivePeerDependencies: + - '@vue/composition-api' + - vue + + '@vueuse/shared@14.1.0(vue@3.5.41)': + dependencies: + vue: 3.5.41 + + '@vueuse/shared@14.4.0(vue@3.5.41)': + dependencies: + vue: 3.5.41 + + abbrev@2.0.0: {} + + ace-builds@1.44.0: {} + + acorn@8.18.0: {} + + ajv@8.20.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.6 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-regex@5.0.1: {} + + ansi-regex@6.3.0: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.3: {} + + aria-query@5.3.1: {} + + async-validator@4.2.5: {} + + asynckit@0.4.0: {} + + axios@1.13.2: + dependencies: + follow-redirects: 1.16.0 + form-data: 4.0.6 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + + axobject-query@4.1.0: {} + + balanced-match@1.0.2: {} + + birpc@2.9.0: {} + + brace-expansion@2.1.4: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + buffer-builder@0.2.0: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bind@1.0.9: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + optional: true + + clipboard@2.0.11: + dependencies: + good-listener: 1.2.2 + select: 1.1.2 + tiny-emitter: 2.1.0 + + clone@2.1.2: {} + + clsx@2.1.1: {} + + codemirror-wrapped-line-indent@1.0.9(@codemirror/language@6.12.4)(@codemirror/state@6.7.1)(@codemirror/view@6.43.9): + dependencies: + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.9 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + colorjs.io@0.5.2: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + commander@10.0.1: {} + + confbox@0.1.8: {} + + confbox@0.2.4: {} + + config-chain@1.1.13: + dependencies: + ini: 1.3.8 + proto-list: 1.2.4 + + copy-anything@4.1.0: {} + + core-js@3.50.0: {} + + crelt@1.0.7: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + csstype@3.2.3: {} + + d3-color@3.1.0: {} + + d3-dispatch@3.0.1: {} + + d3-drag@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-selection: 3.0.0 + + d3-ease@3.0.1: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-selection@3.0.0: {} + + d3-timer@3.0.1: {} + + d3-transition@3.0.1(d3-selection@3.0.0): + dependencies: + d3-color: 3.1.0 + d3-dispatch: 3.0.1 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-timer: 3.0.1 + + d3-zoom@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + d@1.0.2: + dependencies: + es5-ext: 0.10.64 + type: 2.7.3 + + danmu.js@1.2.1: + dependencies: + event-emitter: 0.3.5 + + dayjs@1.11.23: {} + + deep-equal@1.1.2: + dependencies: + is-arguments: 1.2.0 + is-date-object: 1.1.0 + is-regex: 1.2.1 + object-is: 1.1.6 + object-keys: 1.1.1 + regexp.prototype.flags: 1.5.4 + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + + delayed-stream@1.0.0: {} + + delegate@3.2.0: {} + + detect-libc@2.1.2: + optional: true + + devalue@5.9.1: {} + + diff-sequences@29.6.3: {} + + dompurify@3.2.7: + optionalDependencies: + '@types/trusted-types': 2.0.7 + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + eastasianwidth@0.2.0: {} + + echarts@5.6.0: + dependencies: + tslib: 2.3.0 + zrender: 5.6.1 + + editorconfig@1.0.7: + dependencies: + '@one-ini/wasm': 0.1.1 + commander: 10.0.1 + minimatch: 9.0.9 + semver: 7.8.5 + + element-plus@2.14.5(vue@3.5.41): + dependencies: + '@ctrl/tinycolor': 4.2.0 + '@element-plus/icons-vue': 2.3.2(vue@3.5.41) + '@floating-ui/dom': 1.8.0 + '@popperjs/core': '@sxzz/popperjs-es@2.11.8' + '@types/lodash': 4.17.25 + '@types/lodash-es': 4.17.12 + '@vueuse/core': 14.4.0(vue@3.5.41) + async-validator: 4.2.5 + dayjs: 1.11.23 + lodash: 4.18.1 + lodash-es: 4.18.1 + lodash-unified: 1.0.3(@types/lodash-es@4.17.12)(lodash-es@4.18.1)(lodash@4.18.1) + memoize-one: 6.0.0 + normalize-wheel-es: 1.2.0 + vue: 3.5.41 + vue-component-type-helpers: 3.3.11 + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + entities@7.0.1: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.2: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.4 + + es5-ext@0.10.64: + dependencies: + es6-iterator: 2.0.3 + es6-symbol: 3.1.4 + esniff: 2.0.1 + next-tick: 1.1.0 + + es6-iterator@2.0.3: + dependencies: + d: 1.0.2 + es5-ext: 0.10.64 + es6-symbol: 3.1.4 + + es6-symbol@3.1.4: + dependencies: + d: 1.0.2 + ext: 1.7.0 + + esbuild@0.25.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + + escape-string-regexp@5.0.0: {} + + esm-env@1.2.2: {} + + esniff@2.0.1: + dependencies: + d: 1.0.2 + es5-ext: 0.10.64 + event-emitter: 0.3.5 + type: 2.7.3 + + esrap@2.3.6: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + estree-walker@2.0.2: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + event-emitter@0.3.5: + dependencies: + d: 1.0.2 + es5-ext: 0.10.64 + + eventemitter3@2.0.3: {} + + eventemitter3@4.0.7: {} + + exsolve@1.1.1: {} + + ext@1.7.0: + dependencies: + type: 2.7.3 + + extend@3.0.2: {} + + fast-deep-equal@3.1.3: {} + + fast-diff@1.1.2: {} + + fast-diff@1.2.0: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-uri@3.1.6: {} + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.5): + optionalDependencies: + picomatch: 4.0.5 + + file-saver@2.0.5: {} + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + follow-redirects@1.16.0: {} + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + form-data@4.0.6: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.4 + mime-types: 2.1.35 + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + functions-have-names@1.2.3: {} + + fuse.js@7.1.0: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.4 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.2 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob@10.5.0: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.9 + minipass: 7.1.3 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + + good-listener@1.2.2: + dependencies: + delegate: 3.2.0 + + gopd@1.2.0: {} + + has-flag@4.0.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + + hookable@5.5.3: {} + + immutable-json-patch@6.0.3: {} + + immutable@5.1.9: {} + + ini@1.3.8: {} + + is-arguments@1.2.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-date-object@1.1.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + is-reference@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.4 + + isexe@2.0.0: {} + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + jmespath@0.16.0: {} + + js-beautify@1.15.4: + dependencies: + config-chain: 1.1.13 + editorconfig: 1.0.7 + glob: 10.5.0 + js-cookie: 3.0.5 + nopt: 7.2.1 + + js-cookie@3.0.5: {} + + js-tokens@9.0.1: {} + + jsencrypt@3.3.2: {} + + jsep@1.4.0: {} + + json-editor-vue@0.18.1(vue@3.5.41): + dependencies: + vanilla-jsoneditor: 3.13.0 + vue: 3.5.41 + vue-demi: 0.14.10(vue@3.5.41) + transitivePeerDependencies: + - '@typescript-eslint/types' + + json-schema-traverse@1.0.0: {} + + json-source-map@0.6.1: {} + + jsonpath-plus@10.4.0: + dependencies: + '@jsep-plugin/assignment': 1.3.0(jsep@1.4.0) + '@jsep-plugin/regex': 1.0.4(jsep@1.4.0) + jsep: 1.4.0 + + jsonrepair@3.15.0: {} + + local-pkg@0.5.1: + dependencies: + mlly: 1.8.2 + pkg-types: 1.3.1 + + local-pkg@1.2.1: + dependencies: + mlly: 1.8.2 + pkg-types: 2.3.1 + quansync: 0.2.11 + + locate-character@3.0.0: {} + + lodash-es@4.18.1: {} + + lodash-unified@1.0.3(@types/lodash-es@4.17.12)(lodash-es@4.18.1)(lodash@4.18.1): + dependencies: + '@types/lodash-es': 4.17.12 + lodash: 4.18.1 + lodash-es: 4.18.1 + + lodash.clonedeep@4.5.0: {} + + lodash.isequal@4.5.0: {} + + lodash@4.18.1: {} + + lru-cache@10.4.3: {} + + magic-string@0.26.7: + dependencies: + sourcemap-codec: 1.4.8 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + marked@14.0.0: {} + + math-intrinsics@1.1.0: {} + + memoize-one@6.0.0: {} + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.2 + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + minimatch@9.0.9: + dependencies: + brace-expansion: 2.1.4 + + minipass@7.1.3: {} + + mitt@3.0.1: {} + + mlly@1.8.2: + dependencies: + acorn: 8.18.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.4 + + monaco-editor@0.55.1: + dependencies: + dompurify: 3.2.7 + marked: 14.0.0 + + nanoid@3.3.18: {} + + natural-compare-lite@1.4.0: {} + + next-tick@1.1.0: {} + + node-addon-api@7.1.1: + optional: true + + nopt@7.2.1: + dependencies: + abbrev: 2.0.0 + + normalize-wheel-es@1.2.0: {} + + nprogress@0.2.0: {} + + object-is@1.1.6: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + + object-keys@1.1.1: {} + + package-json-from-dist@1.0.1: {} + + pako@2.2.0: {} + + parchment@1.1.4: {} + + path-key@3.1.1: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.3 + + pathe@2.0.3: {} + + perfect-debounce@1.0.0: {} + + picocolors@1.1.1: {} + + picomatch@2.3.2: {} + + picomatch@4.0.5: {} + + pinia@3.0.4(vue@3.5.41): + dependencies: + '@vue/devtools-api': 7.7.10 + vue: 3.5.41 + + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.8.2 + pathe: 2.0.3 + + pkg-types@2.3.1: + dependencies: + confbox: 0.2.4 + exsolve: 1.1.1 + pathe: 2.0.3 + + postcss@8.5.26: + dependencies: + nanoid: 3.3.18 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + proto-list@1.2.4: {} + + proxy-from-env@1.1.0: {} + + quansync@0.2.11: {} + + queue-microtask@1.2.3: {} + + quill-delta@3.6.3: + dependencies: + deep-equal: 1.1.2 + extend: 3.0.2 + fast-diff: 1.1.2 + + quill-delta@4.2.2: + dependencies: + fast-diff: 1.2.0 + lodash.clonedeep: 4.5.0 + lodash.isequal: 4.5.0 + + quill@1.3.7: + dependencies: + clone: 2.1.2 + deep-equal: 1.1.2 + eventemitter3: 2.0.3 + extend: 3.0.2 + parchment: 1.1.4 + quill-delta: 3.6.3 + + readdirp@4.1.2: + optional: true + + regexp.prototype.flags@1.5.4: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-errors: 1.3.0 + get-proto: 1.0.1 + gopd: 1.2.0 + set-function-name: 2.0.2 + + require-from-string@2.0.2: {} + + resize-observer-polyfill@1.5.1: {} + + reusify@1.1.0: {} + + rfdc@1.4.1: {} + + rollup@4.62.5: + dependencies: + '@types/estree': 1.0.9 + optionalDependencies: + '@napi-rs/lzma-linux-x64-gnu': 1.5.1 + '@rollup/rollup-android-arm-eabi': 4.62.5 + '@rollup/rollup-android-arm64': 4.62.5 + '@rollup/rollup-darwin-arm64': 4.62.5 + '@rollup/rollup-darwin-x64': 4.62.5 + '@rollup/rollup-freebsd-arm64': 4.62.5 + '@rollup/rollup-freebsd-x64': 4.62.5 + '@rollup/rollup-linux-arm-gnueabihf': 4.62.5 + '@rollup/rollup-linux-arm-musleabihf': 4.62.5 + '@rollup/rollup-linux-arm64-gnu': 4.62.5 + '@rollup/rollup-linux-arm64-musl': 4.62.5 + '@rollup/rollup-linux-loong64-gnu': 4.62.5 + '@rollup/rollup-linux-loong64-musl': 4.62.5 + '@rollup/rollup-linux-ppc64-gnu': 4.62.5 + '@rollup/rollup-linux-ppc64-musl': 4.62.5 + '@rollup/rollup-linux-riscv64-gnu': 4.62.5 + '@rollup/rollup-linux-riscv64-musl': 4.62.5 + '@rollup/rollup-linux-s390x-gnu': 4.62.5 + '@rollup/rollup-linux-x64-gnu': 4.62.5 + '@rollup/rollup-linux-x64-musl': 4.62.5 + '@rollup/rollup-openbsd-x64': 4.62.5 + '@rollup/rollup-openharmony-arm64': 4.62.5 + '@rollup/rollup-win32-arm64-msvc': 4.62.5 + '@rollup/rollup-win32-ia32-msvc': 4.62.5 + '@rollup/rollup-win32-x64-gnu': 4.62.5 + '@rollup/rollup-win32-x64-msvc': 4.62.5 + fsevents: 2.3.3 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + rxjs@7.8.2: + dependencies: + tslib: 2.8.1 + + sass-embedded-all-unknown@1.97.2: + dependencies: + sass: 1.97.2 + optional: true + + sass-embedded-android-arm64@1.97.2: + optional: true + + sass-embedded-android-arm@1.97.2: + optional: true + + sass-embedded-android-riscv64@1.97.2: + optional: true + + sass-embedded-android-x64@1.97.2: + optional: true + + sass-embedded-darwin-arm64@1.97.2: + optional: true + + sass-embedded-darwin-x64@1.97.2: + optional: true + + sass-embedded-linux-arm64@1.97.2: + optional: true + + sass-embedded-linux-arm@1.97.2: + optional: true + + sass-embedded-linux-musl-arm64@1.97.2: + optional: true + + sass-embedded-linux-musl-arm@1.97.2: + optional: true + + sass-embedded-linux-musl-riscv64@1.97.2: + optional: true + + sass-embedded-linux-musl-x64@1.97.2: + optional: true + + sass-embedded-linux-riscv64@1.97.2: + optional: true + + sass-embedded-linux-x64@1.97.2: + optional: true + + sass-embedded-unknown-all@1.97.2: + dependencies: + sass: 1.97.2 + optional: true + + sass-embedded-win32-arm64@1.97.2: + optional: true + + sass-embedded-win32-x64@1.97.2: + optional: true + + sass-embedded@1.97.2: + dependencies: + '@bufbuild/protobuf': 2.14.0 + buffer-builder: 0.2.0 + colorjs.io: 0.5.2 + immutable: 5.1.9 + rxjs: 7.8.2 + supports-color: 8.1.1 + sync-child-process: 1.0.2 + varint: 6.0.0 + optionalDependencies: + sass-embedded-all-unknown: 1.97.2 + sass-embedded-android-arm: 1.97.2 + sass-embedded-android-arm64: 1.97.2 + sass-embedded-android-riscv64: 1.97.2 + sass-embedded-android-x64: 1.97.2 + sass-embedded-darwin-arm64: 1.97.2 + sass-embedded-darwin-x64: 1.97.2 + sass-embedded-linux-arm: 1.97.2 + sass-embedded-linux-arm64: 1.97.2 + sass-embedded-linux-musl-arm: 1.97.2 + sass-embedded-linux-musl-arm64: 1.97.2 + sass-embedded-linux-musl-riscv64: 1.97.2 + sass-embedded-linux-musl-x64: 1.97.2 + sass-embedded-linux-riscv64: 1.97.2 + sass-embedded-linux-x64: 1.97.2 + sass-embedded-unknown-all: 1.97.2 + sass-embedded-win32-arm64: 1.97.2 + sass-embedded-win32-x64: 1.97.2 + + sass@1.97.2: + dependencies: + chokidar: 4.0.3 + immutable: 5.1.9 + source-map-js: 1.2.1 + optionalDependencies: + '@parcel/watcher': 2.6.0 + optional: true + + scule@1.3.0: {} + + select@1.1.2: {} + + semver@7.8.5: {} + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + signal-exit@4.1.0: {} + + sortablejs@1.14.0: {} + + sortablejs@1.15.7: {} + + source-map-js@1.2.1: {} + + sourcemap-codec@1.4.8: {} + + speakingurl@14.0.1: {} + + splitpanes@3.1.5: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.2.0 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.3.0 + + strip-literal@2.1.1: + dependencies: + js-tokens: 9.0.1 + + style-mod@4.1.3: {} + + superjson@2.2.6: + dependencies: + copy-anything: 4.1.0 + + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + + svelte@5.56.10: + dependencies: + '@jridgewell/remapping': 2.3.5 + '@jridgewell/sourcemap-codec': 1.5.5 + '@sveltejs/acorn-typescript': 1.0.13(acorn@8.18.0) + '@types/estree': 1.0.9 + '@types/trusted-types': 2.0.7 + acorn: 8.18.0 + aria-query: 5.3.1 + axobject-query: 4.1.0 + clsx: 2.1.1 + devalue: 5.9.1 + esm-env: 1.2.2 + esrap: 2.3.6 + is-reference: 3.0.3 + locate-character: 3.0.0 + magic-string: 0.30.21 + zimmerframe: 1.1.4 + transitivePeerDependencies: + - '@typescript-eslint/types' + + sync-child-process@1.0.2: + dependencies: + sync-message-port: 1.2.0 + + sync-message-port@1.2.0: {} + + tiny-emitter@2.1.0: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + tslib@2.3.0: {} + + tslib@2.8.1: {} + + type@2.7.3: {} + + ufo@1.6.4: {} + + unimport@3.14.6(rollup@4.62.5): + dependencies: + '@rollup/pluginutils': 5.4.0(rollup@4.62.5) + acorn: 8.18.0 + escape-string-regexp: 5.0.0 + estree-walker: 3.0.3 + fast-glob: 3.3.3 + local-pkg: 1.2.1 + magic-string: 0.30.21 + mlly: 1.8.2 + pathe: 2.0.3 + picomatch: 4.0.5 + pkg-types: 1.3.1 + scule: 1.3.0 + strip-literal: 2.1.1 + unplugin: 1.16.1 + transitivePeerDependencies: + - rollup + + unplugin-auto-import@0.18.6(@vueuse/core@14.1.0(vue@3.5.41))(rollup@4.62.5): + dependencies: + '@antfu/utils': 0.7.10 + '@rollup/pluginutils': 5.4.0(rollup@4.62.5) + fast-glob: 3.3.3 + local-pkg: 0.5.1 + magic-string: 0.30.21 + minimatch: 9.0.9 + unimport: 3.14.6(rollup@4.62.5) + unplugin: 1.16.1 + optionalDependencies: + '@vueuse/core': 14.1.0(vue@3.5.41) + transitivePeerDependencies: + - rollup + + unplugin-vue-setup-extend-plus@1.0.1: + dependencies: + '@vue/compiler-sfc': 3.5.41 + magic-string: 0.26.7 + unplugin: 1.16.1 + + unplugin@1.16.1: + dependencies: + acorn: 8.18.0 + webpack-virtual-modules: 0.6.2 + + uuid@11.1.1: {} + + vanilla-jsoneditor@3.13.0: + dependencies: + '@codemirror/autocomplete': 6.20.3 + '@codemirror/commands': 6.11.0 + '@codemirror/lang-json': 6.0.2 + '@codemirror/language': 6.12.4 + '@codemirror/lint': 6.9.7 + '@codemirror/search': 6.7.1 + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.9 + '@fortawesome/free-regular-svg-icons': 7.3.1 + '@fortawesome/free-solid-svg-icons': 7.3.1 + '@jsonquerylang/jsonquery': 5.1.1 + '@lezer/highlight': 1.2.3 + '@replit/codemirror-indentation-markers': 6.5.3(@codemirror/language@6.12.4)(@codemirror/state@6.7.1)(@codemirror/view@6.43.9) + ajv: 8.20.0 + codemirror-wrapped-line-indent: 1.0.9(@codemirror/language@6.12.4)(@codemirror/state@6.7.1)(@codemirror/view@6.43.9) + diff-sequences: 29.6.3 + immutable-json-patch: 6.0.3 + jmespath: 0.16.0 + json-source-map: 0.6.1 + jsonpath-plus: 10.4.0 + jsonrepair: 3.15.0 + lodash-es: 4.18.1 + memoize-one: 6.0.0 + natural-compare-lite: 1.4.0 + svelte: 5.56.10 + vanilla-picker: 2.12.3 + transitivePeerDependencies: + - '@typescript-eslint/types' + + vanilla-picker@2.12.3: + dependencies: + '@sphinxxxx/color-conversion': 2.2.2 + + varint@6.0.0: {} + + vite@6.4.1(sass-embedded@1.97.2)(sass@1.97.2): + dependencies: + esbuild: 0.25.12 + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 + postcss: 8.5.26 + rollup: 4.62.5 + tinyglobby: 0.2.17 + optionalDependencies: + fsevents: 2.3.3 + sass: 1.97.2 + sass-embedded: 1.97.2 + + vue-component-type-helpers@3.3.11: {} + + vue-cropper@1.1.1: {} + + vue-demi@0.14.10(vue@3.5.41): + dependencies: + vue: 3.5.41 + + vue-router@4.6.4(vue@3.5.41): + dependencies: + '@vue/devtools-api': 6.6.4 + vue: 3.5.41 + + vue3-ace-editor@2.2.4(ace-builds@1.44.0)(vue@3.5.41): + dependencies: + ace-builds: 1.44.0 + resize-observer-polyfill: 1.5.1 + vue: 3.5.41 + + vue3-json-viewer@2.3.0(vue@3.5.41): + dependencies: + clipboard: 2.0.11 + vue: 3.5.41 + + vue@3.5.41: + dependencies: + '@vue/compiler-dom': 3.5.41 + '@vue/compiler-sfc': 3.5.41 + '@vue/runtime-dom': 3.5.41 + '@vue/server-renderer': 3.5.41 + '@vue/shared': 3.5.41 + + vuedraggable@4.1.0(vue@3.5.41): + dependencies: + sortablejs: 1.14.0 + vue: 3.5.41 + + w3c-keyname@2.2.8: {} + + webpack-virtual-modules@0.6.2: {} + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.2.0 + + xgplayer-subtitles@3.0.26(core-js@3.50.0): + dependencies: + core-js: 3.50.0 + eventemitter3: 4.0.7 + + xgplayer@3.0.26(core-js@3.50.0): + dependencies: + core-js: 3.50.0 + danmu.js: 1.2.1 + delegate: 3.2.0 + eventemitter3: 4.0.7 + xgplayer-subtitles: 3.0.26(core-js@3.50.0) + + zimmerframe@1.1.4: {} + + zrender@5.6.1: + dependencies: + tslib: 2.3.0 diff --git a/src/adapters/URDFAdapter.js b/src/adapters/URDFAdapter.js new file mode 100644 index 0000000..cbbfceb --- /dev/null +++ b/src/adapters/URDFAdapter.js @@ -0,0 +1,461 @@ +/** + * URDF Adapter + * Converts urdf-loaders result to unified model + */ +import * as THREE from 'three'; +import { UnifiedRobotModel, Link, Joint, JointLimits, VisualGeometry, CollisionGeometry, InertialProperties } from '@/models/UnifiedRobotModel.js'; + +export class URDFAdapter { + /** + * Convert urdf-loaders robot object to unified model + * @param {THREE.Group} robot - Robot object returned by urdf-loaders + * @param {string} urdfXML - Original URDF XML content (optional, for extracting inertial data) + * @returns {UnifiedRobotModel} + */ + static convert(robot, urdfXML = null) { + const model = new UnifiedRobotModel(); + model.name = robot.name || 'robot'; + model.threeObject = robot; + + // Mark model type as URDF + if (!robot.userData) robot.userData = {}; + robot.userData.type = 'urdf'; + + if (!robot.links || !robot.joints) { + console.warn('URDF model missing links or joints information'); + return model; + } + + // If XML provided, parse inertial data + let inertialData = {}; + if (urdfXML) { + inertialData = this.parseInertialFromXML(urdfXML); + } + + // Convert links + Object.values(robot.links).forEach(urdfLink => { + const link = this.convertLink(urdfLink); + + // If urdf-loader didn't parse inertial, get from XML + if (!link.inertial && inertialData[urdfLink.name]) { + link.inertial = inertialData[urdfLink.name]; + } + + model.addLink(link); + }); + + // Convert joints + Object.values(robot.joints).forEach(urdfJoint => { + const joint = this.convertJoint(urdfJoint); + model.addJoint(joint); + }); + + // If XML provided, supplement effort and velocity from XML (urdf-loaders may not have parsed) + if (urdfXML) { + this.supplementJointLimitsFromXML(model, urdfXML); + } + + // Find root link (link that is not a child of any joint) + const allChildren = new Set( + Array.from(model.joints.values()).map(j => j.child).filter(c => c) + ); + const rootLinks = Array.from(model.links.keys()).filter( + name => !allChildren.has(name) + ); + if (rootLinks.length > 0) { + model.rootLink = rootLinks[0]; + } + + // Enhance materials for better lighting (MuJoCo style) + this.enhanceMaterials(robot); + + return model; + } + + /** + * Enhance materials for better lighting (MuJoCo style) + * Applies consistent shininess and specular properties to all materials + * Directly modifies materials in place to ensure changes persist + */ + static enhanceMaterials(robotObject) { + robotObject.traverse((child) => { + if (child.isMesh && child.material) { + // Handle material arrays + if (Array.isArray(child.material)) { + child.material = child.material.map(mat => { + return this.enhanceSingleMaterial(mat); + }); + } else { + child.material = this.enhanceSingleMaterial(child.material); + } + } + }); + } + + /** + * Enhance a single material with better lighting properties + * Returns enhanced material (may be cloned or modified in place) + * Saves original properties for lighting toggle + */ + static enhanceSingleMaterial(material) { + if (material.isMeshPhongMaterial || material.isMeshStandardMaterial) { + // Save original properties if not already saved (for lighting toggle) + if (material.userData.originalShininess === undefined) { + material.userData.originalShininess = material.shininess !== undefined ? material.shininess : 30; + // IMPORTANT: Save original color to preserve URDF material colors + if (!material.userData.originalColor && material.color) { + material.userData.originalColor = material.color.clone(); + } + // Save original specular - if material had no specular, save null + if (!material.specular) { + material.userData.originalSpecular = null; + } else if (material.specular.isColor) { + const spec = material.specular; + if (spec.r < 0.1 && spec.g < 0.1 && spec.b < 0.1) { + material.userData.originalSpecular = null; // Likely default + } else { + material.userData.originalSpecular = spec.clone(); + } + } else if (typeof material.specular === 'number') { + if (material.specular === 0x111111 || material.specular < 0x111111) { + material.userData.originalSpecular = null; + } else { + material.userData.originalSpecular = new THREE.Color(material.specular); + } + } else { + material.userData.originalSpecular = null; + } + } + + // IMPORTANT: Preserve original color from URDF material definitions + // Do not modify material.color - urdf-loaders already sets the correct color + + // Apply enhanced lighting (default enabled) + // Increase shininess for better highlights + if (material.shininess === undefined || material.shininess < 50) { + material.shininess = 50; + } + + // Enhance specular reflection - ensure it's a Color object with proper values + if (!material.specular) { + material.specular = new THREE.Color(0.3, 0.3, 0.3); + } else if (material.specular.isColor) { + // If it's already a Color object, update values + if (material.specular.r < 0.2 || material.specular.g < 0.2 || material.specular.b < 0.2) { + material.specular.setRGB(0.3, 0.3, 0.3); + } + } else if (typeof material.specular === 'number') { + // Convert number to Color object + if (material.specular < 0x333333) { + material.specular = new THREE.Color(0.3, 0.3, 0.3); + } else { + // Convert hex to Color + material.specular = new THREE.Color(material.specular); + if (material.specular.r < 0.2) { + material.specular.setRGB(0.3, 0.3, 0.3); + } + } + } + + // Mark material as needing update + material.needsUpdate = true; + return material; + } else if (material.type === 'MeshBasicMaterial') { + // Convert MeshBasicMaterial to MeshPhongMaterial + const oldMaterial = material; + const envMap = typeof window !== 'undefined' && window.app?.sceneManager?.environmentManager?.getEnvironmentMap(); + const newMaterial = new THREE.MeshPhongMaterial({ + color: oldMaterial.color, + map: oldMaterial.map, + transparent: oldMaterial.transparent, + opacity: oldMaterial.opacity, + side: oldMaterial.side, + shininess: 50, + specular: new THREE.Color(0.3, 0.3, 0.3), + envMap: envMap || null, + reflectivity: envMap ? 0.3 : 0 + }); + // Save original properties for lighting toggle + newMaterial.userData.originalShininess = 30; + newMaterial.userData.originalSpecular = null; // MeshBasicMaterial had no specular + return newMaterial; + } + + return material; + } + + static convertLink(urdfLink) { + const link = new Link(urdfLink.name); + link.threeObject = urdfLink; + + // Convert inertial properties + if (urdfLink.inertial) { + link.inertial = this.convertInertial(urdfLink.inertial); + } + + // Note: urdf-loaders has already converted visual and collision to Three.js objects + // We mainly extract metadata, actual meshes are in threeObject + + return link; + } + + static convertJoint(urdfJoint) { + // URDF joint object has jointType property (not type) + const jointType = urdfJoint.jointType || urdfJoint.type || 'fixed'; + const joint = new Joint(urdfJoint.name, jointType); + + // Extract parent name (urdf-loader returns Three.js object reference) + joint.parent = urdfJoint.parent?.name || null; + + // child may not be in urdfJoint.child, but in Three.js children array + // In Three.js scene graph, Joint is parent node, Child Link is Joint's child node + if (urdfJoint.child && urdfJoint.child.name) { + joint.child = urdfJoint.child.name; + } else if (urdfJoint.children && urdfJoint.children.length > 0) { + // Find Link object from children array + const childLink = urdfJoint.children.find(child => + child.isURDFLink || child.type === 'URDFLink' + ); + if (childLink) { + joint.child = childLink.name; + } + } + + joint.threeObject = urdfJoint; + + // Convert origin + if (urdfJoint.origin) { + joint.origin = { + xyz: urdfJoint.origin.xyz || [0, 0, 0], + rpy: urdfJoint.origin.rpy || [0, 0, 0] + }; + } + + // Convert axis + if (urdfJoint.axis) { + joint.axis = { xyz: urdfJoint.axis.xyz || [0, 0, 1] }; + } + + // Convert limits + if (urdfJoint.limit) { + joint.limits = this.convertLimits(urdfJoint.limit); + } + + // Get current value + if (urdfJoint.angle !== undefined) { + joint.currentValue = urdfJoint.angle; + } else if (urdfJoint.jointValue !== undefined) { + joint.currentValue = urdfJoint.jointValue; + } + + return joint; + } + + static convertLimits(limit) { + const limits = new JointLimits(); + if (limit.lower !== undefined) limits.lower = limit.lower; + if (limit.upper !== undefined) limits.upper = limit.upper; + if (limit.effort !== undefined) limits.effort = limit.effort; + if (limit.velocity !== undefined) limits.velocity = limit.velocity; + return limits; + } + + /** + * Supplement joint effort and velocity information from URDF XML + * (because urdf-loaders may not have parsed these attributes) + */ + static supplementJointLimitsFromXML(model, urdfXML) { + try { + const parser = new DOMParser(); + const doc = parser.parseFromString(urdfXML, 'text/xml'); + + const jointElements = doc.querySelectorAll('joint'); + jointElements.forEach(jointEl => { + const jointName = jointEl.getAttribute('name'); + const joint = model.joints.get(jointName); + + if (joint) { + const limitEl = jointEl.querySelector('limit'); + if (limitEl) { + if (!joint.limits) { + joint.limits = new JointLimits(); + } + + // Read effort and velocity + const effort = limitEl.getAttribute('effort'); + const velocity = limitEl.getAttribute('velocity'); + + if (effort !== null) { + joint.limits.effort = parseFloat(effort); + } + if (velocity !== null) { + joint.limits.velocity = parseFloat(velocity); + } + + // Also ensure lower and upper are correctly set + const lower = limitEl.getAttribute('lower'); + const upper = limitEl.getAttribute('upper'); + if (lower !== null) { + joint.limits.lower = parseFloat(lower); + } + if (upper !== null) { + joint.limits.upper = parseFloat(upper); + } + } + } + }); + } catch (error) { + console.error('Failed to supplement joint limit information from XML:', error); + } + } + + static convertInertial(inertial) { + const props = new InertialProperties(); + if (inertial.mass !== undefined) props.mass = inertial.mass; + if (inertial.origin) { + props.origin = { + xyz: inertial.origin.xyz || [0, 0, 0], + rpy: inertial.origin.rpy || [0, 0, 0] + }; + } + if (inertial.ixx !== undefined) props.ixx = inertial.ixx; + if (inertial.iyy !== undefined) props.iyy = inertial.iyy; + if (inertial.izz !== undefined) props.izz = inertial.izz; + if (inertial.ixy !== undefined) props.ixy = inertial.ixy; + if (inertial.ixz !== undefined) props.ixz = inertial.ixz; + if (inertial.iyz !== undefined) props.iyz = inertial.iyz; + return props; + } + + /** + * Parse inertial data from URDF XML + * @param {string} xmlContent - URDF XML content + * @returns {Object} Mapping from link names to inertial data + */ + static parseInertialFromXML(xmlContent) { + const inertialData = {}; + + try { + const parser = new DOMParser(); + const xmlDoc = parser.parseFromString(xmlContent, 'text/xml'); + + // Get all link elements + const links = xmlDoc.getElementsByTagName('link'); + + for (let i = 0; i < links.length; i++) { + const linkElement = links[i]; + const linkName = linkElement.getAttribute('name'); + + // Find inertial element + const inertialElement = linkElement.getElementsByTagName('inertial')[0]; + if (!inertialElement) continue; + + const inertial = new InertialProperties(); + + // Parse mass + const massElement = inertialElement.getElementsByTagName('mass')[0]; + if (massElement) { + inertial.mass = parseFloat(massElement.getAttribute('value')); + } + + // Parse origin + const originElement = inertialElement.getElementsByTagName('origin')[0]; + if (originElement) { + const xyz = originElement.getAttribute('xyz'); + const rpy = originElement.getAttribute('rpy'); + inertial.origin = { + xyz: xyz ? xyz.split(' ').map(parseFloat) : [0, 0, 0], + rpy: rpy ? rpy.split(' ').map(parseFloat) : [0, 0, 0] + }; + } else { + inertial.origin = { xyz: [0, 0, 0], rpy: [0, 0, 0] }; + } + + // Parse inertia + const inertiaElement = inertialElement.getElementsByTagName('inertia')[0]; + if (inertiaElement) { + inertial.ixx = parseFloat(inertiaElement.getAttribute('ixx')) || 0; + inertial.iyy = parseFloat(inertiaElement.getAttribute('iyy')) || 0; + inertial.izz = parseFloat(inertiaElement.getAttribute('izz')) || 0; + inertial.ixy = parseFloat(inertiaElement.getAttribute('ixy')) || 0; + inertial.ixz = parseFloat(inertiaElement.getAttribute('ixz')) || 0; + inertial.iyz = parseFloat(inertiaElement.getAttribute('iyz')) || 0; + } + + inertialData[linkName] = inertial; + } + } catch (error) { + console.error('Failed to parse URDF XML inertial data:', error); + } + + return inertialData; + } + + /** + * Set joint angle (using urdf-loaders' setJointValue method) + * Reference URDFClasses.js setJointValue implementation + */ + static setJointAngle(joint, angle, ignoreLimits = false) { + joint.currentValue = angle; + + // URDF format: use urdf-loader's setJointValue method + if (joint.threeObject) { + // If ignoring limits, temporarily modify URDF joint object's limit values + let originalLimits = null; + if (ignoreLimits && joint.threeObject.limit) { + originalLimits = { + lower: joint.threeObject.limit.lower, + upper: joint.threeObject.limit.upper + }; + joint.threeObject.limit.lower = -Math.PI * 2; + joint.threeObject.limit.upper = Math.PI * 2; + } + + // Prefer setJointValue method (urdf-loader's standard method) + if (typeof joint.threeObject.setJointValue === 'function') { + joint.threeObject.setJointValue(angle); + + // Restore original limits + if (originalLimits && joint.threeObject.limit) { + joint.threeObject.limit.lower = originalLimits.lower; + joint.threeObject.limit.upper = originalLimits.upper; + } + return; + } else if (typeof joint.threeObject.setAngle === 'function') { + joint.threeObject.setAngle(angle); + + // Restore original limits + if (originalLimits && joint.threeObject.limit) { + joint.threeObject.limit.lower = originalLimits.lower; + joint.threeObject.limit.upper = originalLimits.upper; + } + return; + } + + // Restore original limits (if none of the above methods executed) + if (originalLimits && joint.threeObject.limit) { + joint.threeObject.limit.lower = originalLimits.lower; + joint.threeObject.limit.upper = originalLimits.upper; + } + + // If none available, manually set rotation + // Note: For URDF, we usually shouldn't reach here, as urdf-loader should provide setJointValue + console.warn(`Joint ${joint.name} has no setJointValue or setAngle method, attempting manual setup`); + if (joint.type === 'revolute' || joint.type === 'continuous') { + const axis = joint.axis ? new THREE.Vector3(...joint.axis.xyz).normalize() : new THREE.Vector3(0, 0, 1); + // Note: joint.threeObject may not be a Three.js object, cannot directly set rotation + if (joint.threeObject.rotation) { + joint.threeObject.rotation.setFromAxisAngle(axis, angle); + } + } else if (joint.type === 'prismatic') { + const axis = joint.axis ? new THREE.Vector3(...joint.axis.xyz).normalize() : new THREE.Vector3(1, 0, 0); + if (joint.threeObject.position) { + joint.threeObject.position.copy(axis.multiplyScalar(angle)); + } + } + } else { + console.warn(`Joint ${joint.name} has no threeObject`); + } + } +} + diff --git a/src/adapters/XacroAdapter.js b/src/adapters/XacroAdapter.js new file mode 100644 index 0000000..97b4c56 --- /dev/null +++ b/src/adapters/XacroAdapter.js @@ -0,0 +1,616 @@ +/** + * Xacro Adapter + * + * Parses ROS Xacro files and converts them to URDF format using xacro-parser. + * + * Supported Features: + * - Property definitions and substitutions (xacro:property) + * - Macro definitions with parameters (xacro:macro) + * - Conditional blocks (xacro:if, xacro:unless) + * - File inclusions (xacro:include) with package:// and relative paths + * - Python-style boolean constants (True/False) + * - Arithmetic expressions and property evaluation + * + * Compatibility: + * - ROS Jade and later (inOrder=true, requirePrefix=true, localProperties=true) + * - Automatically injects True/False constants for compatibility with ROS xacro files + * + * Usage: + * - Upload all related xacro files (main file and included files) + * - The adapter automatically resolves file inclusions from the uploaded file map + * - Mesh files are resolved using package:// paths or relative paths + */ +import { XacroParser } from 'xacro-parser'; +import { URDFAdapter } from './URDFAdapter.js'; + +export class XacroAdapter { + /** + * Parse xacro content and convert to unified model + * @param {string} xacroContent - Xacro file content + * @param {string} fileName - Xacro file name (for working path) + * @param {Map} fileMap - File map (path -> File object) + * @param {File} file - Original file object (optional) + * @returns {Promise} + */ + static async parse(xacroContent, fileName, fileMap = null, file = null) { + try { + // Create xacro parser + const parser = new XacroParser(); + + // Configure parser for ROS Jade and later (default settings) + parser.inOrder = true; + parser.requirePrefix = true; + parser.localProperties = true; + + // Set working path (directory where xacro file is located) + const workingPath = fileName.includes('/') + ? fileName.substring(0, fileName.lastIndexOf('/') + 1) + : ''; + parser.workingPath = workingPath; + + // Inject Python-style boolean constants as xacro properties + // Some xacro files use True/False (capitalized) in conditions + xacroContent = this.injectBooleanConstants(xacroContent); + + // Extract and set xacro arguments with their default values + // Also add common ROS arguments that might be used without definition + const xacroArgs = this.extractXacroArguments(xacroContent); + + // Add common ROS arguments with sensible defaults if not already defined + if (!xacroArgs.hasOwnProperty('DEBUG')) { + xacroArgs.DEBUG = 'false'; + } + if (!xacroArgs.hasOwnProperty('SELF_COLLIDE')) { + xacroArgs.SELF_COLLIDE = 'false'; + } + + parser.arguments = xacroArgs; + + // If fileMap provided, setup custom file loader + if (fileMap) { + parser.getFileContents = async (path) => { + return await this.loadFileFromMap(path, fileMap, workingPath); + }; + } + + // Parse xacro to URDF XML + const urdfXML = await parser.parse(xacroContent); + + + // Convert XMLDocument to string + const serializer = new XMLSerializer(); + let urdfString = serializer.serializeToString(urdfXML); + + // Clean up the XML string - remove empty xmlns attributes that might cause issues + urdfString = urdfString.replace(/\sxmlns=""/g, ''); + + // Re-parse the URDF string with DOMParser to create a clean XMLDocument + // This removes any xacro-specific nodes that urdf-loader might not handle + const domParser = new DOMParser(); + const cleanUrdfXML = domParser.parseFromString(urdfString, 'text/xml'); + + // Check for parsing errors + const parseError = cleanUrdfXML.querySelector('parsererror'); + if (parseError) { + throw new Error('Generated URDF has invalid XML: ' + parseError.textContent); + } + + // Remove problematic elements that urdf-loader might not handle well + // Remove gazebo elements (plugins, sensors, etc.) + const gazeboElements = cleanUrdfXML.querySelectorAll('gazebo'); + gazeboElements.forEach(el => el.parentNode?.removeChild(el)); + + // Remove transmission elements (these are for Gazebo/ROS control) + const transmissionElements = cleanUrdfXML.querySelectorAll('transmission'); + transmissionElements.forEach(el => el.parentNode?.removeChild(el)); + + // Remove visual/collision elements with empty geometry tags + this.removeEmptyGeometry(cleanUrdfXML); + + // Clean up empty text nodes and comments that might cause issues + this.cleanXMLNodes(cleanUrdfXML.documentElement); + + // Convert the clean XMLDocument back to string for URDFLoader + const finalUrdfString = serializer.serializeToString(cleanUrdfXML); + + // Now use existing URDF loading infrastructure + // Import URDFLoader dynamically + const urdfModule = await import('urdf-loader'); + const URDFLoader = urdfModule.URDFLoader || urdfModule.default || urdfModule; + + return new Promise((resolve, reject) => { + const loader = new URDFLoader(); + loader.parseCollision = true; + + // Extract directory where URDF file is located + const urdfDir = workingPath; + + // If file map provided, setup resource loader (same as URDF loading) + if (fileMap) { + // Parse URDF content, find all used package names + const packages = this.extractPackagesFromURDF(finalUrdfString); + + // Build package map + const packageMap = {}; + packages.forEach(pkg => { + packageMap[pkg] = pkg; + }); + packageMap[''] = ''; + + loader.packages = packageMap; + + // Set URL Modifier (same as URDF loading) + const urlModifier = (url) => { + // Handle blob URLs + if (url.startsWith('blob:')) { + const blobMatch = url.match(/^blob:https?:\/\/[^\/]+\/(.+)$/); + if (blobMatch && blobMatch[1]) { + const fileName = blobMatch[1]; + if (/\.(jpg|jpeg|png|gif|bmp|tga|tiff|webp|dae|stl|obj|gltf|glb)$/i.test(fileName)) { + url = fileName; + } else { + return url; + } + } else { + return url; + } + } + + const isTextureFile = /\.(jpg|jpeg|png|gif|bmp|tga|tiff|webp)$/i.test(url); + const isMeshFile = /\.(dae|stl|obj|gltf|glb)$/i.test(url); + + let meshPath = url; + + // Remove http:// or https:// prefix + if (meshPath.startsWith('http://') || meshPath.startsWith('https://')) { + try { + const urlObj = new URL(meshPath); + meshPath = urlObj.pathname; + if (meshPath.startsWith('/')) { + meshPath = meshPath.substring(1); + } + } catch (e) { + // Invalid URL, use as-is + } + } + + // Remove package:// prefix + if (meshPath.startsWith('package://')) { + meshPath = meshPath.replace(/^package:\/\//, ''); + const parts = meshPath.split('/'); + if (parts.length > 1) { + meshPath = parts.slice(1).join('/'); + } + } + + // Remove leading ./ + meshPath = meshPath.replace(/^\.\//, ''); + + // Handle relative paths + let normalizedPath = meshPath; + if (meshPath.includes('../')) { + const parts = meshPath.split('/'); + const resolvedParts = []; + for (const part of parts) { + if (part === '..') { + resolvedParts.pop(); + } else if (part !== '.' && part !== '') { + resolvedParts.push(part); + } + } + normalizedPath = resolvedParts.join('/'); + } + + // Build full path + const fullPath = urdfDir + normalizedPath; + const altPath = urdfDir + meshPath; + + // Find file in fileMap + let matchedFile = fileMap.get(fullPath); + + if (!matchedFile && altPath !== fullPath) { + matchedFile = fileMap.get(altPath); + } + + if (!matchedFile) { + matchedFile = fileMap.get(normalizedPath); + } + + if (!matchedFile) { + matchedFile = fileMap.get(meshPath); + } + + if (!matchedFile) { + const targetFileName = normalizedPath.split('/').pop() || meshPath.split('/').pop(); + for (const [key, file] of fileMap.entries()) { + const keyFileName = key.split('/').pop(); + if (keyFileName === targetFileName) { + matchedFile = file; + break; + } + } + } + + if (matchedFile) { + const bloburl = URL.createObjectURL(matchedFile); + return bloburl; + } + + return url; + }; + + loader.manager.setURLModifier(urlModifier); + + // Custom loadMeshCb (same as URDF loading) + const originalLoadMeshCb = loader.loadMeshCb || loader.defaultMeshLoader.bind(loader); + loader.loadMeshCb = (path, manager, done) => { + this.findFileInMapByPath(path, fileMap, urdfDir).then(file => { + if (file) { + const ext = (file.name || path).toLowerCase().split('.').pop(); + this.loadMeshFileAsync(file, ext, manager).then(meshObject => { + if (meshObject) { + done(meshObject, null); + } else { + done(null, new Error(`Failed to load mesh file: ${path}`)); + } + }).catch(err => { + console.error(`Failed to load mesh: ${path}`, err); + done(null, err); + }); + } else { + let stlPath = path + stlPath = stlPath.replace('file:///', 'inspection/elfin10') + originalLoadMeshCb(path, manager, done); + } + }).catch(error => { + console.error(`Failed to find file: ${path}`, error); + originalLoadMeshCb(path, manager, done); + }); + }; + } + + // Create temporary URL for URDF content + const blob = new Blob([finalUrdfString], { type: 'text/xml' }); + const url = URL.createObjectURL(blob); + + loader.load(url, (robot) => { + URL.revokeObjectURL(url); + + // Convert to unified model using URDFAdapter + try { + const model = URDFAdapter.convert(robot, finalUrdfString); + resolve(model); + } catch (error) { + console.error('[XacroAdapter] URDF conversion error:', error); + reject(new Error('URDF conversion failed: ' + error.message)); + } + }, undefined, (error) => { + URL.revokeObjectURL(url); + console.error('[XacroAdapter] URDF loading error:', error); + reject(new Error('URDF loading failed: ' + (error.message || error))); + }); + }); + } catch (error) { + console.error('[XacroAdapter] Xacro parsing error:', error); + throw new Error('Xacro parsing failed: ' + error.message); + } + } + + /** + * Load file from fileMap for xacro includes + * @param {string} path - File path + * @param {Map} fileMap - File map + * @param {string} workingPath - Working directory + * @returns {Promise} + */ + static async loadFileFromMap(path, fileMap, workingPath) { + // Clean path - remove leading slash if present + let cleanPath = path; + // if (cleanPath.startsWith('/')) { + // cleanPath = cleanPath.substring(1); + // } + + // // Remove leading ./ + // cleanPath = cleanPath.replace(/^\.\//, ''); + cleanPath = cleanPath.split('///')[1]; + // Try different path combinations + const possiblePaths = [ + cleanPath, + workingPath + cleanPath, + path, + workingPath + path.replace(/^\/+/, ''), + // Also try without the first directory component + cleanPath.includes('/') ? cleanPath.substring(cleanPath.indexOf('/') + 1) : cleanPath, + ]; + + // Try each path + for (const tryPath of possiblePaths) { + const file = fileMap.get(tryPath); + if (file) { + const content = await file.text(); + return content; + } + } + + // Try filename only match + const fileName = cleanPath.split('/').pop(); + for (const [key, file] of fileMap.entries()) { + const keyFileName = key.split('/').pop(); + if (keyFileName === fileName) { + const content = await file.text(); + return content; + } + } + + console.error('[XacroAdapter] Cannot find included file:', path); + throw new Error(`Cannot find included file: ${path}`); + } + + /** + * Remove visual/collision elements with empty geometry tags + * xacro-parser may not fully expand conditional geometry (xacro:if) + * We'll just remove these empty visual elements rather than try to fix them + * @param {XMLDocument} xmlDoc - XML document to clean + */ + static removeEmptyGeometry(xmlDoc) { + // Find all geometry elements + const geometryElements = xmlDoc.querySelectorAll('geometry'); + + geometryElements.forEach(geom => { + // Check if geometry is empty (no child elements) + if (!geom.children || geom.children.length === 0) { + // Find the parent visual or collision element + let parent = geom.parentNode; + if (parent) { + const grandParent = parent.parentNode; + if (grandParent) { + if (parent.nodeName === 'visual' || parent.nodeName === 'collision') { + // Remove empty visual/collision elements + grandParent.removeChild(parent); + } + } + } + } + }); + } + + /** + * Recursively clean XML nodes - remove empty text nodes and comments + * @param {Element} element - Element to clean + */ + static cleanXMLNodes(element) { + if (!element || !element.childNodes) return; + + const nodesToRemove = []; + + // Collect nodes to remove + for (let i = 0; i < element.childNodes.length; i++) { + const node = element.childNodes[i]; + + // Remove comments + if (node.nodeType === Node.COMMENT_NODE) { + nodesToRemove.push(node); + } + // Remove empty or whitespace-only text nodes + else if (node.nodeType === Node.TEXT_NODE) { + if (!node.nodeValue || node.nodeValue.trim() === '') { + nodesToRemove.push(node); + } + } + // Recursively clean element nodes + else if (node.nodeType === Node.ELEMENT_NODE) { + this.cleanXMLNodes(node); + } + } + + // Remove collected nodes + nodesToRemove.forEach(node => { + element.removeChild(node); + }); + } + + /** + * Extract package names from URDF content + * @param {string} urdfContent - URDF content + * @returns {Set} + */ + static extractPackagesFromURDF(urdfContent) { + const packages = new Set(); + const packageRegex = /package:\/\/([^\/]+)/g; + let match; + while ((match = packageRegex.exec(urdfContent)) !== null) { + packages.add(match[1]); + } + return packages; + } + + /** + * Find file in fileMap by path + * @param {string} path - File path + * @param {Map} fileMap - File map + * @param {string} urdfDir - URDF directory + * @returns {Promise} + */ + static async findFileInMapByPath(path, fileMap, urdfDir) { + let meshPath = path; + + // Remove blob: prefix + meshPath = meshPath.replace(/^blob:[^\/]+\//, ''); + + // Remove package:// prefix + if (meshPath.startsWith('package://')) { + meshPath = meshPath.replace(/^package:\/\//, ''); + const parts = meshPath.split('/'); + if (parts.length > 1) { + meshPath = parts.slice(1).join('/'); + } + } + + // Remove leading ./ + meshPath = meshPath.replace(/^\.\//, ''); + + // Build full path + const fullPath = urdfDir + meshPath; + + // Try full path + let file = fileMap.get(fullPath); + if (file) return file; + + // Try path without directory + file = fileMap.get(meshPath); + if (file) return file; + + // Try filename match + const targetFileName = meshPath.split('/').pop(); + for (const [key, f] of fileMap.entries()) { + const keyFileName = key.split('/').pop(); + if (keyFileName === targetFileName) { + return f; + } + } + + return null; + } + + /** + * Load mesh file asynchronously + * @param {File} file - File object + * @param {string} ext - File extension + * @param {THREE.LoadingManager} manager - Loading manager + * @returns {Promise} + */ + static async loadMeshFileAsync(file, ext, manager) { + const THREE = await import('three'); + const blobUrl = URL.createObjectURL(file); + + try { + let meshObject = null; + + switch (ext) { + case 'stl': { + const { STLLoader } = await import('three/examples/jsm/loaders/STLLoader.js'); + const stlLoader = new STLLoader(manager); + const stlGeometry = await new Promise((resolve, reject) => { + stlLoader.load(blobUrl, resolve, undefined, reject); + }); + const stlMaterial = new THREE.MeshPhongMaterial(); + meshObject = new THREE.Mesh(stlGeometry, stlMaterial); + break; + } + + case 'dae': { + const { ColladaLoader } = await import('three/examples/jsm/loaders/ColladaLoader.js'); + const colladaLoader = new ColladaLoader(manager); + const colladaModel = await new Promise((resolve, reject) => { + colladaLoader.load(blobUrl, resolve, undefined, reject); + }); + meshObject = colladaModel.scene; + + // Remove lights + if (meshObject && meshObject.traverse) { + const lightsToRemove = []; + meshObject.traverse(child => { + if (child.isLight) { + lightsToRemove.push(child); + } + }); + lightsToRemove.forEach(light => { + if (light.parent) { + light.parent.remove(light); + } + }); + } + break; + } + + case 'obj': { + const { OBJLoader } = await import('three/examples/jsm/loaders/OBJLoader.js'); + const objLoader = new OBJLoader(manager); + meshObject = await new Promise((resolve, reject) => { + objLoader.load(blobUrl, resolve, undefined, reject); + }); + break; + } + + case 'gltf': + case 'glb': { + const { GLTFLoader } = await import('three/examples/jsm/loaders/GLTFLoader.js'); + const gltfLoader = new GLTFLoader(manager); + const gltfModel = await new Promise((resolve, reject) => { + gltfLoader.load(blobUrl, resolve, undefined, reject); + }); + meshObject = gltfModel.scene; + break; + } + + default: + console.warn(`Unsupported file format: ${ext}`); + URL.revokeObjectURL(blobUrl); + return null; + } + + URL.revokeObjectURL(blobUrl); + return meshObject; + } catch (error) { + URL.revokeObjectURL(blobUrl); + console.error(`Failed to load mesh file: ${file.name}`, error); + throw error; + } + } + + /** + * Inject Python-style boolean constants (True/False) into xacro content + * Some xacro files use capitalized True/False in conditional expressions + * The parameters are passed as strings ("True", "False") but compared to constants + * We define the constants to match the string values for comparison to work + * @param {string} xacroContent - Original xacro content + * @returns {string} - Modified xacro content with boolean constants defined + */ + static injectBooleanConstants(xacroContent) { + // Find the robot tag opening + const robotTagMatch = xacroContent.match(/(]*>)/); + if (!robotTagMatch) { + return xacroContent; + } + + // Define True/False as string properties for comparison + // When mirror_dae="True" is passed, it becomes the string "True" + // We want ${mirror_dae == True} to work, so True must also be "True" + const booleanProperties = ` + + + +`; + + const insertPosition = robotTagMatch.index + robotTagMatch[0].length; + return xacroContent.substring(0, insertPosition) + + booleanProperties + + xacroContent.substring(insertPosition); + } + + /** + * Extract xacro arguments and their default values from xacro content + * Xacro files can define arguments using + * These need to be provided to the parser via parser.arguments + * @param {string} xacroContent - Xacro file content + * @returns {Object} - Object with argument names as keys and default values + */ + static extractXacroArguments(xacroContent) { + const args = {}; + + // Match + // Also match (without xacro: prefix) + const argPattern = /<(?:xacro:)?arg\s+name=["']([^"']+)["'](?:\s+default=["']([^"']*)["'])?/g; + + let match; + while ((match = argPattern.exec(xacroContent)) !== null) { + const argName = match[1]; + const defaultValue = match[2] || 'false'; // Default to 'false' if no default specified + args[argName] = defaultValue; + } + + return args; + } +} + + diff --git a/src/api/device/system.js b/src/api/device/system.js new file mode 100644 index 0000000..ae5b3a5 --- /dev/null +++ b/src/api/device/system.js @@ -0,0 +1,31 @@ +import request from '@/utils/request' + +export function getSafetyState(robotId) { + return request({ + url: '/api/system/safetyState', + method: 'get', + params: { robotId }, + }) +} + +/** + * 恢复机器人运行状态;payload 与旧 recoverSafetyState 保持一致,便于调用方渐进迁移。 + */ +export function restoreOperationalState(data) { + return request({ + url: '/api/system/restoreOperationalState', + method: 'post', + data, + }) +} + +/** + * @deprecated 请改用 restoreOperationalState,旧接口仅保留给未迁移调用方。 + */ +export function recoverSafetyState(data) { + return request({ + url: '/api/system/recoverSafetyState', + method: 'post', + data, + }) +} diff --git a/src/api/flow/flow.js b/src/api/flow/flow.js new file mode 100644 index 0000000..d8ea37b --- /dev/null +++ b/src/api/flow/flow.js @@ -0,0 +1,123 @@ +import request from '@/utils/request' + +// 发布流程 +export function flowDeploy(data) { + return request({ + url: '/flow/publish', + method: 'post', + data: data + }) +} + +export function flowSaveDraft(data) { + return request({ url: '/flow/draft', method: 'put', data }) +} + +export function flowVersionList(itemId) { + return request({ url: `/flow/versions/${itemId}`, method: 'get' }) +} + +export function flowVersionDetail(versionId) { + return request({ url: `/flow/version/${versionId}`, method: 'get' }) +} + +export function flowSubFlowOptions(currentItemId) { + return request({ + url: '/flow/subflows', + method: 'get', + params: { currentItemId } + }) +} + +export function flowRestoreDraft(versionId, data) { + return request({ url: `/flow/version/${versionId}/restore-draft`, method: 'post', data }) +} + +export function flowRollbackVersion(versionId, data) { + return request({ url: `/flow/version/${versionId}/rollback`, method: 'post', 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' + }) +} + +export function getArmStatus(params) { + return request({ + url: '/api/arm/getPose', + method: 'get', + params: params + }) +} + +export function getAgvStatus(params) { + return request({ + url: '/api/agv/getRuntimeState', + method: 'get', + params: params + }) +} + +export function getAgvStations(params) { + return request({ + url: '/api/agv/listStations', + method: 'get', + params: params + }) +} + +export function getArmJointState(params) { + return request({ + url: '/api/arm/getJointState', + method: 'get', + params: params + }) +} diff --git a/src/api/flow/nodeRed.js b/src/api/flow/nodeRed.js new file mode 100644 index 0000000..4ce5ca8 --- /dev/null +++ b/src/api/flow/nodeRed.js @@ -0,0 +1,5 @@ +import request from '@/utils/request' + +export function getNodeRedFlowById(id) { + return request.get(`${import.meta.env.VITE_NODE_RED_URL}/flow/${id}`, {}) +} diff --git a/src/api/login.js b/src/api/login.js new file mode 100644 index 0000000..2165d2b --- /dev/null +++ b/src/api/login.js @@ -0,0 +1,69 @@ +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 unlockScreen(password) { + return request({ + url: '/unlockscreen', + method: 'post', + data: { password } + }) +} + +// 退出方法 +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/resource/device.js b/src/api/resource/device.js new file mode 100644 index 0000000..378cd10 --- /dev/null +++ b/src/api/resource/device.js @@ -0,0 +1,13 @@ +import request from '@/utils/request' + +export function listResourceDevices(params) { + return request({ url: '/resource/devices', method: 'get', params }) +} + +export function getResourceDevice(id) { + return request({ url: `/resource/devices/${encodeURIComponent(id)}`, method: 'get' }) +} + +export function updateResourceDevice(id, data) { + return request({ url: `/resource/devices/${encodeURIComponent(id)}`, method: 'put', data }) +} diff --git a/src/api/resource/map.js b/src/api/resource/map.js new file mode 100644 index 0000000..05cf8b0 --- /dev/null +++ b/src/api/resource/map.js @@ -0,0 +1,24 @@ +import request from '@/utils/request' + +export function listResourceMaps(params) { + return request({ url: '/resource/maps', method: 'get', params }) +} + +export function listRobotMaps(robotId) { + return request({ + url: `/resource/maps/robot/${encodeURIComponent(robotId)}`, + method: 'get', + }) +} + +export function addResourceMap(data) { + return request({ url: '/resource/maps', method: 'post', data }) +} + +export function updateResourceMap(data) { + return request({ url: '/resource/maps', method: 'put', data }) +} + +export function deleteResourceMap(ids) { + return request({ url: `/resource/maps/${ids}`, method: 'delete' }) +} diff --git a/src/api/resource/robot.js b/src/api/resource/robot.js new file mode 100644 index 0000000..1096b5c --- /dev/null +++ b/src/api/resource/robot.js @@ -0,0 +1,77 @@ +import request from '@/utils/request' + +export function listResourceRobots(params) { + return request({ url: '/resource/robots', method: 'get', params }) +} + +export function getResourceRobot(robotId) { + return request({ url: `/resource/robots/${encodeURIComponent(robotId)}`, method: 'get' }) +} + +export function listRobotDevices(robotId, params = {}) { + return request({ + url: `/resource/robots/${encodeURIComponent(robotId)}/devices`, + method: 'get', + params, + }) +} + +export const getRobotList = listResourceRobots + +export function addRobot(data) { + return request({ url: '/resource/robots', method: 'post', data }) +} + +export function updateRobot(data) { + return request({ url: '/resource/robots', method: 'put', data }) +} + +export function deleteRobot(id) { + return request({ url: `/resource/robots/${id}`, method: 'delete' }) +} + +export function getRobotDevices(id) { + return request({ url: `/resource/robots/id/${id}/devices`, method: 'get' }) +} + +export function getRobotDevicesByRobotId(robotId, params = {}) { + return request({ + url: `/resource/robots/${encodeURIComponent(robotId)}/devices`, + method: 'get', + params: { onlineOnly: true, ...params }, + }) +} + +export function getMapByRobot(robotRef) { + return request({ + url: `/resource/robots/${encodeURIComponent(robotRef)}/remote-maps`, + method: 'get', + }) +} + +export function getMapJson(params) { + return request({ + url: `/resource/robots/${encodeURIComponent(params.robotId)}/download-map`, + method: 'get', + params, + }) +} + +export function getRobotLocation(robotRef) { + return request({ + url: `/resource/robots/${encodeURIComponent(robotRef)}/location`, + method: 'get', + }) +} + +export function getRobotStatus(id) { + return request({ url: `/resource/robots/id/${id}`, method: 'get' }) +} + +export function moveRobot(data) { + return request({ url: '/api/agv/setVelocity', method: 'post', data }) +} + +export function stopRobot(data) { + return request({ url: '/api/agv/stopVelocityControl', method: 'post', data }) +} diff --git a/src/api/resource/terminal.js b/src/api/resource/terminal.js new file mode 100644 index 0000000..8a4ae8b --- /dev/null +++ b/src/api/resource/terminal.js @@ -0,0 +1,29 @@ +import request from '@/utils/request' + +export function listResourceTerminals(params) { + return request({ url: '/resource/terminals', method: 'get', params }) +} + +export function addResourceTerminal(data) { + return request({ url: '/resource/terminals', method: 'post', data }) +} + +export function updateResourceTerminal(data) { + return request({ url: '/resource/terminals', method: 'put', data }) +} + +export function getResourceTerminal(id) { + return request({ url: `/resource/terminals/${encodeURIComponent(id)}`, method: 'get' }) +} + +export function deleteResourceTerminals(ids) { + return request({ url: `/resource/terminals/${ids}`, method: 'delete' }) +} + +export function listTerminalRobots(id) { + return request({ url: `/resource/terminals/${encodeURIComponent(id)}/robots`, method: 'get' }) +} + +export function updateTerminalRobots(id, robotIds) { + return request({ url: `/resource/terminals/${encodeURIComponent(id)}/robots`, method: 'put', data: robotIds }) +} 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..b37643d --- /dev/null +++ b/src/api/system/dept.js @@ -0,0 +1,61 @@ +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 updateDeptSort(data) { + return request({ + url: '/system/dept/updateSort', + 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..fdf2138 --- /dev/null +++ b/src/api/system/menu.js @@ -0,0 +1,69 @@ +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 updateMenuSort(data) { + return request({ + url: '/system/menu/updateSort', + 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..5c3ce0f --- /dev/null +++ b/src/api/system/notice.js @@ -0,0 +1,79 @@ +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' + }) +} + +// 首页顶部公告列表(带已读状态) +export function listNoticeTop() { + return request({ + url: '/system/notice/listTop', + method: 'get' + }) +} + +// 标记公告已读 +export function markNoticeRead(noticeId) { + return request({ + url: '/system/notice/markRead', + method: 'post', + params: { noticeId } + }) +} + +// 批量标记已读 +export function markNoticeReadAll(ids) { + return request({ + url: '/system/notice/markReadAll', + method: 'post', + params: { ids } + }) +} + +// 查询公告已读用户列表 +export function listNoticeReadUsers(query) { + return request({ + url: '/system/notice/readUsers/list', + method: 'get', + params: query + }) +} 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..b5e3edd --- /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', + data: 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..fb6fb74 --- /dev/null +++ b/src/api/test/config.js @@ -0,0 +1,85 @@ +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 copyConfig(id) { + return request({ + url: '/test/config/copy/' + id, + method: 'post' + }) +} + +// 修改任务流程编排 +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..5c252a4 --- /dev/null +++ b/src/api/test/detect.js @@ -0,0 +1,51 @@ +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 copyDetect(id) { + return request({ + url: '/test/detect/copy/' + id, + method: 'post' + }) +} + +// 修改检测项配置 +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..742a961 --- /dev/null +++ b/src/api/test/log.js @@ -0,0 +1,92 @@ +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 rerunInst(id) { + return request({ + url: `/test/inst/rerun/${id}`, + method: 'post' + }) +} + +// 查询任务实例日志 +export function getInst(query) { + return request({ + url: '/test/inst/log/detail', + method: 'get', + params: query + }) +} + +// 查询任务实例日志 +export function getFlowRuntimeLog(query) { + return request({ + url: '/test/inst/log/query', + method: 'get', + params: query + }) +} + +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' }) +} + 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/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/add.svg b/src/assets/icons/svg/add.svg new file mode 100644 index 0000000..4069a57 --- /dev/null +++ b/src/assets/icons/svg/add.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/assets/icons/svg/bell.svg b/src/assets/icons/svg/bell.svg new file mode 100644 index 0000000..a7320a0 --- /dev/null +++ b/src/assets/icons/svg/bell.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/consuming.svg b/src/assets/icons/svg/consuming.svg new file mode 100644 index 0000000..9d49ce5 --- /dev/null +++ b/src/assets/icons/svg/consuming.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/enter.svg b/src/assets/icons/svg/enter.svg new file mode 100644 index 0000000..f7cabf2 --- /dev/null +++ b/src/assets/icons/svg/enter.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/more-up.svg b/src/assets/icons/svg/more-up.svg new file mode 100644 index 0000000..d30ac11 --- /dev/null +++ b/src/assets/icons/svg/more-up.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..1a409f5 --- /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/task.svg b/src/assets/icons/svg/task.svg new file mode 100644 index 0000000..1028189 --- /dev/null +++ b/src/assets/icons/svg/task.svg @@ -0,0 +1,5 @@ + + + + + \ 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/timbre.svg b/src/assets/icons/svg/timbre.svg new file mode 100644 index 0000000..aa96de9 --- /dev/null +++ b/src/assets/icons/svg/timbre.svg @@ -0,0 +1,4 @@ + + + + \ 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..fee3ee1 --- /dev/null +++ b/src/assets/styles/btn.scss @@ -0,0 +1,99 @@ +@use './variables.module.scss' as *; + +@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..129980f --- /dev/null +++ b/src/assets/styles/index.scss @@ -0,0 +1,188 @@ +@use './variables.module.scss'; +@use './mixin.scss'; +@use './transition.scss'; +@use './element-ui.scss'; +@use './sidebar.scss'; +@use './btn.scss'; +@use './ruoyi.scss'; +@use './legacy-layout.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/legacy-layout.scss b/src/assets/styles/legacy-layout.scss new file mode 100644 index 0000000..8583865 --- /dev/null +++ b/src/assets/styles/legacy-layout.scss @@ -0,0 +1,6 @@ +/* Keep the migrated application visually consistent with the original CMVR UI. */ +#ruoyi-git, +#ruoyi-doc, +.user-nickname { + display: none !important; +} 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..b3269d4 --- /dev/null +++ b/src/assets/styles/sidebar.scss @@ -0,0 +1,239 @@ +@use './variables.module.scss' as *; + +#app { + + .main-container { + // min-height: 100%; + 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..c13e5e9 --- /dev/null +++ b/src/components/Breadcrumb/index.vue @@ -0,0 +1,98 @@ + + + + + \ No newline at end of file diff --git a/src/components/Crontab/day.vue b/src/components/Crontab/day.vue new file mode 100644 index 0000000..39263f5 --- /dev/null +++ b/src/components/Crontab/day.vue @@ -0,0 +1,174 @@ + + + + \ No newline at end of file diff --git a/src/components/Crontab/hour.vue b/src/components/Crontab/hour.vue new file mode 100644 index 0000000..db77835 --- /dev/null +++ b/src/components/Crontab/hour.vue @@ -0,0 +1,133 @@ + + + + + \ No newline at end of file diff --git a/src/components/Crontab/index.vue b/src/components/Crontab/index.vue new file mode 100644 index 0000000..cbb41ab --- /dev/null +++ b/src/components/Crontab/index.vue @@ -0,0 +1,313 @@ + + + + + \ No newline at end of file diff --git a/src/components/Crontab/min.vue b/src/components/Crontab/min.vue new file mode 100644 index 0000000..65f0946 --- /dev/null +++ b/src/components/Crontab/min.vue @@ -0,0 +1,126 @@ + + + + \ No newline at end of file diff --git a/src/components/Crontab/month.vue b/src/components/Crontab/month.vue new file mode 100644 index 0000000..e561ba4 --- /dev/null +++ b/src/components/Crontab/month.vue @@ -0,0 +1,141 @@ + + + + + \ No newline at end of file diff --git a/src/components/Crontab/result.vue b/src/components/Crontab/result.vue new file mode 100644 index 0000000..cbb49ba --- /dev/null +++ b/src/components/Crontab/result.vue @@ -0,0 +1,540 @@ + + + \ No newline at end of file diff --git a/src/components/Crontab/second.vue b/src/components/Crontab/second.vue new file mode 100644 index 0000000..15e47a0 --- /dev/null +++ b/src/components/Crontab/second.vue @@ -0,0 +1,128 @@ + + + + + \ No newline at end of file diff --git a/src/components/Crontab/week.vue b/src/components/Crontab/week.vue new file mode 100644 index 0000000..e59da22 --- /dev/null +++ b/src/components/Crontab/week.vue @@ -0,0 +1,197 @@ + + + + + \ No newline at end of file diff --git a/src/components/Crontab/year.vue b/src/components/Crontab/year.vue new file mode 100644 index 0000000..14758ba --- /dev/null +++ b/src/components/Crontab/year.vue @@ -0,0 +1,143 @@ + + + + + \ No newline at end of file diff --git a/src/components/DictTag/index.vue b/src/components/DictTag/index.vue new file mode 100644 index 0000000..ce86940 --- /dev/null +++ b/src/components/DictTag/index.vue @@ -0,0 +1,87 @@ + + + + + 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..efd0edc --- /dev/null +++ b/src/components/Editor/index.vue @@ -0,0 +1,276 @@ + + + + + diff --git a/src/components/ExcelImportDialog/index.vue b/src/components/ExcelImportDialog/index.vue new file mode 100644 index 0000000..e73ea28 --- /dev/null +++ b/src/components/ExcelImportDialog/index.vue @@ -0,0 +1,137 @@ + + + diff --git a/src/components/FileUpload/index.vue b/src/components/FileUpload/index.vue new file mode 100644 index 0000000..172719a --- /dev/null +++ b/src/components/FileUpload/index.vue @@ -0,0 +1,251 @@ + + + + diff --git a/src/components/Hamburger/index.vue b/src/components/Hamburger/index.vue new file mode 100644 index 0000000..a2b4980 --- /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..234531b --- /dev/null +++ b/src/components/HeaderSearch/index.vue @@ -0,0 +1,286 @@ + + + + + diff --git a/src/components/IPlayer/config.ts b/src/components/IPlayer/config.ts new file mode 100644 index 0000000..3048428 --- /dev/null +++ b/src/components/IPlayer/config.ts @@ -0,0 +1,59 @@ +export const conf = { + /** + * 基础功能配置 + * */ + autoplay: false, // 是否自动播放,不自动播放,浏览器有限制规则 + autoplayMuted: false, // 是否自动播放(静音播放) + videoInit: true, // 是否默认初始化video,默认初始化,默认true + playsinline: true, // 是否启用内联播放模式,仅移动端生效 + defaultPlaybackRate: 1, // 默认播放速度(可选:0.5/0.75/1/1.5/2等) + volume: 0.72, // 播放音量(可选:0 ~ 1) + loop: false, // 是否循环播放,默认不循环播放 + startTime: 0, // 点播模式下,初始起播时间 + videoAttributes: {}, // video扩展属性,暂且不配置 + lang: 'zh-cn', // 播放器初始显示语言,设置为中文 + fluid: true, // 是否流式布局(宽高优先于流失布局,默认16:9)注掉上方宽高看效果 + fitVideoSize: 'fixed', // 保持容器宽/高,不做适配,按照容器来 + videoFillMode: 'auto', // 宽高不够自动底色填充(fill拉伸填充等...) + seekedStatus: 'play', // 跳转后继续播放 + // 播放器进度条故事点信息数组 + progressDot: [ + { + id: 0, // 唯一标识,用于删除的时候索引 + time: 30, // 展示的时间点,例子为在播放到10s钟的时候展示 + text: '进度条信息提示...', // hover的时候展示文案,可以为空 + duration: 1, // 展示时间跨度,单位为s + style: { // 指定样式 + backgroundColor: 'white' + } + }, + ], + thumbnail: null, // 进度条预览图配置,普通业务用不到 + marginControls: false, // 是否开启画面和控制栏分离模式,不开启空间多一些 + domEventType: 'default', // 响应的事件类型,不用指定,用默认的即可 + /** + * 交互功能配置(一般使用默认即可) + * */ + /** + * 插件配置,根据需求自选 + * */ + icons: {}, // 使用默认的icon图标 + i18n: [], // 使用默认的中文 + // 自定义一些颜色 + commonStyle: { + progressColor: 'green', // 整个进度条颜色 + playedColor: 'chocolate', // 已播放的进度条颜色 + volumeColor: 'pink', // 音量大小竖向滑块颜色 + }, + controls: true, // 是否使用底部控制栏,默认使用 + miniprogress: true, // 是否使用mini进度条(当底部控制栏隐藏时生效) + screenShot: false, // 关闭截图功能 + rotate: true, // 是否使用视频旋转插件,默认不使用 + download: true, // 是否使用下载按钮,一般不用,一般自定义控制 + pip: true, // 使用使用画中画模式,默认不用 + mini: true, // 是否使用小屏幕控件 + cssFullscreen: true, // 是否使用网页样式全屏按钮开关 + playbackRate: [0.5, 1, 1.5, 2, 3], //传入倍速可选数组 + // playbackRate: true, //false,禁用倍速播放(即控制栏不显示) + keyShortcut: false, // 是否开启快捷键模式 +} diff --git a/src/components/IPlayer/index.vue b/src/components/IPlayer/index.vue new file mode 100644 index 0000000..55be899 --- /dev/null +++ b/src/components/IPlayer/index.vue @@ -0,0 +1,62 @@ + + + + + + diff --git a/src/components/IconSelect/index.vue b/src/components/IconSelect/index.vue new file mode 100644 index 0000000..b527b7d --- /dev/null +++ b/src/components/IconSelect/index.vue @@ -0,0 +1,111 @@ + + + + + \ No newline at end of file diff --git a/src/components/IconSelect/requireIcons.js b/src/components/IconSelect/requireIcons.js new file mode 100644 index 0000000..36b7539 --- /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..acd05c5 --- /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..5d829a9 --- /dev/null +++ b/src/components/ImageUpload/index.vue @@ -0,0 +1,254 @@ + + + + + \ No newline at end of file diff --git a/src/components/Pagination/index.vue b/src/components/Pagination/index.vue new file mode 100644 index 0000000..4424707 --- /dev/null +++ b/src/components/Pagination/index.vue @@ -0,0 +1,106 @@ + + + + + \ No newline at end of file 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..6d67021 --- /dev/null +++ b/src/components/RightToolbar/index.vue @@ -0,0 +1,243 @@ + + + + + diff --git a/src/components/RuoYi/Doc/index.vue b/src/components/RuoYi/Doc/index.vue new file mode 100644 index 0000000..8a8a21e --- /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..db1243f --- /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..e154614 --- /dev/null +++ b/src/components/Screenfull/index.vue @@ -0,0 +1,22 @@ + + + + + \ No newline at end of file diff --git a/src/components/SizeSelect/index.vue b/src/components/SizeSelect/index.vue new file mode 100644 index 0000000..1374578 --- /dev/null +++ b/src/components/SizeSelect/index.vue @@ -0,0 +1,43 @@ + + + + + \ No newline at end of file diff --git a/src/components/SvgIcon/index.vue b/src/components/SvgIcon/index.vue new file mode 100644 index 0000000..3cc47a4 --- /dev/null +++ b/src/components/SvgIcon/index.vue @@ -0,0 +1,59 @@ + + + + + diff --git a/src/components/SvgIcon/svgicon.js b/src/components/SvgIcon/svgicon.js new file mode 100644 index 0000000..89e39b5 --- /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/TreePanel/index.vue b/src/components/TreePanel/index.vue new file mode 100644 index 0000000..513b8e3 --- /dev/null +++ b/src/components/TreePanel/index.vue @@ -0,0 +1,756 @@ + + + + + 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..0eabaa4 --- /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..00a0867 --- /dev/null +++ b/src/layout/components/Navbar.vue @@ -0,0 +1,277 @@ + + + + + diff --git a/src/layout/components/Settings/index.vue b/src/layout/components/Settings/index.vue new file mode 100644 index 0000000..3953127 --- /dev/null +++ b/src/layout/components/Settings/index.vue @@ -0,0 +1,281 @@ + + + + + \ No newline at end of file diff --git a/src/layout/components/Sidebar/Link.vue b/src/layout/components/Sidebar/Link.vue new file mode 100644 index 0000000..8011431 --- /dev/null +++ b/src/layout/components/Sidebar/Link.vue @@ -0,0 +1,40 @@ + + + diff --git a/src/layout/components/Sidebar/Logo.vue b/src/layout/components/Sidebar/Logo.vue new file mode 100644 index 0000000..0373c4e --- /dev/null +++ b/src/layout/components/Sidebar/Logo.vue @@ -0,0 +1,105 @@ + + + + + diff --git a/src/layout/components/Sidebar/SidebarItem.vue b/src/layout/components/Sidebar/SidebarItem.vue new file mode 100644 index 0000000..2fa82bb --- /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..33e1141 --- /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..2fa2c69 --- /dev/null +++ b/src/layout/components/TagsView/index.vue @@ -0,0 +1,365 @@ + + + + + + + diff --git a/src/layout/components/TopBar/index.vue b/src/layout/components/TopBar/index.vue new file mode 100644 index 0000000..e42f28f --- /dev/null +++ b/src/layout/components/TopBar/index.vue @@ -0,0 +1,99 @@ + + + + + diff --git a/src/layout/components/TopNav/index.vue b/src/layout/components/TopNav/index.vue new file mode 100644 index 0000000..3810cfe --- /dev/null +++ b/src/layout/components/TopNav/index.vue @@ -0,0 +1,220 @@ + + + + + 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..bea196c --- /dev/null +++ b/src/layout/index.vue @@ -0,0 +1,111 @@ + + + + + diff --git a/src/models/UnifiedRobotModel.js b/src/models/UnifiedRobotModel.js new file mode 100644 index 0000000..1ef44f8 --- /dev/null +++ b/src/models/UnifiedRobotModel.js @@ -0,0 +1,181 @@ +/** + * Unified robot model data interface + * All formats (URDF, MJCF, USD) are converted to this unified format + */ +export class UnifiedRobotModel { + constructor() { + this.name = ''; + this.links = new Map(); // Map + this.joints = new Map(); // Map + this.materials = new Map(); // Map + this.constraints = new Map(); // Map - for parallel mechanism constraints + this.rootLink = null; // Root link name + this.threeObject = null; // Three.js object (if available) + } + + addLink(link) { + this.links.set(link.name, link); + } + + addJoint(joint) { + this.joints.set(joint.name, joint); + } + + addConstraint(constraint) { + this.constraints.set(constraint.name, constraint); + } + + getLink(name) { + return this.links.get(name); + } + + getJoint(name) { + return this.joints.get(name); + } + + getConstraint(name) { + return this.constraints.get(name); + } +} + +/** + * Link interface + */ +export class Link { + constructor(name) { + this.name = name; + this.visuals = []; // VisualGeometry[] + this.collisions = []; // CollisionGeometry[] + this.inertial = null; // InertialProperties + this.threeObject = null; // Three.js object + this.userData = {}; // User-defined data (for adapters to store additional information) + } +} + +/** + * VisualGeometry interface + */ +export class VisualGeometry { + constructor() { + this.name = ''; + this.origin = { xyz: [0, 0, 0], rpy: [0, 0, 0] }; + this.geometry = null; // GeometryType + this.material = null; // Material + this.threeObject = null; // Three.js Mesh + } +} + +/** + * CollisionGeometry interface + */ +export class CollisionGeometry { + constructor() { + this.name = ''; + this.origin = { xyz: [0, 0, 0], rpy: [0, 0, 0] }; + this.geometry = null; // GeometryType + this.threeObject = null; // Three.js Mesh + } +} + +/** + * GeometryType interface + */ +export class GeometryType { + constructor(type) { + this.type = type; // 'box' | 'sphere' | 'cylinder' | 'mesh' + this.size = null; // Size parameters (varies by type) + this.filename = null; // Mesh file path (if mesh type) + } + + clone() { + const cloned = new GeometryType(this.type); + cloned.size = this.size ? { ...this.size } : null; + cloned.filename = this.filename; + return cloned; + } +} + +/** + * InertialProperties interface + */ +export class InertialProperties { + constructor() { + this.mass = 0; + this.origin = { xyz: [0, 0, 0], rpy: [0, 0, 0] }; + this.ixx = 0; + this.iyy = 0; + this.izz = 0; + this.ixy = 0; + this.ixz = 0; + this.iyz = 0; + } +} + +/** + * Joint interface + */ +export class Joint { + constructor(name, type) { + this.name = name; + this.type = type; // 'revolute' | 'prismatic' | 'fixed' | 'continuous' + this.parent = null; // Parent link name + this.child = null; // Child link name + this.origin = { xyz: [0, 0, 0], rpy: [0, 0, 0] }; + this.axis = { xyz: [0, 0, 1] }; // Default z-axis + this.limits = null; // JointLimits + this.currentValue = 0; // Current joint value + this.threeObject = null; // Three.js object (if available) + } +} + +/** + * JointLimits interface + */ +export class JointLimits { + constructor() { + this.lower = -Math.PI; + this.upper = Math.PI; + this.effort = null; + this.velocity = null; + } +} + +/** + * Material interface + */ +export class Material { + constructor(name) { + this.name = name; + this.color = { r: 0.8, g: 0.8, b: 0.8 }; + this.texture = null; + } +} + +/** + * Constraint interface - for describing closed-chain constraints of parallel mechanisms + * Supports MuJoCo equality constraint types + */ +export class Constraint { + constructor(name, type) { + this.name = name; + this.type = type; // 'connect' | 'weld' | 'joint' | 'tendon' | 'distance' + + // Constraint objects (may be body, geom, joint, etc. depending on type) + this.body1 = null; + this.body2 = null; + this.anchor = null; // Connection point coordinates + this.torquescale = null; // Torque scale + + // Joint constraint specific properties + this.joint1 = null; + this.joint2 = null; + this.polycoef = null; // Polynomial coefficients [a0, a1, a2, a3, a4] + + // Visualization object + this.threeObject = null; // Three.js object for displaying constraint + + // Original data (for debugging) + this.userData = {}; + } +} + diff --git a/src/plugins/auth.js b/src/plugins/auth.js new file mode 100644 index 0000000..1354e87 --- /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..2ad84e2 --- /dev/null +++ b/src/plugins/cache.js @@ -0,0 +1,81 @@ +import { storageKey } from '@/runtime/config' + +const sessionCache = { + set (key, value) { + if (!sessionStorage) { + return + } + if (key != null && value != null) { + sessionStorage.setItem(storageKey(key), value) + } + }, + get (key) { + if (!sessionStorage) { + return null + } + if (key == null) { + return null + } + return sessionStorage.getItem(storageKey(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(storageKey(key)) + } +} +const localCache = { + set (key, value) { + if (!localStorage) { + return + } + if (key != null && value != null) { + localStorage.setItem(storageKey(key), value) + } + }, + get (key) { + if (!localStorage) { + return null + } + if (key == null) { + return null + } + return localStorage.getItem(storageKey(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(storageKey(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..e83e43c --- /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..01d6149 --- /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..5b1b5f4 --- /dev/null +++ b/src/plugins/tab.js @@ -0,0 +1,75 @@ +import useTagsViewStore from '@/store/modules/tagsView' +import router from '@/router' + +export default { + // 刷新当前tab页签 + refreshPage(obj) { + const { path, query, matched } = router.currentRoute.value + // 防止在重定向过程中重复刷新 + if (path.startsWith('/redirect/')) { + return Promise.resolve() + } + 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(title, url, params) { + const obj = { path: url, meta: { title: title } } + useTagsViewStore().addView(obj) + return router.push({ path: url, query: params }) + }, + // 修改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..2f2ee77 --- /dev/null +++ b/src/plugins/websocket.js @@ -0,0 +1,229 @@ +const MEDIA_HEADER_SIZE = 26; +const MEDIA_MAGIC = [0x43, 0x4d, 0x56, 0x46]; // CMVF +const MEDIA_MIME_TYPES = { + 1: 'video/mp4; codecs="avc1.42C01E"' +}; + +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.processingMessages = false; + this.maxQueueSize = 100; + this.status = false; + this.channelSubscriptions = new Set(); + } + + 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.restoreChannelSubscriptions(); + this.dispatch('open'); + }; + + this.ws.onmessage = async (event) => { + const data = event.data instanceof Blob ? await event.data.arrayBuffer() : event.data; + if (data instanceof ArrayBuffer && this.isMediaPacket(data)) { + this.dispatchMediaPacket(data); + return; + } + if (this.messageQueue.length >= this.maxQueueSize) { + console.warn('WebSocket控制消息队列已满,丢弃最旧消息'); + this.messageQueue.shift(); + } + this.messageQueue.push(data); + this.processMessageQueue(); + }; + + this.ws.onclose = () => { + console.log('WebSocket 断开'); + this.status = false; + this.stopHeartbeat(); + this.dispatch('close'); + if (!this.isManualClose) { + this.reconnect(); + } + }; + + this.ws.onerror = (err) => { + console.error('WebSocket 错误:', err); + }; + } + + isMediaPacket(buffer) { + if (buffer.byteLength < MEDIA_HEADER_SIZE) return false; + const bytes = new Uint8Array(buffer, 0, MEDIA_MAGIC.length); + return MEDIA_MAGIC.every((value, index) => bytes[index] === value); + } + + dispatchMediaPacket(buffer) { + try { + const view = new DataView(buffer); + const version = view.getUint8(4); + const mediaType = view.getUint8(5); + const timestamp = view.getUint32(8) * 0x100000000 + view.getUint32(12); + const sequence = view.getUint32(16); + const channelLength = view.getUint16(20); + const payloadLength = view.getUint32(22); + const payloadOffset = MEDIA_HEADER_SIZE + channelLength; + if (version !== 1 || payloadOffset + payloadLength !== buffer.byteLength) { + throw new Error('媒体数据包头无效'); + } + const channel = new TextDecoder().decode(new Uint8Array(buffer, MEDIA_HEADER_SIZE, channelLength)); + const mimeType = MEDIA_MIME_TYPES[mediaType]; + if (!mimeType) { + throw new Error(`不支持的媒体类型: ${mediaType}`); + } + this.dispatch(channel, { + media: true, + mimeType, + sequence, + timestamp, + data: buffer.slice(payloadOffset, payloadOffset + payloadLength) + }); + } catch (error) { + console.error('WebSocket媒体包解析失败:', error); + } + } + + async processMessageQueue() { + if (this.processingMessages || !this.ws) return; + this.processingMessages = true; + try { + while (this.messageQueue.length > 0) { + const data = this.messageQueue.shift(); + let message; + if (typeof data === 'string') { + message = JSON.parse(data); + } else if (data instanceof ArrayBuffer) { + const decompressedStream = new Blob([data]) + .stream() + .pipeThrough(new DecompressionStream('gzip')); + const decompressedData = await new Response(decompressedStream).arrayBuffer(); + message = JSON.parse(new TextDecoder().decode(decompressedData)); + } else { + throw new Error(`不支持的WebSocket数据类型: ${typeof data}`); + } + if (message.channel) { + this.dispatch(message.channel, message.payload); + } else if (message.type) { + this.dispatch(message.type, message.payload ?? message.content ?? message); + } + } + } catch (err) { + console.error('WebSocket消息解析失败:', err); + } finally { + this.processingMessages = false; + if (this.messageQueue.length > 0) { + queueMicrotask(() => this.processMessageQueue()); + } + } + } + + dispatch(eventName, payload) { + const callbacks = this.events[eventName]; + if (callbacks) { + callbacks.slice().forEach(callback => callback(payload)); + } + } + + 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 (payload?.type === 'channel_subscription' && payload.channel) { + if (payload.action === 'subscribe') { + this.channelSubscriptions.add(payload.channel); + } else if (payload.action === 'unsubscribe') { + this.channelSubscriptions.delete(payload.channel); + } + } + if (this.ws && this.ws.readyState === 1) { + this.ws.send(JSON.stringify(payload)); + } else if (payload?.type !== 'channel_subscription') { + console.warn('WebSocket 未连接,消息发送失败'); + } + } + + restoreChannelSubscriptions() { + if (!this.ws || this.ws.readyState !== 1) return; + this.channelSubscriptions.forEach((channel) => { + this.ws.send(JSON.stringify({ + type: 'channel_subscription', + action: 'subscribe', + channel, + })); + }); + } + + 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); + } +}; diff --git a/src/router/index.js b/src/router/index.js new file mode 100644 index 0000000..90076fd --- /dev/null +++ b/src/router/index.js @@ -0,0 +1,19 @@ +import { runtimeConfig } from '@/runtime/config' + +// The host application owns its route table. These exports keep the common +// layout and permission store compatible with the host's router facade. +export const constantRoutes = [] +export const dynamicRoutes = [] + +const routerFacade = new Proxy({}, { + get(_target, property) { + const router = runtimeConfig.router + if (!router) { + throw new Error('[cmvr-common] Router has not been configured') + } + const value = router[property] + return typeof value === 'function' ? value.bind(router) : value + } +}) + +export default routerFacade diff --git a/src/runtime/config.js b/src/runtime/config.js new file mode 100644 index 0000000..5fa1e6d --- /dev/null +++ b/src/runtime/config.js @@ -0,0 +1,24 @@ +const defaults = { + apiBaseURL: '', + tokenKey: 'CMVR-Token', + storagePrefix: 'cmvr', + cookieOptions: { path: '/', sameSite: 'Lax' }, + loginPath: '/login', + homePath: '/index', + router: null +} + +export const runtimeConfig = { ...defaults } + +export function configureRuntime(options = {}) { + Object.assign(runtimeConfig, options) + runtimeConfig.cookieOptions = { + ...defaults.cookieOptions, + ...(options.cookieOptions || {}) + } + return runtimeConfig +} + +export function storageKey(key) { + return `${runtimeConfig.storagePrefix}:${key}` +} diff --git a/src/settings.js b/src/settings.js new file mode 100644 index 0000000..b2a32c8 --- /dev/null +++ b/src/settings.js @@ -0,0 +1,67 @@ +export default { + /** + * 网页标题 + */ + title: import.meta.env.VITE_APP_TITLE, + + /** + * 侧边栏主题 深色主题theme-dark,浅色主题theme-light + */ + sideTheme: 'theme-dark', + + /** + * 是否系统布局配置 + */ + showSettings: true, + + /** + * 菜单导航模式 1、纯左侧 2、混合(左侧+顶部) 3、纯顶部 + */ + navType: 1, + + /** + * 是否显示 tagsView + */ + tagsView: true, + + /** + * 持久化标签页 + */ + tagsViewPersist: false, + + /** + * 显示页签图标 + */ + tagsIcon: false, + + /** + * 标签页样式:card 卡片(默认)、chrome 谷歌浏览器风格 + */ + tagsViewStyle: 'card', + + /** + * 是否固定头部 + */ + fixedHeader: false, + + /** + * 是否显示logo + */ + sidebarLogo: true, + + /** + * 是否显示动态标题 + */ + dynamicTitle: false, + + /** + * 是否显示底部版权 + */ + footerVisible: false, + + /** + * 底部版权文本内容 + */ + footerContent: 'Copyright © 2025 CMVR. All Rights Reserved.' +} + diff --git a/src/store/index.js b/src/store/index.js new file mode 100644 index 0000000..944c3c6 --- /dev/null +++ b/src/store/index.js @@ -0,0 +1,3 @@ +const store = createPinia() + +export default store diff --git a/src/store/modules/app.js b/src/store/modules/app.js new file mode 100644 index 0000000..9a92dd1 --- /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..e58e4ec --- /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..a78c228 --- /dev/null +++ b/src/store/modules/flow.js @@ -0,0 +1,13 @@ +import { defineStore } from 'pinia' + +export const useFlowStore = defineStore('flow', { + state: () => ({ + disableForm: false + }), + actions: { + updateDisableForm(value) { + this.disableForm = value + } + } +} +) 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/lock.js b/src/store/modules/lock.js new file mode 100644 index 0000000..4dd0fa3 --- /dev/null +++ b/src/store/modules/lock.js @@ -0,0 +1,27 @@ +const LOCK_KEY = 'screen-lock' +const LOCK_PATH_KEY = 'screen-lock-path' + +export const useLockStore = defineStore('lock', { + state: () => ({ + isLock: JSON.parse(localStorage.getItem(LOCK_KEY) || 'false'), + lockPath: localStorage.getItem(LOCK_PATH_KEY) || '/index' + }), + actions: { + // 锁定屏幕,同时记录当前路径 + lockScreen(currentPath) { + this.lockPath = currentPath || '/index' + localStorage.setItem(LOCK_PATH_KEY, this.lockPath) + this.isLock = true + localStorage.setItem(LOCK_KEY, 'true') + }, + // 解锁屏幕,清除路径 + unlockScreen() { + this.isLock = false + localStorage.setItem(LOCK_KEY, 'false') + this.lockPath = '/index' + localStorage.setItem(LOCK_PATH_KEY, '/index') + } + } +}) + +export default useLockStore diff --git a/src/store/modules/permission.js b/src/store/modules/permission.js new file mode 100644 index 0000000..a7f3835 --- /dev/null +++ b/src/store/modules/permission.js @@ -0,0 +1,158 @@ +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)) + normalizeRouteNames(sdata) + normalizeRouteNames(rdata) + normalizeRouteNames(defaultData) + 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 { + const componentPath = route.component + const component = loadView(componentPath) + if (!component) { + console.error(`[dynamic-route] component not found: ${componentPath}, menu=${route.meta?.title || route.name || route.path}`) + return false + } + route.component = 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 + }) +} + +// Route names are global identifiers in Vue Router. Older menu data can contain +// duplicate names under different parents, which makes later routes replace earlier ones. +function normalizeRouteNames(routes, usedNames = new Set(), parentPath = '') { + routes.forEach(route => { + const fullPath = `${parentPath}/${route.path || ''}`.replace(/\/+/g, '/') + if (route.name) { + const originalName = String(route.name) + let uniqueName = originalName + if (usedNames.has(uniqueName)) { + const suffix = fullPath.replace(/[^a-zA-Z0-9]+/g, '_').replace(/^_|_$/g, '') || 'route' + uniqueName = `${originalName}_${suffix}` + let sequence = 2 + while (usedNames.has(uniqueName)) uniqueName = `${originalName}_${suffix}_${sequence++}` + console.warn(`[dynamic-route] duplicate name "${originalName}" renamed to "${uniqueName}"`) + route.name = uniqueName + } + usedNames.add(uniqueName) + } + if (route.children?.length) normalizeRouteNames(route.children, usedNames, fullPath) + }) +} + +function filterChildren(childrenMap, lastRouter = false) { + var children = [] + childrenMap.forEach(el => { + el.path = lastRouter ? lastRouter.path + '/' + el.path : el.path + if (el.children && el.children.length && el.component === 'ParentView') { + children = children.concat(filterChildren(el.children, el)) + } else { + children.push(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..95841ee --- /dev/null +++ b/src/store/modules/settings.js @@ -0,0 +1,57 @@ +import defaultSettings from '@/settings' +import { useDark, useToggle } from '@vueuse/core' +import { useDynamicTitle } from '@/utils/dynamicTitle' +import { handleThemeStyle } from '@/utils/theme' + +const isDark = useDark() +const toggleDark = useToggle(isDark) + +const { sideTheme, showSettings, navType, tagsView, tagsViewPersist, tagsIcon, tagsViewStyle, fixedHeader, sidebarLogo, dynamicTitle, footerVisible, footerContent } = 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, + navType: storageSetting.navType === undefined ? navType : storageSetting.navType, + tagsView: storageSetting.tagsView === undefined ? tagsView : storageSetting.tagsView, + tagsViewPersist: storageSetting.tagsViewPersist === undefined ? tagsViewPersist : storageSetting.tagsViewPersist, + tagsIcon: storageSetting.tagsIcon === undefined ? tagsIcon : storageSetting.tagsIcon, + tagsViewStyle: storageSetting.tagsViewStyle === undefined ? tagsViewStyle : storageSetting.tagsViewStyle, + fixedHeader: storageSetting.fixedHeader === undefined ? fixedHeader : storageSetting.fixedHeader, + sidebarLogo: storageSetting.sidebarLogo === undefined ? sidebarLogo : storageSetting.sidebarLogo, + dynamicTitle: storageSetting.dynamicTitle === undefined ? dynamicTitle : storageSetting.dynamicTitle, + footerVisible: storageSetting.footerVisible === undefined ? footerVisible : storageSetting.footerVisible, + footerContent: footerContent, + 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() + nextTick(() => { + handleThemeStyle(this.theme) + }) + } + } + }) + +export default useSettingsStore diff --git a/src/store/modules/tagsView.js b/src/store/modules/tagsView.js new file mode 100644 index 0000000..213743a --- /dev/null +++ b/src/store/modules/tagsView.js @@ -0,0 +1,226 @@ +import cache from '@/plugins/cache' +import useSettingsStore from '@/store/modules/settings' + +const PERSIST_KEY = 'tags-view-visited' + +function isPersistEnabled() { + return useSettingsStore().tagsViewPersist +} + +function saveVisitedViews(views) { + if (!isPersistEnabled()) return + const toSave = views.filter(v => !(v.meta && v.meta.affix)).map(v => ({ path: v.path, fullPath: v.fullPath, name: v.name, title: v.title, query: v.query, meta: v.meta })) + cache.local.setJSON(PERSIST_KEY, toSave) +} + +function loadVisitedViews() { + return cache.local.getJSON(PERSIST_KEY) || [] +} + +function clearVisitedViews() { + cache.local.remove(PERSIST_KEY) +} + +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' + }) + ) + saveVisitedViews(this.visitedViews) + }, + addAffixView(view) { + if (this.visitedViews.some(v => v.path === view.path)) return + this.visitedViews.unshift( + 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) + saveVisitedViews(this.visitedViews) + 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) + saveVisitedViews(this.visitedViews) + 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 = [] + clearVisitedViews() + 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 + }) + saveVisitedViews(this.visitedViews) + 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 + }) + saveVisitedViews(this.visitedViews) + resolve([...this.visitedViews]) + }) + }, + // 恢复持久化的 tags + loadPersistedViews() { + const views = loadVisitedViews() + views.forEach(view => { + this.addVisitedView(view) + }) + } + } + }) + +export default useTagsViewStore diff --git a/src/store/modules/user.js b/src/store/modules/user.js new file mode 100644 index 0000000..66ed920 --- /dev/null +++ b/src/store/modules/user.js @@ -0,0 +1,95 @@ +import router from '@/router' +import cache from '@/plugins/cache' +import { ElMessageBox, } from 'element-plus' +import { login, logout, getInfo } from '@/api/login' +import { getToken, setToken, removeToken } from '@/utils/auth' +import { isHttp, isEmpty } from "@/utils/validate" +import useLockStore from '@/store/modules/lock' +import defAva from '@/assets/images/profile.jpg' + +const useUserStore = defineStore( + 'user', + { + state: () => ({ + token: getToken(), + id: '', + name: '', + nickName: '', + 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 + useLockStore().unlockScreen() + 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.nickName = user.nickName + this.avatar = avatar + cache.session.set('pwrChrtype', res.pwdChrtype) + /* 初始密码提示 */ + if(res.isDefaultModifyPwd) { + ElMessageBox.confirm('您的密码还是初始密码,请修改密码!', '安全提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => { + router.push({ name: 'Profile', params: { activeTab: 'resetPwd' } }) + }).catch(() => {}) + } + /* 过期密码提示 */ + if(!res.isDefaultModifyPwd && res.isPasswordExpired) { + ElMessageBox.confirm('您的密码已过期,请尽快修改密码!', '安全提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => { + router.push({ name: 'Profile', params: { activeTab: 'resetPwd' } }) + }).catch(() => {}) + } + 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/actionQueue.js b/src/utils/actionQueue.js new file mode 100644 index 0000000..33af39e --- /dev/null +++ b/src/utils/actionQueue.js @@ -0,0 +1,375 @@ +const deepClone = (value) => JSON.parse(JSON.stringify(value)) + +const ACTION_QUEUE_ACTION = 'SYSTEM_EXECUTE_ACTION_QUEUE' +const REFERENCE_KEYS = new Set(['quote', 'nameQuote']) +const DEFAULT_TOTAL_TIMEOUT_BUFFER_MS = 5000 + +const createKey = (prefix) => { + const random = globalThis.crypto?.randomUUID?.().replace(/-/g, '') + || `${Date.now()}${Math.random().toString(16).slice(2)}` + return `${prefix}_${random}` +} + +const paramsToObject = (node) => Object.fromEntries( + (node?.properties?.nodeParams || []).map((param) => [param.name, param.input]), +) + +const numberValue = (value, fallback = 0) => { + if (value === '' || value === null || value === undefined) return fallback + const result = Number(value) + return Number.isFinite(result) ? result : fallback +} + +const arrayValue = (value) => { + if (Array.isArray(value)) return deepClone(value) + if (typeof value !== 'string') return value + try { + const result = JSON.parse(value) + return Array.isArray(result) ? result : value + } catch { + return value + } +} + +const nodeToQueueStep = (node) => { + const action = node?.properties?.action + const params = paramsToObject(node) + const common = { + stepName: node?.properties?.name || '导入步骤', + sourceNode: deepClone(node), + } + if (node?.type === 'sleep' || String(action).toLowerCase() === 'sleep') { + const durationMs = numberValue(params.delayMs) + return { + ...common, + type: 'DELAY', + timeoutMs: Math.max(durationMs + 1000, 5000), + params: { durationMs }, + } + } + if (action === 'AGV_MOVE_TO_POINT') { + return { + ...common, + type: 'AGV_NAVIGATE_TO_POSE', + timeoutMs: 60000, + params: { + x: numberValue(params.x), + y: numberValue(params.y), + theta: numberValue(params.theta), + maxSpeed: numberValue(params.maxSpeed, 0.5), + blockedTimeoutMs: numberValue(params.blockedTimeoutMs, 60000), + }, + } + } + if (action === 'AGV_MOVE_TO_STATION') { + return { + ...common, + type: 'AGV_NAVIGATE_TO_STATION', + timeoutMs: 60000, + params: { + stationId: params.stationId ?? '', + maxSpeed: numberValue(params.maxSpeed, 0.5), + blockedTimeoutMs: numberValue(params.blockedTimeoutMs, 60000), + }, + } + } + if (action === 'ARM_MOVE_TO_POINT') { + return { + ...common, + type: 'ARM_MOVE_L', + timeoutMs: 3000, + params: { + x: numberValue(params.x), + y: numberValue(params.y), + z: numberValue(params.z), + rx: numberValue(params.rx), + ry: numberValue(params.ry), + rz: numberValue(params.rz), + frame: params.frame || 'BASE', + velocity: numberValue(params.velocity, 2.1), + acceleration: numberValue(params.acceleration, 2.2), + }, + } + } + if (action === 'ARM_MOVE_TO_J') { + return { + ...common, + type: 'ARM_MOVE_J', + timeoutMs: 10000, + params: { + positions: arrayValue(params.target), + velocity: numberValue(params.velocity, 0.1), + acceleration: numberValue(params.acceleration, 0.2), + }, + } + } + return null +} + +const validateConvertibleNodeParams = (node) => { + const action = node?.properties?.action + const params = paramsToObject(node) + const requiredByAction = { + AGV_MOVE_TO_POINT: ['x', 'y', 'theta'], + AGV_MOVE_TO_STATION: ['stationId'], + ARM_MOVE_TO_POINT: ['x', 'y', 'z', 'rx', 'ry', 'rz'], + ARM_MOVE_TO_J: ['target'], + } + const missing = (requiredByAction[action] || []).find((name) => ( + params[name] === '' || params[name] === null || params[name] === undefined + )) + if (missing) return `节点[${node.properties?.name || node.id}]的参数 ${missing} 尚未配置` + if (action === 'ARM_MOVE_TO_J') { + const positions = arrayValue(params.target) + if (!Array.isArray(positions) || positions.length === 0) return '机械臂关节位置必须是非空 JSON 数组' + } + if ((node?.type === 'sleep' || String(action).toLowerCase() === 'sleep') + && numberValue(params.delayMs) < 0) return '睡眠时长不能小于 0' + return '' +} + +const hasReference = (value, referencedNodeId, parentKey = '') => { + if (Array.isArray(value)) { + if (REFERENCE_KEYS.has(parentKey) && value[0] === referencedNodeId) return true + return value.some((item) => hasReference(item, referencedNodeId)) + } + if (!value || typeof value !== 'object') return false + return Object.entries(value).some(([key, item]) => hasReference(item, referencedNodeId, key)) +} + +const hasDynamicInput = (node) => (node?.properties?.nodeParams || []).some((param) => ( + param?.type === 'quote' + || (Array.isArray(param?.quote) && param.quote.length > 0) +)) + +const getNodeParam = (node, name, fallback) => ( + node.properties?.nodeParams?.find((param) => param.name === name)?.input ?? fallback +) + +const setNodeParam = (node, name, input, defaults = {}) => { + node.properties ||= {} + node.properties.nodeParams ||= [] + const param = node.properties.nodeParams.find((item) => item.name === name) + if (param) param.input = input + else node.properties.nodeParams.push({ name, type: 'input', input, ...defaults }) +} + +const queueSteps = (queueNode) => { + const value = getNodeParam(queueNode, 'steps', []) + return Array.isArray(value) ? deepClone(value) : [] +} + +const edgeExists = (edges, sourceNodeId, targetNodeId) => edges.some((edge) => ( + edge.sourceNodeId === sourceNodeId && edge.targetNodeId === targetNodeId +)) + +const createEdge = (sourceNodeId, targetNodeId, sourceAnchorId, targetAnchorId) => ({ + id: createKey('edge'), + type: 'bezier', + sourceNodeId, + targetNodeId, + sourceAnchorId: sourceAnchorId || `${sourceNodeId}_1`, + targetAnchorId: targetAnchorId || `${targetNodeId}_3`, +}) + +const normalizeQueueStepIds = (steps) => steps.map((step, index) => ({ + ...step, + stepId: `step_${index + 1}`, +})) + +const updateQueueDefinition = (queueNode, steps) => { + const normalizedSteps = normalizeQueueStepIds(steps) + setNodeParam(queueNode, 'steps', normalizedSteps, { required: true }) + const timeoutSum = normalizedSteps.reduce((sum, step) => sum + Math.max(0, numberValue(step.timeoutMs)), 0) + const currentTimeout = numberValue(getNodeParam(queueNode, 'totalTimeoutMs', 60000), 60000) + setNodeParam( + queueNode, + 'totalTimeoutMs', + Math.max(currentTimeout, timeoutSum + DEFAULT_TOTAL_TIMEOUT_BUFFER_MS), + { componentType: 'number', required: true }, + ) + const bindings = normalizedSteps.filter((step) => step.deviceBindingKey && step.deviceSlotKey).map((step) => ({ + parameterName: step.deviceBindingKey, + slotKey: step.deviceSlotKey, + })) + queueNode.properties.executionTarget = { + ...(queueNode.properties.executionTarget || {}), + dispatchPolicy: 'ONE', + deviceBindings: bindings, + } +} + +export const isNodeConvertibleToActionQueue = (node) => Boolean(nodeToQueueStep(node)) + +/** + * Converts one workflow node into an edge action-queue step and rewrites its former + * linear position out of the graph. The source definition is retained for extraction. + */ +export const appendNodeToActionQueue = (sourceGraph, nodeId, queueId) => { + const graph = deepClone(sourceGraph || { nodes: [], edges: [] }) + const node = graph.nodes.find((item) => item.id === nodeId) + const queueNode = graph.nodes.find((item) => item.id === queueId) + if (!node || !queueNode) return { ok: false, message: '未找到要移动的节点或动作队列' } + if (node.id === queueNode.id) return { ok: false, message: '动作队列不能放入自身' } + if (queueNode.properties?.action !== ACTION_QUEUE_ACTION) return { ok: false, message: '目标节点不是动作队列' } + const step = nodeToQueueStep(node) + if (!step) return { ok: false, message: `节点[${node.properties?.name || node.id}]暂不支持放入动作队列` } + const paramError = validateConvertibleNodeParams(node) + if (paramError) return { ok: false, message: paramError } + if (node.properties?.parentId !== queueNode.properties?.parentId) { + return { ok: false, message: '节点和动作队列必须位于同一个流程或循环体中' } + } + if (hasDynamicInput(node)) return { ok: false, message: '引用其他节点参数的节点暂不能放入动作队列' } + const referencedBy = graph.nodes.find((item) => item.id !== nodeId && hasReference(item.properties, nodeId)) + if (referencedBy) { + return { ok: false, message: `节点[${referencedBy.properties?.name || referencedBy.id}]正在引用该节点的输出` } + } + + const incoming = graph.edges.filter((edge) => edge.targetNodeId === nodeId) + const outgoing = graph.edges.filter((edge) => edge.sourceNodeId === nodeId) + if (incoming.length > 1 || outgoing.length > 1) { + return { ok: false, message: '存在分支连线的节点不能放入动作队列' } + } + if (incoming.length === 1 + && outgoing.length === 1 + && incoming[0].sourceNodeId === outgoing[0].targetNodeId) { + return { ok: false, message: '节点处于回路中,不能放入动作队列' } + } + + const nodeRoleKey = node.properties?.executionTarget?.roleKey + const queueRoleKey = queueNode.properties?.executionTarget?.roleKey + if (nodeRoleKey && queueRoleKey && nodeRoleKey !== queueRoleKey) { + return { ok: false, message: '节点与动作队列使用的机器人角色不同' } + } + if (nodeRoleKey && !queueRoleKey) queueNode.properties.executionTarget = { roleKey: nodeRoleKey } + + const deviceSlotKey = node.properties?.executionTarget?.deviceSlotKey + if (deviceSlotKey) { + step.deviceBindingKey = createKey('queue_device') + step.deviceSlotKey = deviceSlotKey + } + step.stepId = `step_${queueSteps(queueNode).length + 1}` + const steps = [...queueSteps(queueNode), step] + updateQueueDefinition(queueNode, steps) + + graph.nodes = graph.nodes.filter((item) => item.id !== nodeId) + graph.nodes.forEach((item) => { + if (Array.isArray(item.children)) item.children = item.children.filter((childId) => childId !== nodeId) + }) + graph.edges = graph.edges.filter((edge) => edge.sourceNodeId !== nodeId && edge.targetNodeId !== nodeId) + + const predecessorId = incoming[0]?.sourceNodeId + const successorId = outgoing[0]?.targetNodeId + if (predecessorId && successorId && predecessorId !== successorId + && !edgeExists(graph.edges, predecessorId, successorId)) { + graph.edges.push(createEdge( + predecessorId, + successorId, + incoming[0].sourceAnchorId, + outgoing[0].targetAnchorId, + )) + } + return { ok: true, graph, queueId, step } +} + +const remapNodeAnchors = (node, previousId, nextId) => { + const properties = node.properties || (node.properties = {}) + if (!Array.isArray(properties.anchor)) return + properties.anchor = properties.anchor.map((anchor) => ({ + ...anchor, + id: String(anchor.id || '').startsWith(`${previousId}_`) + ? `${nextId}${String(anchor.id).slice(previousId.length)}` + : anchor.id, + })) +} + +/** Restores an imported queue step as a workflow node. */ +export const extractActionQueueStep = (sourceGraph, queueId, stepIndex, placement = 'detached') => { + const graph = deepClone(sourceGraph || { nodes: [], edges: [] }) + const queueNode = graph.nodes.find((item) => item.id === queueId) + if (!queueNode) return { ok: false, message: '动作队列不存在' } + const steps = queueSteps(queueNode) + const step = steps[stepIndex] + if (!step) return { ok: false, message: '要移出的步骤不存在' } + if (!step.sourceNode) return { ok: false, message: '该步骤不是由工作流节点移入,无法还原为原节点' } + + const node = deepClone(step.sourceNode) + const previousId = node.id + node.id = createKey('node') + remapNodeAnchors(node, previousId, node.id) + delete node.children + node.properties ||= {} + node.properties.parentId = queueNode.properties?.parentId + if (!node.properties.parentId) delete node.properties.parentId + const horizontalOffset = placement === 'before' ? -460 : 460 + node.x = numberValue(queueNode.x) + (placement === 'detached' ? 0 : horizontalOffset) + node.y = numberValue(queueNode.y) + (placement === 'detached' ? 260 : 0) + + const incoming = graph.edges.filter((edge) => edge.targetNodeId === queueId) + const outgoing = graph.edges.filter((edge) => edge.sourceNodeId === queueId) + if ((placement === 'before' && incoming.length > 1) || (placement === 'after' && outgoing.length > 1)) { + return { ok: false, message: '动作队列存在多条相关连线,请选择“仅移出,不连线”' } + } + + graph.nodes.push(node) + if (steps.length === 1) { + graph.nodes = graph.nodes.filter((item) => item.id !== queueId) + graph.edges.forEach((edge) => { + if (edge.sourceNodeId === queueId) { + edge.sourceNodeId = node.id + edge.sourceAnchorId = `${node.id}_1` + } + if (edge.targetNodeId === queueId) { + edge.targetNodeId = node.id + edge.targetAnchorId = `${node.id}_3` + } + }) + return { ok: true, graph, node, removedQueue: true } + } + + steps.splice(stepIndex, 1) + updateQueueDefinition(queueNode, steps) + if (placement === 'before') { + graph.edges = graph.edges.filter((edge) => edge.targetNodeId !== queueId) + const previous = incoming[0] + if (previous) graph.edges.push(createEdge(previous.sourceNodeId, node.id, previous.sourceAnchorId, `${node.id}_3`)) + graph.edges.push(createEdge(node.id, queueId, `${node.id}_1`, `${queueId}_3`)) + } else if (placement === 'after') { + graph.edges = graph.edges.filter((edge) => edge.sourceNodeId !== queueId) + const next = outgoing[0] + graph.edges.push(createEdge(queueId, node.id, `${queueId}_1`, `${node.id}_3`)) + if (next) graph.edges.push(createEdge(node.id, next.targetNodeId, `${node.id}_1`, next.targetAnchorId)) + } + return { ok: true, graph, node, removedQueue: false } +} + +export const getActionQueueStepName = (step, index) => { + const name = String(step?.stepName || '').trim() + return name || `步骤 ${index + 1}` +} + +export const moveActionQueueStep = (steps, index, offset) => { + const targetIndex = index + offset + if (!Array.isArray(steps) || index < 0 || targetIndex < 0 || targetIndex >= steps.length) return false + const [step] = steps.splice(index, 1) + steps.splice(targetIndex, 0, step) + return true +} + +export const copyActionQueueStep = (step, overrides = {}) => { + const copied = deepClone(step) + const originalName = String(step?.stepName || '').trim() + return { + ...copied, + ...overrides, + stepName: `${originalName || '未命名步骤'} - 副本`, + } +} + +export const toActionQueueExecutionSteps = (steps) => (Array.isArray(steps) ? steps : []).map((step, index) => ({ + stepId: `step_${index + 1}`, + type: step.type, + ...(step.deviceId ? { deviceId: step.deviceId } : {}), + timeoutMs: Number(step.timeoutMs || 0), + params: deepClone(step.params || {}), +})) diff --git a/src/utils/agvRuntimeStatus.js b/src/utils/agvRuntimeStatus.js new file mode 100644 index 0000000..5d34334 --- /dev/null +++ b/src/utils/agvRuntimeStatus.js @@ -0,0 +1,18 @@ +const AGV_RUNTIME_STATUS = { + 0: { label: '未知', type: 'info', className: 'unknown' }, + 1: { label: '已断开', type: 'info', className: 'offline' }, + 2: { label: '空闲', type: 'success', className: 'online' }, + 3: { label: '手动模式', type: 'primary', className: 'manual' }, + 4: { label: '自动运行', type: 'primary', className: 'running' }, + 5: { label: '充电中', type: 'warning', className: 'charging' }, + 6: { label: '已暂停', type: 'warning', className: 'paused' }, + 7: { label: '已停止', type: 'info', className: 'stopped' }, + 8: { label: '故障', type: 'danger', className: 'fault' }, + 9: { label: '急停', type: 'danger', className: 'emergency' }, +} + +const resolveStatus = value => AGV_RUNTIME_STATUS[Number(value)] || AGV_RUNTIME_STATUS[0] + +export const agvRuntimeStatusLabel = value => resolveStatus(value).label +export const agvRuntimeStatusType = value => resolveStatus(value).type +export const agvRuntimeStatusClass = value => resolveStatus(value).className diff --git a/src/utils/auth.js b/src/utils/auth.js new file mode 100644 index 0000000..1d40650 --- /dev/null +++ b/src/utils/auth.js @@ -0,0 +1,14 @@ +import Cookies from 'js-cookie' +import { runtimeConfig } from '@/runtime/config' + +export function getToken() { + return Cookies.get(runtimeConfig.tokenKey) +} + +export function setToken(token) { + return Cookies.set(runtimeConfig.tokenKey, token, runtimeConfig.cookieOptions) +} + +export function removeToken() { + return Cookies.remove(runtimeConfig.tokenKey, runtimeConfig.cookieOptions) +} diff --git a/src/utils/convertNodeRedToVueFlow.js b/src/utils/convertNodeRedToVueFlow.js new file mode 100644 index 0000000..14d97f8 --- /dev/null +++ b/src/utils/convertNodeRedToVueFlow.js @@ -0,0 +1,76 @@ +import { getNodeRedFlowById as getFlowById } from '@/api/flow/nodeRed'; + +const loadNodeRedData = async (id) => { + const data = await getFlowById(id); + return Array.isArray(data?.nodes) ? data.nodes : []; +}; + +export const convertNodeRedToVueFlow = async (id) => { + const source = await loadNodeRedData(id); + const nodes = source.map((node) => { + const isGroup = node.type === "group"; + const width = isGroup ? Number(node.w || 300) + 80 : 200; + const height = isGroup ? Number(node.h || 180) + 80 : 100; + const center = { x: Number(node.x || 0) * 2, y: Number(node.y || 0) * 2 }; + return { + id: node.id, + type: isGroup ? "runtime-group" : "runtime-node", + position: { x: center.x - width / 2, y: center.y - height / 2 }, + width, + height, + draggable: false, + selectable: true, + connectable: false, + deletable: false, + zIndex: isGroup ? -1 : 1, + data: { + text: node.name || node.type, + properties: { ...node, state: "unexecuted" }, + children: isGroup ? node.nodes || [] : [], + }, + }; + }); + + const edges = []; + source.forEach((node) => { + (node.wires || []).forEach((targets) => { + (targets || []).forEach((targetId) => { + edges.push({ + id: `edge-${node.id}-${targetId}`, + source: node.id, + target: targetId, + sourceHandle: `${node.id}_1`, + targetHandle: `${targetId}_3`, + type: "bezier", + animated: true, + selectable: true, + focusable: true, + interactionWidth: 32, + markerEnd: { + type: "arrowclosed", + color: "#2f80ed", + width: 20, + height: 20, + }, + style: { stroke: "#2f80ed", strokeWidth: 2 }, + }); + }); + }); + }); + + return { nodes, edges }; +}; + +export const updateDerivedNodeStates = (nodes, getProperties, setProperties) => { + const ignored = ["inject", "cmvrLoop", "startCamera", "getImage", "stopCamera", "startVideo", "stopVideo", "end"]; + nodes.filter((node) => !ignored.includes(node.data.properties.type)).forEach((current) => { + nodes.forEach((target) => { + if (current.data.properties.wires?.[0]?.includes(target.id) && getProperties(target.id).state !== "unexecuted") { + setProperties(current.id, { state: "success" }); + } + if (current.data.properties.wires?.[1]?.includes(target.id)) { + setProperties(current.id, { state: getProperties(target.id).state === "failed" ? "failed" : "running" }); + } + }); + }); +}; diff --git a/src/utils/dict.js b/src/utils/dict.js new file mode 100644 index 0000000..b154f72 --- /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..ca4aa5d --- /dev/null +++ b/src/utils/dynamicTitle.js @@ -0,0 +1,14 @@ +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..5344c54 --- /dev/null +++ b/src/utils/flow.js @@ -0,0 +1,513 @@ +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 matchedEdges = edges.filter(item => { + return item.sourceNodeId === nodeId && item.sourceAnchorId === sourceAnchorId + }) + + matchedEdges.forEach(edge => lf.deleteEdge(edge.id)) +} + +const buildParentByChild = (data) => { + const result = new Map(); + ;(data.nodes || []).forEach((node) => { + ;(node.children || []).forEach((childId) => result.set(String(childId), String(node.id))); + }); + ;(data.nodes || []).forEach((node) => { + if (node.properties?.parentId) result.set(String(node.id), String(node.properties.parentId)); + }); + return result; +}; + +const collectPredecessors = (data, targetId, result) => { + const pending = [String(targetId)]; + while (pending.length) { + const currentId = pending.pop(); + ;(data.edges || []).forEach((edge) => { + if (String(edge.targetNodeId) !== currentId) return; + const sourceId = String(edge.sourceNodeId); + if (result.has(sourceId)) return; + result.add(sourceId); + pending.push(sourceId); + }); + } +}; + +/** + * Returns nodes whose values are available before the target executes. Loop children + * may read earlier siblings and nodes before their loop, but never later children, + * another loop body, or nodes after the loop. + */ +export const getUpstreamNodeIds = (data, nodeId) => { + const graph = data || { nodes: [], edges: [] }; + const targetId = String(nodeId); + const nodeById = new Map((graph.nodes || []).map((node) => [String(node.id), node])); + const parentByChild = buildParentByChild(graph); + const result = new Set( + (graph.nodes || []).filter((node) => node.type === 'start').map((node) => String(node.id)) + ); + + // Walk from the node through every containing loop. At each level, only nodes in + // that exact scope are visible; this also supports legacy nested-loop workflows. + let currentTargetId = targetId; + let currentScopeId = parentByChild.get(targetId) || ''; + const visitedScopes = new Set(); + while (!visitedScopes.has(currentTargetId)) { + visitedScopes.add(currentTargetId); + const upstream = new Set(); + collectPredecessors(graph, currentTargetId, upstream); + upstream.forEach((sourceId) => { + const sourceScopeId = parentByChild.get(sourceId) || ''; + if (sourceScopeId === currentScopeId && nodeById.has(sourceId)) result.add(sourceId); + }); + if (!currentScopeId) break; + currentTargetId = currentScopeId; + currentScopeId = parentByChild.get(currentScopeId) || ''; + } + result.delete(targetId); + return result; +}; + +export const getUpstreamInput = (nodeId) => { + const data = lf.getGraphData(); + const startIds = new Set( + data.nodes.filter((node) => node.type === 'start').map((node) => String(node.id)) + ); + const upstreamIds = getUpstreamNodeIds(data, nodeId); + return getInput(nodeId) + .filter((option) => upstreamIds.has(String(option.value))) + .map((option) => ({ + ...option, + children: option.children.filter((group) => + startIds.has(String(option.value)) ? group.value === 'input' : group.value === 'output' + ) + })) + .filter((option) => option.children.some((group) => group.children?.length)); +} + +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; +} + +export const transformRecognizeNodeData = (source) => { + // 初始化结果对象 + const result = { + nodeParams: [], + alarmRules: [] + }; + + // 遍历每一个顶级节点 + source.forEach(item => { + const { name, children } = item; + if (!name) return; // 过滤无name的无效节点 + + // 处理普通节点(config/headers/params):直接取 children 数组 + if (name !== 'alarmRules') { + result['nodeParams'].push(item); + return; + } + + if (name === 'alarmRules') { + if (Array.isArray(item.input)) { + result[name] = item.input; + return; + } + + // 兼容旧数据:旧结构把 level/operator/threshold/message + // 作为 alarmRules.children 中的字段描述保存。 + const legacyRule = Object.fromEntries( + (children || []) + .filter(child => ['level', 'operator', 'threshold', 'message'].includes(child.name)) + .map(child => [child.name, child.input]) + ); + result[name] = Object.keys(legacyRule).length ? [legacyRule] : []; + return; + } + + // 专门处理 tts 节点(特殊结构) + // if (name === 'alarmRules') { + // const bodyObj = { + // level: '', + // operator: '', + // threshold: '', + // message: '' + // }; + + // // 遍历body的子项,赋值到对应字段 + // children?.forEach(child => { + // const childName = child.name; + // if (childName === 'level') { + // bodyObj.level = child.input; + // } + // if (childName === 'operator') { + // bodyObj.operator = child.input; + // } + // if (childName === 'threshold') { + // bodyObj.threshold = child.input; + // } + // if (childName === 'message') { + // bodyObj.message = child.input; + // } + // }); + + // result.alarmRules = bodyObj; + // } + }); + + return result; +} diff --git a/src/utils/flowExecution.js b/src/utils/flowExecution.js new file mode 100644 index 0000000..56bc7f9 --- /dev/null +++ b/src/utils/flowExecution.js @@ -0,0 +1,120 @@ +import { getTaskConfig } from '@/api/test/config' +import { getDetect } from '@/api/test/detect' +import { + getResourceRoles, + mergeResourceRoleDefinitions, + normalizeFlowResourceSchema, + validateFlowResources, +} from '@/utils/flowResources' + +const clone = (value) => JSON.parse(JSON.stringify(value || [])) + +const parseFlowData = (itemId, flowData) => { + if (!flowData) throw new Error(`流程项[${itemId}]还没有配置工作流`) + try { + const graph = typeof flowData === 'string' ? JSON.parse(flowData) : flowData + const normalizedGraph = normalizeFlowResourceSchema(graph) + const validation = validateFlowResources(normalizedGraph) + if (validation.errors.length) { + throw new Error(validation.errors[0].message) + } + return validation.graph + } catch (error) { + throw new Error(`流程项[${itemId}]不可执行:${error.message || '工作流数据无法解析'}`) + } +} + +const assertCompatibleDefinitions = (roleGroups) => { + const rolesByKey = new Map() + roleGroups.flat().forEach((role) => { + const existing = rolesByKey.get(role.roleKey) + if (!existing) { + rolesByKey.set(role.roleKey, clone([role])[0]) + return + } + const slotsByKey = new Map((existing.deviceSlots || []).map((slot) => [slot.slotKey, slot])) + ;(role.deviceSlots || []).forEach((slot) => { + const existingSlot = slotsByKey.get(slot.slotKey) + if (existingSlot && String(existingSlot.deviceKind ?? '') !== String(slot.deviceKind ?? '')) { + throw new Error(`机器人角色“${existing.displayName}”的设备用途“${slot.displayName}”在不同流程中类型不一致`) + } + if (!existingSlot) existing.deviceSlots.push(clone([slot])[0]) + }) + }) +} + +export const loadFlowItemRuntimeRoles = async (items = []) => { + if (items.some((item) => !item?.itemId)) throw new Error('任务编排中存在缺少流程项ID的数据') + const uniqueItems = Array.from(new Map( + items.filter((item) => item?.itemId).map((item) => [item.itemId, item]), + ).values()) + if (!uniqueItems.length) throw new Error('当前任务没有编排任何流程项') + + const roleGroups = await Promise.all(uniqueItems.map(async (item) => { + const response = await getDetect(item.itemId) + const graph = parseFlowData(item.itemName || item.itemId, response.data?.flowData) + return getResourceRoles(graph) + })) + assertCompatibleDefinitions(roleGroups) + return mergeResourceRoleDefinitions(roleGroups) +} + +/** + * Loads runtime roles without merging equal role keys across flow items. + * Task orchestration needs one independent assignment per detection item because + * two items may use different physical robots even when their workflows define + * the same logical role (for example, `default_robot`). + */ +export const loadFlowItemScopedRuntimeRoles = async (items = []) => { + if (items.some((item) => !item?.itemId)) throw new Error('任务编排中存在缺少流程项ID的数据') + if (!items.length) throw new Error('当前任务没有编排任何流程项') + + const occurrences = new Map() + const roleGroups = await Promise.all(items.map(async (item) => { + const occurrence = (occurrences.get(String(item.itemId)) || 0) + 1 + occurrences.set(String(item.itemId), occurrence) + const response = await getDetect(item.itemId) + const graph = parseFlowData(item.itemName || item.itemId, response.data?.flowData) + return getResourceRoles(graph).map((role) => ({ + ...clone([role])[0], + roleKey: `${item.itemId}:${occurrence}:${role.roleKey}`, + sourceRoleKey: role.roleKey, + itemId: item.itemId, + itemOccurrence: occurrence, + itemName: item.itemName || String(item.itemId), + displayName: `${item.itemName || item.itemId} / ${role.displayName}`, + })) + })) + return roleGroups.flat() +} + +export const loadTaskRuntimeRoles = async (taskId) => { + if (!taskId) throw new Error('任务没有关联流程编排') + const response = await getTaskConfig({ taskId }) + return loadFlowItemRuntimeRoles(response.rows || []) +} + +export const loadTaskScopedRuntimeRoles = async (taskId) => { + if (!taskId) throw new Error('任务没有关联流程编排') + const response = await getTaskConfig({ taskId }) + return loadFlowItemScopedRuntimeRoles(response.rows || []) +} + +export const createRuntimeAssignments = (roles = [], primaryRobotId = '') => roles.map((role, index) => ({ + roleKey: role.roleKey, + ...(role.sourceRoleKey ? { sourceRoleKey: role.sourceRoleKey } : {}), + ...(role.itemId != null ? { itemId: role.itemId } : {}), + ...(role.itemOccurrence != null ? { itemOccurrence: role.itemOccurrence } : {}), + members: [{ + memberKey: `${role.roleKey}_1`, + robotId: index === 0 ? primaryRobotId || role.legacyRobotId || '' : role.legacyRobotId || '', + devices: Object.fromEntries((role.deviceSlots || []).map((slot) => [ + slot.slotKey, + slot.legacyDeviceId || '', + ])), + }], +})) + +export const firstAssignedRobotId = (assignments = []) => ( + clone(assignments).flatMap((item) => item.members || []).find((member) => member.robotId)?.robotId || '' +) diff --git a/src/utils/flowResources.js b/src/utils/flowResources.js new file mode 100644 index 0000000..b583464 --- /dev/null +++ b/src/utils/flowResources.js @@ -0,0 +1,500 @@ +import { getDeviceKindForAction } from '@/utils/robotDevice' + +export const FLOW_SCHEMA_VERSION = 2 +export const DEFAULT_ROLE_KEY = 'default_executor' +export const FLOW_NODE_RESOURCE_OWNER = 'FLOW_NODE' +export const USER_RESOURCE_OWNER = 'USER' +// 图片分析节点的输出契约会被节点配置、参数面板和旧流程迁移复用,集中定义避免字段漂移。 +export const IMAGE_ANALYSIS_OUTPUT_PARAMS = Object.freeze([ + { name: 'result', type: 'object', desc: '按提示词返回的结果', children: [], disabled: true }, + { name: 'resultText', type: 'string', desc: '模型返回的原始文本', disabled: true }, + { name: 'matched', type: 'boolean', desc: '找图是否匹配', disabled: true }, + { name: 'matchScore', type: 'number', desc: '匹配分数或定位置信度', disabled: true }, + { name: 'coordinates', type: 'array', desc: '原图坐标 [x1,y1,x2,y2]', children: [], disabled: true }, + { name: 'center', type: 'array', desc: '目标中心点 [x,y]', children: [], disabled: true }, + { name: 'matchedDescription', type: 'string', desc: '模型实际定位的目标描述', disabled: true }, + { name: 'refinementMethod', type: 'string', desc: '坐标精修方式', disabled: true }, + { name: 'matchCount', type: 'number', desc: '匹配项数量', disabled: true }, + { name: 'matches', type: 'array', desc: '全部匹配项', children: [], disabled: true }, + { name: 'selectedMatchIndex', type: 'number', desc: '当前选择的匹配序号', disabled: true }, +]) + +const DEVICE_KIND_META = Object.freeze({ + 1: { key: 'agv', name: '移动底盘' }, + 2: { key: 'arm', name: '机械臂' }, + 4: { key: 'bio_head', name: '仿生头' }, + 5: { key: 'camera', name: '摄像头' }, + 9: { key: 'microphone', name: '麦克风' }, + 12: { key: 'robot', name: '机器人本体' }, + 13: { key: 'speaker', name: '扬声器' }, +}) + +const clone = (value) => JSON.parse(JSON.stringify(value ?? null)) +const normalizedText = (value) => String(value || '').trim() +const LEGACY_NODE_TYPE_ALIASES = Object.freeze({ + device_universal: 'deviceUniversal', +}) + +export const createResourceKey = (prefix) => { + const random = globalThis.crypto?.randomUUID?.().replace(/-/g, '').slice(0, 10) + || `${Date.now()}${Math.random().toString(16).slice(2, 8)}` + return `${prefix}_${random}` +} + +export const createDefaultRobotRole = (legacyRobotId = '') => ({ + roleKey: DEFAULT_ROLE_KEY, + resourceType: 'ROBOT', + displayName: '执行机器人', + required: true, + quantity: { min: 1, max: 1 }, + deviceSlots: [], + ...(legacyRobotId ? { legacyRobotId } : {}), +}) + +export const createDeviceSlot = (deviceKind, existingSlots = [], displayName = '') => { + const meta = DEVICE_KIND_META[Number(deviceKind)] || { key: 'device', name: '执行设备' } + const usedKeys = new Set(existingSlots.map((slot) => slot.slotKey)) + let index = 1 + let slotKey = meta.key + while (usedKeys.has(slotKey)) slotKey = `${meta.key}_${++index}` + const sameKindCount = existingSlots.filter((slot) => Number(slot.deviceKind) === Number(deviceKind)).length + return { + slotKey, + displayName: displayName || `${meta.name}${sameKindCount ? sameKindCount + 1 : ''}`, + deviceKind: deviceKind == null ? null : Number(deviceKind), + required: true, + allowReuse: false, + } +} + +export const getStartNode = (graph = globalThis.lf?.getGraphData?.()) => ( + graph?.nodes?.find((node) => node.type === 'start') +) + +export const getResourceRoles = (graph = globalThis.lf?.getGraphData?.()) => ( + clone(getStartNode(graph)?.properties?.resourceRoles || []) || [] +) + +const isRobotNode = (node) => node?.properties?.nodeType === 'EDGE' +export const ACTION_QUEUE_ACTION = 'SYSTEM_EXECUTE_ACTION_QUEUE' +export const actionRequiresDevice = (action) => ( + getDeviceKindForAction(action) != null || action === 'DEVICE_EXECUTE_JSON_COMMAND' +) +export const nodeRequiresDevice = (node) => actionRequiresDevice(node?.properties?.action) + +const resolveLegacyRobotId = (startProperties) => ( + startProperties?.inputParams?.find((item) => item.name === 'robotId')?.input + || startProperties?.robotId + || '' +) + +const findOrCreateLegacySlot = (role, node, legacySlotByIdentity, legacyDeviceId = '') => { + const kind = getDeviceKindForAction(node.properties?.action) + const normalizedDeviceId = normalizedText(legacyDeviceId) + const identity = `${role.roleKey}:${kind ?? 'any'}:${normalizedDeviceId || 'default'}` + if (legacySlotByIdentity.has(identity)) return legacySlotByIdentity.get(identity) + + const slot = createDeviceSlot(kind, role.deviceSlots) + slot.managedBy = FLOW_NODE_RESOURCE_OWNER + if (normalizedDeviceId) slot.legacyDeviceId = normalizedDeviceId + role.deviceSlots.push(slot) + legacySlotByIdentity.set(identity, slot) + return slot +} + +/** + * Loads old graphs as Schema V2 without mutating the source object. Physical IDs are + * retained only as migration hints and are consumed by the trial-run assignment form. + */ +export const normalizeFlowResourceSchema = (sourceGraph) => { + const graph = clone(sourceGraph || { nodes: [], edges: [] }) + graph.nodes = Array.isArray(graph.nodes) ? graph.nodes : [] + graph.edges = Array.isArray(graph.edges) ? graph.edges : [] + graph.nodes.forEach((node) => { + node.type = LEGACY_NODE_TYPE_ALIASES[node.type] || node.type + // 早期图片分析图数据可能只保存节点类型,加载时补齐运行端识别所需的协议字段。 + const properties = node.properties || (node.properties = {}) + if (['AGV_MOVE_TO_POINT', 'AGV_MOVE_TO_STATION'].includes(properties.action)) { + properties.nodeParams = Array.isArray(properties.nodeParams) ? properties.nodeParams : [] + if (!properties.nodeParams.some((param) => param?.name === 'blockedTimeoutMs')) { + properties.nodeParams.push({ + name: 'blockedTimeoutMs', + type: 'input', + componentType: 'number', + input: 60000, + min: 1, + required: true, + disabled: true, + }) + } + } + if (node.type === 'imageAnalysis') { + const properties = node.properties || (node.properties = {}) + properties.action = 'IMAGE_ANALYZE' + properties.nodeType = 'LLM' + properties.outputType = 'json' + properties.outputParams = clone(IMAGE_ANALYSIS_OUTPUT_PARAMS) + } + // 视频分析新增 result/detailedOutput 后,打开旧流程时补齐输出字段,后续节点才能稳定引用。 + if (node.properties?.action === 'VIDEO_ANALYZE') { + const outputParams = Array.isArray(node.properties.outputParams) + ? node.properties.outputParams + : [] + if (!outputParams.some((param) => param?.name === 'result')) { + outputParams.unshift({ name: 'result', type: 'string', desc: '分析结果', disabled: true }) + } + if (!outputParams.some((param) => param?.name === 'detailedOutput')) { + outputParams.push({ + name: 'detailedOutput', type: 'boolean', desc: '是否返回详细事件证据', disabled: true, + }) + } + node.properties.outputParams = outputParams + } + }) + const start = getStartNode(graph) + if (!start) return graph + + const startProperties = start.properties || (start.properties = {}) + const legacyRobotId = resolveLegacyRobotId(startProperties) + const hasExplicitRoles = Array.isArray(startProperties.resourceRoles) + const hasRobotNodes = graph.nodes.some(isRobotNode) + const roles = hasExplicitRoles + ? startProperties.resourceRoles + : (hasRobotNodes || legacyRobotId ? [createDefaultRobotRole(legacyRobotId)] : []) + if (hasRobotNodes && roles.length === 0) roles.push(createDefaultRobotRole(legacyRobotId)) + + roles.forEach((role, index) => { + role.roleKey ||= index === 0 ? DEFAULT_ROLE_KEY : createResourceKey('robot_role') + role.resourceType = 'ROBOT' + role.displayName ||= `机器人角色${index + 1}` + role.required = role.required !== false + role.quantity ||= { min: 1, max: 1 } + role.deviceSlots = Array.isArray(role.deviceSlots) ? role.deviceSlots : [] + role.deviceSlots.forEach((slot) => delete slot.requiredCapabilities) + }) + + startProperties.schemaVersion = FLOW_SCHEMA_VERSION + startProperties.resourceRoles = roles + startProperties.inputParams = (startProperties.inputParams || []).filter((item) => item.name !== 'robotId') + + const defaultRole = roles[0] + const legacySlotByIdentity = new Map() + roles.forEach((role) => role.deviceSlots.forEach((slot) => { + const identity = `${role.roleKey}:${slot.deviceKind ?? 'any'}:${slot.legacyDeviceId || 'default'}` + legacySlotByIdentity.set(identity, slot) + })) + + graph.nodes.forEach((node) => { + const properties = node.properties || (node.properties = {}) + if (!isRobotNode(node)) { + delete properties.executionTarget + return + } + const target = properties.executionTarget || { + roleKey: defaultRole.roleKey, + dispatchPolicy: 'ONE', + } + target.roleKey ||= defaultRole.roleKey + target.dispatchPolicy ||= 'ONE' + const isActionQueue = properties.action === ACTION_QUEUE_ACTION + if (!isActionQueue) target.deviceSlotKey ||= target.deviceBindings?.[0]?.slotKey || '' + target.deviceBindings = Array.isArray(target.deviceBindings) ? target.deviceBindings : [] + + const targetRole = roles.find((role) => role.roleKey === target.roleKey) || defaultRole + const deviceParam = properties.nodeParams?.find((param) => param.name === 'deviceId') + if (nodeRequiresDevice(node) && !target.deviceSlotKey) { + const slot = findOrCreateLegacySlot(targetRole, node, legacySlotByIdentity, deviceParam?.input) + if (slot) target.deviceSlotKey = slot.slotKey + } + const referencedSlotKeys = new Set([ + target.deviceSlotKey, + ...target.deviceBindings.map((binding) => binding?.slotKey), + ].filter(Boolean)) + targetRole.deviceSlots.forEach((slot) => { + if (referencedSlotKeys.has(slot.slotKey) && !slot.managedBy) slot.managedBy = FLOW_NODE_RESOURCE_OWNER + }) + if (!isActionQueue) delete target.deviceBindings + if (isActionQueue) delete target.deviceSlotKey + properties.nodeParams = (properties.nodeParams || []).filter((param) => param.name !== 'deviceId') + properties.executionTarget = target + }) + + return graph +} + +export const ensureNodeExecutionResources = (sourceGraph, nodeId) => { + const graph = normalizeFlowResourceSchema(sourceGraph) + const node = graph.nodes.find((item) => item.id === nodeId) + return { graph, node, start: getStartNode(graph) } +} + +/** + * Removes role definitions and device usages that were registered automatically + * for flow nodes and are no longer referenced by any remaining node. + */ +export const pruneUnusedNodeResources = (sourceGraph) => { + const graph = clone(sourceGraph || { nodes: [], edges: [] }) + const start = getStartNode(graph) + if (!start?.properties?.resourceRoles) { + return { graph, start, removedRoles: 0, removedSlots: 0 } + } + + const usedRoleKeys = new Set() + const usedSlotsByRole = new Map() + graph.nodes.filter(isRobotNode).forEach((node) => { + const target = node.properties?.executionTarget + if (!target?.roleKey) return + usedRoleKeys.add(target.roleKey) + if (!usedSlotsByRole.has(target.roleKey)) usedSlotsByRole.set(target.roleKey, new Set()) + if (target.deviceSlotKey) usedSlotsByRole.get(target.roleKey).add(target.deviceSlotKey) + ;(target.deviceBindings || []).forEach((binding) => { + if (binding?.slotKey) usedSlotsByRole.get(target.roleKey).add(binding.slotKey) + }) + }) + graph.nodes.filter((node) => node?.properties?.action === 'SUB_FLOW').forEach((node) => { + ;(node.properties?.subFlow?.resourceRoles || []).forEach((role) => { + if (!role?.roleKey) return + usedRoleKeys.add(role.roleKey) + if (!usedSlotsByRole.has(role.roleKey)) usedSlotsByRole.set(role.roleKey, new Set()) + ;(role.deviceSlots || []).forEach((slot) => { + if (slot?.slotKey) usedSlotsByRole.get(role.roleKey).add(slot.slotKey) + }) + }) + }) + + let removedSlots = 0 + start.properties.resourceRoles.forEach((role) => { + const usedSlotKeys = usedSlotsByRole.get(role.roleKey) || new Set() + role.deviceSlots = (role.deviceSlots || []).filter((slot) => { + const shouldRemove = slot.managedBy === FLOW_NODE_RESOURCE_OWNER && !usedSlotKeys.has(slot.slotKey) + if (shouldRemove) removedSlots += 1 + return !shouldRemove + }) + }) + + const beforeRoleCount = start.properties.resourceRoles.length + start.properties.resourceRoles = start.properties.resourceRoles.filter((role, index) => ( + index === 0 + || role.managedBy !== FLOW_NODE_RESOURCE_OWNER + || usedRoleKeys.has(role.roleKey) + )) + + return { + graph, + start, + removedRoles: beforeRoleCount - start.properties.resourceRoles.length, + removedSlots, + } +} + +export const validateFlowResources = (sourceGraph) => { + const graph = normalizeFlowResourceSchema(sourceGraph) + const roles = getResourceRoles(graph) + const errors = validateFlowReferences(graph) + const roleKeys = new Set() + + roles.forEach((role) => { + if (!normalizedText(role.displayName)) errors.push({ nodeId: getStartNode(graph)?.id, message: '机器人角色名称不能为空' }) + if (roleKeys.has(role.roleKey)) errors.push({ nodeId: getStartNode(graph)?.id, message: `机器人角色标识重复:${role.roleKey}` }) + roleKeys.add(role.roleKey) + const slotKeys = new Set() + ;(role.deviceSlots || []).forEach((slot) => { + if (!normalizedText(slot.displayName)) errors.push({ nodeId: getStartNode(graph)?.id, message: `${role.displayName}存在未命名的设备用途` }) + if (slotKeys.has(slot.slotKey)) errors.push({ nodeId: getStartNode(graph)?.id, message: `${role.displayName}的设备用途标识重复` }) + slotKeys.add(slot.slotKey) + }) + }) + + graph.nodes.filter(isRobotNode).forEach((node) => { + const target = node.properties?.executionTarget + const role = roles.find((item) => item.roleKey === target?.roleKey) + if (!role) { + errors.push({ nodeId: node.id, message: `${node.properties?.name || node.id}未选择有效的机器人角色` }) + return + } + if (target.deviceSlotKey && !(role.deviceSlots || []).some((slot) => slot.slotKey === target.deviceSlotKey)) { + errors.push({ nodeId: node.id, message: `${node.properties?.name || node.id}引用的设备用途不存在` }) + } + ;(target.deviceBindings || []).forEach((binding) => { + if (!binding?.parameterName || !binding?.slotKey) { + errors.push({ nodeId: node.id, message: `${node.properties?.name || node.id}存在未配置的队列设备` }) + } else if (!(role.deviceSlots || []).some((slot) => slot.slotKey === binding.slotKey)) { + errors.push({ nodeId: node.id, message: `${node.properties?.name || node.id}引用的队列设备用途不存在` }) + } + }) + if (nodeRequiresDevice(node) + && !target.deviceSlotKey) { + errors.push({ nodeId: node.id, message: `${node.properties?.name || node.id}未选择设备用途` }) + } + }) + return { graph, errors } +} + +const collectActiveQuotes = (value, result = []) => { + if (Array.isArray(value)) { + value.forEach((item) => collectActiveQuotes(item, result)) + return result + } + if (!value || typeof value !== 'object') return result + if (value.type === 'quote' && Array.isArray(value.quote) && value.quote.length >= 3) { + result.push(value.quote) + } + if (value.nameType === 'quote' && Array.isArray(value.nameQuote) && value.nameQuote.length >= 3) { + result.push(value.nameQuote) + } + Object.values(value).forEach((item) => collectActiveQuotes(item, result)) + return result +} + +const findReferenceCycle = (dependencies) => { + const states = new Map() + const stack = [] + const visit = (nodeId) => { + if (states.get(nodeId) === 2) return null + if (states.get(nodeId) === 1) { + const index = stack.indexOf(nodeId) + return [...stack.slice(Math.max(0, index)), nodeId] + } + states.set(nodeId, 1) + stack.push(nodeId) + for (const sourceId of dependencies.get(nodeId) || []) { + if (!dependencies.has(sourceId)) continue + const cycle = visit(sourceId) + if (cycle) return cycle + } + stack.pop() + states.set(nodeId, 2) + return null + } + for (const nodeId of dependencies.keys()) { + const cycle = visit(nodeId) + if (cycle) return cycle + } + return null +} + +/** Validates persisted references as well as choices made through the parameter panels. */ +export const validateFlowReferences = (graph) => { + const nodes = Array.isArray(graph?.nodes) ? graph.nodes : [] + const nodeById = new Map(nodes.map((node) => [String(node.id), node])) + const referencesByNode = new Map() + const dependencies = new Map() + + nodes.forEach((node) => { + const references = collectActiveQuotes(node.properties || []) + .map((quote) => quote.map((part) => String(part))) + referencesByNode.set(String(node.id), references) + dependencies.set(String(node.id), new Set(references.map((quote) => quote[0]))) + }) + + const cycle = findReferenceCycle(dependencies) + if (cycle) { + const names = cycle.map((nodeId) => nodeById.get(nodeId)?.properties?.name || nodeId) + return [{ + nodeId: cycle[0], + message: `节点参数存在循环引用:${names.join(' -> ')}`, + }] + } + + return [] +} + +export const sanitizeFlowResourceDefinition = (sourceGraph) => { + const graph = normalizeFlowResourceSchema(sourceGraph) + const start = getStartNode(graph) + ;(start?.properties?.resourceRoles || []).forEach((role) => { + delete role.legacyRobotId + ;(role.deviceSlots || []).forEach((slot) => delete slot.legacyDeviceId) + }) + graph.nodes.filter(isRobotNode).forEach((node) => { + node.properties.nodeParams = (node.properties?.nodeParams || []).filter((param) => param.name !== 'deviceId') + }) + return graph +} + +export const mergeCopiedResourceRoles = (targetGraph, sourceRoles = [], copiedNodes = []) => { + const graph = normalizeFlowResourceSchema(targetGraph) + const start = getStartNode(graph) + const targetRoles = start.properties.resourceRoles + let addedRoles = 0 + let addedSlots = 0 + + copiedNodes.filter(isRobotNode).forEach((node) => { + const target = node.properties?.executionTarget + if (!target?.roleKey) return + const sourceRole = sourceRoles.find((role) => role.roleKey === target.roleKey) + let targetRole = targetRoles.find((role) => role.roleKey === target.roleKey) + || targetRoles.find((role) => sourceRole && role.displayName === sourceRole.displayName) + + if (!targetRole && sourceRole) { + targetRole = clone(sourceRole) + targetRole.managedBy = FLOW_NODE_RESOURCE_OWNER + delete targetRole.legacyRobotId + ;(targetRole.deviceSlots || []).forEach((slot) => { + slot.managedBy = FLOW_NODE_RESOURCE_OWNER + delete slot.legacyDeviceId + }) + targetRoles.push(targetRole) + addedRoles += 1 + addedSlots += targetRole.deviceSlots?.length || 0 + } + targetRole ||= targetRoles[0] + target.roleKey = targetRole.roleKey + + const remapSlot = (slotKey) => { + if (!slotKey || targetRole.deviceSlots?.some((slot) => slot.slotKey === slotKey)) return slotKey + const sourceSlot = sourceRole?.deviceSlots?.find((slot) => slot.slotKey === slotKey) + const sameSlot = targetRole.deviceSlots?.find((slot) => ( + sourceSlot && slot.displayName === sourceSlot.displayName && Number(slot.deviceKind) === Number(sourceSlot.deviceKind) + )) + if (sameSlot) return sameSlot.slotKey + const newSlot = sourceSlot ? clone(sourceSlot) : createDeviceSlot(getDeviceKindForAction(node.properties?.action), targetRole.deviceSlots) + newSlot.managedBy = FLOW_NODE_RESOURCE_OWNER + delete newSlot.legacyDeviceId + targetRole.deviceSlots.push(newSlot) + addedSlots += 1 + return newSlot.slotKey + } + target.deviceSlotKey = remapSlot(target.deviceSlotKey) + ;(target.deviceBindings || []).forEach((binding) => { + binding.slotKey = remapSlot(binding.slotKey) + }) + const bindingSlotByName = new Map((target.deviceBindings || []).map((binding) => ( + [binding.parameterName, binding.slotKey] + ))) + const queueStepsParam = node.properties?.nodeParams?.find((param) => param.name === 'steps') + if (Array.isArray(queueStepsParam?.input)) { + queueStepsParam.input.forEach((step) => { + if (bindingSlotByName.has(step?.deviceBindingKey)) { + step.deviceSlotKey = bindingSlotByName.get(step.deviceBindingKey) + } + }) + } + }) + + return { graph, start, addedRoles, addedSlots } +} + +export const getDeviceKindName = (deviceKind) => DEVICE_KIND_META[Number(deviceKind)]?.name || '任意设备' + +export const mergeResourceRoleDefinitions = (roleGroups = []) => { + const merged = [] + roleGroups.flat().forEach((sourceRole) => { + let role = merged.find((item) => item.roleKey === sourceRole.roleKey) + if (!role) { + merged.push(clone(sourceRole)) + return + } + role.required = role.required !== false || sourceRole.required !== false + ;(sourceRole.deviceSlots || []).forEach((sourceSlot) => { + const slot = role.deviceSlots.find((item) => item.slotKey === sourceSlot.slotKey) + if (!slot) { + role.deviceSlots.push(clone(sourceSlot)) + return + } + slot.required = slot.required !== false || sourceSlot.required !== false + slot.allowReuse = slot.allowReuse === true && sourceSlot.allowReuse === true + }) + }) + return merged +} 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/drawingDefault.js b/src/utils/generator/drawingDefault.js new file mode 100644 index 0000000..3345c84 --- /dev/null +++ b/src/utils/generator/drawingDefault.js @@ -0,0 +1,37 @@ +export const drawingDefaultValue = [] + +export function initDrawingDefaultValue() { + if (drawingDefaultValue.length === 0) { + drawingDefaultValue.push({ + 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: '手机号格式错误' + }] + }) + } +} + +export function cleanDrawingDefaultValue() { + drawingDefaultValue.splice(0, drawingDefaultValue.length) +} diff --git a/src/utils/generator/html.js b/src/utils/generator/html.js new file mode 100644 index 0000000..4b29841 --- /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" :value="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" :value="item.label" :disabled="item.disabled" ${border} />`) + } + 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..07cc994 --- /dev/null +++ b/src/utils/index.js @@ -0,0 +1,407 @@ +import { parseTime } from './ruoyi' + +/** + * 表格时间格式化 + */ +export function formatDate(cellValue) { + if (cellValue == null || cellValue == "") return "" + const date = new Date(cellValue) + const year = date.getFullYear() + const month = date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1 + const day = date.getDate() < 10 ? '0' + date.getDate() : date.getDate() + const hours = date.getHours() < 10 ? '0' + date.getHours() : date.getHours() + const minutes = date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes() + const 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 (let 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 typeof mimeType === 'string' && audioMimeTypes.includes(mimeType.toLowerCase()) +} + 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/pagedRequest.js b/src/utils/pagedRequest.js new file mode 100644 index 0000000..e8a7711 --- /dev/null +++ b/src/utils/pagedRequest.js @@ -0,0 +1,45 @@ +const DEFAULT_PAGE_SIZE = 200 +const DEFAULT_MAX_PAGES = 1000 + +/** + * Loads every page from a standard TableDataInfo endpoint. Endpoints that ignore + * page parameters are also supported because their first response already reaches total. + */ +export async function fetchAllPagedRows(fetchPage, params = {}, options = {}) { + const pageSize = Math.max(1, Number(options.pageSize || DEFAULT_PAGE_SIZE)) + const maxPages = Math.max(1, Number(options.maxPages || DEFAULT_MAX_PAGES)) + const rows = [] + const seenIds = new Set() + let lastResponse = { code: 200, rows: [], total: 0 } + + for (let pageNum = 1; pageNum <= maxPages; pageNum += 1) { + const response = await fetchPage({ ...params, pageNum, pageSize }) + lastResponse = response || lastResponse + const batch = Array.isArray(response?.rows) ? response.rows : [] + let addedCount = 0 + batch.forEach((row) => { + const id = row?.id + if (id !== undefined && id !== null && id !== '') { + const key = String(id) + if (seenIds.has(key)) return + seenIds.add(key) + } + rows.push(row) + addedCount += 1 + }) + + const total = Number(response?.total) + const hasTotal = Number.isFinite(total) && total >= 0 + if (addedCount === 0 && hasTotal && rows.length < total) { + throw new Error('日志分页没有返回新记录,无法保证数据完整') + } + if (batch.length === 0 + || addedCount === 0 + || (hasTotal && rows.length >= total) + || (!hasTotal && batch.length < pageSize)) { + return { ...lastResponse, rows, total: hasTotal ? total : rows.length } + } + } + + throw new Error(`日志记录超过 ${pageSize * maxPages} 条,未能完整加载`) +} diff --git a/src/utils/passwordRule.js b/src/utils/passwordRule.js new file mode 100644 index 0000000..1c73fad --- /dev/null +++ b/src/utils/passwordRule.js @@ -0,0 +1,73 @@ +/** + * 密码强度规则 + * 根据参数 chrtype 动态生成校验规则 + * + * chrtype 说明: + * 0 - 任意字符(默认) + * 1 - 纯数字(0-9) + * 2 - 纯字母(a-z / A-Z) + * 3 - 字母 + 数字(必须同时包含) + * 4 - 字母 + 数字 + 特殊字符(必须同时包含,特殊字符:~!@#$%^&*()-=_+) + */ + +import cache from '@/plugins/cache' + +// 密码限制类型 +const pwdChrType = ref(cache.session.get('pwrChrtype') || '0') + +// 各类型对应的正则、错误提示 +const PWD_RULES = { + '0': { pattern: /^[^<>"'|\\]+$/, message: '密码不能包含非法字符:< > " \' \\ |' }, + '1': { pattern: /^[0-9]+$/, message: '密码只能为数字(0-9)' }, + '2': { pattern: /^[a-zA-Z]+$/, message: '密码只能为英文字母(a-z、A-Z)' }, + '3': { pattern: /^(?=.*[a-zA-Z])(?=.*[0-9])[a-zA-Z0-9]+$/, message: '密码必须同时包含字母和数字' }, + '4': { pattern: /^(?=.*[A-Za-z])(?=.*\d)(?=.*[~!@#$%^&*()\-=_+])[A-Za-z\d~!@#$%^&*()\-=_+]+$/, message: '密码必须同时包含字母、数字和特殊字符(~!@#$%^&*()-=_+)' } +} + +export function usePasswordRule() { + // 默认密码校验 + const pwdValidator = computed(() => { + const rule = PWD_RULES[pwdChrType.value] || PWD_RULES['0'] + return [ + { required: true, message: '密码不能为空', trigger: 'blur' }, + { min: 6, max: 20, message: '密码长度必须介于 6 和 20 之间', trigger: 'blur' }, + { pattern: rule.pattern, message: rule.message, trigger: 'blur' } + ] + }) + // 校验prompt的inputValidator函数 + const pwdPromptValidator = (value) => { + const rule = PWD_RULES['0'] + if (!value || value.length < 6 || value.length > 20) { + return '密码长度必须介于 6 和 20 之间' + } + if (!rule.pattern.test(value)) { + return rule.message + } + } + // 个人中心密码校验 + const infoPwdValidator = computed(() => { + const rule = PWD_RULES[pwdChrType.value] || PWD_RULES['0'] + return [ + { required: true, message: '新密码不能为空', trigger: 'blur' }, + { min: 6, max: 20, message: '新密码长度必须介于 6 和 20 之间', trigger: 'blur' }, + { pattern: rule.pattern, message: rule.message, trigger: 'blur' } + ] + }) + // 注册页面密码校验 + const registerPwdValidator = computed(() => { + const rule = PWD_RULES['0'] + return [ + { required: true, message: '请输入您的密码', trigger: 'blur' }, + { min: 6, max: 20, message: '用户密码长度必须介于 6 和 20 之间', trigger: 'blur' }, + { pattern: rule.pattern, message: rule.message, trigger: 'blur' } + ] + }) + + return { + pwdChrType, + pwdValidator, + infoPwdValidator, + pwdPromptValidator, + registerPwdValidator + } +} diff --git a/src/utils/permission.js b/src/utils/permission.js new file mode 100644 index 0000000..d354852 --- /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..e12efd0 --- /dev/null +++ b/src/utils/request.js @@ -0,0 +1,154 @@ +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' +import { runtimeConfig } from '@/runtime/config' + +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: runtimeConfig.apiBaseURL, + // 超时 + timeout: 10000 +}) + +// request拦截器 +service.interceptors.request.use(config => { + // 是否需要设置 token + const isToken = (config.headers || {}).isToken === false + // 是否需要防止数据重复提交 + const isRepeatSubmit = (config.headers || {}).repeatSubmit === false + // 间隔时间(ms),小于此时间视为重复提交 + const interval = (config.headers || {}).interval || 1000 + 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 // 请求时间 + 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 = runtimeConfig.loginPath + }) + }).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.slice(-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/robotDevice.js b/src/utils/robotDevice.js new file mode 100644 index 0000000..7117333 --- /dev/null +++ b/src/utils/robotDevice.js @@ -0,0 +1,58 @@ +/** + * 平台设备类型枚举,供流程资源匹配、机器人设备筛选和界面展示共同使用。 + * Object.freeze 防止运行期间被改写,数值与后端 deviceKind 协议保持一致。 + */ +export const DEVICE_KIND = Object.freeze({ + AGV: 1, + ARM: 2, + BIO_HEAD: 4, + CAMERA: 5, + MICROPHONE: 9, + ROBOT: 12, + SPEAKER: 13, +}) + +/** + * 流程动作到所需设备类型的全局映射。 + * 作用范围为所有根据 action 判断执行设备的流程模块;未列出的动作视为没有固定设备类型。 + */ +export const DEVICE_KIND_BY_ACTION = Object.freeze({ + AGV_MOVE_TO_POINT: DEVICE_KIND.AGV, + AGV_MOVE_TO_STATION: DEVICE_KIND.AGV, + ARM_MOVE_TO_POINT: DEVICE_KIND.ARM, + ARM_MOVE_TO_J: DEVICE_KIND.ARM, + // 机械臂使能/去使能虽然没有运动参数,仍必须绑定 ARM 设备, + // 这样运行前的资源校验和设备下拉筛选才能为节点分配正确的机械臂终端。 + ARM_ENABLE: DEVICE_KIND.ARM, + ARM_DISABLE: DEVICE_KIND.ARM, + TOUCH: DEVICE_KIND.ARM, + BIO_HEAD_SPEAK_START: DEVICE_KIND.BIO_HEAD, + BIO_HEAD_SPEAK_STOP: DEVICE_KIND.BIO_HEAD, + BIO_HEAD_SPECIAL_EXPRESSION: DEVICE_KIND.BIO_HEAD, + CAMERA_START: DEVICE_KIND.CAMERA, + CAMERA_STOP: DEVICE_KIND.CAMERA, + CAMERA_GETRGBIMAGE: DEVICE_KIND.CAMERA, + CAMERA_RECORDING_START: DEVICE_KIND.CAMERA, + CAMERA_RECORDING_STOP: DEVICE_KIND.CAMERA, + MICROPHONE_START: DEVICE_KIND.MICROPHONE, + MICROPHONE_STOP: DEVICE_KIND.MICROPHONE, + SPEAKER_PLAYAUDIO: DEVICE_KIND.SPEAKER, + VI_PLAY_CORPUS: DEVICE_KIND.SPEAKER, +}) + +/** + * 查询某个流程动作所需的设备类型。 + * 直接读取统一映射;未知或无需固定设备的动作返回 undefined,由调用方决定降级策略。 + */ +export const getDeviceKindForAction = (action) => DEVICE_KIND_BY_ACTION[action] + +/** + * 将机器人设备接口数据转换为 Element Plus 下拉框选项,供全局设备选择器复用。 + * 每项以 deviceId 作为值;设备名有效且不同于 ID 时展示“名称 (ID)”,否则仅展示 ID。 + */ +export const toDeviceOptions = (devices = []) => devices.map((device) => ({ + value: device.deviceId, + label: device.deviceName && device.deviceName !== device.deviceId + ? `${device.deviceName} (${device.deviceId})` + : device.deviceId, +})) diff --git a/src/utils/ruoyi.js b/src/utils/ruoyi.js new file mode 100644 index 0000000..a7bfbdf --- /dev/null +++ b/src/utils/ruoyi.js @@ -0,0 +1,228 @@ +/** + * 通用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 "" + } + const 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(",") + } + const actions = [] + const currentSeparator = undefined === separator ? "," : separator + const temp = value.split(currentSeparator) + Object.keys(value.split(currentSeparator)).some((val) => { + let 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) { + let flag = true, i = 1 + str = str.replace(/%s/g, function () { + const 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 (const 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) { + const config = { + id: id || 'id', + parentId: parentId || 'parentId', + childrenList: children || 'children' + } + + const childrenListMap = {} + const tree = [] + for (const d of data) { + const id = d[config.id] + childrenListMap[id] = d + if (!d[config.childrenList]) { + d[config.childrenList] = [] + } + } + + for (const d of data) { + const parentId = d[config.parentId] + const parentObj = childrenListMap[parentId] + if (!parentObj) { + tree.push(d) + } else { + parentObj[config.childrenList].push(d) + } + } + return tree +} + +/** +* 参数处理 +* @param {*} params 参数 +*/ +export function tansParams(params) { + let result = '' + for (const propName of Object.keys(params)) { + const value = params[propName] + const 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') { + const params = propName + '[' + key + ']' + const 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..6e23799 --- /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 +const 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 + const animateScroll = function() { + // increment the time + currentTime += increment + // find the value with the quadratic in-out easing function + const 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..fcd4123 --- /dev/null +++ b/src/utils/theme.js @@ -0,0 +1,64 @@ +// 处理主题样式 +export function handleThemeStyle(theme) { + const isDark = typeof document !== 'undefined' && document.documentElement.classList.contains('dark') + const primary = isDark ? softenPrimaryForDark(theme) : theme + document.documentElement.style.setProperty('--el-color-primary', primary) + for (let i = 1; i <= 9; i++) { + document.documentElement.style.setProperty(`--el-color-primary-light-${i}`, `${getLightColor(primary, i / 10)}`) + } + for (let i = 1; i <= 9; i++) { + document.documentElement.style.setProperty(`--el-color-primary-dark-${i}`, `${getDarkColor(primary, i / 10)}`) + } +} + +/** 混合两种十六进制颜色 */ +export function mixHexColors(fg, bg, t) { + const a = hexToRgb(String(fg).replace('#', '')) + const b = hexToRgb(String(bg).replace('#', '')) + const out = [0, 1, 2].map((i) => Math.round(a[i] * (1 - t) + b[i] * t)) + return rgbToHex(out[0], out[1], out[2]) +} + +/** 暗色模式下柔化主题色 */ +export function softenPrimaryForDark(theme) { + return mixHexColors(theme, '#2d3036', 0.34) +} + +// 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..d159859 --- /dev/null +++ b/src/utils/validate.js @@ -0,0 +1,119 @@ +/** + * 路径匹配器 + * @param {string} pattern + * @param {string} path + * @returns {Boolean} + */ +export function isPathMatch(pattern, path) { + const regexPattern = pattern + .replace(/([.+^${}()|\[\]\\])/g, '\\$1') + .replace(/\*\*/g, '__DOUBLE_STAR__') + .replace(/\*/g, '[^/]*') + .replace(/__DOUBLE_STAR__/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..b361368 --- /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/ActionQueueSingleRun.vue b/src/views/flow/components/ActionQueueSingleRun.vue new file mode 100644 index 0000000..baf3a56 --- /dev/null +++ b/src/views/flow/components/ActionQueueSingleRun.vue @@ -0,0 +1,281 @@ + + + + + diff --git a/src/views/flow/components/Aside.vue b/src/views/flow/components/Aside.vue new file mode 100644 index 0000000..a818eef --- /dev/null +++ b/src/views/flow/components/Aside.vue @@ -0,0 +1,251 @@ + + + + + + diff --git a/src/views/flow/components/FormItemRecursive.vue b/src/views/flow/components/FormItemRecursive.vue new file mode 100644 index 0000000..7c0e324 --- /dev/null +++ b/src/views/flow/components/FormItemRecursive.vue @@ -0,0 +1,242 @@ + + + + + + + diff --git a/src/views/flow/components/LoopIterationResults.vue b/src/views/flow/components/LoopIterationResults.vue new file mode 100644 index 0000000..cb311a6 --- /dev/null +++ b/src/views/flow/components/LoopIterationResults.vue @@ -0,0 +1,217 @@ + + + + + diff --git a/src/views/flow/components/NodeState.vue b/src/views/flow/components/NodeState.vue new file mode 100644 index 0000000..d53e966 --- /dev/null +++ b/src/views/flow/components/NodeState.vue @@ -0,0 +1,202 @@ + + + + + + + + diff --git a/src/views/flow/components/NodeTitle.vue b/src/views/flow/components/NodeTitle.vue new file mode 100644 index 0000000..f8e4e76 --- /dev/null +++ b/src/views/flow/components/NodeTitle.vue @@ -0,0 +1,338 @@ + + + + + + diff --git a/src/views/flow/components/OuterNode.vue b/src/views/flow/components/OuterNode.vue new file mode 100644 index 0000000..0cdc977 --- /dev/null +++ b/src/views/flow/components/OuterNode.vue @@ -0,0 +1,50 @@ + + + + + + diff --git a/src/views/flow/components/ParamsDrawer.vue b/src/views/flow/components/ParamsDrawer.vue new file mode 100644 index 0000000..d26cd31 --- /dev/null +++ b/src/views/flow/components/ParamsDrawer.vue @@ -0,0 +1,161 @@ + + + + + + diff --git a/src/views/flow/components/RuntimeAssignmentsEditor.vue b/src/views/flow/components/RuntimeAssignmentsEditor.vue new file mode 100644 index 0000000..96a41e3 --- /dev/null +++ b/src/views/flow/components/RuntimeAssignmentsEditor.vue @@ -0,0 +1,411 @@ + + + + + + diff --git a/src/views/flow/components/RuntimeParamsEditor.vue b/src/views/flow/components/RuntimeParamsEditor.vue new file mode 100644 index 0000000..2b8784a --- /dev/null +++ b/src/views/flow/components/RuntimeParamsEditor.vue @@ -0,0 +1,122 @@ + + + + + + diff --git a/src/views/flow/components/SubFlowExecutionResults.vue b/src/views/flow/components/SubFlowExecutionResults.vue new file mode 100644 index 0000000..54f6c45 --- /dev/null +++ b/src/views/flow/components/SubFlowExecutionResults.vue @@ -0,0 +1,173 @@ + + + + + diff --git a/src/views/flow/components/TaskExecutionLog.vue b/src/views/flow/components/TaskExecutionLog.vue new file mode 100644 index 0000000..c698d7d --- /dev/null +++ b/src/views/flow/components/TaskExecutionLog.vue @@ -0,0 +1,889 @@ + + + + + diff --git a/src/views/flow/components/TestRun.vue b/src/views/flow/components/TestRun.vue new file mode 100644 index 0000000..c823b81 --- /dev/null +++ b/src/views/flow/components/TestRun.vue @@ -0,0 +1,319 @@ + + + + + + diff --git a/src/views/flow/components/params/ActionQueueParams.vue b/src/views/flow/components/params/ActionQueueParams.vue new file mode 100644 index 0000000..104bc86 --- /dev/null +++ b/src/views/flow/components/params/ActionQueueParams.vue @@ -0,0 +1,585 @@ + + + + + diff --git a/src/views/flow/components/params/BasicNodeParams.vue b/src/views/flow/components/params/BasicNodeParams.vue new file mode 100644 index 0000000..dfb1413 --- /dev/null +++ b/src/views/flow/components/params/BasicNodeParams.vue @@ -0,0 +1,598 @@ + + + + + + diff --git a/src/views/flow/components/params/CodeNodeParams.vue b/src/views/flow/components/params/CodeNodeParams.vue new file mode 100644 index 0000000..bdac619 --- /dev/null +++ b/src/views/flow/components/params/CodeNodeParams.vue @@ -0,0 +1,501 @@ + + + + + + diff --git a/src/views/flow/components/params/DeviceUniversal.vue b/src/views/flow/components/params/DeviceUniversal.vue new file mode 100644 index 0000000..b3f1b95 --- /dev/null +++ b/src/views/flow/components/params/DeviceUniversal.vue @@ -0,0 +1,521 @@ + + + + + + diff --git a/src/views/flow/components/params/EndParams.vue b/src/views/flow/components/params/EndParams.vue new file mode 100644 index 0000000..a80fdf7 --- /dev/null +++ b/src/views/flow/components/params/EndParams.vue @@ -0,0 +1,158 @@ + + + + + + diff --git a/src/views/flow/components/params/HttpNodeParams.vue b/src/views/flow/components/params/HttpNodeParams.vue new file mode 100644 index 0000000..b3417fb --- /dev/null +++ b/src/views/flow/components/params/HttpNodeParams.vue @@ -0,0 +1,704 @@ + + + + + + diff --git a/src/views/flow/components/params/ImageAnalysisParams.vue b/src/views/flow/components/params/ImageAnalysisParams.vue new file mode 100644 index 0000000..779cfad --- /dev/null +++ b/src/views/flow/components/params/ImageAnalysisParams.vue @@ -0,0 +1,431 @@ + + + + + + diff --git a/src/views/flow/components/params/MediaAnalysisParams.vue b/src/views/flow/components/params/MediaAnalysisParams.vue new file mode 100644 index 0000000..b53d4af --- /dev/null +++ b/src/views/flow/components/params/MediaAnalysisParams.vue @@ -0,0 +1,567 @@ + + + + + + diff --git a/src/views/flow/components/params/RecognizeNodeParams.vue b/src/views/flow/components/params/RecognizeNodeParams.vue new file mode 100644 index 0000000..b339d05 --- /dev/null +++ b/src/views/flow/components/params/RecognizeNodeParams.vue @@ -0,0 +1,387 @@ + + + + + + diff --git a/src/views/flow/components/params/RobotExecutionTarget.vue b/src/views/flow/components/params/RobotExecutionTarget.vue new file mode 100644 index 0000000..b87c223 --- /dev/null +++ b/src/views/flow/components/params/RobotExecutionTarget.vue @@ -0,0 +1,120 @@ + + + + + diff --git a/src/views/flow/components/params/SdAgentNodeParams.vue b/src/views/flow/components/params/SdAgentNodeParams.vue new file mode 100644 index 0000000..64a580d --- /dev/null +++ b/src/views/flow/components/params/SdAgentNodeParams.vue @@ -0,0 +1,390 @@ + + + + + + diff --git a/src/views/flow/components/params/StartParams.vue b/src/views/flow/components/params/StartParams.vue new file mode 100644 index 0000000..cf68381 --- /dev/null +++ b/src/views/flow/components/params/StartParams.vue @@ -0,0 +1,233 @@ + + + + + + diff --git a/src/views/flow/components/params/SubFlowParams.vue b/src/views/flow/components/params/SubFlowParams.vue new file mode 100644 index 0000000..65c60d8 --- /dev/null +++ b/src/views/flow/components/params/SubFlowParams.vue @@ -0,0 +1,291 @@ + + + + + + diff --git a/src/views/flow/components/params/SwitchNodeParams.vue b/src/views/flow/components/params/SwitchNodeParams.vue new file mode 100644 index 0000000..2073e29 --- /dev/null +++ b/src/views/flow/components/params/SwitchNodeParams.vue @@ -0,0 +1,413 @@ + + + + + + diff --git a/src/views/flow/components/params/useQuote.js b/src/views/flow/components/params/useQuote.js new file mode 100644 index 0000000..35c9861 --- /dev/null +++ b/src/views/flow/components/params/useQuote.js @@ -0,0 +1,43 @@ +import { ref } from 'vue' +import { getUpstreamInput } from '@/utils/flow' + +export function useQuote(nodeId) { + const quoteOptions = ref([]) + const cascaderRefs = ref([]) + + const refreshQuoteOptions = () => { + quoteOptions.value = getUpstreamInput(nodeId) || [] + } + + // The option tree must exist before Cascader can render a persisted path. + refreshQuoteOptions() + + const handleTypeChange = (property) => { + if (!property) return + if (property.type === 'input') { + property.quote = [] + } else { + property.input = '' + refreshQuoteOptions() + } + } + + const cascaderChange = (value, property) => { + if (!property) return + property.quote = Array.isArray(value) ? [...value] : [] + property.quoteType = value?.[1] === 'input' ? 'input' : 'output' + } + + const visibleChange = (visible) => { + if (visible) refreshQuoteOptions() + } + + return { + quoteOptions, + cascaderRefs, + handleTypeChange, + cascaderChange, + visibleChange, + refreshQuoteOptions, + } +} diff --git a/src/views/flow/composables/useFlowEvents.js b/src/views/flow/composables/useFlowEvents.js new file mode 100644 index 0000000..39353ae --- /dev/null +++ b/src/views/flow/composables/useFlowEvents.js @@ -0,0 +1,27 @@ +/** + * 文件说明:流程事件订阅组合逻辑,并在卸载时释放监听器。 + * 作用范围:仅服务于流程设计器模块。 + */ +import { onUnmounted } from "vue"; +import { emitter } from "@/utils/eventBus"; + +export const useFlowEvents = () => { + const listeners = []; + + /** + * 订阅 onFlowEvent 对应的数据或交互;作用范围仅限当前组件或模块。 + * @param {*} type 目标类型 + * @param {*} handler 事件处理函数 + */ + const onFlowEvent = (type, handler) => { + emitter.on(type, handler); + listeners.push([type, handler]); + }; + + onUnmounted(() => { + listeners.forEach(([type, handler]) => emitter.off(type, handler)); + listeners.length = 0; + }); + + return { onFlowEvent }; +}; diff --git a/src/views/flow/config.js b/src/views/flow/config.js new file mode 100644 index 0000000..cb7451c --- /dev/null +++ b/src/views/flow/config.js @@ -0,0 +1,897 @@ +/** + * 文件说明:定义流程设计器可用的节点、表单字段及下拉选项等静态配置。 + * 作用范围:仅服务于流程设计器模块。 + */ +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 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' + +/** + * 处理与 http method options 相关的状态或数据,该影响范围仅限当前模块。 + */ +const httpMethodOptions = () => { + return [{ + value: 'POST', + label: 'POST' + }, { + value: 'GET', + label: 'GET' + }, { + value: 'PUT', + label: 'PUT' + }, { + value: 'DELETE', + label: 'DELETE' + }] +} + +/** + * 处理与 express options 相关的状态或数据,该影响范围仅限当前模块。 + */ +const expressOptions = () => { + return [{ + value: 1, + label: '高兴' + }, { + value: 2, + label: '惊讶' + }, { + value: 3, + label: '愤怒' + }, { + value: 4, + label: '悲伤' + }, { + value: 5, + label: '睡觉' + }, { + value: 6, + label: '打哈欠' + }] +} + +/** + * 处理与 task type options 相关的状态或数据,该影响范围仅限当前模块。 + */ +const taskTypeOptions = () => { + return [{ + value: 'voice', + label: '语音交互' + }, { + value: 'touch', + label: '触控交互' + }] +} + +/** + * 处理与 voice options 相关的状态或数据,该影响范围仅限当前模块。 + */ +const voiceOptions = () => { + return [{ + value: 'x4_xiaoyan', + label: 'x4_xiaoyan' + }] +} + +/** + * 处理与 inspection event types 相关的状态或数据,该影响范围仅限当前模块。 + */ +const inspectionEventTypes = () => { + return [{ + value: 'No-Glove', + label: '没戴手套' + }, { + value: 'No-Helmet-Glove', + label: '没戴头盔' + }] +} + +export const collapseList = [ + { + collapseTitle: "相机", + nodeList: [ + { + icon: cameraSvg, + name: "开启相机", + type: "serviceNode", + desc: "在获取相机照片时,需要先启动相机", + action: 'CAMERA_START', + nodeType: 'EDGE', + nodeParams: [], + outputParams: [], + outputType: 'json' + }, + { + icon: cameraSvg, + name: "获取图片", + type: "serviceNode", + desc: "获取相机拍摄的照片", + action: 'CAMERA_GETRGBIMAGE', + nodeType: 'EDGE', + nodeParams: [], + outputType: 'img', + outputParams: [{ name: 'imageUrl', type: 'array', desc: '图片地址数组', children: [], disabled: true }] + }, + { + icon: cameraSvg, + name: "关闭相机", + type: "serviceNode", + desc: "获取相机照片后,关闭该相机", + action: 'CAMERA_STOP', + nodeType: 'EDGE', + nodeParams: [], + outputParams: [], + outputType: 'json' + }, + ], + }, + { + collapseTitle: "录像", + nodeList: [ + { + icon: videoSvg, + name: "开启录像", + type: "serviceNode", + desc: "开启摄像头,并开始录像", + action: 'CAMERA_RECORDING_START', + nodeType: 'EDGE', + nodeParams: [], + outputParams: [], + outputType: 'json' + }, + { + icon: videoSvg, + name: "结束录像", + type: "serviceNode", + desc: "停止录像,并关闭摄像头", + action: 'CAMERA_RECORDING_STOP', + nodeType: 'EDGE', + nodeParams: [], + 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: planSvg, + name: "子流程", + type: "subFlow", + action: 'SUB_FLOW', + nodeType: 'NONE', + desc: "执行同一项目中已发布的工作流", + subFlow: {}, + nodeParams: [], + 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: '{"a": 123}' }, + { 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: ` +// 方法定义不能修改 +/** + * 处理与 r 相关的状态或数据,该影响范围仅限当前模块。 + * @param {*} params 待处理的参数集合 + */ +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: "AGV控制", + type: "serviceNode", + desc: "控制机器人的底盘", + action: 'AGV_MOVE_TO_POINT', + nodeType: 'EDGE', + nodeParams: [ + { name: "x", type: "input", input: "", disabled: true }, + { name: "y", type: "input", input: "", disabled: true }, + { name: "theta", type: "input", input: "", disabled: true }, + { name: "blockedTimeoutMs", type: "input", componentType: "number", input: 60000, min: 1, required: true, disabled: true }, + ], + outputType: 'json' + }, + { + icon: videoSvg, + name: "AGV站点导航", + type: "serviceNode", + desc: "从AGV站点列表选择目标站点并导航", + action: 'AGV_MOVE_TO_STATION', + nodeType: 'EDGE', + nodeParams: [ + { name: "stationId", type: "input", input: "", componentType: 'select', selectOptions: [], forceSelect: true, disabled: true }, + { name: "blockedTimeoutMs", type: "input", componentType: "number", input: 60000, min: 1, required: true, disabled: true } + ], + outputType: 'json' + }, + { + icon: cameraSvg, + name: "通用指令", + type: "deviceUniversal", + desc: "执行设备通用指令", + action: 'DEVICE_EXECUTE_JSON_COMMAND', + nodeType: 'EDGE', + nodeParams: [ + { name: "requestJson", type: "input", input: `{ + "a":123 +}`, required: false, disabled: true }, + ], + outputParams: [], + outputType: 'json' + }, + { + icon: planSvg, + name: "动作队列", + type: "actionQueue", + desc: "在边缘端按顺序连续执行多个动作", + action: 'SYSTEM_EXECUTE_ACTION_QUEUE', + nodeType: 'EDGE', + nodeParams: [ + { name: "actionId", type: "input", input: "", required: false }, + { name: "totalTimeoutMs", type: "input", componentType: 'number', input: 60000, required: true }, + { name: "steps", type: "input", input: [], required: true }, + ], + outputParams: [ + { name: 'actionId', type: 'string', desc: '动作队列标识' }, + { name: 'completedSteps', type: 'number', desc: '已完成步骤数' }, + { name: 'result', type: 'string', desc: '执行结果' }, + ], + outputType: 'json' + }, + { + icon: videoSvg, + name: "机械臂", + type: "serviceNode", + desc: "控制机器人的手臂", + action: 'ARM_MOVE_TO_POINT', + nodeType: 'EDGE', + nodeParams: [ + { name: "x", type: "input", input: "", disabled: true }, + { name: "y", type: "input", input: "", disabled: true }, + { name: "z", type: "input", input: "", disabled: true }, + { name: "rx", type: "input", input: "", disabled: true }, + { name: "ry", type: "input", input: "", disabled: true }, + { name: "rz", type: "input", input: "", disabled: true }, + { name: "velocity", type: "input", componentType: 'number', max: 5, step: 0.01, input: 0.1, disabled: true }, + { name: "acceleration", type: "input", componentType: 'number', max: 5, step: 0.01, input: 0.2, disabled: true }, + ], + outputType: 'json' + }, + { + icon: videoSvg, + name: "机械臂关节运动", + type: "serviceNode", + desc: "通过关节空间插值运动到目标关节位置", + action: 'ARM_MOVE_TO_J', + nodeType: 'EDGE', + nodeParams: [ + { name: "target", type: "input", input: "[0, 0, 0, 0, 0, 0]", disabled: true }, + { name: "velocity", type: "input", componentType: 'number', max: 5, step: 0.01, input: 0.1, disabled: true }, + { name: "acceleration", type: "input", componentType: 'number', max: 5, step: 0.01, input: 0.2, disabled: true }, + ], + outputType: 'json' + }, + // 使能状态由两个独立节点显式控制。节点本身不接收参数,也不依赖前一运动节点的隐式使能, + // 因而可在流程开始、异常恢复或任务收尾位置按业务需要自由编排。 + { + icon: videoSvg, + name: "开启机械臂使能", + type: "serviceNode", + desc: "开启机械臂使能,后续运动节点不会自动使能", + action: 'ARM_ENABLE', + nodeType: 'EDGE', + nodeParams: [], + outputParams: [], + outputType: 'json' + }, + // 关闭节点与开启节点使用不同 action,边缘执行器据此下发去使能指令; + // 空 outputParams 表示该节点只负责设备状态切换,不向后续节点暴露业务输出。 + { + icon: videoSvg, + name: "关闭机械臂使能", + type: "serviceNode", + desc: "关闭机械臂使能", + action: 'ARM_DISABLE', + nodeType: 'EDGE', + nodeParams: [], + outputParams: [], + outputType: 'json' + }, + { + icon: cameraSvg, + name: "开始监听报警事件", + type: "serviceNode", + desc: "开始监听报警事件", + action: 'INSPECTION_ALERT_LISTEN_START', + nodeType: 'EDGE', + nodeParams: [ + { name: "robotId", type: "input", input: "", disabled: true }, + { name: "eventTypes", type: "input", input: "", componentType: 'select', selectOptions: inspectionEventTypes(), disabled: true } + ], + outputParams: [], + outputType: 'json' + }, + { + icon: cameraSvg, + name: "结束监听报警事件", + type: "serviceNode", + desc: "结束监听报警事件", + action: 'INSPECTION_ALERT_LISTEN_STOP', + nodeType: 'EDGE', + nodeParams: [ + { name: "robotId", type: "input", input: "", disabled: true }, + { name: "eventTypes", type: "input", input: "", componentType: 'select', selectOptions: inspectionEventTypes(), disabled: true } + ], + outputParams: [], + outputType: 'json' + }, + { + icon: dialogueSvg, + name: "巡检仪表读数识别", + type: "recognize", + desc: "巡检仪表读数识别", + action: 'INSPECTION_METER_RECOGNIZE', + nodeType: "LLM", + outputType: 'json', + nodeParams: [ + { name: 'imageUrl', type: "input", input: "", disabled: true }, + { name: 'resultName', type: "input", input: "", disabled: true }, + { name: "alarmRules", type: "input", input: [], disabled: true } + ], + outputParams: [] + }, + { + icon: cameraSvg, + name: "人工巡检判断", + type: "serviceNode", + desc: "创建人工巡检判断任务", + action: 'INSPECTION_MANUAL_REVIEW_CREATE', + nodeType: 'EDGE', + nodeParams: [ + { name: "resultName", type: "input", input: "", disabled: true }, + { name: "videoUrl", type: "input", input: "", required: false, disabled: true }, + { name: "imageUrl", type: "input", input: "", required: false, disabled: true }, + { name: "reviewCriteria", type: "input", input: "", disabled: true }, + { name: "defaultAlarmLevel", type: "input", componentType: 'number', max: 3, step: 1, input: 1, disabled: true }, + ], + outputParams: [], + outputType: 'json' + }, + ], + }, + { + collapseTitle: "音频", + nodeList: [ + { + icon: microphoneSvg, + name: "启动麦克风", + type: "serviceNode", + desc: "用于启动麦克风的节点", + action: 'MICROPHONE_START', + nodeType: 'EDGE', + nodeParams: [], + outputType: 'json' + }, + { + icon: microphoneSvg, + name: "停止麦克风", + type: "serviceNode", + desc: "用于停止麦克风的节点", + action: 'MICROPHONE_STOP', + nodeType: 'EDGE', + nodeParams: [], + outputType: 'audio', + outputParams: [{ name: 'audioUrl', type: 'array', children: [], desc: '录音播放地址数组', disabled: true }] + }, + { + icon: audioSvg, + name: "播放音频", + type: "serviceNode", + desc: "用于播放音频的节点", + action: 'SPEAKER_PLAYAUDIO', + nodeType: 'EDGE', + nodeParams: [ + { name: "audioPath", type: "input", input: "", disabled: true } + ], + outputType: 'json' + }, + ], + }, + { + collapseTitle: "大模型", + nodeList: [ + { + icon: microphoneSvg, + name: "声音类型检测", + type: "mediaAnalysis", + desc: "判断录音是否属于指定的声音类型", + action: "AUDIO_EVENT_CLASSIFY", + nodeType: "LLM", + nodeParams: [ + { name: "profileCode", type: "input", input: "aima.power_state.v1", required: true, disabled: true }, + { name: "expectedLabel", type: "input", input: "", required: true, disabled: true }, + { name: "expectedLabelName", type: "input", input: "", required: false, disabled: true }, + { name: "audioUrl", type: "quote", input: "", quote: "", required: true, disabled: true }, + { name: "asynchronous", type: "input", input: false, required: false, disabled: true, componentType: "switch" }, + ], + outputType: "json", + outputParams: [ + { name: "passed", type: "boolean", desc: "是否检测到目标声音", disabled: true }, + { name: "expectedLabel", type: "string", desc: "目标声音类型编码", disabled: true }, + { name: "expectedLabelName", type: "string", desc: "目标声音类型名称", disabled: true }, + { name: "label", type: "string", desc: "实际声音类型编码,未匹配时为UNKNOWN", disabled: true }, + { name: "labelName", type: "string", desc: "实际声音类型名称", disabled: true }, + { name: "matched", type: "boolean", desc: "是否识别到已知声音", disabled: true }, + { name: "confidence", type: "number", desc: "最高匹配度", disabled: true }, + { name: "decisionReason", type: "string", desc: "检测结论", disabled: true }, + { name: "scores", type: "object", desc: "各分类匹配度", children: [], disabled: true }, + ], + }, + { + icon: videoSvg, + name: "视频智能分析", + type: "mediaAnalysis", + desc: "按分析场景理解测试录像并输出结构化结论", + action: "VIDEO_ANALYZE", + nodeType: "LLM", + nodeParams: [ + { name: "profileCode", type: "input", input: "aima.power_video.v1", required: true, disabled: true }, + { name: "videoUrl", type: "quote", input: "", quote: "", required: true, disabled: true }, + { name: "instruction", type: "input", input: "", required: true, disabled: true }, + { name: "analysisMode", type: "input", input: "AUTO", required: false, disabled: true }, + // 默认返回精简结论;用户开启详细输出时,执行端才补充事件证据。 + { name: "detailedOutput", type: "input", input: false, required: false, disabled: true, componentType: "switch" }, + { name: "analysisTuning", type: "input", input: { sampleFps: 2, maxWidth: 960, confidenceThreshold: 0.75, fallbackToAccurate: true }, required: false, disabled: true }, + { name: "asynchronous", type: "input", input: false, required: false, disabled: true, componentType: "switch" }, + ], + outputType: "json", + outputParams: [ + { name: "result", type: "string", desc: "分析结果", disabled: true }, + { name: "passed", type: "boolean", desc: "是否通过", disabled: true }, + { name: "confidence", type: "number", desc: "判定置信度", disabled: true }, + { name: "evidenceSufficient", type: "boolean", desc: "证据是否充分", disabled: true }, + { name: "decisionReason", type: "string", desc: "判定原因", disabled: true }, + { name: "conclusion", type: "string", desc: "分析结论", disabled: true }, + { name: "summary", type: "string", desc: "分析摘要", disabled: true }, + { name: "events", type: "array", desc: "关键事件", children: [], disabled: true }, + { name: "analysisMode", type: "string", desc: "实际分析模式", disabled: true }, + { name: "fallback", type: "boolean", desc: "是否切换精确分析", disabled: true }, + { name: "detailedOutput", type: "boolean", desc: "是否返回详细事件证据", disabled: true }, + ], + }, + { + // 通用图片分析不内置通过/不通过判定,返回结构完全由用户提示词约定。 + icon: cameraSvg, + name: "通用图片分析", + type: "imageAnalysis", + desc: "按照提示词分析一张或多张图片并返回指定内容", + action: 'IMAGE_ANALYZE', + nodeType: "LLM", + nodeParams: [ + { name: "profileCode", type: "input", input: "common.image_analysis.v1", required: true, disabled: true }, + { name: "imageUrl", type: "quote", input: "", quote: "", required: true, disabled: true }, + { name: "analysisMethod", type: "input", input: "VISION_MODEL", required: false, disabled: true }, + { name: "prompt", type: "input", input: "", required: true, disabled: true, componentType: "textarea" }, + { name: "analysisMode", type: "input", input: "FAST", required: false, disabled: true }, + { name: "analysisTuning", type: "input", input: { maxWidth: 896, maxImages: 12, maxOutputTokens: 384, matchThreshold: 0.48, matchSelector: "BEST", matchIndex: 1, matchSort: "ROW_MAJOR", locateConfidenceThreshold: 0.45 }, required: false, disabled: true }, + { name: "asynchronous", type: "input", input: false, required: false, disabled: true, componentType: "switch" }, + ], + outputType: "json", + outputParams: [ + { name: "result", type: "object", desc: "按提示词返回的结果", children: [], disabled: true }, + { name: "resultText", type: "string", desc: "模型返回的原始文本", disabled: true }, + { name: "matched", type: "boolean", desc: "找图是否匹配", disabled: true }, + { name: "matchScore", type: "number", desc: "匹配分数或定位置信度", disabled: true }, + { name: "coordinates", type: "array", desc: "原图坐标 [x1,y1,x2,y2]", children: [], disabled: true }, + { name: "center", type: "array", desc: "目标中心点 [x,y]", children: [], disabled: true }, + { name: "matchedDescription", type: "string", desc: "模型实际定位的目标描述", disabled: true }, + { name: "refinementMethod", type: "string", desc: "坐标精修方式", disabled: true }, + { name: "matchCount", type: "number", desc: "匹配项数量", disabled: true }, + { name: "matches", type: "array", desc: "全部匹配项", children: [], disabled: true }, + { name: "selectedMatchIndex", type: "number", desc: "当前选择的匹配序号", disabled: true }, + ], + }, + { + 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: "用于播放语料的节点,需要输入语音路径,执行设备由流程角色配置", + action: 'VI_PLAY_CORPUS', + nodeType: "EDGE", + outputType: 'json', + nodeParams: [ + { name: 'audioPath', 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: "touchParams", type: "input", input: "", disabled: true } + ], + outputType: 'json', + }, + { + icon: expressionSvg, + name: "开始说话", + type: "serviceNode", + desc: "控制机器人说话", + action: 'BIO_HEAD_SPEAK_START', + nodeType: "EDGE", + nodeParams: [], + outputType: 'json', + }, + { + icon: expressionSvg, + name: "停止说话", + type: "serviceNode", + desc: "停止机器人说话", + action: 'BIO_HEAD_SPEAK_STOP', + nodeType: "EDGE", + nodeParams: [], + outputType: 'json', + }, + { + icon: expressionSvg, + name: "表情", + type: "serviceNode", + desc: "控制机器人的面部表情", + action: 'BIO_HEAD_SPECIAL_EXPRESSION', + nodeType: "EDGE", + nodeParams: [ + { 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..e175c93 --- /dev/null +++ b/src/views/flow/index.vue @@ -0,0 +1,2994 @@ + + + + + diff --git a/src/views/flow/nodes/common/endNode.vue b/src/views/flow/nodes/common/endNode.vue new file mode 100644 index 0000000..17d291c --- /dev/null +++ b/src/views/flow/nodes/common/endNode.vue @@ -0,0 +1,24 @@ + + + + diff --git a/src/views/flow/nodes/common/selectAreaNode.vue b/src/views/flow/nodes/common/selectAreaNode.vue new file mode 100644 index 0000000..27be9eb --- /dev/null +++ b/src/views/flow/nodes/common/selectAreaNode.vue @@ -0,0 +1,128 @@ + + + + diff --git a/src/views/flow/nodes/common/serviceNode.vue b/src/views/flow/nodes/common/serviceNode.vue new file mode 100644 index 0000000..b8cc687 --- /dev/null +++ b/src/views/flow/nodes/common/serviceNode.vue @@ -0,0 +1,169 @@ + + + + + + diff --git a/src/views/flow/nodes/common/startNode.vue b/src/views/flow/nodes/common/startNode.vue new file mode 100644 index 0000000..a88f2e4 --- /dev/null +++ b/src/views/flow/nodes/common/startNode.vue @@ -0,0 +1,101 @@ + + + + diff --git a/src/views/flow/nodes/function/actionQueue.vue b/src/views/flow/nodes/function/actionQueue.vue new file mode 100644 index 0000000..c711e3b --- /dev/null +++ b/src/views/flow/nodes/function/actionQueue.vue @@ -0,0 +1,25 @@ + + + + + diff --git a/src/views/flow/nodes/function/codeNode.vue b/src/views/flow/nodes/function/codeNode.vue new file mode 100644 index 0000000..63e3112 --- /dev/null +++ b/src/views/flow/nodes/function/codeNode.vue @@ -0,0 +1,146 @@ + + + + + + diff --git a/src/views/flow/nodes/function/currentLoop.vue b/src/views/flow/nodes/function/currentLoop.vue new file mode 100644 index 0000000..4299bcc --- /dev/null +++ b/src/views/flow/nodes/function/currentLoop.vue @@ -0,0 +1,262 @@ + + + + + + diff --git a/src/views/flow/nodes/function/deviceUniversal.vue b/src/views/flow/nodes/function/deviceUniversal.vue new file mode 100644 index 0000000..d10473c --- /dev/null +++ b/src/views/flow/nodes/function/deviceUniversal.vue @@ -0,0 +1,146 @@ + + + + + + diff --git a/src/views/flow/nodes/function/httpNode.vue b/src/views/flow/nodes/function/httpNode.vue new file mode 100644 index 0000000..f58bd26 --- /dev/null +++ b/src/views/flow/nodes/function/httpNode.vue @@ -0,0 +1,146 @@ + + + + + + diff --git a/src/views/flow/nodes/function/loop.vue b/src/views/flow/nodes/function/loop.vue new file mode 100644 index 0000000..702416b --- /dev/null +++ b/src/views/flow/nodes/function/loop.vue @@ -0,0 +1,265 @@ + + + + diff --git a/src/views/flow/nodes/function/recognize.vue b/src/views/flow/nodes/function/recognize.vue new file mode 100644 index 0000000..e9f4279 --- /dev/null +++ b/src/views/flow/nodes/function/recognize.vue @@ -0,0 +1,130 @@ + + + + + + diff --git a/src/views/flow/nodes/function/sdAgent.vue b/src/views/flow/nodes/function/sdAgent.vue new file mode 100644 index 0000000..458ee6c --- /dev/null +++ b/src/views/flow/nodes/function/sdAgent.vue @@ -0,0 +1,130 @@ + + + + + + diff --git a/src/views/flow/nodes/function/sleep.vue b/src/views/flow/nodes/function/sleep.vue new file mode 100644 index 0000000..37c5f44 --- /dev/null +++ b/src/views/flow/nodes/function/sleep.vue @@ -0,0 +1,129 @@ + + + + + + diff --git a/src/views/flow/nodes/function/stopLoop.vue b/src/views/flow/nodes/function/stopLoop.vue new file mode 100644 index 0000000..e2f57c9 --- /dev/null +++ b/src/views/flow/nodes/function/stopLoop.vue @@ -0,0 +1,205 @@ + + + + + + + + diff --git a/src/views/flow/nodes/function/subEnd.vue b/src/views/flow/nodes/function/subEnd.vue new file mode 100644 index 0000000..29b57df --- /dev/null +++ b/src/views/flow/nodes/function/subEnd.vue @@ -0,0 +1,19 @@ + + + + diff --git a/src/views/flow/nodes/function/subFlow.vue b/src/views/flow/nodes/function/subFlow.vue new file mode 100644 index 0000000..546c8f8 --- /dev/null +++ b/src/views/flow/nodes/function/subFlow.vue @@ -0,0 +1,97 @@ + + + + + + + diff --git a/src/views/flow/nodes/function/subStart.vue b/src/views/flow/nodes/function/subStart.vue new file mode 100644 index 0000000..0675d52 --- /dev/null +++ b/src/views/flow/nodes/function/subStart.vue @@ -0,0 +1,19 @@ + + + + diff --git a/src/views/flow/nodes/function/switch.vue b/src/views/flow/nodes/function/switch.vue new file mode 100644 index 0000000..8d63bb2 --- /dev/null +++ b/src/views/flow/nodes/function/switch.vue @@ -0,0 +1,319 @@ + + + + + + diff --git a/src/views/flow/vue-flow/FlowEdge.vue b/src/views/flow/vue-flow/FlowEdge.vue new file mode 100644 index 0000000..ca50187 --- /dev/null +++ b/src/views/flow/vue-flow/FlowEdge.vue @@ -0,0 +1,95 @@ + + + + + + diff --git a/src/views/flow/vue-flow/FlowNode.vue b/src/views/flow/vue-flow/FlowNode.vue new file mode 100644 index 0000000..a44b597 --- /dev/null +++ b/src/views/flow/vue-flow/FlowNode.vue @@ -0,0 +1,419 @@ + + + + + + diff --git a/src/views/flow/vue-flow/adapter.js b/src/views/flow/vue-flow/adapter.js new file mode 100644 index 0000000..c3c66e1 --- /dev/null +++ b/src/views/flow/vue-flow/adapter.js @@ -0,0 +1,990 @@ +/** + * 文件说明:旧流程数据与 Vue Flow 状态、操作 API 之间的兼容适配层。 + * 作用范围:仅服务于流程设计器模块。 + */ +import { nextTick } from "vue"; +import { v4 as uuid } from "uuid"; +import { MarkerType } from "@vue-flow/core"; + +/** + * 克隆 clone 对应的数据或交互;作用范围仅限画布适配层。 + * @param {*} value 待处理的新值 + */ +const clone = (value) => JSON.parse(JSON.stringify(value ?? {})); + +const GROUP_TYPES = new Set(["loop", "selectArea"]); +const TARGET_ONLY_TYPES = new Set(["end", "stopLoop", "subEnd"]); +const SOURCE_ONLY_TYPES = new Set(["start", "subStart"]); +const LOOP_BODY_INSET = { left: 24, top: 112, right: 24, bottom: 24 }; +const GROUP_BODY_INSET = { left: 24, top: 120, right: 24, bottom: 24 }; +const LOOP_MIN_SIZE = { width: 520, height: 360 }; + +/** + * 处理 loopChildExtent 对应的数据或交互;作用范围仅限画布适配层。 + */ +const loopChildExtent = () => ({ + range: "parent", + padding: [ + LOOP_BODY_INSET.top, + LOOP_BODY_INSET.right, + LOOP_BODY_INSET.bottom, + LOOP_BODY_INSET.left, + ], +}); + +/** + * 编组 groupChildExtent 对应的数据或交互;作用范围仅限画布适配层。 + */ +const groupChildExtent = () => ({ + range: "parent", + padding: [ + GROUP_BODY_INSET.top, + GROUP_BODY_INSET.right, + GROUP_BODY_INSET.bottom, + GROUP_BODY_INSET.left, + ], +}); + +/** + * 处理 nodeSize 对应的数据或交互;作用范围仅限画布适配层。 + * @param {*} node 当前流程节点 + */ +const nodeSize = (node) => { + // 分支节点采用统一的标准卡片尺寸,忽略旧流程数据中遗留的大尺寸记录。 + if (node.type === "branch") return { width: 372, height: 180 }; + const width = Number(node.properties?.width || node.width); + const height = Number(node.properties?.height || node.height); + if (Number.isFinite(width) && Number.isFinite(height)) { + return { width, height }; + } + if (GROUP_TYPES.has(node.type)) return { width: 720, height: 460 }; + if (TARGET_ONLY_TYPES.has(node.type) || SOURCE_ONLY_TYPES.has(node.type)) { + return { width: 328, height: 120 }; + } + return { width: 372, height: 180 }; +}; + +/** + * 处理 edgeId 对应的数据或交互;作用范围仅限画布适配层。 + */ +const edgeId = () => `edge_${uuid().replace(/-/g, "")}`; +/** + * 处理 nodeId 对应的数据或交互;作用范围仅限画布适配层。 + */ +const nodeId = () => uuid().replace(/-/g, ""); + +/** + * 处理 toVueNode 对应的数据或交互;作用范围仅限画布适配层。 + * @param {*} node 当前流程节点 + * @param {*} sizeOverride 调用方传入的 sizeOverride 参数 + * @param {*} positionOverride 调用方传入的 positionOverride 参数 + */ +const toVueNode = (node, sizeOverride, positionOverride) => { + const normalized = clone(node); + normalized.id ||= nodeId(); + normalized.properties ||= {}; + const overrideWidth = Number(sizeOverride?.width); + const overrideHeight = Number(sizeOverride?.height); + const size = Number.isFinite(overrideWidth) && overrideWidth > 0 + && Number.isFinite(overrideHeight) && overrideHeight > 0 + ? { width: overrideWidth, height: overrideHeight } + : nodeSize(normalized); + const center = { + x: Number(normalized.x) || 0, + y: Number(normalized.y) || 0, + }; + const overrideX = Number(positionOverride?.x); + const overrideY = Number(positionOverride?.y); + const position = Number.isFinite(overrideX) && Number.isFinite(overrideY) + ? { x: overrideX, y: overrideY } + : { + x: center.x - size.width / 2, + y: center.y - size.height / 2, + }; + + const dimensions = GROUP_TYPES.has(normalized.type) + ? { width: size.width, height: size.height } + : { width: size.width }; + + return { + id: normalized.id, + type: "flow-node", + position, + ...dimensions, + draggable: normalized.draggable !== false, + selectable: true, + deletable: !SOURCE_ONLY_TYPES.has(normalized.type) && normalized.type !== "end", + connectable: true, + zIndex: GROUP_TYPES.has(normalized.type) ? 0 : 2, + data: { + flowType: normalized.type, + properties: normalized.properties, + children: Array.isArray(normalized.children) ? normalized.children : [], + text: normalized.text, + backend: normalized, + originalCenter: center, + originalPosition: position, + size, + }, + }; +}; + +/** + * 处理 toVueEdge 对应的数据或交互;作用范围仅限画布适配层。 + * @param {*} edge 调用方传入的 edge 参数 + */ +const toVueEdge = (edge) => { + const normalized = clone(edge); + normalized.id ||= edgeId(); + return { + id: normalized.id, + source: normalized.sourceNodeId, + target: normalized.targetNodeId, + sourceHandle: normalized.sourceAnchorId || null, + targetHandle: normalized.targetAnchorId || null, + type: "flow-edge", + animated: true, + selectable: true, + focusable: true, + deletable: true, + interactionWidth: 32, + markerEnd: { + type: MarkerType.ArrowClosed, + color: "#ff6b35", + width: 20, + height: 20, + }, + style: { stroke: "#ff6b35", strokeWidth: 2 }, + data: { backend: normalized }, + }; +}; + +/** + * 关联 attachChildrenToGroups 对应的数据或交互;作用范围仅限画布适配层。 + * @param {*} nodes 调用方传入的 nodes 参数 + */ +const attachChildrenToGroups = (nodes) => { + // 先建立节点索引和显式父子关系,再用旧数据中的 children 补齐缺失关系。 + // 最后统一换算相对坐标,避免父节点尚未就绪时提前计算造成位置漂移。 + const nodeLookup = new Map(nodes.map((node) => [node.id, node])); + const declaredParents = new Map(); + + nodes.filter((node) => GROUP_TYPES.has(node.data.flowType)).forEach((group) => { + (group.data.children || []).forEach((childId) => { + if (!declaredParents.has(childId)) declaredParents.set(childId, group.id); + }); + }); + + nodes.forEach((child) => { + const parentId = child.data.properties?.parentId || declaredParents.get(child.id); + const parent = nodeLookup.get(parentId); + if (!parent || !GROUP_TYPES.has(parent.data.flowType) || GROUP_TYPES.has(child.data.flowType)) return; + + const isLoop = parent.data.flowType === "loop"; + const relativePosition = { + x: isLoop + ? Math.max(child.position.x - parent.position.x, LOOP_BODY_INSET.left) + : child.position.x - parent.position.x, + y: isLoop + ? Math.max(child.position.y - parent.position.y, LOOP_BODY_INSET.top) + : child.position.y - parent.position.y, + }; + child.parentNode = parentId; + child.extent = isLoop ? loopChildExtent() : groupChildExtent(); + child.expandParent = false; + child.position = relativePosition; + child.zIndex = 5; + child.data.properties = { ...child.data.properties, parentId }; + parent.data.children = Array.from(new Set([...(parent.data.children || []), child.id])); + }); + + nodes.filter((node) => node.data.flowType === "loop").forEach((group) => { + const children = nodes.filter((node) => node.parentNode === group.id); + if (!children.length) return; + const requiredWidth = Math.max(...children.map((child) => ( + child.position.x + Number(child.data.size?.width || 0) + LOOP_BODY_INSET.right + ))); + const requiredHeight = Math.max(...children.map((child) => ( + child.position.y + Number(child.data.size?.height || 0) + LOOP_BODY_INSET.bottom + ))); + const width = Math.max(Number(group.data.size?.width || 0), LOOP_MIN_SIZE.width, requiredWidth); + const height = Math.max(Number(group.data.size?.height || 0), LOOP_MIN_SIZE.height, requiredHeight); + group.width = width; + group.height = height; + group.data.size = { width, height }; + group.data.properties = { ...group.data.properties, width, height }; + }); + + return nodes.sort((left, right) => Number(Boolean(left.parentNode)) - Number(Boolean(right.parentNode))); +}; + +export const toVueFlowData = (graph = {}, options = {}) => { + const nodes = attachChildrenToGroups((graph.nodes || []).map((node) => ( + toVueNode(node, options.nodeSizes?.get(node.id), options.nodePositions?.get(node.id)) + ))); + const nodeLookup = new Map(nodes.map((node) => [node.id, node])); + const edges = (graph.edges || []).map((edge) => { + const vueEdge = toVueEdge(edge); + const source = nodeLookup.get(vueEdge.source); + const target = nodeLookup.get(vueEdge.target); + if (source?.parentNode || target?.parentNode) vueEdge.zIndex = 4; + return vueEdge; + }); + return { nodes, edges }; +}; + +export const createFlowFacade = (store) => { + const handlers = new Map(); + const componentInstances = new Map(); + const loopLayoutStates = new Map(); + let historySuspended = false; + let history = []; + let historyIndex = -1; + + /** + * 获取 getNodes 对应的数据或交互;作用范围仅限画布适配层。 + */ + const getNodes = () => store.getNodes.value || []; + /** + * 获取 getEdges 对应的数据或交互;作用范围仅限画布适配层。 + */ + const getEdges = () => store.getEdges.value || []; + + /** + * 处理 backendNode 对应的数据或交互;作用范围仅限画布适配层。 + * @param {*} node 当前流程节点 + */ + const backendNode = (node) => { + const result = clone(node.data.backend); + const size = { + width: Number(node.width || node.dimensions?.width || node.data.size?.width || 0), + height: Number(node.height || node.dimensions?.height || node.data.size?.height || 0), + }; + const parent = node.parentNode ? store.findNode(node.parentNode) : null; + const parentPosition = parent?.position || parent?.computedPosition || { x: 0, y: 0 }; + const absolutePosition = parent + ? { x: parentPosition.x + node.position.x, y: parentPosition.y + node.position.y } + : (node.position || node.computedPosition); + result.id = node.id; + result.type = node.data.flowType; + if (GROUP_TYPES.has(result.type)) { + result.x = absolutePosition.x + size.width / 2; + result.y = absolutePosition.y + size.height / 2; + } else { + const originalCenter = node.data.originalCenter || { + x: absolutePosition.x + size.width / 2, + y: absolutePosition.y + size.height / 2, + }; + const originalPosition = node.data.originalPosition || absolutePosition; + result.x = originalCenter.x + absolutePosition.x - originalPosition.x; + result.y = originalCenter.y + absolutePosition.y - originalPosition.y; + } + result.properties = clone(node.data.properties || {}); + if (parent?.data?.flowType === "selectArea") delete result.properties.parentId; + if (GROUP_TYPES.has(result.type)) { + result.properties.width = size.width; + result.properties.height = size.height; + } + if (node.data.children?.length || GROUP_TYPES.has(result.type)) { + result.children = [...(node.data.children || [])]; + } else { + delete result.children; + } + return result; + }; + + /** + * 处理 backendEdge 对应的数据或交互;作用范围仅限画布适配层。 + * @param {*} edge 调用方传入的 edge 参数 + */ + const backendEdge = (edge) => ({ + ...clone(edge.data?.backend), + id: edge.id, + type: edge.data?.backend?.type || "bezier", + sourceNodeId: edge.source, + targetNodeId: edge.target, + sourceAnchorId: edge.sourceHandle || `${edge.source}_1`, + targetAnchorId: edge.targetHandle || `${edge.target}_3`, + }); + + /** + * 处理 emit 对应的数据或交互;作用范围仅限画布适配层。 + * @param {*} name 名称或事件名称 + * @param {*} payload 事件数据 + */ + const emit = (name, payload) => { + (handlers.get(name) || []).forEach((handler) => handler(payload)); + }; + + /** + * 处理 recordHistory 对应的数据或交互;作用范围仅限画布适配层。 + */ + const recordHistory = () => { + if (historySuspended) return; + const graph = facade.getGraphData(); + const serialized = JSON.stringify(graph); + if (history[historyIndex]?.serialized === serialized) return; + history = history.slice(0, historyIndex + 1); + history.push({ serialized, graph: clone(graph) }); + if (history.length > 100) history.shift(); + historyIndex = history.length - 1; + }; + + /** + * 更新 changed 对应的数据或交互;作用范围仅限画布适配层。 + */ + const changed = () => { + if (historySuspended) return; + recordHistory(); + emit("history:change", { data: facade.getGraphData() }); + }; + + /** + * 获取 getNodeRect 对应的数据或交互;作用范围仅限画布适配层。 + * @param {*} node 当前流程节点 + */ + const getNodeRect = (node) => { + const parent = node.parentNode ? store.findNode(node.parentNode) : null; + const parentPosition = parent?.position || parent?.computedPosition || { x: 0, y: 0 }; + const position = parent + ? { x: parentPosition.x + node.position.x, y: parentPosition.y + node.position.y } + : (node.position || node.computedPosition); + return { + x: Number(position?.x || 0), + y: Number(position?.y || 0), + width: Number(node.width || node.dimensions?.width || node.data.size?.width || 0), + height: Number(node.height || node.dimensions?.height || node.data.size?.height || 0), + }; + }; + + /** + * 获取 getLoopLayoutState 对应的数据或交互;作用范围仅限画布适配层。 + * @param {*} group 调用方传入的 group 参数 + */ + const getLoopLayoutState = (group) => { + const current = getNodeRect(group); + const existing = loopLayoutStates.get(group.id); + /** + * 处理 if 对应的数据或交互;作用范围仅限画布适配层。 + * @param {*} !existing 调用方传入的 !existing 参数 + */ + if (!existing) { + const state = { base: { ...current }, last: { ...current } }; + loopLayoutStates.set(group.id, state); + return state; + } + + const sizeChanged = Math.abs(current.width - existing.last.width) > 1 + || Math.abs(current.height - existing.last.height) > 1; + /** + * 处理 if 对应的数据或交互;作用范围仅限画布适配层。 + * @param {*} sizeChanged 调用方传入的 sizeChanged 参数 + */ + if (sizeChanged) { + existing.base = { ...current }; + } else { + const dx = current.x - existing.last.x; + const dy = current.y - existing.last.y; + existing.base.x += dx; + existing.base.y += dy; + } + existing.last = { ...current }; + return existing; + }; + + /** + * 布局 layoutLoop 对应的数据或交互;作用范围仅限画布适配层。 + * @param {*} group 调用方传入的 group 参数 + * @param {*} activeChildId 调用方传入的 activeChildId 参数 + * @param {*} activePosition 调用方传入的 activePosition 参数 + */ + const layoutLoop = (group, activeChildId, activePosition) => { + const state = getLoopLayoutState(group); + const children = getNodes().filter((node) => node.parentNode === group.id); + if (!children.length) return false; + + const snapshots = children.map((child) => { + const rect = getNodeRect(child); + const position = child.id === activeChildId && activePosition + ? { x: activePosition.x, y: activePosition.y } + : { x: rect.x, y: rect.y }; + return { child, position, width: rect.width, height: rect.height }; + }); + const contentLeft = Math.min(...snapshots.map(({ position }) => position.x)); + const contentTop = Math.min(...snapshots.map(({ position }) => position.y)); + const contentRight = Math.max(...snapshots.map(({ position, width }) => position.x + width)); + const contentBottom = Math.max(...snapshots.map(({ position, height }) => position.y + height)); + const left = Math.min(state.base.x, contentLeft - LOOP_BODY_INSET.left); + const top = Math.min(state.base.y, contentTop - LOOP_BODY_INSET.top); + const right = Math.max( + state.base.x + Math.max(state.base.width, LOOP_MIN_SIZE.width), + contentRight + LOOP_BODY_INSET.right, + ); + const bottom = Math.max( + state.base.y + Math.max(state.base.height, LOOP_MIN_SIZE.height), + contentBottom + LOOP_BODY_INSET.bottom, + ); + const width = right - left; + const height = bottom - top; + const properties = { ...group.data.properties, width, height }; + + store.updateNode(group.id, { + position: { x: left, y: top }, + computedPosition: { ...group.computedPosition, x: left, y: top }, + dimensions: { width, height }, + width, + height, + }); + store.updateNodeData(group.id, { + ...group.data, + properties, + size: { width, height }, + }, { replace: true }); + snapshots.forEach(({ child, position }) => { + store.updateNode(child.id, { + position: { x: position.x - left, y: position.y - top }, + computedPosition: { ...child.computedPosition, x: position.x, y: position.y }, + extent: loopChildExtent(), + expandParent: false, + }); + }); + state.last = { x: left, y: top, width, height }; + nextTick(() => store.updateNodeInternals([group.id, ...children.map((child) => child.id)])); + return true; + }; + + /** + * 获取 getAnchors 对应的数据或交互;作用范围仅限画布适配层。 + * @param {*} node 当前流程节点 + */ + const getAnchors = (node) => { + const type = node.data.flowType; + /** + * 处理 if 对应的数据或交互;作用范围仅限画布适配层。 + * @param {*} type 目标类型 + */ + if (type === "branch") { + return [ + { id: `${node.id}_entry`, name: "left" }, + ...(node.data.properties?.anchor || []).map((item) => ({ ...item, name: "right" })), + ]; + } + if (TARGET_ONLY_TYPES.has(type)) return [{ id: `${node.id}_3`, name: "left" }]; + if (SOURCE_ONLY_TYPES.has(type)) return [{ id: `${node.id}_1`, name: "right" }]; + if (type === "selectArea") return []; + return [ + { id: `${node.id}_1`, name: "right" }, + { id: `${node.id}_3`, name: "left" }, + ]; + }; + + /** + * 处理 modelFor 对应的数据或交互;作用范围仅限画布适配层。 + * @param {*} id 目标元素 ID + */ + const modelFor = (id) => { + const node = store.findNode(id); + if (!node) return null; + const model = { + id, + type: node.data.flowType, + get x() { return backendNode(node).x; }, + get y() { return backendNode(node).y; }, + get width() { return node.dimensions?.width || node.data.size?.width || 0; }, + get height() { return node.dimensions?.height || node.data.size?.height || 0; }, + get left() { return getNodeRect(node).x; }, + get top() { return getNodeRect(node).y; }, + get parentId() { return node.parentNode || null; }, + get anchors() { return getAnchors(node); }, + get children() { return new Set(node.data.children || []); }, + set children(value) { + const children = Array.from(value || []); + store.updateNodeData(id, { ...node.data, children }, { replace: true }); + changed(); + }, + get draggable() { return node.draggable !== false; }, + set draggable(value) { store.updateNode(id, { draggable: Boolean(value) }); }, + getProperties: () => facade.getProperties(id), + setProperties: (properties) => facade.setProperties(id, properties), + setComponentInstance: (instance) => componentInstances.set(id, instance), + closePopover: () => componentInstances.get(id)?.closePopover?.(), + updateSize: () => store.updateNodeInternals([id]), + setCustomProperties: () => store.updateNodeInternals([id]), + }; + return model; + }; + + const facade = { + /** + * 订阅 on 对应的数据或交互;作用范围仅限画布适配层。 + * @param {*} name 名称或事件名称 + * @param {*} handler 事件处理函数 + */ + on(name, handler) { + const list = handlers.get(name) || []; + list.push(handler); + handlers.set(name, list); + return () => facade.off(name, handler); + }, + /** + * 订阅 once 对应的数据或交互;作用范围仅限画布适配层。 + * @param {*} name 名称或事件名称 + * @param {*} handler 事件处理函数 + */ + once(name, handler) { + /** + * 处理 wrapped 对应的数据或交互;作用范围仅限画布适配层。 + * @param {*} payload 事件数据 + */ + const wrapped = (payload) => { + facade.off(name, wrapped); + handler(payload); + }; + facade.on(name, wrapped); + }, + /** + * 取消订阅 off 对应的数据或交互;作用范围仅限画布适配层。 + * @param {*} name 名称或事件名称 + * @param {*} handler 事件处理函数 + */ + off(name, handler) { + if (!handler) handlers.delete(name); + else handlers.set(name, (handlers.get(name) || []).filter((item) => item !== handler)); + }, + emit, + markChanged: changed, + /** + * 获取 getGraphData 对应的数据或交互;作用范围仅限画布适配层。 + */ + getGraphData() { + return { + nodes: getNodes().map(backendNode), + edges: getEdges().map(backendEdge), + }; + }, + /** + * 渲染 render 对应的数据或交互;作用范围仅限画布适配层。 + * @param {*} graph 调用方传入的 graph 参数 + * @param {*} options 行为配置 + */ + render(graph, options = {}) { + historySuspended = true; + loopLayoutStates.clear(); + const nodeSizes = options.preserveNodeSizes === true + ? new Map(getNodes().map((node) => [node.id, { + width: Number(node.dimensions?.width || node.width || node.data.size?.width || 0), + height: Number(node.dimensions?.height || node.height || node.data.size?.height || 0), + }])) + : options.nodeSizes; + const nodePositions = options.preserveNodePositions === true + ? new Map(getNodes().map((node) => { + const rect = getNodeRect(node); + return [node.id, { x: rect.x, y: rect.y }]; + })) + : options.nodePositions; + const data = toVueFlowData(graph, { nodeSizes, nodePositions }); + /** + * 处理 finishRender 对应的数据或交互;作用范围仅限画布适配层。 + */ + const finishRender = () => { + historySuspended = false; + if (options.resetHistory !== false) { + history = []; + historyIndex = -1; + recordHistory(); + } + nextTick(() => store.updateNodeInternals()); + }; + /** + * 处理 applyData 对应的数据或交互;作用范围仅限画布适配层。 + */ + const applyData = () => { + store.setNodes(data.nodes); + store.setEdges(data.edges); + finishRender(); + }; + + if (options.replaceNodes === true) { + // Changing parentNode is structural in Vue Flow. Unmount in one tick + // and remount in the next so node wrappers and edges cannot retain + // different graph-node instances for the same id. + store.setEdges([]); + store.setNodes([]); + nextTick(applyData); + } else { + applyData(); + } + }, + /** + * 新增 addNode 对应的数据或交互;作用范围仅限画布适配层。 + * @param {*} node 当前流程节点 + */ + addNode(node) { + const vueNode = toVueNode(node); + store.addNodes(vueNode); + changed(); + return backendNode(vueNode); + }, + /** + * 删除 deleteNode 对应的数据或交互;作用范围仅限画布适配层。 + * @param {*} id 目标元素 ID + */ + deleteNode(id) { + const target = store.findNode(id); + if (target?.data?.flowType === "selectArea") { + facade.ungroup(id); + return; + } + const parentId = target?.parentNode; + const childIds = target?.data?.children || []; + const removeChildren = target?.data?.flowType === "loop" && childIds.length > 0; + const nodeIds = removeChildren ? [id, ...childIds] : id; + const removedIds = new Set(Array.isArray(nodeIds) ? nodeIds : [nodeIds]); + store.removeNodes(nodeIds, true, removeChildren); + getNodes().forEach((node) => { + if (node.data.children?.includes(id) && !removedIds.has(node.id)) { + store.updateNodeData(node.id, { + ...node.data, + children: node.data.children.filter((childId) => childId !== id), + }, { replace: true }); + } + }); + componentInstances.delete(id); + childIds.forEach((childId) => componentInstances.delete(childId)); + changed(); + if (parentId) nextTick(() => facade.resizeGroupToChildren(parentId)); + }, + /** + * 新增 addEdge 对应的数据或交互;作用范围仅限画布适配层。 + * @param {*} edge 调用方传入的 edge 参数 + */ + addEdge(edge) { + const vueEdge = toVueEdge(edge); + const source = store.findNode(vueEdge.source); + const target = store.findNode(vueEdge.target); + if (source?.parentNode || target?.parentNode) vueEdge.zIndex = 4; + store.addEdges(vueEdge); + changed(); + return backendEdge(vueEdge); + }, + /** + * 删除 deleteEdge 对应的数据或交互;作用范围仅限画布适配层。 + * @param {*} id 目标元素 ID + */ + deleteEdge(id) { + store.removeEdges(id); + changed(); + }, + /** + * 删除 deleteEdgeByNodeId 对应的数据或交互;作用范围仅限画布适配层。 + * @param {*} id 目标元素 ID + */ + deleteEdgeByNodeId(id) { + store.removeEdges(getEdges().filter((edge) => edge.source === id || edge.target === id)); + changed(); + }, + /** + * 获取 getProperties 对应的数据或交互;作用范围仅限画布适配层。 + * @param {*} id 目标元素 ID + */ + getProperties(id) { + return clone(store.findNode(id)?.data?.properties || {}); + }, + /** + * 设置 setProperties 对应的数据或交互;作用范围仅限画布适配层。 + * @param {*} id 目标元素 ID + * @param {*} properties 调用方传入的 properties 参数 + */ + setProperties(id, properties) { + const node = store.findNode(id); + if (!node) return; + store.updateNodeData(id, { ...node.data, properties: clone(properties) }, { replace: true }); + nextTick(() => store.updateNodeInternals([id])); + changed(); + }, + getNodeModelById: modelFor, + /** + * 获取 getSelectElements 对应的数据或交互;作用范围仅限画布适配层。 + */ + getSelectElements() { + return { + nodes: (store.getSelectedNodes.value || []).map(backendNode), + edges: (store.getSelectedEdges.value || []).map(backendEdge), + }; + }, + /** + * 清理 clearSelectElements 对应的数据或交互;作用范围仅限画布适配层。 + */ + clearSelectElements() { + store.removeSelectedElements(); + }, + /** + * 处理 selectElementById 对应的数据或交互;作用范围仅限画布适配层。 + * @param {*} id 目标元素 ID + */ + selectElementById(id) { + store.removeSelectedElements(); + if (!id) return; + const node = store.findNode(id); + if (node) store.addSelectedNodes([node]); + const edge = store.findEdge(id); + if (edge) store.addSelectedEdges([edge]); + }, + /** + * 处理 selectElementsByIds 对应的数据或交互;作用范围仅限画布适配层。 + * @param {*} ids 调用方传入的 ids 参数 + */ + selectElementsByIds(ids = []) { + store.removeSelectedElements(); + const nodes = ids.map((id) => store.findNode(id)).filter(Boolean); + if (nodes.length) store.addSelectedNodes(nodes); + }, + /** + * 处理 focusOn 对应的数据或交互;作用范围仅限画布适配层。 + * @param {*} { id } 调用方传入的 { id } 参数 + */ + focusOn({ id }) { + const node = store.findNode(id); + if (!node) return; + const center = backendNode(node); + store.setCenter(center.x, center.y, { zoom: Math.max(store.viewport.value?.zoom || 1, 0.7), duration: 300 }); + }, + /** + * 处理 fitView 对应的数据或交互;作用范围仅限画布适配层。 + */ + fitView() { + return store.fitView({ padding: 0.18, duration: 300 }); + }, + /** + * 处理 translateCenter 对应的数据或交互;作用范围仅限画布适配层。 + */ + translateCenter() { + return facade.fitView(); + }, + /** + * 缩放 zoom 对应的数据或交互;作用范围仅限画布适配层。 + * @param {*} scale 缩放比例 + * @param {*} center 缩放中心 + */ + zoom(scale, center) { + if (Array.isArray(center) && center.length === 2) { + return store.setCenter(center[0], center[1], { zoom: scale, duration: 250 }); + } + return store.zoomTo(scale, { duration: 250 }); + }, + /** + * 获取 getPointByClient 对应的数据或交互;作用范围仅限画布适配层。 + * @param {*} x 客户端横坐标 + * @param {*} y 客户端纵坐标 + */ + getPointByClient(x, y) { + const point = store.screenToFlowCoordinate({ x, y }); + return { canvasOverlayPosition: point, domOverlayPosition: point }; + }, + /** + * 新增 addToGroup 对应的数据或交互;作用范围仅限画布适配层。 + * @param {*} groupId 分组节点 ID + * @param {*} childId 子节点 ID + * @param {*} dropCenter 拖放中心坐标 + */ + addToGroup(groupId, childId, dropCenter) { + const group = store.findNode(groupId); + const child = store.findNode(childId); + if (!group || !child || GROUP_TYPES.has(child.data.flowType)) return; + const previousParentId = child.parentNode || child.data?.properties?.parentId; + const graph = facade.getGraphData(); + const groupData = graph.nodes.find((node) => node.id === groupId); + const childData = graph.nodes.find((node) => node.id === childId); + if (!groupData || !childData) return; + + // 节点从循环体外进入循环体后,原连线的两端已处于不同执行作用域,必须全部断开。 + // 在重新渲染父子结构前直接过滤图数据,可保证入边、出边和层级变更在同一次更新中完成。 + const enteringLoop = group.data.flowType === "loop" && previousParentId !== groupId; + if (enteringLoop) { + graph.edges = graph.edges.filter((edge) => ( + edge.sourceNodeId !== childId && edge.targetNodeId !== childId + )); + } + + groupData.children = Array.from(new Set([...(groupData.children || []), childId])); + childData.properties = { ...(childData.properties || {}), parentId: groupId }; + if (dropCenter) { + childData.x = dropCenter.x; + childData.y = dropCenter.y; + } + facade.render(graph, { + resetHistory: false, + replaceNodes: true, + preserveNodeSizes: true, + preserveNodePositions: true, + }); + nextTick(() => { + store.updateNodeInternals([groupId, childId]); + changed(); + }); + }, + /** + * 编组 groupNodes 对应的数据或交互;作用范围仅限画布适配层。 + * @param {*} groupNode 调用方传入的 groupNode 参数 + * @param {*} childIds 子节点 ID 集合 + */ + groupNodes(groupNode, childIds = []) { + // 先计算选中节点的外接矩形确定容器尺寸,再把子节点坐标转换为容器内相对坐标。 + // 与分组外节点相连的边保持不变,此处仅更新节点层级和几何信息。 + const graph = facade.getGraphData(); + const selectedIds = new Set(childIds); + const children = graph.nodes.filter((node) => ( + selectedIds.has(node.id) + && !GROUP_TYPES.has(node.type) + && !store.findNode(node.id)?.parentNode + )); + if (!children.length) return null; + + const group = clone(groupNode); + group.id ||= nodeId(); + group.type = "selectArea"; + group.properties = { ...(group.properties || {}), action: "NONE" }; + group.children = children.map((node) => node.id); + children.forEach((node) => { + node.properties = { ...(node.properties || {}), parentId: group.id }; + }); + graph.nodes.push(group); + facade.render(graph, { + resetHistory: false, + replaceNodes: true, + preserveNodeSizes: true, + preserveNodePositions: true, + }); + nextTick(() => { + store.updateNodeInternals([group.id, ...group.children]); + changed(); + }); + return clone(group); + }, + /** + * 处理 ungroup 对应的数据或交互;作用范围仅限画布适配层。 + * @param {*} groupId 分组节点 ID + */ + ungroup(groupId) { + const graph = facade.getGraphData(); + const group = graph.nodes.find((node) => node.id === groupId && node.type === "selectArea"); + if (!group) return false; + const childIds = new Set([ + ...(group.children || []), + ...graph.nodes.filter((node) => node.properties?.parentId === groupId).map((node) => node.id), + ]); + graph.nodes = graph.nodes.filter((node) => node.id !== groupId); + graph.nodes.forEach((node) => { + if (!childIds.has(node.id)) return; + const properties = { ...(node.properties || {}) }; + delete properties.parentId; + node.properties = properties; + }); + facade.render(graph, { + resetHistory: false, + replaceNodes: true, + preserveNodeSizes: true, + preserveNodePositions: true, + }); + nextTick(() => { + store.updateNodeInternals([...childIds]); + changed(); + }); + return true; + }, + /** + * 准备 prepareLoopChildDrag 对应的数据或交互;作用范围仅限画布适配层。 + * @param {*} childId 子节点 ID + */ + prepareLoopChildDrag(childId) { + const child = store.findNode(childId); + const group = child?.parentNode ? store.findNode(child.parentNode) : null; + if (!group || group.data.flowType !== "loop") return; + store.updateNode(childId, { + extent: loopChildExtent(), + expandParent: false, + }); + getLoopLayoutState(group); + const rect = getNodeRect(child); + return { groupId: group.id, position: { x: rect.x, y: rect.y } }; + }, + /** + * 布局 layoutLoopDuringDrag 对应的数据或交互;作用范围仅限画布适配层。 + * @param {*} groupId 分组节点 ID + * @param {*} childId 子节点 ID + * @param {*} position 调用方传入的 position 参数 + */ + layoutLoopDuringDrag(groupId, childId, position) { + const group = store.findNode(groupId); + if (!group || group.data.flowType !== "loop" || !position) return; + layoutLoop(group, childId, position); + }, + /** + * 调整 resizeGroupToChildren 对应的数据或交互;作用范围仅限画布适配层。 + * @param {*} groupId 分组节点 ID + * @param {*} options 行为配置 + */ + resizeGroupToChildren(groupId, options = {}) { + // 循环容器沿用统一的内边距和最小尺寸约束,并按配置决定是否记录变更。 + // 拖拽过程可关闭变更标记,避免高频移动产生多余历史记录。 + const group = store.findNode(groupId); + if (!group || group.data.flowType !== "loop") return; + if (layoutLoop(group) && options.markChanged !== false) changed(); + }, + /** + * 移动 moveNodeBy 对应的数据或交互;作用范围仅限画布适配层。 + * @param {*} id 目标元素 ID + * @param {*} dx 横向位移 + * @param {*} dy 纵向位移 + */ + moveNodeBy(id, dx, dy) { + const node = store.findNode(id); + if (!node) return; + store.updateNode(id, { + position: { + x: node.position.x + dx, + y: node.position.y + dy, + }, + }); + changed(); + }, + /** + * 撤销 undo 对应的数据或交互;作用范围仅限画布适配层。 + */ + undo() { + if (historyIndex <= 0) return false; + historyIndex -= 1; + const snapshot = clone(history[historyIndex].graph); + facade.render(snapshot, { resetHistory: false, replaceNodes: true }); + nextTick(() => emit("history:change", { data: facade.getGraphData() })); + return true; + }, + /** + * 重做 redo 对应的数据或交互;作用范围仅限画布适配层。 + */ + redo() { + if (historyIndex >= history.length - 1) return false; + historyIndex += 1; + const snapshot = clone(history[historyIndex].graph); + facade.render(snapshot, { resetHistory: false, replaceNodes: true }); + nextTick(() => emit("history:change", { data: facade.getGraphData() })); + return true; + }, + /** + * 注册 registerComponentInstance 对应的数据或交互;作用范围仅限画布适配层。 + * @param {*} id 目标元素 ID + * @param {*} instance 组件实例 + */ + registerComponentInstance(id, instance) { + if (instance) componentInstances.set(id, instance); + }, + extension: { + selectionSelect: { + openSelectionSelect: () => emit("selection:open"), + closeSelectionSelect: () => emit("selection:close"), + }, + }, + }; + + return facade; +}; diff --git a/src/views/login.vue b/src/views/login.vue new file mode 100644 index 0000000..36116e2 --- /dev/null +++ b/src/views/login.vue @@ -0,0 +1,239 @@ + + + + + diff --git a/src/views/monitor/cache/index.vue b/src/views/monitor/cache/index.vue new file mode 100644 index 0000000..271cc8e --- /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..6515fcd --- /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..29c1aa7 --- /dev/null +++ b/src/views/monitor/druid/index.vue @@ -0,0 +1,13 @@ + + + diff --git a/src/views/monitor/job/detail.vue b/src/views/monitor/job/detail.vue new file mode 100644 index 0000000..ab09d50 --- /dev/null +++ b/src/views/monitor/job/detail.vue @@ -0,0 +1,217 @@ + + + + + diff --git a/src/views/monitor/job/index.vue b/src/views/monitor/job/index.vue new file mode 100644 index 0000000..25b4532 --- /dev/null +++ b/src/views/monitor/job/index.vue @@ -0,0 +1,435 @@ + + + diff --git a/src/views/monitor/job/log.vue b/src/views/monitor/job/log.vue new file mode 100644 index 0000000..8651c74 --- /dev/null +++ b/src/views/monitor/job/log.vue @@ -0,0 +1,251 @@ + + + diff --git a/src/views/monitor/logininfor/index.vue b/src/views/monitor/logininfor/index.vue new file mode 100644 index 0000000..86f9f0b --- /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..fee70fe --- /dev/null +++ b/src/views/monitor/online/index.vue @@ -0,0 +1,109 @@ + + + diff --git a/src/views/monitor/operlog/detail.vue b/src/views/monitor/operlog/detail.vue new file mode 100644 index 0000000..6ebc593 --- /dev/null +++ b/src/views/monitor/operlog/detail.vue @@ -0,0 +1,152 @@ + + + diff --git a/src/views/monitor/operlog/index.vue b/src/views/monitor/operlog/index.vue new file mode 100644 index 0000000..9648905 --- /dev/null +++ b/src/views/monitor/operlog/index.vue @@ -0,0 +1,262 @@ + + + diff --git a/src/views/monitor/server/index.vue b/src/views/monitor/server/index.vue new file mode 100644 index 0000000..60ff80c --- /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..99dd4f5 --- /dev/null +++ b/src/views/redirect/index.vue @@ -0,0 +1,14 @@ + + + \ No newline at end of file diff --git a/src/views/register.vue b/src/views/register.vue new file mode 100644 index 0000000..254cc8e --- /dev/null +++ b/src/views/register.vue @@ -0,0 +1,219 @@ + + + + + diff --git a/src/views/resource/device/index.vue b/src/views/resource/device/index.vue new file mode 100644 index 0000000..a28535e --- /dev/null +++ b/src/views/resource/device/index.vue @@ -0,0 +1,104 @@ + + + + + diff --git a/src/views/resource/map/index.vue b/src/views/resource/map/index.vue new file mode 100644 index 0000000..745e05e --- /dev/null +++ b/src/views/resource/map/index.vue @@ -0,0 +1,189 @@ + + + diff --git a/src/views/resource/robot/index.vue b/src/views/resource/robot/index.vue new file mode 100644 index 0000000..04f3b84 --- /dev/null +++ b/src/views/resource/robot/index.vue @@ -0,0 +1,339 @@ + + + + + diff --git a/src/views/resource/terminal/index.vue b/src/views/resource/terminal/index.vue new file mode 100644 index 0000000..8bb484f --- /dev/null +++ b/src/views/resource/terminal/index.vue @@ -0,0 +1,75 @@ + + + + + diff --git a/src/views/system/config/index.vue b/src/views/system/config/index.vue new file mode 100644 index 0000000..beaaa7f --- /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..9c8b317 --- /dev/null +++ b/src/views/system/dept/index.vue @@ -0,0 +1,334 @@ + + + diff --git a/src/views/system/dict/data.vue b/src/views/system/dict/data.vue new file mode 100644 index 0000000..d96fd1f --- /dev/null +++ b/src/views/system/dict/data.vue @@ -0,0 +1,362 @@ + + + diff --git a/src/views/system/dict/detail.vue b/src/views/system/dict/detail.vue new file mode 100644 index 0000000..472267b --- /dev/null +++ b/src/views/system/dict/detail.vue @@ -0,0 +1,202 @@ + + + + + diff --git a/src/views/system/dict/index.vue b/src/views/system/dict/index.vue new file mode 100644 index 0000000..1e64264 --- /dev/null +++ b/src/views/system/dict/index.vue @@ -0,0 +1,346 @@ + + + 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..9bc0ee8 --- /dev/null +++ b/src/views/system/menu/index.vue @@ -0,0 +1,508 @@ + + + diff --git a/src/views/system/notice/ReadUsers.vue b/src/views/system/notice/ReadUsers.vue new file mode 100644 index 0000000..7590bef --- /dev/null +++ b/src/views/system/notice/ReadUsers.vue @@ -0,0 +1,118 @@ + + + + + diff --git a/src/views/system/notice/index.vue b/src/views/system/notice/index.vue new file mode 100644 index 0000000..7541a48 --- /dev/null +++ b/src/views/system/notice/index.vue @@ -0,0 +1,306 @@ + + + diff --git a/src/views/system/post/index.vue b/src/views/system/post/index.vue new file mode 100644 index 0000000..316e0d7 --- /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..6848eb4 --- /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..c191194 --- /dev/null +++ b/src/views/system/role/index.vue @@ -0,0 +1,584 @@ + + + diff --git a/src/views/system/role/selectUser.vue b/src/views/system/role/selectUser.vue new file mode 100644 index 0000000..58b621a --- /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..28b40e7 --- /dev/null +++ b/src/views/system/user/authRole.vue @@ -0,0 +1,123 @@ + + + diff --git a/src/views/system/user/index.vue b/src/views/system/user/index.vue new file mode 100644 index 0000000..6528964 --- /dev/null +++ b/src/views/system/user/index.vue @@ -0,0 +1,459 @@ + + + diff --git a/src/views/system/user/profile/index.vue b/src/views/system/user/profile/index.vue new file mode 100644 index 0000000..dfb4a66 --- /dev/null +++ b/src/views/system/user/profile/index.vue @@ -0,0 +1,94 @@ + + + diff --git a/src/views/system/user/profile/resetPwd.vue b/src/views/system/user/profile/resetPwd.vue new file mode 100644 index 0000000..e454938 --- /dev/null +++ b/src/views/system/user/profile/resetPwd.vue @@ -0,0 +1,60 @@ + + + diff --git a/src/views/system/user/profile/userAvatar.vue b/src/views/system/user/profile/userAvatar.vue new file mode 100644 index 0000000..64d2c94 --- /dev/null +++ b/src/views/system/user/profile/userAvatar.vue @@ -0,0 +1,180 @@ + + + + + \ No newline at end of file diff --git a/src/views/system/user/profile/userInfo.vue b/src/views/system/user/profile/userInfo.vue new file mode 100644 index 0000000..9fcc09b --- /dev/null +++ b/src/views/system/user/profile/userInfo.vue @@ -0,0 +1,67 @@ + + + diff --git a/src/views/system/user/view.vue b/src/views/system/user/view.vue new file mode 100644 index 0000000..593a163 --- /dev/null +++ b/src/views/system/user/view.vue @@ -0,0 +1,175 @@ + + + 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/tool/build/CodeTypeDialog.vue b/src/views/tool/build/CodeTypeDialog.vue new file mode 100644 index 0000000..de0beb7 --- /dev/null +++ b/src/views/tool/build/CodeTypeDialog.vue @@ -0,0 +1,71 @@ + + + \ No newline at end of file diff --git a/src/views/tool/build/DraggableItem.vue b/src/views/tool/build/DraggableItem.vue new file mode 100644 index 0000000..9ae2354 --- /dev/null +++ b/src/views/tool/build/DraggableItem.vue @@ -0,0 +1,68 @@ + + \ No newline at end of file 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..5fe80fb --- /dev/null +++ b/src/views/tool/build/RightPanel.vue @@ -0,0 +1,906 @@ + + + + + \ No newline at end of file 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..64a9317 --- /dev/null +++ b/src/views/tool/build/index.vue @@ -0,0 +1,660 @@ + + + + + diff --git a/src/views/tool/gen/basicInfoForm.vue b/src/views/tool/gen/basicInfoForm.vue new file mode 100644 index 0000000..92c3ca9 --- /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..b5fedde --- /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..9433527 --- /dev/null +++ b/src/views/tool/gen/editTable.vue @@ -0,0 +1,212 @@ + + + diff --git a/src/views/tool/gen/genInfoForm.vue b/src/views/tool/gen/genInfoForm.vue new file mode 100644 index 0000000..5247772 --- /dev/null +++ b/src/views/tool/gen/genInfoForm.vue @@ -0,0 +1,328 @@ + + + diff --git a/src/views/tool/gen/importTable.vue b/src/views/tool/gen/importTable.vue new file mode 100644 index 0000000..a73ad7a --- /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..9359592 --- /dev/null +++ b/src/views/tool/gen/index.vue @@ -0,0 +1,309 @@ + + + diff --git a/src/views/tool/swagger/index.vue b/src/views/tool/swagger/index.vue new file mode 100644 index 0000000..a3b03bb --- /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..fc711dc --- /dev/null +++ b/vite.config.js @@ -0,0 +1,72 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import AutoImport from 'unplugin-auto-import/vite' +import setupExtend from 'unplugin-vue-setup-extend-plus/vite' +import path from 'node:path' +import { readFileSync } from 'node:fs' + +const packageJson = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8')) +const externalDependencies = [ + ...Object.keys(packageJson.dependencies || {}), + ...Object.keys(packageJson.peerDependencies || {}) +] + +const peerDependencies = [ + 'vue', + 'vue-router', + 'pinia', + 'element-plus', + '@element-plus/icons-vue' +] + +export default defineConfig({ + plugins: [ + vue(), + AutoImport({ + imports: [ + 'vue', + 'vue-router', + 'pinia', + { + '@/utils/dict': ['useDict'], + '@/utils/ruoyi': ['selectDictLabel'] + } + ], + dts: false + }), + setupExtend() + ], + resolve: { + alias: { + '@': path.resolve(import.meta.dirname, './src') + }, + extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue'] + }, + css: { + preprocessorOptions: { + scss: { + additionalData: `@use "${path.resolve(import.meta.dirname, './src/style/variable.scss').replaceAll('\\', '/')}" as *;` + } + } + }, + build: { + target: 'es2022', + sourcemap: true, + cssCodeSplit: false, + lib: { + entry: path.resolve(import.meta.dirname, 'src/index.js'), + formats: ['es'], + fileName: () => 'index.js', + cssFileName: 'style' + }, + rollupOptions: { + external: (id) => externalDependencies.some((dependency) => id === dependency || id.startsWith(`${dependency}/`)), + output: { + chunkFileNames: 'chunks/[name]-[hash].js', + assetFileNames: (assetInfo) => assetInfo.name === 'style.css' + ? 'style.css' + : 'assets/[name]-[hash][extname]' + } + } + } +})