--- /dev/null
+{
+ "name": "stationbuilder-vue",
+ "version": "0.1.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "stationbuilder-vue",
+ "version": "0.1.0",
+ "dependencies": {
+ "@element-plus/icons-vue": "^2.3.1",
+ "element-plus": "^2.11.0",
+ "qrcode": "^1.5.4",
+ "vue": "^3.5.43",
+ "xlsx": "^0.18.5"
+ },
+ "devDependencies": {
+ "@types/node": "22.19.19",
+ "@vitejs/plugin-vue": "^6.0.9",
+ "less": "^4.9.1",
+ "playwright": "^1.48.0",
+ "vite": "8.0.13"
+ },
+ "engines": {
+ "node": ">=22.13.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.29.7",
+ "resolved": "http://mirrors.tencent.com/npm/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
+ "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.29.7",
+ "resolved": "http://mirrors.tencent.com/npm/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
+ "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.9",
+ "resolved": "http://mirrors.tencent.com/npm/@babel/parser/-/parser-7.29.9.tgz",
+ "integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==",
+ "dependencies": {
+ "@babel/types": "^7.29.8"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.8",
+ "resolved": "http://mirrors.tencent.com/npm/@babel/types/-/types-7.29.8.tgz",
+ "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.29.7",
+ "@babel/helper-validator-identifier": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@ctrl/tinycolor": {
+ "version": "4.2.1",
+ "resolved": "http://mirrors.tencent.com/npm/@ctrl/tinycolor/-/tinycolor-4.2.1.tgz",
+ "integrity": "sha512-Vh5uy9Y1JBMId6J1f68TMXnZrTCvNdo9Fu2k0d1YnJngR+sM5kWTKl441c/3TFs+cG0NWOjRWgQ/iaWhbYx+7A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@element-plus/icons-vue": {
+ "version": "2.3.2",
+ "resolved": "http://mirrors.tencent.com/npm/@element-plus/icons-vue/-/icons-vue-2.3.2.tgz",
+ "integrity": "sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==",
+ "license": "MIT",
+ "peerDependencies": {
+ "vue": "^3.2.0"
+ }
+ },
+ "node_modules/@emnapi/core": {
+ "version": "1.10.0",
+ "resolved": "http://mirrors.tencent.com/npm/@emnapi/core/-/core-1.10.0.tgz",
+ "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/wasi-threads": "1.2.1",
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/runtime": {
+ "version": "1.10.0",
+ "resolved": "http://mirrors.tencent.com/npm/@emnapi/runtime/-/runtime-1.10.0.tgz",
+ "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/wasi-threads": {
+ "version": "1.2.1",
+ "resolved": "http://mirrors.tencent.com/npm/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
+ "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@floating-ui/core": {
+ "version": "1.8.0",
+ "resolved": "http://mirrors.tencent.com/npm/@floating-ui/core/-/core-1.8.0.tgz",
+ "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/utils": "^0.2.12"
+ }
+ },
+ "node_modules/@floating-ui/dom": {
+ "version": "1.8.0",
+ "resolved": "http://mirrors.tencent.com/npm/@floating-ui/dom/-/dom-1.8.0.tgz",
+ "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/core": "^1.8.0",
+ "@floating-ui/utils": "^0.2.12"
+ }
+ },
+ "node_modules/@floating-ui/utils": {
+ "version": "0.2.12",
+ "resolved": "http://mirrors.tencent.com/npm/@floating-ui/utils/-/utils-0.2.12.tgz",
+ "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==",
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.6.0",
+ "resolved": "http://mirrors.tencent.com/npm/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz",
+ "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw=="
+ },
+ "node_modules/@napi-rs/wasm-runtime": {
+ "version": "1.2.4",
+ "resolved": "http://mirrors.tencent.com/npm/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.4.tgz",
+ "integrity": "sha512-AJxoUD2/15ESHbvpcyjU274nsAPLuOtPHCk0vKJM5pj//Fg/B1FXNWjPnXTT9PymCYYiHo4zPj0ZomXBKhoy7g==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@tybys/wasm-util": "^0.10.3"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=23.5.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ },
+ "peerDependencies": {
+ "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4",
+ "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4"
+ }
+ },
+ "node_modules/@oxc-project/types": {
+ "version": "0.130.0",
+ "resolved": "http://mirrors.tencent.com/npm/@oxc-project/types/-/types-0.130.0.tgz",
+ "integrity": "sha512-ibD2usx9JRu7f5pu2tMKMI4cpA4NgXJQoYRP4pQ7Pxmn1l6k/53qWtQWZayhYy3X4QZkt90Ot+mJEaeXouio6Q==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/Boshen"
+ }
+ },
+ "node_modules/@popperjs/core": {
+ "name": "@sxzz/popperjs-es",
+ "version": "2.11.8",
+ "resolved": "http://mirrors.tencent.com/npm/@sxzz/popperjs-es/-/popperjs-es-2.11.8.tgz",
+ "integrity": "sha512-wOwESXvvED3S8xBmcPWHs2dUuzrE4XiZeFu7e1hROIJkm02a49N120pmOXxY33sBb6hArItm5W5tcg1cBtV+HQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/popperjs"
+ }
+ },
+ "node_modules/@rolldown/binding-android-arm64": {
+ "version": "1.0.1",
+ "resolved": "http://mirrors.tencent.com/npm/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.1.tgz",
+ "integrity": "sha512-fJI3I0r3C3Oj/zdBCpaCmBRZYf07xpaq4yCfDDoSFm+beWNzbIl26puW8RraUdugoJw/95zerNOn6jasAhzSmg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-arm64": {
+ "version": "1.0.1",
+ "resolved": "http://mirrors.tencent.com/npm/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.1.tgz",
+ "integrity": "sha512-cKnAhWEsV7TPcA/5EAteDp6KcJZBQ2G+BqE7zayMMi7kMvwRsbv7WT9aOnn0WNl4SKEIf43vjS31iUPu80nzXg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-x64": {
+ "version": "1.0.1",
+ "resolved": "http://mirrors.tencent.com/npm/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.1.tgz",
+ "integrity": "sha512-YKrVwQjIRBPo+5G/u03wGjbdy4q7pyzCe93DK9VJ7zkVmeg8LJ7GbgsiHWdR4xSoe4CAXRD7Bcjgbtr64bkXNg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-freebsd-x64": {
+ "version": "1.0.1",
+ "resolved": "http://mirrors.tencent.com/npm/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.1.tgz",
+ "integrity": "sha512-z/oBsREo46SsFqBwYtFe0kpJeBijAT48O/WXLI4suiCLBkr03RTtTJMCzSdDd2znlh8VJizL09XVkQgk8IZonw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
+ "version": "1.0.1",
+ "resolved": "http://mirrors.tencent.com/npm/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.1.tgz",
+ "integrity": "sha512-ik8q7GM11zxvYxFc2PeDcT6TBvhCQMaUxfph/M5l9sKuTs/Sjg3L+Byw0F7w0ZVLBZmx30P+gG0ECzzN+MFcmQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-gnu": {
+ "version": "1.0.1",
+ "resolved": "http://mirrors.tencent.com/npm/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.1.tgz",
+ "integrity": "sha512-QoSx2EkyrrdZ6kcyE8stqZ62t0Yra8Fs5ia9lOxJrh6TMQJK7gQKmscdTHf7pOXKREKrVwOtJcQG3qVSfc866A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-musl": {
+ "version": "1.0.1",
+ "resolved": "http://mirrors.tencent.com/npm/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.1.tgz",
+ "integrity": "sha512-uwNwFpwKeNiZawfAWBgg0VIztPTV3ihhh1vV334h9ivnNLorxnQMU6Fz8wG1Zb4Qh9LC1/MkcyT3YlDXG3Rsgg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-ppc64-gnu": {
+ "version": "1.0.1",
+ "resolved": "http://mirrors.tencent.com/npm/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.1.tgz",
+ "integrity": "sha512-zY1bul7OWr7DFBiJ++wofXvnr8B45ce3QsQUhKrIhXsygAh7bTkwyeM1bi1a2g5C/yC/N8TZyGDEoMfm/l9mpg==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-s390x-gnu": {
+ "version": "1.0.1",
+ "resolved": "http://mirrors.tencent.com/npm/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.1.tgz",
+ "integrity": "sha512-0frlsT/f4Ft6I7SMESTKnF3cZsdicQn1dCMkF/jT9wDLE+gGoiQfv1nmT9e+s7s/fekvvy6tZM2jHvI2tkbJDQ==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-gnu": {
+ "version": "1.0.1",
+ "resolved": "http://mirrors.tencent.com/npm/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.1.tgz",
+ "integrity": "sha512-XABVmGp9Tg0WspTVvwduTc4fpqy6JnAUrSQe6OuyqD/03nI7r0O9OWUkMIwFrjKAIqolvqoA4ZrJppgwE0Gxmw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-musl": {
+ "version": "1.0.1",
+ "resolved": "http://mirrors.tencent.com/npm/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.1.tgz",
+ "integrity": "sha512-bV4fzswuzVcKD90o/VM6QqKxnxlDq0g2BISDLNVmxrnhpv1DDbyPhCIjYfvzYLV+MvkKKnQt2Q6AO86SEBULUQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-openharmony-arm64": {
+ "version": "1.0.1",
+ "resolved": "http://mirrors.tencent.com/npm/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.1.tgz",
+ "integrity": "sha512-/Mh0Zhq3OP7fVs0kcQHZP6lZEthMGTaSf8UBQYSFEZDWGXXlEC+nJ6EqenaK2t4LBXMe3A+K/G2BVXXdtOr4PQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-wasm32-wasi": {
+ "version": "1.0.1",
+ "resolved": "http://mirrors.tencent.com/npm/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.1.tgz",
+ "integrity": "sha512-+1xc9X45l8ufsBAm6Gjvx2qDRIY9lTVt0cgWNcJ+1gdhXvkbxePA60yRTwSTuXL09CMhyJmjpV7E3NoyxbqFQQ==",
+ "cpu": [
+ "wasm32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "1.10.0",
+ "@emnapi/runtime": "1.10.0",
+ "@napi-rs/wasm-runtime": "^1.1.4"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-arm64-msvc": {
+ "version": "1.0.1",
+ "resolved": "http://mirrors.tencent.com/npm/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.1.tgz",
+ "integrity": "sha512-1D+UqZdfnuR+Jy1GgMJwi85bD40H21uNmOPRWQhw4oRSuolZ/B5rixZ45DK2KXOTCvmVCecauWgEhbw8bI7tOw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-x64-msvc": {
+ "version": "1.0.1",
+ "resolved": "http://mirrors.tencent.com/npm/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.1.tgz",
+ "integrity": "sha512-INAycaWuhlOK3wk4mRHGsdgwYWmd9cChdPdE9bwWmy6rn9VqVNYNFGhOdXrofXUxwHIncSiPNb8tNm8knDVIeQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.1",
+ "resolved": "http://mirrors.tencent.com/npm/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
+ "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
+ "dev": true
+ },
+ "node_modules/@tybys/wasm-util": {
+ "version": "0.10.4",
+ "resolved": "http://mirrors.tencent.com/npm/@tybys/wasm-util/-/wasm-util-0.10.4.tgz",
+ "integrity": "sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@types/lodash": {
+ "version": "4.17.25",
+ "resolved": "http://mirrors.tencent.com/npm/@types/lodash/-/lodash-4.17.25.tgz",
+ "integrity": "sha512-+K1NIO8I+F9/wNulfVvu23QYd0Pe9/OCqRrim4NoYIf1VoEDL90Ve4ClzpyqBLc7NpGGWRvYNCKZ1BE/Jpf8dQ==",
+ "license": "MIT"
+ },
+ "node_modules/@types/lodash-es": {
+ "version": "4.17.12",
+ "resolved": "http://mirrors.tencent.com/npm/@types/lodash-es/-/lodash-es-4.17.12.tgz",
+ "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/lodash": "*"
+ }
+ },
+ "node_modules/@types/node": {
+ "version": "22.19.19",
+ "resolved": "http://mirrors.tencent.com/npm/@types/node/-/node-22.19.19.tgz",
+ "integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~6.21.0"
+ }
+ },
+ "node_modules/@types/web-bluetooth": {
+ "version": "0.0.21",
+ "resolved": "http://mirrors.tencent.com/npm/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz",
+ "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==",
+ "license": "MIT"
+ },
+ "node_modules/@vitejs/plugin-vue": {
+ "version": "6.0.9",
+ "resolved": "http://mirrors.tencent.com/npm/@vitejs/plugin-vue/-/plugin-vue-6.0.9.tgz",
+ "integrity": "sha512-rD/MORlhaZMlXWW0rEn4FB4wMinWC0z/D7Ye160S5+Rs1mC8ZDAdcDp4SjKfUhG5t8q1ktcPVw4xuTkPRlzrSA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@rolldown/pluginutils": "^1.0.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "peerDependencies": {
+ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0",
+ "vue": "^3.2.25"
+ }
+ },
+ "node_modules/@vue/compiler-core": {
+ "version": "3.5.43",
+ "resolved": "http://mirrors.tencent.com/npm/@vue/compiler-core/-/compiler-core-3.5.43.tgz",
+ "integrity": "sha512-zdiLhnbe1QQqgDT8xZMpNmyqZ3qlI+/Q/FHQco57Kwl/b05HhCzN6eVGN9QU9rbga4CrS0H5SYY8VGHZCt/1Hg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.8",
+ "@vue/shared": "3.5.43",
+ "entities": "^7.0.1",
+ "estree-walker": "^2.0.2",
+ "source-map-js": "^1.2.1"
+ }
+ },
+ "node_modules/@vue/compiler-dom": {
+ "version": "3.5.43",
+ "resolved": "http://mirrors.tencent.com/npm/@vue/compiler-dom/-/compiler-dom-3.5.43.tgz",
+ "integrity": "sha512-PEZoAk3NQmsn/ejMzSOCyTYqwGqczrWm70PuhBKjjv1+TCoQAaO/zOqNwjV+honlNstT5ILxtc+8r8UUfj+iEQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-core": "3.5.43",
+ "@vue/shared": "3.5.43"
+ }
+ },
+ "node_modules/@vue/compiler-sfc": {
+ "version": "3.5.43",
+ "resolved": "http://mirrors.tencent.com/npm/@vue/compiler-sfc/-/compiler-sfc-3.5.43.tgz",
+ "integrity": "sha512-FCbrG3XNCRl+js3huuKx4IVHBLTvMkJhVepjbxSPu1gn4yWLaYtGNQdjJGZaMytXB6qb76qQDDmDSLy/vkmleQ==",
+ "dependencies": {
+ "@babel/parser": "^7.29.8",
+ "@vue/compiler-core": "3.5.43",
+ "@vue/compiler-dom": "3.5.43",
+ "@vue/compiler-ssr": "3.5.43",
+ "@vue/shared": "3.5.43",
+ "estree-walker": "^2.0.2",
+ "magic-string": "^0.30.21",
+ "postcss": "^8.5.28",
+ "source-map-js": "^1.2.1"
+ }
+ },
+ "node_modules/@vue/compiler-ssr": {
+ "version": "3.5.43",
+ "resolved": "http://mirrors.tencent.com/npm/@vue/compiler-ssr/-/compiler-ssr-3.5.43.tgz",
+ "integrity": "sha512-GF62orf7KiJX9RqrHNGrYBudsQGD0OhJ5nDs90O8UiDuS40+YMYomiXu6w6EuvtXuRDc3MSNis3EaaSKAVSWpg==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-dom": "3.5.43",
+ "@vue/shared": "3.5.43"
+ }
+ },
+ "node_modules/@vue/reactivity": {
+ "version": "3.5.43",
+ "resolved": "http://mirrors.tencent.com/npm/@vue/reactivity/-/reactivity-3.5.43.tgz",
+ "integrity": "sha512-G/c9GyOZNI2jVaaS6OX1EF1SSFSv7H0ERqNTl4+DTFMlZmB5eVAB53aLQNam/7NL2NPtaDD7RdVrzf8uJzMuOA==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/shared": "3.5.43"
+ }
+ },
+ "node_modules/@vue/runtime-core": {
+ "version": "3.5.43",
+ "resolved": "http://mirrors.tencent.com/npm/@vue/runtime-core/-/runtime-core-3.5.43.tgz",
+ "integrity": "sha512-hU6U6VnVhBGQDpvlnnDlIB8ZGJBiOcgk2lh/0InltHiz3D8oSkluvuvY+do1G2H3+udeKFsmaBlgVYP7gXQzEw==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/reactivity": "3.5.43",
+ "@vue/shared": "3.5.43"
+ }
+ },
+ "node_modules/@vue/runtime-dom": {
+ "version": "3.5.43",
+ "resolved": "http://mirrors.tencent.com/npm/@vue/runtime-dom/-/runtime-dom-3.5.43.tgz",
+ "integrity": "sha512-Bb2Jc0YjjJdMt1SJmb9b2L/IWd3I8lIT9x9eS/xvvP9CiVgna0ffua74xKRmt4/uSJ+0r4iN8ex1jrqkhQGWQw==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/reactivity": "3.5.43",
+ "@vue/runtime-core": "3.5.43",
+ "@vue/shared": "3.5.43",
+ "csstype": "^3.2.3"
+ }
+ },
+ "node_modules/@vue/server-renderer": {
+ "version": "3.5.43",
+ "resolved": "http://mirrors.tencent.com/npm/@vue/server-renderer/-/server-renderer-3.5.43.tgz",
+ "integrity": "sha512-l2Ygjv9NehV94PSBxNWsAHC0j/eIIKbn92mBuWXAPGLnn6HfJ6MH5ubsd+Nk0YoZ5FRuxWI1P2VSoh+dbPQhCQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-ssr": "3.5.43",
+ "@vue/runtime-dom": "3.5.43",
+ "@vue/shared": "3.5.43"
+ }
+ },
+ "node_modules/@vue/shared": {
+ "version": "3.5.43",
+ "resolved": "http://mirrors.tencent.com/npm/@vue/shared/-/shared-3.5.43.tgz",
+ "integrity": "sha512-uksS7YGMR5NZyr4JNq0Rp+QyLns0ueaz20KwzIPW9R0LH1Vnt4E+XUM29PNseEbf1www2gOuhuDi5AKOIXag9Q==",
+ "license": "MIT"
+ },
+ "node_modules/@vueuse/core": {
+ "version": "15.0.0",
+ "resolved": "http://mirrors.tencent.com/npm/@vueuse/core/-/core-15.0.0.tgz",
+ "integrity": "sha512-Zv67h5mrk7YHDJsRObx+DXaCBz6RKBUpxqLesJ+3xZNPG6yuQs7ja7kg3veFrrouYLWAqydQ7CwGEbLVxB43Ng==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/web-bluetooth": "^0.0.21",
+ "@vueuse/metadata": "15.0.0",
+ "@vueuse/shared": "15.0.0"
+ },
+ "engines": {
+ "node": ">=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ },
+ "peerDependencies": {
+ "vue": "^3.5.0"
+ }
+ },
+ "node_modules/@vueuse/metadata": {
+ "version": "15.0.0",
+ "resolved": "http://mirrors.tencent.com/npm/@vueuse/metadata/-/metadata-15.0.0.tgz",
+ "integrity": "sha512-sfWE5LADAKJ0KnjtGliMdSN8Ij/L7uP6rXIZlgPloAI4zvvefbw6H5Lt7KJA6RqQOYgVqWF5Guj50HHMIzygBg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@vueuse/shared": {
+ "version": "15.0.0",
+ "resolved": "http://mirrors.tencent.com/npm/@vueuse/shared/-/shared-15.0.0.tgz",
+ "integrity": "sha512-vbNXhGrbV53xPOMSyOzMHQRvhMgZf7oNDw2RChUetVxwTIEkInQ1joZ9JbT0G3z2n2AJNjvmCWeM9ySYrnvogA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ },
+ "peerDependencies": {
+ "vue": "^3.5.0"
+ }
+ },
+ "node_modules/adler-32": {
+ "version": "1.3.1",
+ "resolved": "http://mirrors.tencent.com/npm/adler-32/-/adler-32-1.3.1.tgz",
+ "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==",
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "http://mirrors.tencent.com/npm/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "http://mirrors.tencent.com/npm/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/async-validator": {
+ "version": "4.2.5",
+ "resolved": "http://mirrors.tencent.com/npm/async-validator/-/async-validator-4.2.5.tgz",
+ "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==",
+ "license": "MIT"
+ },
+ "node_modules/camelcase": {
+ "version": "5.3.1",
+ "resolved": "http://mirrors.tencent.com/npm/camelcase/-/camelcase-5.3.1.tgz",
+ "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/cfb": {
+ "version": "1.2.2",
+ "resolved": "http://mirrors.tencent.com/npm/cfb/-/cfb-1.2.2.tgz",
+ "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
+ "dependencies": {
+ "adler-32": "~1.3.0",
+ "crc-32": "~1.2.0"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/cliui": {
+ "version": "6.0.0",
+ "resolved": "http://mirrors.tencent.com/npm/cliui/-/cliui-6.0.0.tgz",
+ "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.0",
+ "wrap-ansi": "^6.2.0"
+ }
+ },
+ "node_modules/codepage": {
+ "version": "1.15.0",
+ "resolved": "http://mirrors.tencent.com/npm/codepage/-/codepage-1.15.0.tgz",
+ "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "http://mirrors.tencent.com/npm/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "http://mirrors.tencent.com/npm/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
+ },
+ "node_modules/copy-anything": {
+ "version": "3.0.5",
+ "resolved": "http://mirrors.tencent.com/npm/copy-anything/-/copy-anything-3.0.5.tgz",
+ "integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-what": "^4.1.8"
+ },
+ "engines": {
+ "node": ">=12.13"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/mesqueeb"
+ }
+ },
+ "node_modules/crc-32": {
+ "version": "1.2.2",
+ "resolved": "http://mirrors.tencent.com/npm/crc-32/-/crc-32-1.2.2.tgz",
+ "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
+ "license": "Apache-2.0",
+ "bin": {
+ "crc32": "bin/crc32.njs"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "http://mirrors.tencent.com/npm/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "license": "MIT"
+ },
+ "node_modules/dayjs": {
+ "version": "1.11.23",
+ "resolved": "http://mirrors.tencent.com/npm/dayjs/-/dayjs-1.11.23.tgz",
+ "integrity": "sha512-QDTCU0M0MxR3hQfnlDJfwekQiaanm1ubOD231u73WBckQ/fsamwRLiE2GBz6D3a/xF1NgfiDLJjXBa1hYOYTtQ==",
+ "license": "MIT"
+ },
+ "node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "http://mirrors.tencent.com/npm/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/decamelize": {
+ "version": "1.2.0",
+ "resolved": "http://mirrors.tencent.com/npm/decamelize/-/decamelize-1.2.0.tgz",
+ "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "http://mirrors.tencent.com/npm/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/dijkstrajs": {
+ "version": "1.0.3",
+ "resolved": "http://mirrors.tencent.com/npm/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
+ "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
+ "license": "MIT"
+ },
+ "node_modules/element-plus": {
+ "version": "2.14.6",
+ "resolved": "http://mirrors.tencent.com/npm/element-plus/-/element-plus-2.14.6.tgz",
+ "integrity": "sha512-GH0fMoMgVEKkHimoaKvg5B00NmfPbQ+qxX6/JrO7F+dLAC3atrZr2Y2MoccHMxS22M7QE12/s0DG0Zs49iwJ/A==",
+ "license": "MIT",
+ "dependencies": {
+ "@ctrl/tinycolor": "^4.2.1",
+ "@element-plus/icons-vue": "^2.3.2",
+ "@floating-ui/dom": "^1.8.0",
+ "@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.8",
+ "@types/lodash": "^4.17.24",
+ "@types/lodash-es": "^4.17.12",
+ "@vueuse/core": "15.0.0",
+ "async-validator": "^4.2.5",
+ "dayjs": "^1.11.20",
+ "lodash": "^4.18.1",
+ "lodash-es": "^4.18.1",
+ "lodash-unified": "^1.0.3",
+ "memoize-one": "^6.0.0",
+ "normalize-wheel-es": "^1.2.0",
+ "vue-component-type-helpers": "^3.3.11"
+ },
+ "peerDependencies": {
+ "vue": "^3.3.7"
+ }
+ },
+ "node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "http://mirrors.tencent.com/npm/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "license": "MIT"
+ },
+ "node_modules/entities": {
+ "version": "7.0.1",
+ "resolved": "http://mirrors.tencent.com/npm/entities/-/entities-7.0.1.tgz",
+ "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/errno": {
+ "version": "0.1.8",
+ "resolved": "http://mirrors.tencent.com/npm/errno/-/errno-0.1.8.tgz",
+ "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==",
+ "dev": true,
+ "optional": true,
+ "dependencies": {
+ "prr": "~1.0.1"
+ },
+ "bin": {
+ "errno": "cli.js"
+ }
+ },
+ "node_modules/estree-walker": {
+ "version": "2.0.2",
+ "resolved": "http://mirrors.tencent.com/npm/estree-walker/-/estree-walker-2.0.2.tgz",
+ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
+ "license": "MIT"
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "http://mirrors.tencent.com/npm/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/find-up": {
+ "version": "4.1.0",
+ "resolved": "http://mirrors.tencent.com/npm/find-up/-/find-up-4.1.0.tgz",
+ "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^5.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/frac": {
+ "version": "1.1.2",
+ "resolved": "http://mirrors.tencent.com/npm/frac/-/frac-1.1.2.tgz",
+ "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "http://mirrors.tencent.com/npm/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/get-caller-file": {
+ "version": "2.0.5",
+ "resolved": "http://mirrors.tencent.com/npm/get-caller-file/-/get-caller-file-2.0.5.tgz",
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+ "license": "ISC",
+ "engines": {
+ "node": "6.* || 8.* || >= 10.*"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "http://mirrors.tencent.com/npm/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "dev": true,
+ "optional": true
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "http://mirrors.tencent.com/npm/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "http://mirrors.tencent.com/npm/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-what": {
+ "version": "4.1.16",
+ "resolved": "http://mirrors.tencent.com/npm/is-what/-/is-what-4.1.16.tgz",
+ "integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.13"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/mesqueeb"
+ }
+ },
+ "node_modules/less": {
+ "version": "4.9.1",
+ "resolved": "http://mirrors.tencent.com/npm/less/-/less-4.9.1.tgz",
+ "integrity": "sha512-orp15PfJvvNDIqJdVWzMI9Sjpjp3VTiw3sfvbB+67LlISTEn8uVT2EdYSuyl02BLvaftv6sdk9Umnxmm5rckmg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "copy-anything": "^3.0.5",
+ "parse-node-version": "^1.0.1"
+ },
+ "bin": {
+ "lessc": "bin/lessc"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "errno": "^0.1.1",
+ "graceful-fs": "^4.1.2",
+ "make-dir": "^5.1.0",
+ "mime": "^1.4.1",
+ "needle": "^3.1.0",
+ "probe-image-size": "^7.2.3",
+ "source-map": "~0.6.0"
+ }
+ },
+ "node_modules/lightningcss": {
+ "version": "1.33.0",
+ "resolved": "http://mirrors.tencent.com/npm/lightningcss/-/lightningcss-1.33.0.tgz",
+ "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==",
+ "dev": true,
+ "license": "MPL-2.0",
+ "dependencies": {
+ "detect-libc": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "lightningcss-android-arm64": "1.33.0",
+ "lightningcss-darwin-arm64": "1.33.0",
+ "lightningcss-darwin-x64": "1.33.0",
+ "lightningcss-freebsd-x64": "1.33.0",
+ "lightningcss-linux-arm-gnueabihf": "1.33.0",
+ "lightningcss-linux-arm64-gnu": "1.33.0",
+ "lightningcss-linux-arm64-musl": "1.33.0",
+ "lightningcss-linux-x64-gnu": "1.33.0",
+ "lightningcss-linux-x64-musl": "1.33.0",
+ "lightningcss-win32-arm64-msvc": "1.33.0",
+ "lightningcss-win32-x64-msvc": "1.33.0"
+ }
+ },
+ "node_modules/lightningcss-android-arm64": {
+ "version": "1.33.0",
+ "resolved": "http://mirrors.tencent.com/npm/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz",
+ "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-arm64": {
+ "version": "1.33.0",
+ "resolved": "http://mirrors.tencent.com/npm/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz",
+ "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-x64": {
+ "version": "1.33.0",
+ "resolved": "http://mirrors.tencent.com/npm/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz",
+ "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-freebsd-x64": {
+ "version": "1.33.0",
+ "resolved": "http://mirrors.tencent.com/npm/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz",
+ "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
+ "version": "1.33.0",
+ "resolved": "http://mirrors.tencent.com/npm/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz",
+ "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-gnu": {
+ "version": "1.33.0",
+ "resolved": "http://mirrors.tencent.com/npm/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz",
+ "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-musl": {
+ "version": "1.33.0",
+ "resolved": "http://mirrors.tencent.com/npm/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz",
+ "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-gnu": {
+ "version": "1.33.0",
+ "resolved": "http://mirrors.tencent.com/npm/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz",
+ "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-musl": {
+ "version": "1.33.0",
+ "resolved": "http://mirrors.tencent.com/npm/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz",
+ "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-arm64-msvc": {
+ "version": "1.33.0",
+ "resolved": "http://mirrors.tencent.com/npm/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz",
+ "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-x64-msvc": {
+ "version": "1.33.0",
+ "resolved": "http://mirrors.tencent.com/npm/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz",
+ "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/locate-path": {
+ "version": "5.0.0",
+ "resolved": "http://mirrors.tencent.com/npm/locate-path/-/locate-path-5.0.0.tgz",
+ "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/lodash": {
+ "version": "4.18.1",
+ "resolved": "http://mirrors.tencent.com/npm/lodash/-/lodash-4.18.1.tgz",
+ "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q=="
+ },
+ "node_modules/lodash-es": {
+ "version": "4.18.1",
+ "resolved": "http://mirrors.tencent.com/npm/lodash-es/-/lodash-es-4.18.1.tgz",
+ "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A=="
+ },
+ "node_modules/lodash-unified": {
+ "version": "1.0.3",
+ "resolved": "http://mirrors.tencent.com/npm/lodash-unified/-/lodash-unified-1.0.3.tgz",
+ "integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/lodash-es": "*",
+ "lodash": "*",
+ "lodash-es": "*"
+ }
+ },
+ "node_modules/lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "http://mirrors.tencent.com/npm/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "dev": true,
+ "optional": true
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "http://mirrors.tencent.com/npm/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/make-dir": {
+ "version": "5.1.0",
+ "resolved": "http://mirrors.tencent.com/npm/make-dir/-/make-dir-5.1.0.tgz",
+ "integrity": "sha512-IfpFq6UM39dUNiphpA6uDezNx/AvWyhwfICWPR3t1VspkgkMZrL+Rk1RbN1bx+aeNYwOrqGJgEgV3yotk+ZUVw==",
+ "dev": true,
+ "optional": true,
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/memoize-one": {
+ "version": "6.0.0",
+ "resolved": "http://mirrors.tencent.com/npm/memoize-one/-/memoize-one-6.0.0.tgz",
+ "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==",
+ "license": "MIT"
+ },
+ "node_modules/mime": {
+ "version": "1.6.0",
+ "resolved": "http://mirrors.tencent.com/npm/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+ "dev": true,
+ "optional": true,
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "http://mirrors.tencent.com/npm/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "dev": true,
+ "optional": true
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.19",
+ "resolved": "http://mirrors.tencent.com/npm/nanoid/-/nanoid-3.3.19.tgz",
+ "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/needle": {
+ "version": "3.5.0",
+ "resolved": "http://mirrors.tencent.com/npm/needle/-/needle-3.5.0.tgz",
+ "integrity": "sha512-jaQyPKKk2YokHrEg+vFDYxXIHTCBgiZwSHOoVx/8V3GIBS8/VN6NdVRmg8q1ERtPkMvmOvebsgga4sAj5hls/w==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "iconv-lite": "^0.6.3",
+ "sax": "^1.2.4"
+ },
+ "bin": {
+ "needle": "bin/needle"
+ },
+ "engines": {
+ "node": ">= 4.4.x"
+ }
+ },
+ "node_modules/normalize-wheel-es": {
+ "version": "1.2.0",
+ "resolved": "http://mirrors.tencent.com/npm/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz",
+ "integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/p-limit": {
+ "version": "2.3.0",
+ "resolved": "http://mirrors.tencent.com/npm/p-limit/-/p-limit-2.3.0.tgz",
+ "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+ "license": "MIT",
+ "dependencies": {
+ "p-try": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "4.1.0",
+ "resolved": "http://mirrors.tencent.com/npm/p-locate/-/p-locate-4.1.0.tgz",
+ "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/p-try": {
+ "version": "2.2.0",
+ "resolved": "http://mirrors.tencent.com/npm/p-try/-/p-try-2.2.0.tgz",
+ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/parse-node-version": {
+ "version": "1.0.1",
+ "resolved": "http://mirrors.tencent.com/npm/parse-node-version/-/parse-node-version-1.0.1.tgz",
+ "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "http://mirrors.tencent.com/npm/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "http://mirrors.tencent.com/npm/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.7",
+ "resolved": "http://mirrors.tencent.com/npm/picomatch/-/picomatch-4.0.7.tgz",
+ "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/playwright": {
+ "version": "1.48.0",
+ "resolved": "http://mirrors.tencent.com/npm/playwright/-/playwright-1.48.0.tgz",
+ "integrity": "sha512-qPqFaMEHuY/ug8o0uteYJSRfMGFikhUysk8ZvAtfKmUK3kc/6oNl/y3EczF8OFGYIi/Ex2HspMfzYArk6+XQSA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright-core": "1.48.0"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "fsevents": "2.3.2"
+ }
+ },
+ "node_modules/playwright-core": {
+ "version": "1.48.0",
+ "resolved": "http://mirrors.tencent.com/npm/playwright-core/-/playwright-core-1.48.0.tgz",
+ "integrity": "sha512-RBvzjM9rdpP7UUFrQzRwR8L/xR4HyC1QXMzGYTbf1vjw25/ya9NRAVnXi/0fvFopjebvyPzsmoK58xxeEOaVvA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "playwright-core": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/playwright/node_modules/fsevents": {
+ "version": "2.3.2",
+ "resolved": "http://mirrors.tencent.com/npm/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/pngjs": {
+ "version": "5.0.0",
+ "resolved": "http://mirrors.tencent.com/npm/pngjs/-/pngjs-5.0.0.tgz",
+ "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.28",
+ "resolved": "http://mirrors.tencent.com/npm/postcss/-/postcss-8.5.28.tgz",
+ "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.18",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/probe-image-size": {
+ "version": "7.4.0",
+ "resolved": "http://mirrors.tencent.com/npm/probe-image-size/-/probe-image-size-7.4.0.tgz",
+ "integrity": "sha512-cdEprVtZxV+awMde9X+4jILBFYh4CARxVrQaMl4wY4YcPWbul9jntXrIW95NInBDyJwcVUP3U0T6yukN8rMBaQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/puzrin"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/nodeca"
+ }
+ ],
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "lodash.merge": "^4.6.2",
+ "needle": "^2.5.2",
+ "stream-parser": "~0.3.1"
+ }
+ },
+ "node_modules/probe-image-size/node_modules/debug": {
+ "version": "3.2.7",
+ "resolved": "http://mirrors.tencent.com/npm/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
+ "node_modules/probe-image-size/node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "http://mirrors.tencent.com/npm/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/probe-image-size/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "http://mirrors.tencent.com/npm/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "optional": true
+ },
+ "node_modules/probe-image-size/node_modules/needle": {
+ "version": "2.9.1",
+ "resolved": "http://mirrors.tencent.com/npm/needle/-/needle-2.9.1.tgz",
+ "integrity": "sha512-6R9fqJ5Zcmf+uYaFgdIHmLwNldn5HbK8L5ybn7Uz+ylX/rnOsSp1AHcvQSrCaFN+qNM1wpymHqD7mVasEOlHGQ==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "debug": "^3.2.6",
+ "iconv-lite": "^0.4.4",
+ "sax": "^1.2.4"
+ },
+ "bin": {
+ "needle": "bin/needle"
+ },
+ "engines": {
+ "node": ">= 4.4.x"
+ }
+ },
+ "node_modules/prr": {
+ "version": "1.0.1",
+ "resolved": "http://mirrors.tencent.com/npm/prr/-/prr-1.0.1.tgz",
+ "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==",
+ "dev": true,
+ "optional": true
+ },
+ "node_modules/qrcode": {
+ "version": "1.5.4",
+ "resolved": "http://mirrors.tencent.com/npm/qrcode/-/qrcode-1.5.4.tgz",
+ "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
+ "license": "MIT",
+ "dependencies": {
+ "dijkstrajs": "^1.0.1",
+ "pngjs": "^5.0.0",
+ "yargs": "^15.3.1"
+ },
+ "bin": {
+ "qrcode": "bin/qrcode"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/require-directory": {
+ "version": "2.1.1",
+ "resolved": "http://mirrors.tencent.com/npm/require-directory/-/require-directory-2.1.1.tgz",
+ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/require-main-filename": {
+ "version": "2.0.0",
+ "resolved": "http://mirrors.tencent.com/npm/require-main-filename/-/require-main-filename-2.0.0.tgz",
+ "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
+ "license": "ISC"
+ },
+ "node_modules/rolldown": {
+ "version": "1.0.1",
+ "resolved": "http://mirrors.tencent.com/npm/rolldown/-/rolldown-1.0.1.tgz",
+ "integrity": "sha512-X0KQHljNnEkWNqqiz9zJrGunh1B0HgOxLXvnFpCOcadzcy5qohZ3tqMEUg00vncoRovXuK3ZqCT9KnnKzoInFQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@oxc-project/types": "=0.130.0",
+ "@rolldown/pluginutils": "^1.0.0"
+ },
+ "bin": {
+ "rolldown": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "optionalDependencies": {
+ "@rolldown/binding-android-arm64": "1.0.1",
+ "@rolldown/binding-darwin-arm64": "1.0.1",
+ "@rolldown/binding-darwin-x64": "1.0.1",
+ "@rolldown/binding-freebsd-x64": "1.0.1",
+ "@rolldown/binding-linux-arm-gnueabihf": "1.0.1",
+ "@rolldown/binding-linux-arm64-gnu": "1.0.1",
+ "@rolldown/binding-linux-arm64-musl": "1.0.1",
+ "@rolldown/binding-linux-ppc64-gnu": "1.0.1",
+ "@rolldown/binding-linux-s390x-gnu": "1.0.1",
+ "@rolldown/binding-linux-x64-gnu": "1.0.1",
+ "@rolldown/binding-linux-x64-musl": "1.0.1",
+ "@rolldown/binding-openharmony-arm64": "1.0.1",
+ "@rolldown/binding-wasm32-wasi": "1.0.1",
+ "@rolldown/binding-win32-arm64-msvc": "1.0.1",
+ "@rolldown/binding-win32-x64-msvc": "1.0.1"
+ }
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "http://mirrors.tencent.com/npm/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/sax": {
+ "version": "1.6.1",
+ "resolved": "http://mirrors.tencent.com/npm/sax/-/sax-1.6.1.tgz",
+ "integrity": "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "optional": true,
+ "engines": {
+ "node": ">=11.0.0"
+ }
+ },
+ "node_modules/set-blocking": {
+ "version": "2.0.0",
+ "resolved": "http://mirrors.tencent.com/npm/set-blocking/-/set-blocking-2.0.0.tgz",
+ "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
+ "license": "ISC"
+ },
+ "node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "http://mirrors.tencent.com/npm/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true,
+ "optional": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "http://mirrors.tencent.com/npm/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/ssf": {
+ "version": "0.11.2",
+ "resolved": "http://mirrors.tencent.com/npm/ssf/-/ssf-0.11.2.tgz",
+ "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "frac": "~1.1.2"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/stream-parser": {
+ "version": "0.3.1",
+ "resolved": "http://mirrors.tencent.com/npm/stream-parser/-/stream-parser-0.3.1.tgz",
+ "integrity": "sha512-bJ/HgKq41nlKvlhccD5kaCr/P+Hu0wPNKPJOH7en+YrJu/9EgqUF+88w5Jb6KNcjOFMhfX4B2asfeAtIGuHObQ==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "debug": "2"
+ }
+ },
+ "node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "http://mirrors.tencent.com/npm/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "http://mirrors.tencent.com/npm/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.17",
+ "resolved": "http://mirrors.tencent.com/npm/tinyglobby/-/tinyglobby-0.2.17.tgz",
+ "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "http://mirrors.tencent.com/npm/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "dev": true,
+ "license": "0BSD",
+ "optional": true
+ },
+ "node_modules/undici-types": {
+ "version": "6.21.0",
+ "resolved": "http://mirrors.tencent.com/npm/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/vite": {
+ "version": "8.0.13",
+ "resolved": "http://mirrors.tencent.com/npm/vite/-/vite-8.0.13.tgz",
+ "integrity": "sha512-MFtjBYgzmSxmgA4RAfjIyXWpGe1oALnjgUTzzV7QLx/TKxCzjtMH6Fd9/eVK+5Fg1qNoz5VAwsmMs/NofrmJvw==",
+ "dev": true,
+ "dependencies": {
+ "lightningcss": "^1.32.0",
+ "picomatch": "^4.0.4",
+ "postcss": "^8.5.14",
+ "rolldown": "1.0.1",
+ "tinyglobby": "^0.2.16"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "@vitejs/devtools": "^0.1.18",
+ "esbuild": "^0.27.0 || ^0.28.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "@vitejs/devtools": {
+ "optional": true
+ },
+ "esbuild": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vue": {
+ "version": "3.5.43",
+ "resolved": "http://mirrors.tencent.com/npm/vue/-/vue-3.5.43.tgz",
+ "integrity": "sha512-o5qZoksdnjIKvW1srZ3ab7pcDNYAerBjRe54D0LBLfRdCYFrSgBHVXokMas35czQc0//lmx4/tuY4ZNQ+Rf2Ng==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-dom": "3.5.43",
+ "@vue/compiler-sfc": "3.5.43",
+ "@vue/runtime-dom": "3.5.43",
+ "@vue/server-renderer": "3.5.43",
+ "@vue/shared": "3.5.43"
+ },
+ "peerDependencies": {
+ "typescript": "*"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vue-component-type-helpers": {
+ "version": "3.3.11",
+ "resolved": "http://mirrors.tencent.com/npm/vue-component-type-helpers/-/vue-component-type-helpers-3.3.11.tgz",
+ "integrity": "sha512-LwcxzeliO9fkQcpJG0PoX8X5kmAhKmH9wkpDLxNabwzkQ9Zeib2YVHwFV4pcWmMLfXVfjr/dSV+DaJ3cIPgSNA==",
+ "license": "MIT"
+ },
+ "node_modules/which-module": {
+ "version": "2.0.1",
+ "resolved": "http://mirrors.tencent.com/npm/which-module/-/which-module-2.0.1.tgz",
+ "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
+ "license": "ISC"
+ },
+ "node_modules/wmf": {
+ "version": "1.0.2",
+ "resolved": "http://mirrors.tencent.com/npm/wmf/-/wmf-1.0.2.tgz",
+ "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/word": {
+ "version": "0.3.0",
+ "resolved": "http://mirrors.tencent.com/npm/word/-/word-0.3.0.tgz",
+ "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/wrap-ansi": {
+ "version": "6.2.0",
+ "resolved": "http://mirrors.tencent.com/npm/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
+ "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/xlsx": {
+ "version": "0.18.5",
+ "resolved": "http://mirrors.tencent.com/npm/xlsx/-/xlsx-0.18.5.tgz",
+ "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "adler-32": "~1.3.0",
+ "cfb": "~1.2.1",
+ "codepage": "~1.15.0",
+ "crc-32": "~1.2.1",
+ "ssf": "~0.11.2",
+ "wmf": "~1.0.1",
+ "word": "~0.3.0"
+ },
+ "bin": {
+ "xlsx": "bin/xlsx.njs"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/y18n": {
+ "version": "4.0.3",
+ "resolved": "http://mirrors.tencent.com/npm/y18n/-/y18n-4.0.3.tgz",
+ "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
+ "license": "ISC"
+ },
+ "node_modules/yargs": {
+ "version": "15.4.1",
+ "resolved": "http://mirrors.tencent.com/npm/yargs/-/yargs-15.4.1.tgz",
+ "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
+ "license": "MIT",
+ "dependencies": {
+ "cliui": "^6.0.0",
+ "decamelize": "^1.2.0",
+ "find-up": "^4.1.0",
+ "get-caller-file": "^2.0.1",
+ "require-directory": "^2.1.1",
+ "require-main-filename": "^2.0.0",
+ "set-blocking": "^2.0.0",
+ "string-width": "^4.2.0",
+ "which-module": "^2.0.0",
+ "y18n": "^4.0.0",
+ "yargs-parser": "^18.1.2"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/yargs-parser": {
+ "version": "18.1.3",
+ "resolved": "http://mirrors.tencent.com/npm/yargs-parser/-/yargs-parser-18.1.3.tgz",
+ "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
+ "license": "ISC",
+ "dependencies": {
+ "camelcase": "^5.0.0",
+ "decamelize": "^1.2.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ }
+ }
+}
"preview": "vite preview --config vite.config.js --port 37834"
},
"dependencies": {
- "@ant-design/icons-vue": "^7.0.1",
- "ant-design-vue": "^4.2.6",
+ "@element-plus/icons-vue": "^2.3.1",
+ "element-plus": "^2.11.0",
+ "qrcode": "^1.5.4",
"vue": "^3.5.43",
"xlsx": "^0.18.5"
},
"devDependencies": {
"@types/node": "22.19.19",
"@vitejs/plugin-vue": "^6.0.9",
+ "less": "^4.9.1",
+ "playwright": "^1.48.0",
"vite": "8.0.13"
}
}
<script setup>
-import { defineComponent, onMounted } from "vue";
-import { ConfigProvider, theme as antdTheme } from "ant-design-vue";
-import zhCN from "ant-design-vue/es/locale/zh_CN";
-import brandTheme from "./styles/theme.json";
+import { ElConfigProvider } from "element-plus";
+import zhCn from "element-plus/es/locale/lang/zh-cn";
import Home from "./Home.vue";
-
-/** Serialize official antd tokens without turning unitless values into lengths. */
-function tokenCssValue(name, value) {
- if (typeof value !== "number") return value;
- if (/^(lineHeight|fontWeight|opacity|zIndex|sizeStep|sizeUnit)/i.test(name)) return String(value);
- const length = /^(fontSize|borderRadius|controlHeight|size|padding|margin)|(?:Size|Height|Width|Radius|Padding|Margin)/;
- return length.test(name) ? `${value}px` : String(value);
-}
-
-const companyTheme = { ...brandTheme, cssVar: { key: "stationbuilder" } };
-const kebab = (name) =>
- name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/([A-Z])([A-Z][a-z])/g, "$1-$2").toLowerCase();
-
-// Vue port of React company-theme.tsx: ant-design-vue has no `theme.getDesignToken`
-// export, so resolve the merged design token via the useToken() composable from a
-// null-rendering child inside the ConfigProvider, then inject the `--ant-*`
-// variables that the app styles depend on (React renders an equivalent
-// <style data-station-brand-tokens> inside its ConfigProvider).
-const BrandTokenStyle = defineComponent({
- name: "BrandTokenStyle",
- setup() {
- const { token } = antdTheme.useToken();
- onMounted(() => {
- const resolved = token.value || {};
- const declarations = Object.entries(resolved)
- .filter(([, value]) => typeof value === "string" || typeof value === "number")
- .map(([name, value]) => "--ant-" + kebab(name) + ":" + tokenCssValue(name, value));
- for (const [component, tokens] of Object.entries(brandTheme.components)) {
- for (const [name, value] of Object.entries(tokens)) {
- declarations.push("--ant-" + kebab(component) + "-" + kebab(name) + ":" + tokenCssValue(name, value));
- }
- }
- const style = document.createElement("style");
- style.setAttribute("data-station-brand-tokens", "");
- style.textContent = ":root{" + declarations.join(";") + "}";
- document.head.appendChild(style);
- });
- return () => null;
- },
-});
</script>
<template>
- <ConfigProvider :theme="companyTheme" :locale="zhCN" :button="{ autoInsertSpace: false }">
- <BrandTokenStyle />
+ <ElConfigProvider :locale="zhCn">
<Home />
- </ConfigProvider>
+ </ElConfigProvider>
</template>
<script setup>
import { ref, computed, watch, onMounted, nextTick, h } from "vue";
-import { message, Drawer, Dropdown, Tag } from "ant-design-vue";
-import {
- AimOutlined, ApartmentOutlined, BarsOutlined, BlockOutlined, CaretDownOutlined,
- CaretRightOutlined, ControlOutlined, CopyOutlined, DeleteOutlined, DownloadOutlined,
- EditOutlined, EllipsisOutlined, EyeOutlined, LinkOutlined, PlusOutlined,
- PlusSquareOutlined, RobotOutlined, SearchOutlined, SnippetsOutlined, UploadOutlined,
- ZoomInOutlined, ZoomOutOutlined,
-} from "@ant-design/icons-vue";
+import { ElMessage, ElDrawer, ElDropdown, ElTag } from "element-plus";
+import { Aim, OfficeBuilding, Menu, Close, ArrowDown, ArrowRight, Operation, CopyDocument, Delete, Download, Edit, MoreFilled, View, Link, Plus, MagicStick, Search, DocumentCopy, Upload, ZoomIn, ZoomOut } from "@element-plus/icons-vue";
import UiButton from "./ui/UiButton.vue";
import UiInput from "./ui/UiInput.vue";
// setup binding instead.
const win = window;
-const notify = (m) => { message.info(m, 2.2); };
+const notify = (m) => { ElMessage.info(m, 2.2); };
const confirm = (msg) => Promise.resolve(window.confirm(msg));
// ---- state ----
: await fetchDb(`/sites/${sid}/cubes/prompt?${new URLSearchParams({ name: name || "", content: content || "" }).toString()}`);
promptData.value = { name, text: data.prompt };
modal.value = "prompt";
- } catch (e) { notify("读取提示词失败: " + e.message); }
+ } catch (e) { notify("读取提示词失败: " + e.ElMessage); }
};
const viewSitePrompt = async () => {
const sid = site.value?.id ?? siteId.value;
if (sid == null) { notify("请先创建或打开一个站点"); return; }
try { const data = await fetchDb(`/sites/${sid}/prompt`); promptData.value = { name: "站点级提示词(通用上下文)", text: data.prompt }; modal.value = "prompt"; }
- catch (e) { notify("读取提示词失败: " + e.message); }
+ catch (e) { notify("读取提示词失败: " + e.ElMessage); }
};
const viewCompletePrompt = async () => {
if (!site.value?.id) { notify("请先创建或打开一个站点"); return; }
const qs = new URLSearchParams({ name: site.value.name || "", domain: site.value.domain || "", description: site.value.description || "" }).toString();
const data = await fetchDb(`/sites/${site.value.id}/complete-prompt?${qs}`);
promptData.value = { name: "AI 智能补全", text: data.prompt }; modal.value = "completePrompt";
- } catch (e) { notify("读取提示词失败: " + e.message); }
+ } catch (e) { notify("读取提示词失败: " + e.ElMessage); }
};
const viewCompleteAssetsPrompt = async () => {
if (!site.value?.id) { notify("请先创建或打开一个站点"); return; }
const qs = new URLSearchParams({ name: site.value.name || "", domain: site.value.domain || "", description: site.value.description || "", supplement: site.value.supplement || "" }).toString();
const data = await fetchDb(`/sites/${site.value.id}/complete-assets-prompt?${qs}`);
promptData.value = { name: "AI 资产补全", text: data.prompt }; modal.value = "completePrompt";
- } catch (e) { notify("读取提示词失败: " + e.message); }
+ } catch (e) { notify("读取提示词失败: " + e.ElMessage); }
};
const viewCompleteCubesPrompt = async () => {
if (!site.value?.id) { notify("请先创建或打开一个站点"); return; }
const qs = new URLSearchParams({ name: site.value.name || "", domain: site.value.domain || "", description: site.value.description || "", supplement: site.value.supplement || "" }).toString();
const data = await fetchDb(`/sites/${site.value.id}/complete-cubes-prompt?${qs}`);
promptData.value = { name: "AI Cube 页面补全", text: data.prompt }; modal.value = "completePrompt";
- } catch (e) { notify("读取提示词失败: " + e.message); }
+ } catch (e) { notify("读取提示词失败: " + e.ElMessage); }
};
const viewDtCompletePrompt = async (id, name, domain, description) => {
try { const qs = new URLSearchParams({ name, domain, description }).toString(); const data = await fetchDb(`/domain-templates/${id}/complete-prompt?${qs}`); promptData.value = { name: "AI 智能补全", text: data.prompt }; modal.value = "completePrompt"; }
- catch (e) { notify("读取提示词失败: " + e.message); }
+ catch (e) { notify("读取提示词失败: " + e.ElMessage); }
};
const viewDtCompleteAssetsPrompt = async (id, name, domain, description, supplement) => {
try { const qs = new URLSearchParams({ name, domain, description, supplement }).toString(); const data = await fetchDb(`/domain-templates/${id}/complete-assets-prompt?${qs}`); promptData.value = { name: "AI 资产补全", text: data.prompt }; modal.value = "completePrompt"; }
- catch (e) { notify("读取提示词失败: " + e.message); }
+ catch (e) { notify("读取提示词失败: " + e.ElMessage); }
};
const viewDtCompleteCubesPrompt = async (id, name, domain, description, supplement) => {
try { const qs = new URLSearchParams({ name, domain, description, supplement }).toString(); const data = await fetchDb(`/domain-templates/${id}/complete-cubes-prompt?${qs}`); promptData.value = { name: "AI Cube 页面补全", text: data.prompt }; modal.value = "completePrompt"; }
- catch (e) { notify("读取提示词失败: " + e.message); }
+ catch (e) { notify("读取提示词失败: " + e.ElMessage); }
};
const viewDeviceCpCompletePrompt = async (id) => {
try { const data = await fetchDb(`/device-templates/${id}/complete-collection-points-prompt`); promptData.value = { name: "AI 采集点补全", text: data.prompt }; modal.value = "completePrompt"; }
- catch (e) { notify("读取提示词失败: " + e.message); }
+ catch (e) { notify("读取提示词失败: " + e.ElMessage); }
};
const openFeedback = (cube) => {
notify(`已根据修改意见重新生成「${cube.name}」`);
refining.value = false; modal.value = ""; feedbackCube.value = null;
try { const refreshed = await fetchDb(`/sites/${site.value.id}`); site.value = refreshed; siteId.value = site.value.id; } catch {}
- } catch (e) { notify("重新生成失败:" + (e?.message || e)); }
+ } catch (e) { notify("重新生成失败:" + (e?.ElMessage || e)); }
finally { clearTimeout(watchdog); refining.value = false; }
};
watch([aiMessages, aiOpen], async () => { await nextTick(); if (aiEndRef.value) aiEndRef.value.scrollIntoView({ block: "end" }); });
const loadPageStyles = async () => {
- try { const d = await fetchDb("/page-styles"); pageStyles.value = d || []; } catch (e) { notify("加载页面样式失败: " + e.message); }
+ try { const d = await fetchDb("/page-styles"); pageStyles.value = d || []; } catch (e) { notify("加载页面样式失败: " + e.ElMessage); }
};
onMounted(() => {
domainOptions.value = (d || []).map((x) => ({ id: x.id, name: x.name }));
muTree.value = m || [];
pageStyles.value = ps || [];
- }).catch((e) => notify("加载失败: " + e.message)).finally(() => setLoading(false));
+ }).catch((e) => notify("加载失败: " + e.ElMessage)).finally(() => setLoading(false));
});
const reloadDomains = async () => {
const d = await fetchDb("/domains"); const arr = d || [];
domainOptions.value = arr.map((x) => ({ id: x.id, name: x.name }));
domains.value = arr.map((x) => x.name);
- } catch (e) { notify("加载领域失败: " + e.message); }
+ } catch (e) { notify("加载领域失败: " + e.ElMessage); }
};
const handleDomainDeleted = async (id) => { await reloadDomains(); if (domainFilter.value === id) domainFilter.value = null; };
sites.value = sites.value.filter((s) => !ids.includes(s.id));
if (siteId.value != null && ids.includes(siteId.value)) { site.value = null; siteId.value = null; }
notify(`已删除 ${ids.length} 个站点`);
- } catch (e) { notify("删除失败: " + e.message); }
+ } catch (e) { notify("删除失败: " + e.ElMessage); }
};
const confirmUnsaved = async () => {
if (!dirty.value) return true;
}
page.value = target;
window.history.replaceState({}, "", "?page=" + target + (curId ? "&id=" + curId : ""));
- } catch (e) { notify("操作失败: " + (e?.message || e)); }
+ } catch (e) { notify("操作失败: " + (e?.ElMessage || e)); }
};
const createSiteFromDomainTemplate = async (templateId) => {
if (!(await confirmUnsaved())) return;
if (dirty.value && site.value) await fetchDb(`/sites/${site.value.id}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(site.value) });
dirty.value = false; sites.value = await fetchDb("/sites");
};
-const saveConfigurationDraft = async () => { if (configurationSaving.value) return; configurationSaving.value = true; try { await saveCurrentConfiguration(); notify("草稿已保存"); } catch (error) { notify("保存失败:" + error.message); } finally { configurationSaving.value = false; } };
+const saveConfigurationDraft = async () => { if (configurationSaving.value) return; configurationSaving.value = true; try { await saveCurrentConfiguration(); notify("草稿已保存"); } catch (error) { notify("保存失败:" + error.ElMessage); } finally { configurationSaving.value = false; } };
const changeConfigurationStep = async (section) => {
if (configurationSaving.value) return; configurationSaving.value = true;
try { await saveCurrentConfiguration(); if (page.value === "siteEdit") siteEditSection.value = section; else await go(section, site.value?.id); }
- catch (error) { notify("请先完成保存:" + error.message); } finally { configurationSaving.value = false; }
+ catch (error) { notify("请先完成保存:" + error.ElMessage); } finally { configurationSaving.value = false; }
};
-const backToHierarchy = async () => { if (configurationSaving.value) return; configurationSaving.value = true; try { await saveCurrentConfiguration(); await go("projects"); } catch (error) { notify("请先完成保存:" + error.message); } finally { configurationSaving.value = false; } };
+const backToHierarchy = async () => { if (configurationSaving.value) return; configurationSaving.value = true; try { await saveCurrentConfiguration(); await go("projects"); } catch (error) { notify("请先完成保存:" + error.ElMessage); } finally { configurationSaving.value = false; } };
const nextConfigurationStep = async () => {
const section = page.value === "siteEdit" ? siteEditSection.value : page.value;
const index = SITE_EDIT_STEPS.indexOf(section);
if (index < 4) return changeConfigurationStep(SITE_EDIT_STEPS[index + 1]);
if (configurationSaving.value) return; configurationSaving.value = true;
- try { await saveCurrentConfiguration(); await go("generate", site.value?.id); } catch (error) { notify("请先完成保存:" + error.message); } finally { configurationSaving.value = false; }
+ try { await saveCurrentConfiguration(); await go("generate", site.value?.id); } catch (error) { notify("请先完成保存:" + error.ElMessage); } finally { configurationSaving.value = false; }
};
const applyTemplateToSite = async (templateId, replace) => {
if (!site.value) return;
await fetchDb(`/sites/${site.value.id}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(next) });
sites.value = sites.value.map((s) => (s.id === site.value.id ? { ...s, name: next.name, domain: next.domain, status: next.status } : s));
dirty.value = false;
- } catch (e) { notify("保存失败: " + e.message); }
+ } catch (e) { notify("保存失败: " + e.ElMessage); }
finally { saving.value = false; }
};
const saveAssets = async (assets) => {
if (!site.value) return;
site.value = { ...site.value, assets }; saving.value = true;
try { await fetchDb(`/sites/${site.value.id}/assets`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ assets }) }); }
- catch (e) { notify("资产保存失败: " + e.message); } finally { saving.value = false; }
+ catch (e) { notify("资产保存失败: " + e.ElMessage); } finally { saving.value = false; }
};
const saveMu = async (selected) => {
if (!site.value) return;
try { const saved = await fetchDb(`/sites/${site.value.id}/mu`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ selected }) }); site.value = saved; }
- catch (e) { notify("MU 选择保存失败: " + e.message); throw e; }
+ catch (e) { notify("MU 选择保存失败: " + e.ElMessage); throw e; }
};
const addDomain = async (name) => {
const n = (name || "").trim();
if (!n) return;
if (n.length > 6) { notify("领域名称不超过 6 个字"); return; }
try { await fetchDb("/domains", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: n }) }); await reloadDomains(); }
- catch (e) { notify("添加领域失败: " + e.message); }
+ catch (e) { notify("添加领域失败: " + e.ElMessage); }
};
const doAddDomain = async () => { if (!newDomainName.value.trim()) return; await addDomain(newDomainName.value); addingDomain.value = false; newDomainName.value = ""; };
const uploadFile = async (file) => {
const result = await fetchDb(`/sites/${site.value.id}/files`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ filename: file.name, mime: file.type, data: base64 }) });
site.value = { ...site.value, files: [...site.value.files, result] };
notify("已上传 " + file.name);
- } catch (e) { notify("上传失败: " + e.message); }
+ } catch (e) { notify("上传失败: " + e.ElMessage); }
};
const sendAi = async () => {
const text = aiDraft.value.trim();
const overview = sites.value && sites.value.length
? { total: sites.value.length, truncated: sites.value.length > OVERVIEW_MAX, sites: sites.value.slice(0, OVERVIEW_MAX).map((s) => ({ name: s.name, domain: s.domain, status: s.status, device_count: s.device_count, cube_count: s.cube_count, gen_count: s.gen_count, mu_count: s.mu_count })) }
: { total: 0, truncated: false, sites: [] };
- const res = await fetch("/api/chat", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ message: text, model: model.value, sessionId: chatSessionId, history: aiMessages.value.map((m) => ({ role: m.role, content: m.content })).slice(-20), site: siteInfo, overview }) });
+ const res = await fetch("/api/chat", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ElMessage: text, model: model.value, sessionId: chatSessionId, history: aiMessages.value.map((m) => ({ role: m.role, content: m.content })).slice(-20), site: siteInfo, overview }) });
if (!res.ok || !res.body) { const t = await res.text().catch(() => ""); throw new Error(t || "HTTP " + res.status); }
const reader = res.body.getReader(); const dec = new TextDecoder(); let acc = "";
for (;;) { const { done, value } = await reader.read(); if (done) break; acc += dec.decode(value, { stream: true }); aiMessages.value = (() => { const c = aiMessages.value.slice(); c[c.length - 1] = { role: "assistant", content: acc }; return c; })(); }
if (!acc.trim()) aiMessages.value = (() => { const c = aiMessages.value.slice(); c[c.length - 1] = { role: "assistant", content: "(模型未返回内容)" }; return c; })();
} catch (e) {
- aiMessages.value = (() => { const c = aiMessages.value.slice(); const last = c[c.length - 1]; if (last && last.role === "assistant") { c[c.length - 1] = { role: "assistant", content: last.content + "\n[连接出错] " + (e?.message || e) }; } else c.push({ role: "assistant", content: "[连接出错] " + (e?.message || e) }); return c; })();
+ aiMessages.value = (() => { const c = aiMessages.value.slice(); const last = c[c.length - 1]; if (last && last.role === "assistant") { c[c.length - 1] = { role: "assistant", content: last.content + "\n[连接出错] " + (e?.ElMessage || e) }; } else c.push({ role: "assistant", content: "[连接出错] " + (e?.ElMessage || e) }); return c; })();
} finally { aiBusy.value = false; }
};
<small>领域分类</small>
<div v-for="d in domainOptions" :key="d.id" class="domain-nav-row" :class="page === 'domains' && domainFilter === d.id ? ' active' : ''">
<UiButton :class="page === 'domains' && domainFilter === d.id ? 'active' : ''" @click="() => { domainFilter = d.id; go('domains'); }"><b>◆</b><span class="domain-nav-name">{{ d.name }}</span></UiButton>
- <UiButton v-if="d.name !== '其他'" className="domain-nav-del" title="删除领域" @click="async () => { if (!win.confirm(`确定删除领域「${d.name}」?其下的方案将移入「其他」领域。`)) return; try { await fetchDb(`/domains/${d.id}`, { method: 'DELETE' }); notify('已删除领域,相关方案已归入「其他」'); await handleDomainDeleted(d.id); } catch (e) { notify('删除失败:' + e.message); } }">×</UiButton>
+ <UiButton v-if="d.name !== '其他'" className="domain-nav-del" title="删除领域" @click="async () => { if (!win.confirm(`确定删除领域「${d.name}」?其下的方案将移入「其他」领域。`)) return; try { await fetchDb(`/domains/${d.id}`, { method: 'DELETE' }); notify('已删除领域,相关方案已归入「其他」'); await handleDomainDeleted(d.id); } catch (e) { notify('删除失败:' + e.ElMessage); } }">×</UiButton>
</div>
<div v-if="addingDomain" class="domain-nav-row add">
<UiInput className="domain-nav-input" autoFocus :value="newDomainName" placeholder="领域名称" @change="(e) => (newDomainName = e.target.value)" @keydown="async (e) => { if (e.key === 'Enter') { await doAddDomain(); } else if (e.key === 'Escape') { addingDomain = false; newDomainName = ''; } }" />
</div>
<div>
<label v-if="page === 'siteEdit'" class="site-edit-switcher"><span>当前站点</span>
- <UiSelect :value="siteId ?? ''" @change="async (e) => { const id = Number(e.target.value); if (id === siteId) return; try { await saveCurrentConfiguration(); await loadSite(id); siteEditSection = 'basic'; } catch (error) { notify('切换失败:' + error.message); return; } win.history.replaceState({}, '', `?page=siteEdit&id=${id}`); }">
+ <UiSelect :value="siteId ?? ''" @change="async (e) => { const id = Number(e.target.value); if (id === siteId) return; try { await saveCurrentConfiguration(); await loadSite(id); siteEditSection = 'basic'; } catch (error) { notify('切换失败:' + error.ElMessage); return; } win.history.replaceState({}, '', `?page=siteEdit&id=${id}`); }">
<option v-for="item in sites" :key="item.id" :value="item.id">{{ item.name || '未命名站点' }}</option>
</UiSelect>
</label>
- <PrimaryButton v-if="page === 'domains' && openDomainTemplateId == null" @click="openDomainTemplateId = 'create'"><UploadOutlined /> 导入领域方案</PrimaryButton>
+ <PrimaryButton v-if="page === 'domains' && openDomainTemplateId == null" @click="openDomainTemplateId = 'create'"><Upload /> 导入领域方案</PrimaryButton>
<PrimaryButton v-if="page === 'domains' && openDomainTemplateId != null" secondary @click="openDomainTemplateId = null">← 返回方案列表</PrimaryButton>
<PrimaryButton v-if="domainFilter != null && page === 'domains' && openDomainTemplateId == null" secondary @click="domainFilter = null">清除领域筛选:{{ domainOptions.find((d) => d.id === domainFilter)?.name || '' }}</PrimaryButton>
<PrimaryButton v-if="page === 'domainSettings'" @click="() => { newDomainName = ''; addingDomain = true; }">+ 新增领域</PrimaryButton>
// Shared device-template card + helpers (ported from app/page.tsx).
// Extracted so both the DomainSettings page and the DeviceTemplates page can reuse it.
import { ref, computed, h } from "vue";
-import { Drawer, Button as AntButton } from "ant-design-vue";
-import {
- EditOutlined, DeleteOutlined, CopyOutlined, PlusOutlined, RobotOutlined,
- SearchOutlined, ControlOutlined,
-} from "@ant-design/icons-vue";
+import { ElDrawer } from "element-plus";
+import { Edit, Delete, DocumentCopy, Plus, MagicStick, Search, Operation } from "@element-plus/icons-vue";
import UiButton from "../ui/UiButton.vue";
import UiInput from "../ui/UiInput.vue";
import UiSelect from "../ui/UiSelect.vue";
import { useConfirm } from "../pages/domain/useConfirm.js";
import { useStreamComplete, streamInto } from "../pages/domain/domainCommon.js";
-// React local `Button` (PrimaryButton primary/default + "button" class).
-const Button = PrimaryButton;
+// React local `ElButton` (PrimaryButton primary/default + "button" class).
+const ElButton = PrimaryButton;
const CP_RANGE_PRESETS = ["0~100", "0~1000", "-20~80", "-40~85", "0~50", "0~5", "0~1", "0~100%"];
p.notify("采集点已保存");
p.onChange();
} catch (e) {
- p.notify("保存失败:" + (e && e.message ? e.message : e));
+ p.notify("保存失败:" + (e && e.ElMessage ? e.ElMessage : e));
}
};
p.notify("属性已保存");
p.onChange();
} catch (e) {
- p.notify("保存失败:" + (e && e.message ? e.message : e));
+ p.notify("保存失败:" + (e && e.ElMessage ? e.ElMessage : e));
}
};
cpAi.setLog((prev) => prev + "\n⏹ 已停止\n");
p.notify("AI 采集点补全已停止");
} else {
- p.notify("AI 采集点补全失败:" + (e && e.message ? e.message : e));
+ p.notify("AI 采集点补全失败:" + (e && e.ElMessage ? e.ElMessage : e));
}
} finally {
clearTimeout(wd); cpAi.setCompleting(false); abortRef.value = null;
h("div", { class: "alarm-rule-head" }, [
h("b", {}, "报警规则"),
h("div", {}, [
- h("label", { class: "alarm-rule-search" }, [h(SearchOutlined), h(UiInput, { value: alarmQuery.value, onChange: (e) => (alarmQuery.value = e.target.value), placeholder: "报警名称" })]),
- h(Button, { onClick: () => (alarmDraft.value = newAlarmDraft()) }, [h(PlusOutlined), " 新增"]),
+ h("label", { class: "alarm-rule-search" }, [h(Search), h(UiInput, { value: alarmQuery.value, onChange: (e) => (alarmQuery.value = e.target.value), placeholder: "报警名称" })]),
+ h(ElButton, { onClick: () => (alarmDraft.value = newAlarmDraft()) }, [h(Plus), " 新增"]),
]),
]),
h(UiTable, { class: "data-table alarm-rule-table" }, () => [
h("td", { class: "alarm-description-cell", title: rule.description }, rule.description || "—"),
h("td", {}, h(UiButton, { type: "button", role: "switch", "aria-checked": rule.enabled, class: rule.enabled ? "alarm-switch on" : "alarm-switch", onClick: () => toggleAlarm(rule.id) }, [h("i")])),
h("td", {}, h("div", { class: "alarm-row-actions" }, [
- h(AntButton, { type: "text", title: "编辑", "aria-label": "编辑报警规则", icon: h(EditOutlined), onClick: () => editAlarm(rule) }),
- h(AntButton, { type: "text", title: "复制", "aria-label": "复制报警规则", icon: h(CopyOutlined), onClick: () => copyAlarm(rule) }),
- h(AntButton, { type: "text", danger: true, title: "删除", "aria-label": "删除报警规则", icon: h(DeleteOutlined), onClick: () => removeAlarm(rule) }),
+ h(ElButton, { type: "text", title: "编辑", "aria-label": "编辑报警规则", icon: h(Edit), onClick: () => editAlarm(rule) }),
+ h(ElButton, { type: "text", title: "复制", "aria-label": "复制报警规则", icon: h(DocumentCopy), onClick: () => copyAlarm(rule) }),
+ h(ElButton, { type: "text", danger: true, title: "删除", "aria-label": "删除报警规则", icon: h(Delete), onClick: () => removeAlarm(rule) }),
])),
])
),
]),
]),
h("div", { class: "field-assist", style: { marginTop: 16 } }, [
- h(Button, { secondary: true, onClick: () => (aiOpen.value = true) }, [h(RobotOutlined), " AI 智能补全"]),
+ h(ElButton, { secondary: true, onClick: () => (aiOpen.value = true) }, [h(MagicStick), " AI 智能补全"]),
h("span", { class: "muted" }, "基于设备名称 / 领域 / 说明,AI 推荐合适的采集点并追加"),
]),
- h(Drawer, {
+ h(ElDrawer, {
class: "alarm-rule-drawer",
title: alarmDraft.value && alarmDraft.value.id == null ? "新增报警规则" : "编辑报警规则",
width: 1000,
open: !!alarmDraft.value,
onClose: () => (alarmDraft.value = null),
footer: () => h("div", { class: "node-drawer-footer" }, [
- h(Button, { secondary: true, onClick: () => (alarmDraft.value = null) }, "取消"),
- h(Button, { onClick: saveAlarm }, "确定"),
+ h(ElButton, { secondary: true, onClick: () => (alarmDraft.value = null) }, "取消"),
+ h(ElButton, { onClick: saveAlarm }, "确定"),
]),
}, () =>
alarmDraft.value
h("header", {}, [
h("b", {}, "触发规则列表"),
h("span", {}, [
- h(Button, { secondary: true, onClick: () => p.notify("规则策略设定将在后续接入") }, [h(ControlOutlined), " 规则策略设定"]),
- h(Button, { onClick: addAlarmTrigger }, [h(PlusOutlined), " 新增"]),
+ h(ElButton, { secondary: true, onClick: () => p.notify("规则策略设定将在后续接入") }, [h(Operation), " 规则策略设定"]),
+ h(ElButton, { onClick: addAlarmTrigger }, [h(Plus), " 新增"]),
]),
]),
h(UiTable, { class: "data-table alarm-trigger-table" }, () => [
h("td", {}, h(UiSelect, { value: trigger.point, onChange: (e) => updateAlarmTrigger(trigger.id, "point", e.target.value) }, () => [h("option", { value: "" }, "请选择"), ...cps.value.map((point) => h("option", { key: point.key, value: point.name }, point.name || "未命名采集点"))])),
h("td", {}, h(UiSelect, { value: trigger.operator, onChange: (e) => updateAlarmTrigger(trigger.id, "operator", e.target.value) }, () => [h("option", {}, ">"), h("option", {}, ">="), h("option", {}, "<"), h("option", {}, "<="), h("option", {}, "="), h("option", {}, "!=")])),
h("td", {}, h(UiInput, { value: trigger.threshold, onChange: (e) => updateAlarmTrigger(trigger.id, "threshold", e.target.value), placeholder: "请输入" })),
- h("td", {}, h(AntButton, { type: "text", danger: true, title: "删除触发规则", icon: h(DeleteOutlined), onClick: () => removeAlarmTrigger(trigger.id) })),
+ h("td", {}, h(ElButton, { type: "text", danger: true, title: "删除触发规则", icon: h(Delete), onClick: () => removeAlarmTrigger(trigger.id) })),
])
)),
]),
])
: null
),
- h(Drawer, {
+ h(ElDrawer, {
title: "AI 智能补全",
width: 440,
open: aiOpen.value,
onClose: () => (aiOpen.value = false),
}, () =>
h("div", { class: "ai-assist-drawer" }, [
- h("div", { class: "ai-assist-intro" }, [h(RobotOutlined), h("div", {}, [h("h3", {}, "补充采集点"), h("p", {}, "AI 将根据设备名称、所属领域和说明生成合适的采集点,并以追加模式写入该设备模板。")])]),
+ h("div", { class: "ai-assist-intro" }, [h(MagicStick), h("div", {}, [h("h3", {}, "补充采集点"), h("p", {}, "AI 将根据设备名称、所属领域和说明生成合适的采集点,并以追加模式写入该设备模板。")])]),
h("p", { class: "hint" }, "为当前设备模板补充一套合适的采集点(追加模式,不覆盖已手动录入的采集点)。"),
h(UiActions, { class: "drawer-actions" }, () => [
- h(Button, { disabled: cpAi.completing.value, onClick: runCpComplete }, cpAi.completing.value ? "采集中…" : "开始补全"),
- cpAi.completing.value ? h(Button, { secondary: true, onClick: stopCpComplete }, "停止") : null,
- h(Button, { secondary: true, onClick: viewCpCompletePrompt }, "查看提示词"),
+ h(ElButton, { disabled: cpAi.completing.value, onClick: runCpComplete }, cpAi.completing.value ? "采集中…" : "开始补全"),
+ cpAi.completing.value ? h(ElButton, { secondary: true, onClick: stopCpComplete }, "停止") : null,
+ h(ElButton, { secondary: true, onClick: viewCpCompletePrompt }, "查看提示词"),
]),
(cpAi.completing.value || cpAi.log.value)
? h("div", { class: "modal-console" }, [
: null,
])
),
- h("div", { style: { marginTop: 8 } }, [h(Button, { onClick: save }, "保存设备")]),
+ h("div", { style: { marginTop: 8 } }, [h(ElButton, { onClick: save }, "保存设备")]),
]),
];
},
<template>
<button type="button" class="dropdown-btn">
- <a-dropdown
+ <el-dropdown
:disabled="disabled"
:trigger="trigger"
:placement="placement"
- :open="visible"
:class="'self-dropdown'"
@open-change="(open) => emit('openChange', open)"
>
<span v-if="value" class="filter-text">{{ value }}</span>
<span class="text">{{ label }}</span>
</div>
- <template #overlay>
+ <template #dropdown>
<slot />
</template>
- </a-dropdown>
+ </el-dropdown>
</button>
</template>
<script setup>
-import { Form } from "ant-design-vue";
const props = defineProps({
label: { type: String, default: "" },
required: { type: Boolean, default: false },
</script>
<template>
- <Form.Item class="field" :label="label" :required="required" :extra="hint" layout="vertical">
+ <el-form-item :required="required" class="field">
+ <template #label>{{ label }}<span v-if="hint" class="field-extra">({{ hint }})</span></template>
<slot />
- </Form.Item>
+ </el-form-item>
</template>
:class="['page-icon', { disabled: atLast }]"
@click="() => !atLast && emit('pageChange', totalPages)"
/>
- <a-select
+ <el-select
class="page-size"
- :value="pageSize"
+ :model-value="pageSize"
:options="[10, 20, 50, 100].map((v) => ({ label: v + ' / page', value: v }))"
@change="(v) => emit('pageSizeChange', v)"
/>
<script setup>
-import { computed, h } from "vue";
-import { Card } from "ant-design-vue";
+import { useSlots } from "vue";
import UiActions from "../ui/UiActions.vue";
const props = defineProps({
title: { type: String, default: "" },
className: { type: String, default: "" },
});
-const slots = defineSlots();
-
-const titleVNode = computed(() => (props.title ? h("strong", props.title) : undefined));
+const slots = useSlots();
</script>
<template>
- <Card
- class="panel"
- :class="className"
- :title="titleVNode"
- :class-names="{ header: 'panel-head', body: 'panel-body' }"
- >
- <template v-if="title && slots.tools" #extra>
+ <el-card class="panel" :class="className" :title="title">
+ <template v-if="slots.tools" #extra>
<UiActions class="station-inline-actions"><slot name="tools" /></UiActions>
</template>
<slot />
- </Card>
+ </el-card>
</template>
<script setup>
-import { Button as AntButton } from "ant-design-vue";
-
-// Port of React local `Button`: primary by default, secondary => default.
+// Port of React local `ElButton`: primary by default, secondary => default.
const props = defineProps({
secondary: { type: Boolean, default: false },
onClick: { type: Function, default: null },
</script>
<template>
- <AntButton
+ <el-button
:title="title"
:disabled="disabled"
:onClick="onClick"
+ :native-type="'button'"
:type="secondary ? 'default' : 'primary'"
class="button"
:class="secondary ? 'secondary' : ''"
>
<slot />
- </AntButton>
+ </el-button>
</template>
--- /dev/null
+<script setup>
+import { ref, onMounted, watch } from "vue";
+import QRCodeLib from "qrcode";
+
+const props = defineProps({
+ size: { type: Number, default: 120 },
+ value: { type: String, default: "" },
+ bordered: { type: Boolean, default: true },
+});
+
+const canvas = ref(null);
+
+function render() {
+ if (canvas.value && props.value) {
+ QRCodeLib.toCanvas(
+ canvas.value,
+ props.value,
+ { width: props.size, margin: props.bordered ? 2 : 0 },
+ () => {}
+ );
+ }
+}
+
+onMounted(render);
+watch(() => [props.value, props.size, props.bordered], render);
+</script>
+
+<template>
+ <canvas
+ ref="canvas"
+ :style="bordered ? 'border:1px solid #d9d9d9;border-radius:4px' : ''"
+ ></canvas>
+</template>
</script>
<template>
- <a-input
+ <el-input
class="search-input"
:class="className"
- :value="value"
+ :model-value="value"
:placeholder="placeholder"
- allow-clear
- @change="(e) => emit('change', e.target.value)"
+ clearable
+ @input="(val) => emit('change', val)"
>
<template #prefix>
<span class="search-prefix">
<SvgIcon name="search" :size="24" />
</span>
</template>
- </a-input>
+ </el-input>
</template>
-<script setup>
-import { h } from "vue";
-import SvgIcon from "./SvgIcon.vue";
+<script>
+import { h, ref } from "vue";
+import { ElMenu, ElMenuItem, ElSubMenu } from "element-plus";
-defineProps({
- items: { type: Array, default: () => [] },
- collapsed: { type: Boolean, default: false },
- selectedKeys: { type: Array, default: () => [] },
- openKeys: { type: Array, default: null },
-});
+export default {
+ props: {
+ items: { type: Array, default: () => [] },
+ collapsed: { type: Boolean, default: false },
+ selectedKeys: { type: Array, default: () => [] },
+ openKeys: { type: Array, default: null },
+ },
+ emits: ["click", "openChange"],
+ setup(props, { emit }) {
+ const openeds = ref(props.openKeys ? [...props.openKeys] : []);
-const emit = defineEmits(["click", "openChange"]);
+ function renderItem(item) {
+ const key = String(item.key);
+ const label = [item.icon, item.label];
+ if (item.children && item.children.length) {
+ return h(
+ ElSubMenu,
+ { index: key, key },
+ {
+ title: () => label,
+ default: () => item.children.map(renderItem),
+ }
+ );
+ }
+ return h(ElMenuItem, { index: key, key }, () => label);
+ }
-const expandIcon = ({ isOpen }) =>
- h(SvgIcon, { name: isOpen ? "arrow-down" : "arrow-right", size: 24 });
+ return () =>
+ h(
+ ElMenu,
+ {
+ mode: "vertical",
+ collapse: props.collapsed,
+ defaultActive: props.selectedKeys[0] ? String(props.selectedKeys[0]) : undefined,
+ defaultOpeneds: openeds.value,
+ class: "station-side-menu",
+ onSelect: (key) => emit("click", { key }),
+ onOpen: (k) => {
+ if (!openeds.value.includes(k)) openeds.value.push(k);
+ emit("openChange", openeds.value.slice());
+ },
+ onClose: (k) => {
+ openeds.value = openeds.value.filter((x) => x !== k);
+ emit("openChange", openeds.value.slice());
+ },
+ },
+ { default: () => props.items.map(renderItem) }
+ );
+ },
+};
</script>
-
-<template>
- <a-menu
- mode="inline"
- theme="light"
- :inline-collapsed="collapsed"
- :selected-keys="selectedKeys"
- :open-keys="openKeys"
- :items="items"
- :expand-icon="expandIcon"
- @click="(info) => emit('click', info)"
- @open-change="(keys) => emit('openChange', keys)"
- />
-</template>
<script setup>
import { computed } from "vue";
-import { Tag } from "ant-design-vue";
const props = defineProps({ value: { type: String, default: "" } });
const tone = computed(() => {
const v = props.value;
- if (/不可用|失败|异常|failed/.test(v)) return "bad";
- if (/完成|正常|成功|通过|推荐|可用|已配置|已生成|已上传|generated/.test(v)) return "ok";
- if (/生成中|校验中|告警|running/.test(v)) return "warn";
- return "muted";
+ if (/不可用|失败|异常|failed/.test(v)) return "danger";
+ if (/完成|正常|成功|通过|推荐|可用|已配置|已生成|已上传|generated/.test(v)) return "success";
+ if (/生成中|校验中|告警|running/.test(v)) return "warning";
+ return "info";
});
</script>
<template>
- <Tag class="status" :class="tone"><slot>{{ value }}</slot></Tag>
+ <el-tag class="status" :class="tone" :type="tone"><slot>{{ value }}</slot></el-tag>
</template>
import { createApp } from "vue";
-import Antd from "ant-design-vue";
-import "ant-design-vue/dist/reset.css";
+import ElementPlus from "element-plus";
+import "element-plus/dist/index.css";
// Brand / layout styles — order mirrors the React app's src/main.tsx.
import "./styles/globals.css";
import "./styles/theme-overrides.css";
import "./styles/styles.less";
import "./styles/brand-layout.css";
+import "./styles/brand-tokens.css";
// Brand component styles.
import "./components/menu.less";
box.textContent += `[${title}] ${detail}\n\n`;
} catch (e) { /* ignore */ }
}
-window.addEventListener("error", (e) => showFatalError("error", (e.message || "") + "\n" + (e.filename || "") + ":" + (e.lineno || "") + "\n" + (e.error && e.error.stack ? e.error.stack : "")));
+window.addEventListener("error", (e) => showFatalError("error", (e.ElMessage || "") + "\n" + (e.filename || "") + ":" + (e.lineno || "") + "\n" + (e.error && e.error.stack ? e.error.stack : "")));
window.addEventListener("unhandledrejection", (e) => showFatalError("unhandledrejection", String(e.reason && e.reason.stack ? e.reason.stack : e.reason)));
const app = createApp(App);
app.config.errorHandler = (err, instance, info) => {
showFatalError("vue:" + info, String(err && err.stack ? err.stack : err));
};
-app.use(Antd).mount("#root");
+app.use(ElementPlus).mount("#root");
<script setup>
import { ref, computed, watch, onMounted, h } from "vue";
-import { Tag } from "ant-design-vue";
-import { EyeOutlined } from "@ant-design/icons-vue";
+import { ElTag } from "element-plus";
+import { View } from "@element-plus/icons-vue";
import UiButton from "../ui/UiButton.vue";
import UiInput from "../ui/UiInput.vue";
: /生成中|校验中|告警|running/.test(v)
? "warn"
: "muted";
- return h(Tag, { class: "status " + tone }, slots.default);
+ return h(ElTag, { class: "status " + tone }, slots.default);
};
// ---- device template loading (filtered by site domain) ----
}
notify("AI 资产补全已完成");
} catch (e) {
- notify("AI 资产补全失败:" + (e?.message || e));
+ notify("AI 资产补全失败:" + (e?.ElMessage || e));
} finally {
clearTimeout(watchdog);
completing.value = false;
:class="'asset-eye-btn' + (matchedTemplate(row) ? '' : ' disabled')"
:title="matchedTemplate(row) ? `查看模板:${matchedTemplate(row).name}` : '请先选择设备模板'"
@click="() => onEye(row)"
- ><EyeOutlined /></UiButton>
+ ><View /></UiButton>
</td>
<td>
<UiInput
<script setup>
import { ref, computed, watch, nextTick, onMounted } from "vue";
-import { Drawer, Button } from "ant-design-vue";
-import { RobotOutlined, EyeOutlined } from "@ant-design/icons-vue";
+import { ElDrawer, ElButton } from "element-plus";
+import { MagicStick, View } from "@element-plus/icons-vue";
import UiButton from "../ui/UiButton.vue";
import UiInput from "../ui/UiInput.vue";
}
notify("AI 智能补全已完成,已写入补充描述");
} catch (e) {
- notify("AI 智能补全失败:" + (e?.message || e));
+ notify("AI 智能补全失败:" + (e?.ElMessage || e));
} finally {
clearTimeout(watchdog);
completing.value = false;
placeholder="新领域名称(最多6字)"
@change="(e) => (newDomain = e.target.value)"
/>
- <Button
+ <ElButton
secondary
:disabled="!newDomain || domainTooLong"
@click="() => { if (props.onAddDomain) props.onAddDomain(newDomain); newDomain = ''; showAdd = false; }"
>
保存
- </Button>
+ </ElButton>
</div>
</Field>
<div class="form-section-heading" id="site-description">
<UiInput textarea :value="site.description" @change="(e) => change({ description: e.target.value })" />
</Field>
<div class="field-assist">
- <Button secondary @click="() => (aiOpen = true)"><RobotOutlined /> AI 智能补全</Button>
+ <ElButton secondary @click="() => (aiOpen = true)"><MagicStick /> AI 智能补全</ElButton>
<span class="muted">补充要求、填写建议与参考资料均在右侧抽屉中维护</span>
</div>
<div v-if="saving" class="save-indicator">正在保存…</div>
</div>
</Panel>
</div>
- <Drawer title="AI 智能补全" :width="440" :open="aiOpen" :onClose="() => (aiOpen = false)">
+ <ElDrawer title="AI 智能补全" :width="440" :open="aiOpen" :onClose="() => (aiOpen = false)">
<div class="ai-assist-drawer">
<div class="ai-assist-intro">
- <RobotOutlined />
+ <MagicStick />
<div>
<h3>补充站点需求</h3>
<p>AI 将根据站点名称、所属领域和基本情况描述生成补充要求。生成结果会写入下方,仍可继续编辑。</p>
</ul>
</Field>
<UiActions className="drawer-actions">
- <Button :disabled="completing" @click="runComplete">{{ completing ? "补全中…" : "开始补全" }}</Button>
- <Button secondary @click="() => props.onViewCompletePrompt && props.onViewCompletePrompt()">查看提示词</Button>
+ <ElButton :disabled="completing" @click="runComplete">{{ completing ? "补全中…" : "开始补全" }}</ElButton>
+ <ElButton secondary @click="() => props.onViewCompletePrompt && props.onViewCompletePrompt()">查看提示词</ElButton>
</UiActions>
<div v-if="completing || completeLog" class="modal-console">
<div class="modal-console-head">生成进度</div>
<pre class="modal-console-body" ref="completeConsoleRef">{{ completeLog || "(等待 AI 分析…)" }}</pre>
</div>
</div>
- </Drawer>
+ </ElDrawer>
</div>
<Panel v-else>
<div class="empty-state">加载中…</div>
<script setup>
import { ref, computed, watch, onMounted, h } from "vue";
-import { Button as AntButton, Dropdown, Tag } from "ant-design-vue";
-import { EditOutlined, DeleteOutlined, EllipsisOutlined } from "@ant-design/icons-vue";
+import { ElButton as ElButton, ElDropdown, ElTag } from "element-plus";
+import { Edit, Delete, MoreFilled } from "@element-plus/icons-vue";
import UiButton from "../ui/UiButton.vue";
import UiSelect from "../ui/UiSelect.vue";
import UiTable from "../ui/UiTable.vue";
try {
list.value = (await fetchDb("/device-templates")) || [];
} catch (e) {
- props.notify("加载失败:" + (e && e.message ? e.message : e));
+ props.notify("加载失败:" + (e && e.ElMessage ? e.ElMessage : e));
} finally {
loading.value = false;
}
await load();
props.notify("设备模板已删除");
} catch (e) {
- props.notify("删除失败:" + (e && e.message ? e.message : e));
+ props.notify("删除失败:" + (e && e.ElMessage ? e.ElMessage : e));
}
}
function rowMenu(row) {
return {
items: [
- { key: "edit", label: "编辑模板", icon: h(EditOutlined) },
- { key: "delete", label: "删除", danger: true, icon: h(DeleteOutlined) },
+ { key: "edit", label: "编辑模板", icon: h(Edit) },
+ { key: "delete", label: "删除", danger: true, icon: h(Delete) },
],
onClick: ({ key }) => (key === "edit" ? (openId.value = row.id) : remove(row)),
};
<option v-for="domain in domainOptions" :key="domain.id" :value="domain.name">{{ domain.name }}</option>
</UiSelect>
<UiActions class="filter-actions">
- <AntButton @click="() => { query = ''; pointFilter = ''; domainFilter = ''; }">重置</AntButton>
+ <ElButton @click="() => { query = ''; pointFilter = ''; domainFilter = ''; }">重置</ElButton>
<PrimaryButton @click="openId = 'create'">+ 新增模板</PrimaryButton>
</UiActions>
</div>
</td>
<td class="domain-cell">
<div class="domain-tag-list">
- <Tag v-for="n in domainNames(row)" :key="n" class="tag-new" :color="domainTagColor(n)">{{ n }}</Tag>
+ <ElTag v-for="n in domainNames(row)" :key="n" class="tag-new" :color="domainTagColor(n)">{{ n }}</ElTag>
</div>
</td>
<td @click.stop>
- <Dropdown :trigger="['click']" :menu="rowMenu(row)">
- <AntButton type="text" aria-label="更多操作" :icon="h(EllipsisOutlined)" />
- </Dropdown>
+ <ElDropdown :trigger="['click']" :menu="rowMenu(row)">
+ <ElButton type="text" aria-label="更多操作" :icon="h(MoreFilled)" />
+ </ElDropdown>
</td>
</tr>
</tbody>
<script setup>
import { ref, computed, watch, onMounted, h } from "vue";
-import { Drawer, Button as AntButton } from "ant-design-vue";
-import {
- EditOutlined, DeleteOutlined, CopyOutlined, PlusOutlined, RobotOutlined,
- SearchOutlined, ControlOutlined, EllipsisOutlined,
-} from "@ant-design/icons-vue";
+import { ElDrawer } from "element-plus";
+import { Edit, Delete, DocumentCopy, Plus, MagicStick, Search, Operation, MoreFilled } from "@element-plus/icons-vue";
import UiButton from "../ui/UiButton.vue";
import UiInput from "../ui/UiInput.vue";
import UiSelect from "../ui/UiSelect.vue";
import { useConfirm } from "../pages/domain/useConfirm.js";
import { DeviceImageField, DeviceTemplateCard, CP_RANGE_PRESETS, fileToBase64 } from "../components/DeviceTemplateCard.js";
-// React local `Button` (AntButton primary/default + "button" class).
-const Button = PrimaryButton;
-// React `Status` (Tag with tone class).
+// React local `ElButton` (ElButton primary/default + "button" class).
+const ElButton = PrimaryButton;
+// React `Status` (ElTag with tone class).
const Status = StatusTag;
const CURRENT_USER_EMAIL = "wang.yu@advantech.com";
await props.onDeleted(row.id);
props.notify("已删除领域,相关方案已归入「其他」");
} catch (e) {
- props.notify("删除失败:" + (e && e.message ? e.message : e));
+ props.notify("删除失败:" + (e && e.ElMessage ? e.ElMessage : e));
}
}
p.notify("已创建");
p.onCreated(r.id);
} catch (e) {
- p.notify("创建失败:" + (e && e.message ? e.message : e));
+ p.notify("创建失败:" + (e && e.ElMessage ? e.ElMessage : e));
}
};
return () =>
"领域描述",
h(UiInput, { textarea: true, value: description.value, onChange: (e) => (description.value = e.target.value), rows: 3 }),
]),
- h("div", { style: { gridColumn: "1 / -1" } }, [h(Button, { onClick: save }, "创建")]),
+ h("div", { style: { gridColumn: "1 / -1" } }, [h(ElButton, { onClick: save }, "创建")]),
])
);
},
try {
list.value = (await fetchDb("/domain-templates")) || [];
} catch (e) {
- p.notify("加载失败:" + (e && e.message ? e.message : e));
+ p.notify("加载失败:" + (e && e.ElMessage ? e.ElMessage : e));
} finally {
loading.value = false;
}
p.notify("已删除");
load();
} catch (e) {
- p.notify("删除失败:" + (e && e.message ? e.message : e));
+ p.notify("删除失败:" + (e && e.ElMessage ? e.ElMessage : e));
}
};
p.notify("已创建设备模版");
p.onDone(r.id);
} catch (e) {
- p.notify("创建失败:" + (e && e.message ? e.message : e));
+ p.notify("创建失败:" + (e && e.ElMessage ? e.ElMessage : e));
}
};
return () =>
h("label", {}, ["设备名称", h(UiInput, { value: name.value, onChange: (e) => (name.value = e.target.value), placeholder: "如:PLC 控制器" })]),
h("label", { style: { gridColumn: "1 / -1" } }, ["设备图片", h(DeviceImageField, { imageUrl: imgPreview.value, onPick })]),
h("label", { style: { gridColumn: "1 / -1" } }, ["说明", h(UiInput, { textarea: true, value: description.value, onChange: (e) => (description.value = e.target.value), rows: 2 })]),
- h("div", { style: { gridColumn: "1 / -1" } }, [h(Button, { onClick: add }, "创建")]),
+ h("div", { style: { gridColumn: "1 / -1" } }, [h(ElButton, { onClick: add }, "创建")]),
])
);
},
try {
list.value = (await fetchDb("/device-templates")) || [];
} catch (e) {
- p.notify("加载失败:" + (e && e.message ? e.message : e));
+ p.notify("加载失败:" + (e && e.ElMessage ? e.ElMessage : e));
} finally {
loading.value = false;
}
]),
h(Panel, {
title: "设备模版",
- tools: () => h(Button, { onClick: () => (openId.value = "create") }, "+ 新增设备模版"),
+ tools: () => h(ElButton, { onClick: () => (openId.value = "create") }, "+ 新增设备模版"),
}, () =>
h("div", {}, [
h("div", { class: "list-toolbar" }, [h(UiInput, { class: "search", placeholder: "搜索设备名称 / 说明", value: q.value, onChange: (e) => (q.value = e.target.value) })]),
p.notify(checked ? "已选用设备模版" : "已移除设备模版");
p.onChange();
} catch (e) {
- p.notify("保存失败:" + (e && e.message ? e.message : e));
+ p.notify("保存失败:" + (e && e.ElMessage ? e.ElMessage : e));
const back = new Set(sel.value);
if (checked) back.delete(id); else back.add(id);
sel.value = back;
h("div", { class: "page-row-head" }, [
h("b", {}, name.value),
h("span", {}, editing.value
- ? [h(Button, { onClick: save }, "保存"), " ", h(UiButton, { class: "text-button", onClick: () => (editing.value = false) }, "取消")]
+ ? [h(ElButton, { onClick: save }, "保存"), " ", h(UiButton, { class: "text-button", onClick: () => (editing.value = false) }, "取消")]
: [h(UiButton, { class: "text-button", onClick: () => (editing.value = true) }, "编辑"), " ", h(UiButton, { class: "text-button", onClick: del }, "删除")]),
]),
editing.value
<script setup>
import { ref, computed, watch } from "vue";
-import { Drawer, Button, Tooltip } from "ant-design-vue";
-import {
- ApartmentOutlined,
- CaretRightOutlined,
- CaretDownOutlined,
- ArrowUpOutlined,
- ArrowDownOutlined,
- EditOutlined,
- EyeOutlined,
-} from "@ant-design/icons-vue";
+import { ElDrawer, ElButton, ElTooltip } from "element-plus";
+import { OfficeBuilding, ArrowRight, ArrowDown, ArrowUp, Edit, View } from "@element-plus/icons-vue";
import UiButton from "../ui/UiButton.vue";
import UiInput from "../ui/UiInput.vue";
import UiSelect from "../ui/UiSelect.vue";
<div class="station-icon-control">
<div class="station-icon-image">
<img v-if="appIcon" :src="appIcon" alt="应用 Icon" />
- <ApartmentOutlined v-else />
+ <OfficeBuilding v-else />
</div>
<label :class="saving ? 'station-icon-upload disabled' : 'station-icon-upload'">
<span>{{ appIcon ? "更换 Icon" : "上传 Icon" }}</span>
<header>
<h2>菜单列表 <small>{{ sources.length }} 个页面</small></h2>
<UiActions className="station-application-menu-actions">
- <Button :disabled="saving" @click="collapseAll">{{ collapsed.size ? "展开全部" : "收起全部" }}</Button>
- <Button :disabled="saving" @click="() => (restoreConfirm = true)">恢复默认</Button>
- <Button type="primary" @click="openPreview">
- <template #icon><EyeOutlined /></template>
+ <ElButton :disabled="saving" @click="collapseAll">{{ collapsed.size ? "展开全部" : "收起全部" }}</ElButton>
+ <ElButton :disabled="saving" @click="() => (restoreConfirm = true)">恢复默认</ElButton>
+ <ElButton type="primary" @click="openPreview">
+ <template #icon><View /></template>
预览菜单
- </Button>
+ </ElButton>
</UiActions>
</header>
<div class="station-application-menu-scroll">
:aria-expanded="!collapsed.has(item.id)"
@click="() => toggle(item.id)"
>
- <CaretRightOutlined v-if="collapsed.has(item.id)" />
- <CaretDownOutlined v-else />
+ <ArrowRight v-if="collapsed.has(item.id)" />
+ <ArrowDown v-else />
</UiButton>
</template>
<span v-else class="station-menu-child-guide" />
<td>{{ kind(item) }}</td>
<td>
<div class="station-menu-row-actions">
- <Tooltip :title="!movable(item) ? '保持原有菜单结构与顺序' : '上移'">
- <Button
+ <ElTooltip :title="!movable(item) ? '保持原有菜单结构与顺序' : '上移'">
+ <ElButton
type="text"
:aria-label="`上移菜单${item.label}`"
:disabled="saving || !movable(item) || rowIndex(item) <= 0"
@click="() => move(item, -1)"
>
- <template #icon><ArrowUpOutlined /></template>
- </Button>
- </Tooltip>
- <Tooltip :title="!movable(item) ? '保持原有菜单结构与顺序' : '下移'">
- <Button
+ <template #icon><ArrowUp /></template>
+ </ElButton>
+ </ElTooltip>
+ <ElTooltip :title="!movable(item) ? '保持原有菜单结构与顺序' : '下移'">
+ <ElButton
type="text"
:aria-label="`下移菜单${item.label}`"
:disabled="saving || !movable(item) || rowIndex(item) >= rowSiblings(item).length - 1"
@click="() => move(item, 1)"
>
- <template #icon><ArrowDownOutlined /></template>
- </Button>
- </Tooltip>
- <Tooltip :title="fixedGroup(item) ? '领域菜单名称固定' : '编辑'">
- <Button
+ <template #icon><ArrowDown /></template>
+ </ElButton>
+ </ElTooltip>
+ <ElTooltip :title="fixedGroup(item) ? '领域菜单名称固定' : '编辑'">
+ <ElButton
type="text"
:aria-label="`编辑菜单${item.label}`"
:disabled="saving || fixedGroup(item)"
@click="() => (editDraft = { ...item })"
>
- <template #icon><EditOutlined /></template>
- </Button>
- </Tooltip>
+ <template #icon><Edit /></template>
+ </ElButton>
+ </ElTooltip>
</div>
</td>
</tr>
<span>{{ saving ? "正在保存…" : (!props.site.mu?.length && !props.site.page_config?.standard_pages_initialized ? "标准功能默认全选,可在第四步调整" : "增减页面请返回第四步“页面配置”") }}</span>
</footer>
</section>
- <Drawer
+ <ElDrawer
className="station-info-drawer station-menu-edit-drawer"
:width="560"
title="编辑菜单"
</div>
<template #footer>
<UiActions className="station-drawer-actions">
- <Button @click="() => (editDraft = null)">取消</Button>
- <Button type="primary" @click="confirmEditDraft">确定</Button>
+ <ElButton @click="() => (editDraft = null)">取消</ElButton>
+ <ElButton type="primary" @click="confirmEditDraft">确定</ElButton>
</UiActions>
</template>
- </Drawer>
- <Drawer
+ </ElDrawer>
+ <ElDrawer
className="station-info-drawer station-application-preview-drawer"
:width="460"
title="菜单预览"
<div class="station-application-preview" :style="{ '--application-color': color }">
<header>
<img v-if="appIcon" :src="appIcon" alt="应用 Icon" />
- <ApartmentOutlined v-else />
+ <OfficeBuilding v-else />
<strong>{{ appName || "未命名应用" }}</strong>
</header>
<nav>
@click="() => (item.page_type ? (previewPage = item.id) : toggle(item.id, true))"
>
<span>{{ item.label }}</span>
- <CaretRightOutlined v-if="!item.page_type && previewCollapsed.has(item.id)" />
- <CaretDownOutlined v-else-if="!item.page_type" />
+ <ArrowRight v-if="!item.page_type && previewCollapsed.has(item.id)" />
+ <ArrowDown v-else-if="!item.page_type" />
</UiButton>
</div>
</nav>
<small>菜单预览仅展示入口,不打开实际页面。</small>
</div>
</div>
- </Drawer>
+ </ElDrawer>
<UiOverlay v-if="restoreConfirm" className="modal-backdrop">
<div class="modal station-replace-confirm">
<header><h2>恢复默认菜单</h2><UiButton @click="() => (restoreConfirm = false)">×</UiButton></header>
<p>按第四步的页面选择恢复菜单名称和排序。应用名称、Icon 与色系保持不变,点击“保存草稿”后生效。</p>
</div>
<footer>
- <Button @click="() => (restoreConfirm = false)">取消</Button>
- <Button type="primary" @click="restoreDefault">恢复默认</Button>
+ <ElButton @click="() => (restoreConfirm = false)">取消</ElButton>
+ <ElButton type="primary" @click="restoreDefault">恢复默认</ElButton>
</footer>
</div>
</UiOverlay>
<script setup>
import { computed } from "vue";
-import { Button } from "ant-design-vue";
-import { DownloadOutlined } from "@ant-design/icons-vue";
+import { ElButton } from "element-plus";
+import { Download } from "@element-plus/icons-vue";
import Panel from "../components/Panel.vue";
import UiButton from "../ui/UiButton.vue";
<small>{{ x[2] }}</small>
<div style="display: flex; gap: 6px">
<UiButton>返回修改</UiButton>
- <UiButton class="text-button"><DownloadOutlined /> 下载</UiButton>
+ <UiButton class="text-button"><Download /> 下载</UiButton>
</div>
</div>
</div>
<div style="margin-top: 16px; text-align: right">
- <Button type="primary" class="button"><DownloadOutlined /> 下载全部</Button>
+ <ElButton type="primary" class="button"><Download /> 下载全部</ElButton>
</div>
</Panel>
</div>
<script setup>
import { ref, computed, watch, onMounted, onUnmounted, h, defineComponent } from "vue";
-import { Drawer, Dropdown, QRCode, Button as AntButton } from "ant-design-vue";
-import {
- CopyOutlined, SnippetsOutlined, ControlOutlined, DeleteOutlined, PlusSquareOutlined,
- PlusOutlined, BlockOutlined, ApartmentOutlined, BarsOutlined, ZoomOutOutlined,
- ZoomInOutlined, AimOutlined, SearchOutlined, EditOutlined, LinkOutlined, UploadOutlined,
-} from "@ant-design/icons-vue";
+import { ElDrawer, ElDropdown, ElButton as ElButton } from "element-plus"
+import QrCode from "@/components/QrCode.vue";
+import { CopyDocument, DocumentCopy, Operation, Delete, Plus, Close, OfficeBuilding, Menu, ZoomOut, ZoomIn, Aim, Search, Edit, Link, Upload } from "@element-plus/icons-vue";
import UiButton from "../ui/UiButton.vue";
import UiInput from "../ui/UiInput.vue";
draft.value = { ...g, structure: normalizeLegacy(g.structure) };
selectedId.value = draft.value.structure[0]?.id || null;
} catch (e) {
- const message = "加载资产组失败:" + (e?.message || e);
- loadError.value = message;
- notify(message);
+ const ElMessage = "加载资产组失败:" + (e?.ElMessage || e);
+ loadError.value = ElMessage;
+ notify(ElMessage);
} finally {
loading.value = false;
}
batchTab.value = "online";
notify(`已读取 ${rows.length} 行配置`);
} catch (e) {
- notify("读取文件失败:" + (e?.message || e));
+ notify("读取文件失败:" + (e?.ElMessage || e));
}
};
bindingImportOpen.value = false;
notify(`已导入 ${rows.length} 个点位参数`);
} catch (e) {
- notify("导入失败:" + (e?.message || e));
+ notify("导入失败:" + (e?.ElMessage || e));
}
};
await saveAll();
notify("资产组已保存");
} catch (e) {
- notify("保存失败:" + (e?.message || e));
+ notify("保存失败:" + (e?.ElMessage || e));
}
};
if (p.onAddChild) p.onAddChild(p.node);
},
},
- () => h(PlusOutlined)
+ () => h(Plus)
),
]
),
<section class="asset-tree-workspace">
<div class="asset-editor-toolbar">
<div class="asset-tool-group">
- <AntButton type="text" class="asset-tool-button" title="复制节点" aria-label="复制节点" :disabled="!selected" :icon="h(CopyOutlined)" @click="() => copySelected(false)" />
- <AntButton type="text" class="asset-tool-button" title="深度复制节点及其下级" aria-label="深度复制节点" :disabled="!selected" :icon="h(SnippetsOutlined)" @click="() => copySelected(true)" />
- <AntButton type="text" class="asset-tool-button" title="节点属性设置" aria-label="节点属性设置" :disabled="!selected" :icon="h(ControlOutlined)" @click="() => editSelected('properties')" />
- <AntButton type="text" class="asset-tool-button" title="删除节点" aria-label="删除节点" danger :disabled="!selected" :icon="h(DeleteOutlined)" @click="deleteNode" />
+ <ElButton type="text" class="asset-tool-button" title="复制节点" aria-label="复制节点" :disabled="!selected" :icon="h(DocumentCopy)" @click="() => copySelected(false)" />
+ <ElButton type="text" class="asset-tool-button" title="深度复制节点及其下级" aria-label="深度复制节点" :disabled="!selected" :icon="h(DocumentCopy)" @click="() => copySelected(true)" />
+ <ElButton type="text" class="asset-tool-button" title="节点属性设置" aria-label="节点属性设置" :disabled="!selected" :icon="h(Operation)" @click="() => editSelected('properties')" />
+ <ElButton type="text" class="asset-tool-button" title="删除节点" aria-label="删除节点" danger :disabled="!selected" :icon="h(Delete)" @click="deleteNode" />
<span class="asset-tool-divider" />
- <AntButton type="text" class="asset-tool-button primary" title="添加同级节点" aria-label="添加同级节点" :disabled="!selected" :icon="h(PlusSquareOutlined)" @click="addSibling" />
- <AntButton type="text" class="asset-tool-button primary" title="添加子节点" aria-label="添加子节点" :icon="h(PlusOutlined)" @click="() => addNode(selected)" />
- <Dropdown
+ <ElButton type="text" class="asset-tool-button primary" title="添加同级节点" aria-label="添加同级节点" :disabled="!selected" :icon="h(Plus)" @click="addSibling" />
+ <ElButton type="text" class="asset-tool-button primary" title="添加子节点" aria-label="添加子节点" :icon="h(Plus)" @click="() => addNode(selected)" />
+ <ElDropdown
trigger="['click']"
:menu="{ items: assetTemplates.map((name) => ({ key: name, label: `应用${name}` })), onClick: ({ key }) => bindTemplate(key) }"
>
- <AntButton type="text" class="asset-tool-button primary" title="应用资产模板" aria-label="应用资产模板" :disabled="!selected" :icon="h(BlockOutlined)" />
- </Dropdown>
+ <ElButton type="text" class="asset-tool-button primary" title="应用资产模板" aria-label="应用资产模板" :disabled="!selected" :icon="h(Close)" />
+ </ElDropdown>
</div>
<div class="asset-view-switch" role="group" aria-label="视图切换">
- <UiButton :class="viewMode === 'tree' ? 'active' : ''" title="资产树视图" @click="setViewMode('tree')"><ApartmentOutlined /></UiButton>
- <UiButton :class="viewMode === 'list' ? 'active' : ''" title="列表视图" @click="setViewMode('list')"><BarsOutlined /></UiButton>
+ <UiButton :class="viewMode === 'tree' ? 'active' : ''" title="资产树视图" @click="setViewMode('tree')"><OfficeBuilding /></UiButton>
+ <UiButton :class="viewMode === 'list' ? 'active' : ''" title="列表视图" @click="setViewMode('list')"><Menu /></UiButton>
</div>
<div class="asset-canvas-tools">
- <AntButton type="text" class="asset-tool-button" title="缩小" aria-label="缩小" :icon="h(ZoomOutOutlined)" @click="changeZoom((v) => Math.max(0.55, Number((v - 0.1).toFixed(2))))" />
- <AntButton type="text" class="asset-tool-button" title="放大" aria-label="放大" :icon="h(ZoomInOutlined)" @click="changeZoom((v) => Math.min(1.6, Number((v + 0.1).toFixed(2))))" />
- <AntButton type="text" class="asset-tool-button" title="适合窗口" aria-label="适合窗口" :icon="h(AimOutlined)" @click="fitCanvas" />
- <label class="asset-tree-search"><SearchOutlined /><UiInput :value="search" @change="(e) => (search = e.target.value)" placeholder="搜索资产节点" /></label>
+ <ElButton type="text" class="asset-tool-button" title="缩小" aria-label="缩小" :icon="h(ZoomOut)" @click="changeZoom((v) => Math.max(0.55, Number((v - 0.1).toFixed(2))))" />
+ <ElButton type="text" class="asset-tool-button" title="放大" aria-label="放大" :icon="h(ZoomIn)" @click="changeZoom((v) => Math.min(1.6, Number((v + 0.1).toFixed(2))))" />
+ <ElButton type="text" class="asset-tool-button" title="适合窗口" aria-label="适合窗口" :icon="h(Aim)" @click="fitCanvas" />
+ <label class="asset-tree-search"><Search /><UiInput :value="search" @change="(e) => (search = e.target.value)" placeholder="搜索资产节点" /></label>
</div>
</div>
</div>
<!-- node config drawer -->
- <Drawer
+ <ElDrawer
class="asset-node-drawer"
title="节点配置"
:width="'58vw'"
<label class="node-image-preview">
<input type="file" accept="image/jpeg,image/png,image/bmp,image/svg+xml" hidden @change="onNodeImageChange" />
<img v-if="nodeDraft.image_data" :src="nodeDraft.image_data" alt="节点图片" />
- <BlockOutlined v-else />
+ <Close v-else />
</label>
<div>
<label class="node-upload-link">
<input type="file" accept="image/jpeg,image/png,image/bmp,image/svg+xml" hidden @change="onNodeImageChange" />
- <UploadOutlined /> 图片上传
+ <Upload /> 图片上传
</label>
<small>支持格式 jpeg/jpg/png/bmp/svg,文件不超过 2 MB</small>
<UiButton v-if="nodeDraft.image_data" class="text-button" @click="patchNode({ image_data: '' })">移除图片</UiButton>
</div>
</section>
<section class="node-config-section"><h3>节点二维码</h3>
- <QRCode :size="112" :value="`stationbuilder://site/${group.site_id}/asset/${nodeDraft.id}`" :bordered="false" />
+ <QrCode :size="112" :value="`stationbuilder://site/${group.site_id}/asset/${nodeDraft.id}`" :bordered="false" />
</section>
</div>
<span>{{ nodeDraft.bindings.find((b) => b.source === component)?.group || "—" }}</span>
<span>{{ nodeDraft.bindings.find((b) => b.source === component)?.device_name || "—" }}</span>
<span>
- <UiButton title="编辑点位" @click="selectBindingComponent(component)"><EditOutlined /></UiButton>
- <UiButton title="绑定边缘设备" @click="bindEdgeComponent(component)"><LinkOutlined /></UiButton>
+ <UiButton title="编辑点位" @click="selectBindingComponent(component)"><Edit /></UiButton>
+ <UiButton title="绑定边缘设备" @click="bindEdgeComponent(component)"><Link /></UiButton>
</span>
</div>
</div>
<UiInput :value="item.precision || ''" @change="(e) => updateBinding(index, { precision: e.target.value })" />
<UiInput :value="item.mapping || ''" @change="(e) => updateBinding(index, { mapping: e.target.value })" />
<UiInput :value="item.description || ''" @change="(e) => updateBinding(index, { description: e.target.value })" />
- <UiButton class="trash" title="删除参数" @click="removeBinding(index)"><DeleteOutlined /></UiButton>
+ <UiButton class="trash" title="删除参数" @click="removeBinding(index)"><Delete /></UiButton>
</div>
<div v-if="!nodeDraft.bindings.some((b) => b.source === bindingComponent)" class="binding-empty">暂无数据,请先在组件列表中绑定设备或批量导入点位。</div>
</div>
</section>
</div>
</UiTabbedRegion>
- </Drawer>
+ </ElDrawer>
<!-- binding import dialog -->
<div v-if="bindingImportOpen" class="dlg-backdrop binding-import-backdrop" @mousedown="bindingImportOpen = false">
<script setup>
import { ref, computed, watch, onUnmounted, h } from "vue";
-import { Drawer, Button } from "ant-design-vue";
-import { EyeOutlined } from "@ant-design/icons-vue";
+import { ElDrawer, ElButton } from "element-plus";
+import { View } from "@element-plus/icons-vue";
import UiButton from "../ui/UiButton.vue";
import UiInput from "../ui/UiInput.vue";
import SearchInput from "../components/SearchInput.vue";
} catch (error) {
if (active) {
catalogError.value = true;
- props.notify("加载领域仪表板失败:" + error.message);
+ props.notify("加载领域仪表板失败:" + error.ElMessage);
}
} finally {
if (active) catalogLoading.value = false;
try {
await props.onMuChange(keys);
} catch (error) {
- props.notify("保存页面选择失败:" + error.message);
+ props.notify("保存页面选择失败:" + error.ElMessage);
} finally {
saving.value = false;
}
try {
await props.onPageConfigChange(config);
} catch (error) {
- props.notify("保存页面选择失败:" + error.message);
+ props.notify("保存页面选择失败:" + error.ElMessage);
} finally {
saving.value = false;
}
catalog.value = result.catalog;
props.onSiteChange && props.onSiteChange(result.site);
} catch (error) {
- props.notify("加载领域仪表板失败:" + error.message);
+ props.notify("加载领域仪表板失败:" + error.ElMessage);
} finally {
saving.value = false;
}
<UiInput type="checkbox" :aria-label="`选择仪表板${card.name}`" :disabled="saving || catalogLoading" :checked="cardSelected(card)" @change="() => changeDashboard(card)" />
{{ card.name }}
</label>
- <Button type="text" :aria-label="`预览${card.name}`" @click="() => (preview = card)">
- <template #icon><EyeOutlined /></template>
- </Button>
+ <ElButton type="text" :aria-label="`预览${card.name}`" @click="() => (preview = card)">
+ <template #icon><View /></template>
+ </ElButton>
</footer>
</article>
</div>
</article>
</div>
</section>
- <Drawer
+ <ElDrawer
className="station-info-drawer station-dashboard-preview-drawer"
:width="'80vw'"
:title="preview?.name || '仪表板预览'"
@close="() => (preview = null)"
>
<RenderVNode v-if="preview" :node="dashboardImage(preview, true)" />
- </Drawer>
+ </ElDrawer>
</div>
</template>
<script setup>
import { ref, computed, watch } from "vue";
-import { Button } from "ant-design-vue";
+import { ElButton } from "element-plus";
import UiSelect from "../ui/UiSelect.vue";
import UiButton from "../ui/UiButton.vue";
import UiTabbedRegion from "../ui/UiTabbedRegion.vue";
const d = await fetchDb(`/sites/${props.site.id}/cubes/${cube.value.id}/file`);
jsonText.value = JSON.stringify(d, null, 2);
} catch (e) {
- jsonText.value = "加载失败:" + (e?.message || e);
+ jsonText.value = "加载失败:" + (e?.ElMessage || e);
} finally {
loadingJson.value = false;
}
</div>
<div v-else class="preview-feedback">
- <Button @click="() => setModal('feedback')">提出修改意见</Button>
+ <ElButton @click="() => setModal('feedback')">提出修改意见</ElButton>
</div>
</UiTabbedRegion>
</div>
<script setup>
import { ref, computed, watch, onMounted, h } from "vue";
-import { Button, Drawer, Dropdown, Tag } from "ant-design-vue";
-import {
- ApartmentOutlined, CaretDownOutlined, CaretRightOutlined, CheckOutlined,
- DeleteOutlined, EditOutlined, EllipsisOutlined, PlusOutlined,
-} from "@ant-design/icons-vue";
+import { ElButton, ElDrawer, ElDropdown, ElTag } from "element-plus";
+import { OfficeBuilding, ArrowDown, ArrowRight, Check, Delete, Edit, MoreFilled, Plus } from "@element-plus/icons-vue";
import UiButton from "../ui/UiButton.vue";
import UiInput from "../ui/UiInput.vue";
const site = await fetchDb(`/sites/${id}`);
draft.value = { ...site, mode: site.template_id ? "template" : "blank" };
} catch (e) {
- notify("加载站点信息失败:" + (e?.message || e));
+ notify("加载站点信息失败:" + (e?.ElMessage || e));
}
};
const toggle = (id) => {
if (keepOpen) draft.value = { ...draft.value, id: undefined, name: "", description: "" };
else draft.value = null;
} catch (e) {
- notify("保存失败:" + (e?.message || e));
+ notify("保存失败:" + (e?.ElMessage || e));
} finally {
saving.value = false;
}
.then((d) => {
templates.value = d || [];
})
- .catch((e) => notify("加载模板失败:" + (e?.message || e)))
+ .catch((e) => notify("加载模板失败:" + (e?.ElMessage || e)))
.finally(() => {
templatesLoading.value = false;
});
</template>
<section class="station-list-panel">
<div class="station-hierarchy-note">
- <ApartmentOutlined />
+ <OfficeBuilding />
<span>先建立站点层级,再进入各站点配置。新建时可选择模板,暂不配置站点内部内容。</span>
<span>顶级站点 <b>{{ topCount }}</b> · 子站点 <b>{{ subCount }}</b></span>
</div>
<option v-for="item in domains" :key="item.id">{{ item.name }}</option>
</UiSelect>
<div>
- <Button @click="() => { query = ''; domain = ''; status = ''; }">重置</Button>
- <Button @click="toggleCollapseAll">{{ collapsed.size ? '展开全部' : '收起全部' }}</Button>
- <Button type="primary" :icon="h(PlusOutlined)" @click="() => openCreate()">新增站点</Button>
+ <ElButton @click="() => { query = ''; domain = ''; status = ''; }">重置</ElButton>
+ <ElButton @click="toggleCollapseAll">{{ collapsed.size ? '展开全部' : '收起全部' }}</ElButton>
+ <ElButton type="primary" :icon="h(Plus)" @click="() => openCreate()">新增站点</ElButton>
</div>
</div>
<div class="station-table-scroll">
:aria-label="(collapsed.has(row.site.id) ? '展开' : '收起') + row.site.name"
@click="toggle(row.site.id)"
>
- <CaretRightOutlined v-if="collapsed.has(row.site.id)" />
- <CaretDownOutlined v-else />
+ <ArrowRight v-if="collapsed.has(row.site.id)" />
+ <ArrowDown v-else />
</UiButton>
</template>
<span v-else class="station-tree-spacer" />
<small v-if="row.site.template_id && !row.site.template_applied_at" class="station-pending-template">待应用</small>
</td>
<td>
- <Tag :color="isConfigured(row.site) ? 'success' : 'default'">{{ isConfigured(row.site) ? '已配置' : '待配置' }}</Tag>
+ <ElTag :color="isConfigured(row.site) ? 'success' : 'default'">{{ isConfigured(row.site) ? '已配置' : '待配置' }}</ElTag>
</td>
<td>{{ (children.get(row.site.id) || []).length }}</td>
<td>{{ row.site.updated_at || '—' }}</td>
<td>
<div class="station-table-actions">
- <Button type="link" @click="() => props.onConfigure && props.onConfigure(row.site.id)">配置站点</Button>
- <Dropdown
+ <ElButton type="link" @click="() => props.onConfigure && props.onConfigure(row.site.id)">配置站点</ElButton>
+ <ElDropdown
:trigger="['click']"
:menu="{
items: [
- { key: 'basic', label: '编辑基本信息', icon: h(EditOutlined) },
- { key: 'child', label: '新增子站点', icon: h(PlusOutlined) },
- { key: 'delete', label: '删除站点', danger: true, icon: h(DeleteOutlined) },
+ { key: 'basic', label: '编辑基本信息', icon: h(Edit) },
+ { key: 'child', label: '新增子站点', icon: h(Plus) },
+ { key: 'delete', label: '删除站点', danger: true, icon: h(Delete) },
],
onClick: ({ key }) => {
if (key === 'basic') openEdit(row.site.id);
},
}"
>
- <Button type="text" :icon="h(EllipsisOutlined)" :aria-label="row.site.name + '更多操作'" />
- </Dropdown>
+ <ElButton type="text" :icon="h(MoreFilled)" :aria-label="row.site.name + '更多操作'" />
+ </ElDropdown>
</div>
</td>
</tr>
</section>
</UiTabbedRegion>
- <Drawer
+ <ElDrawer
class="station-info-drawer"
:width="720"
:title="draft?.id ? '编辑站点基本信息' : '新增站点'"
>
<template #footer>
<UiActions class="station-drawer-actions">
- <Button :disabled="saving" @click="() => (draft = null)">取消</Button>
- <Button v-if="!draft?.id" :disabled="saving" @click="() => submit(true)">创建并继续新增</Button>
- <Button type="primary" :loading="saving" @click="() => submit()">{{ draft?.id ? '保存' : '创建站点' }}</Button>
+ <ElButton :disabled="saving" @click="() => (draft = null)">取消</ElButton>
+ <ElButton v-if="!draft?.id" :disabled="saving" @click="() => submit(true)">创建并继续新增</ElButton>
+ <ElButton type="primary" :loading="saving" @click="() => submit()">{{ draft?.id ? '保存' : '创建站点' }}</ElButton>
</UiActions>
</template>
<template v-if="draft">
</template>
</section>
</template>
- </Drawer>
+ </ElDrawer>
</div>
</template>
<script setup>
import { computed, h } from "vue";
-import { Button, Dropdown } from "ant-design-vue";
-import {
- DownloadOutlined,
- EditOutlined,
- EllipsisOutlined,
- EyeOutlined,
-} from "@ant-design/icons-vue";
+import { ElButton, ElDropdown } from "element-plus";
+import { Download, Edit, MoreFilled, View } from "@element-plus/icons-vue";
import Panel from "../components/Panel.vue";
import StatusTag from "../components/StatusTag.vue";
import UiActions from "../ui/UiActions.vue";
const cubeMenu = (cube) => ({
items: [
- { key: "json", label: "下载 JSON", icon: h(DownloadOutlined) },
+ { key: "json", label: "下载 JSON", icon: h(Download) },
{ key: "prompt", label: "查看提示词" },
...(cube.gen_file_id
- ? [{ key: "feedback", label: "提交修改意见", icon: h(EditOutlined) }]
+ ? [{ key: "feedback", label: "提交修改意见", icon: h(Edit) }]
: []),
],
onClick: async ({ key }) => {
window.open(url, "_blank");
setTimeout(() => URL.revokeObjectURL(url), 60000);
} catch (e) {
- props.notify("预览打开失败:" + (e?.message || e));
+ props.notify("预览打开失败:" + (e?.ElMessage || e));
}
};
</script>
<StatusTag>{{ site.status === "generated" ? "生成完成" : "草稿" }}</StatusTag>
</div>
<UiActions class="result-actions">
- <Button class="button secondary" @click="() => go('assets')"><EditOutlined /> 修改配置</Button>
- <Button type="primary" class="button" @click="downloadAll">
- <DownloadOutlined /> 下载全部
- </Button>
- <Dropdown :trigger="['click']" :menu="actionMenu">
- <Button class="button secondary"><EllipsisOutlined /> 更多</Button>
- </Dropdown>
+ <ElButton class="button secondary" @click="() => go('assets')"><Edit /> 修改配置</ElButton>
+ <ElButton type="primary" class="button" @click="downloadAll">
+ <Download /> 下载全部
+ </ElButton>
+ <ElDropdown :trigger="['click']" :menu="actionMenu">
+ <ElButton class="button secondary"><MoreFilled /> 更多</ElButton>
+ </ElDropdown>
</UiActions>
</div>
<h3>{{ cube.name }}</h3>
<p>{{ cube.content || "无描述" }}</p>
<footer>
- <Button class="button secondary" @click="openPreview(cube)"><EyeOutlined /> 预览</Button>
- <Dropdown v-if="cube.id" :trigger="['click']" :menu="cubeMenu(cube)">
- <Button class="button secondary"><EllipsisOutlined /> 更多</Button>
- </Dropdown>
+ <ElButton class="button secondary" @click="openPreview(cube)"><View /> 预览</ElButton>
+ <ElDropdown v-if="cube.id" :trigger="['click']" :menu="cubeMenu(cube)">
+ <ElButton class="button secondary"><MoreFilled /> 更多</ElButton>
+ </ElDropdown>
</footer>
</div>
</article>
<script setup>
import { computed } from "vue";
-import { Tag } from "ant-design-vue";
+import { ElTag } from "element-plus";
import UiButton from "../ui/UiButton.vue";
const props = defineProps({
</div>
<div>
<span>站点模板</span><b>{{ site.template_name || "未选择" }}</b>
- <Tag :color="tagColor">{{ tagText }}</Tag>
+ <ElTag :color="tagColor">{{ tagText }}</ElTag>
</div>
</div>
</template>
<script setup>
import { ref, computed, watch, h } from "vue";
-import { Button as AntButton, Dropdown, Drawer } from "ant-design-vue";
-import { EditOutlined, DeleteOutlined, EllipsisOutlined, EyeOutlined } from "@ant-design/icons-vue";
+import { ElButton as ElButton, ElDropdown, ElDrawer } from "element-plus";
+import { Edit, Delete, MoreFilled, View } from "@element-plus/icons-vue";
import UiButton from "../ui/UiButton.vue";
import UiTable from "../ui/UiTable.vue";
import UiActions from "../ui/UiActions.vue";
await fetchDb(`/page-styles/${row.id}`, { method: "DELETE" });
await afterChange("页面样式已删除");
} catch (e) {
- props.notify("删除失败:" + (e && e.message ? e.message : e));
+ props.notify("删除失败:" + (e && e.ElMessage ? e.ElMessage : e));
}
}
function rowMenu(row) {
return {
items: [
- { key: "edit", label: "编辑样式", icon: h(EditOutlined) },
- { key: "delete", label: "删除", danger: true, icon: h(DeleteOutlined) },
+ { key: "edit", label: "编辑样式", icon: h(Edit) },
+ { key: "delete", label: "删除", danger: true, icon: h(Delete) },
],
onClick: ({ key }) =>
key === "edit"
</td>
<td>{{ row.created_at || "—" }}</td>
<td>
- <AntButton type="link" :icon="h(EyeOutlined)" @click.stop="preview = row">查看预览</AntButton>
+ <ElButton type="link" :icon="h(View)" @click.stop="preview = row">查看预览</ElButton>
</td>
<td @click.stop>
- <Dropdown :trigger="['click']" :menu="rowMenu(row)">
- <AntButton type="text" aria-label="更多操作" :icon="h(EllipsisOutlined)" />
- </Dropdown>
+ <ElDropdown :trigger="['click']" :menu="rowMenu(row)">
+ <ElButton type="text" aria-label="更多操作" :icon="h(MoreFilled)" />
+ </ElDropdown>
</td>
</tr>
</tbody>
@page-size-change="(s) => { pageSize = s; pageIndex = 1; }"
/>
</Panel>
- <Drawer :title="preview?.name || '页面样式预览'" :width="560" :open="!!preview" @close="preview = null">
+ <ElDrawer :title="preview?.name || '页面样式预览'" :width="560" :open="!!preview" @close="preview = null">
<template #extra>
<PrimaryButton v-if="preview" @click="() => { editor = { open: true, id: preview.id }; preview = null; }">编辑样式</PrimaryButton>
</template>
<dd>{{ preview.created_at || "—" }}</dd>
</dl>
</div>
- </Drawer>
+ </ElDrawer>
<StyleEditorModal
v-if="editor.open"
:id="editor.id"
import PrimaryButton from "../components/PrimaryButton.vue";
import { MODELS } from "../lib/meta.js";
-// React `Button` maps to PrimaryButton (AntButton primary/default + "button" class).
-const Button = PrimaryButton;
+// React `ElButton` maps to PrimaryButton (ElButton primary/default + "button" class).
+const ElButton = PrimaryButton;
const props = defineProps({
model: { type: String, default: "" },
<span>此处集中维护原侧栏底部的内部工具配置。保存后,新建的 AI 会话将使用所选默认模型。</span>
</div>
<UiActions className="settings-actions">
- <Button @click="() => { onModelChange(draftModel); notify('内部工具配置已保存'); }">保存配置</Button>
+ <ElButton @click="() => { onModelChange(draftModel); notify('内部工具配置已保存'); }">保存配置</ElButton>
</UiActions>
</div>
</Panel>
<script setup>
import { ref, computed, watch, onMounted } from "vue";
-import { Tag } from "ant-design-vue";
-import {
- SearchOutlined,
- ApartmentOutlined,
- BlockOutlined,
- SnippetsOutlined,
- BarsOutlined,
-} from "@ant-design/icons-vue";
+import { ElTag } from "element-plus";
+import { Search, OfficeBuilding, Close, DocumentCopy, Menu } from "@element-plus/icons-vue";
import UiButton from "../../ui/UiButton.vue";
import UiInput from "../../ui/UiInput.vue";
import UiSelect from "../../ui/UiSelect.vue";
try {
list.value = (await fetchDb("/domain-templates")) || [];
} catch (e) {
- notify("加载失败:" + (e && e.message ? e.message : e));
+ notify("加载失败:" + (e && e.ElMessage ? e.ElMessage : e));
} finally {
loading.value = false;
}
</section>
<div class="domain-showcase-filter">
<label>
- <SearchOutlined />
+ <Search />
<UiInput :value="query" @change="(e) => (query = e.target.value)" placeholder="搜索领域或方案名称" />
</label>
<UiSelect :value="domainSel" @change="(e) => (domainSel = e.target.value)">
<header>
<div class="domain-showcase-mark">{{ domainScene(row.domain_name || "").code }}</div>
<div>
- <Tag class="tag-new" :color="domainTagColor(friendlyDomainName(row.domain_name))">{{ friendlyDomainName(row.domain_name) }}</Tag>
- <Tag class="status" :class="statusTone(statusText(row))">{{ statusText(row) }}</Tag>
+ <ElTag class="tag-new" :color="domainTagColor(friendlyDomainName(row.domain_name))">{{ friendlyDomainName(row.domain_name) }}</ElTag>
+ <ElTag class="status" :class="statusTone(statusText(row))">{{ statusText(row) }}</ElTag>
</div>
</header>
<h2>{{ row.name || "未命名领域方案" }}</h2>
:alt="row.name || domainScene(row.domain_name || '').code"
/>
<div class="domain-showcase-capabilities">
- <span><ApartmentOutlined /><b>资产</b><small>{{ row.device_count || 0 }} 类设备</small></span>
- <span><BlockOutlined /><b>页面</b><small>{{ row.page_count || 0 }} 仪表板 + SC</small></span>
- <span><SnippetsOutlined /><b>知识</b><small>{{ row.doc_count || 0 }} 文档 + 百科</small></span>
- <span><BarsOutlined /><b>菜单</b><small>标准结构</small></span>
+ <span><OfficeBuilding /><b>资产</b><small>{{ row.device_count || 0 }} 类设备</small></span>
+ <span><Close /><b>页面</b><small>{{ row.page_count || 0 }} 仪表板 + SC</small></span>
+ <span><DocumentCopy /><b>知识</b><small>{{ row.doc_count || 0 }} 文档 + 百科</small></span>
+ <span><Menu /><b>菜单</b><small>标准结构</small></span>
</div>
<footer>
<span>版本 {{ row.version || "1.0" }}</span>
import Panel from "../../components/Panel.vue";
import UiButton from "../../ui/UiButton.vue";
import UiInput from "../../ui/UiInput.vue";
-import Button from "../../components/PrimaryButton.vue";
+import ElButton from "../../components/PrimaryButton.vue";
import DeviceImageField from "./DeviceImageField.vue";
import { fetchDb } from "../../lib/api.js";
props.notify("已创建设备模版");
props.onDone(r.id);
} catch (e) {
- props.notify("创建失败:" + (e && e.message ? e.message : e));
+ props.notify("创建失败:" + (e && e.ElMessage ? e.ElMessage : e));
}
}
</script>
<label>设备名称<UiInput :value="name" @change="(e) => (name = e.target.value)" placeholder="如:PLC 控制器" /></label>
<label style="grid-column: 1 / -1">设备图片<DeviceImageField :image-url="imgPreview" :on-pick="onPick" /></label>
<label style="grid-column: 1 / -1">说明<UiInput textarea :rows="2" :value="description" @change="(e) => (description = e.target.value)" /></label>
- <div style="grid-column: 1 / -1"><Button @click="add">创建</Button></div>
+ <div style="grid-column: 1 / -1"><ElButton @click="add">创建</ElButton></div>
</div>
</Panel>
</template>
<script setup>
import { ref, computed, watch, onMounted } from "vue";
-import { Tag } from "ant-design-vue";
-import {
- ApartmentOutlined,
- BlockOutlined,
- AimOutlined,
- SnippetsOutlined,
- EllipsisOutlined,
- UploadOutlined,
- PlusOutlined,
- EditOutlined,
- DownloadOutlined,
- LinkOutlined,
- EyeOutlined,
-} from "@ant-design/icons-vue";
+import { ElTag } from "element-plus";
+import { OfficeBuilding, Close, Aim, DocumentCopy, MoreFilled, Upload, Plus, Edit, Download, Link, View } from "@element-plus/icons-vue";
import UiButton from "../../ui/UiButton.vue";
import UiSelect from "../../ui/UiSelect.vue";
import UiTable from "../../ui/UiTable.vue";
import Status from "../../components/StatusTag.vue";
import Panel from "../../components/Panel.vue";
import Field from "../../components/Field.vue";
-import Button from "../../components/PrimaryButton.vue";
+import ElButton from "../../components/PrimaryButton.vue";
import DomainPageThumb from "../../components/DomainPageThumb.vue";
import SearchInput from "../../components/SearchInput.vue";
import { fetchDb, formatInstances, domainTagColor } from "../../lib/api.js";
try {
await props.onCreateSite(props.id);
} catch (e) {
- props.notify("根据模板创建站点失败:" + (e && e.message ? e.message : e));
+ props.notify("根据模板创建站点失败:" + (e && e.ElMessage ? e.ElMessage : e));
} finally {
creatingSiteRef.value = false;
creatingSite.value = false;
dashboardDomain.value = d.domain_name || props.doms.find((domain) => domain.id === d.domain_id)?.name || "";
muTree.value = mt || [];
} catch (e) {
- props.notify("加载方案失败:" + (e && e.message ? e.message : e));
+ props.notify("加载方案失败:" + (e && e.ElMessage ? e.ElMessage : e));
} finally {
loading.value = false;
}
editing.value = false;
await load();
} catch (e) {
- props.notify("保存失败:" + (e && e.message ? e.message : e));
+ props.notify("保存失败:" + (e && e.ElMessage ? e.ElMessage : e));
}
}
);
const download = (label) => props.notify(`${label}配置 ZIP 已准备(设计示意)`);
const capabilityCards = [
- { key: "assets", icon: ApartmentOutlined, title: "资产", subtitle: "设备列表与资产组结构", count: `${assets.value.length} 个类型` },
- { key: "pages", icon: BlockOutlined, title: "领域扩展仪表板", subtitle: "领域仪表板页面", count: `${extensionPages.value.length} 个仪表板` },
- { key: "twins", icon: AimOutlined, title: "数字孪生场景", subtitle: "SC 数字孪生场景画面", count: `${twinScenes.value.length} 个场景` },
- { key: "knowledge", icon: SnippetsOutlined, title: "知识", subtitle: "领域文档与参考链接", count: `${knowledgeDocuments.value.length} 份文档 / ${knowledgeLinks.value.length} 个链接` },
+ { key: "assets", icon: OfficeBuilding, title: "资产", subtitle: "设备列表与资产组结构", count: `${assets.value.length} 个类型` },
+ { key: "pages", icon: Close, title: "领域扩展仪表板", subtitle: "领域仪表板页面", count: `${extensionPages.value.length} 个仪表板` },
+ { key: "twins", icon: Aim, title: "数字孪生场景", subtitle: "SC 数字孪生场景画面", count: `${twinScenes.value.length} 个场景` },
+ { key: "knowledge", icon: DocumentCopy, title: "知识", subtitle: "领域文档与参考链接", count: `${knowledgeDocuments.value.length} 份文档 / ${knowledgeLinks.value.length} 个链接` },
];
const previewTitle = computed(() => capabilityCards.find((x) => x.key === preview.value)?.title || "");
const visibleDashboards = computed(() =>
</UiSelect>
</label>
<UiActions class="dashboard-manager-actions">
- <Button secondary @click="() => notify('导入功能将在后续实现')"><UploadOutlined /> 导入</Button>
- <Button @click="() => notify('新增功能将在后续实现')"><PlusOutlined /> 新增</Button>
+ <ElButton secondary @click="() => notify('导入功能将在后续实现')"><Upload /> 导入</ElButton>
+ <ElButton @click="() => notify('新增功能将在后续实现')"><Plus /> 新增</ElButton>
<SearchInput :value="dashboardQuery" @change="(v) => (dashboardQuery = v)" placeholder="请输入仪表板名称" />
</UiActions>
</header>
<div class="dashboard-manager-grid">
<article v-for="(page, index) in visibleDashboards" :key="page.id">
- <UiButton class="dashboard-card-more" type="button" aria-label="`${page.name}更多操作`"><EllipsisOutlined /></UiButton>
+ <UiButton class="dashboard-card-more" type="button" aria-label="`${page.name}更多操作`"><MoreFilled /></UiButton>
<h3>{{ page.name }}</h3>
<p>更新时间:2026-{{ String((index % 8) + 1).padStart(2, "0") }}-{{ String((index % 21) + 5).padStart(2, "0") }} {{ String(9 + (index % 8)).padStart(2, "0") }}:30:00</p>
<Status v-if="page.status !== 'generated'">未发布</Status>
<section :class="['domain-capability-head', scene.tone]">
<div class="domain-capability-code">{{ scene.code }}</div>
<div class="domain-capability-copy">
- <div><Tag class="tag-new" :color="domainTagColor(domainName)">{{ domainName }}</Tag><Status>{{ detail.status === "available" ? "可用" : "不可用" }}</Status><span>版本 {{ detail.version || "1.0" }}</span></div>
+ <div><ElTag class="tag-new" :color="domainTagColor(domainName)">{{ domainName }}</ElTag><Status>{{ detail.status === "available" ? "可用" : "不可用" }}</Status><span>版本 {{ detail.version || "1.0" }}</span></div>
<h2>{{ detail.name || "未命名领域方案" }}</h2>
<p>{{ detail.description || scene.desc }}</p>
</div>
<UiActions class="domain-capability-actions">
- <Button secondary @click="() => (editing = true)"><EditOutlined /> 维护方案</Button>
- <Button secondary @click="() => (packageOpen = true)"><DownloadOutlined /> 下载完整方案</Button>
- <Button :disabled="creatingSite" @click="createFromTemplate" title="使用当前领域方案的基本信息、资产与标准页面配置创建草稿站点">
- <PlusOutlined /> {{ creatingSite ? "正在创建站点…" : "根据模板创建领域站点" }}
- </Button>
+ <ElButton secondary @click="() => (editing = true)"><Edit /> 维护方案</ElButton>
+ <ElButton secondary @click="() => (packageOpen = true)"><Download /> 下载完整方案</ElButton>
+ <ElButton :disabled="creatingSite" @click="createFromTemplate" title="使用当前领域方案的基本信息、资产与标准页面配置创建草稿站点">
+ <Plus /> {{ creatingSite ? "正在创建站点…" : "根据模板创建领域站点" }}
+ </ElButton>
</UiActions>
</section>
:title="asset.device_name || '设备模板'"
@click="(e) => { e.stopPropagation(); if (asset.device_template_id) props.onOpenDeviceTemplate(asset.device_template_id); }"
>
- <span><BlockOutlined /></span><b>{{ asset.device_name || "设备模板" }}</b>
+ <span><Close /></span><b>{{ asset.device_name || "设备模板" }}</b>
</div>
</div>
<div v-if="assets.length === 0" class="device-template-empty">当前领域暂未关联设备模板</div>
<div class="capability-section-title"><b>文档</b><span>{{ knowledgeDocuments.length }} 份</span></div>
<div class="knowledge-item-list">
<div v-for="item in knowledgeDocuments" :key="item.name" class="knowledge-item">
- <span class="knowledge-type document"><SnippetsOutlined /></span>
+ <span class="knowledge-type document"><DocumentCopy /></span>
<div><b>{{ item.name }}</b><small>{{ item.meta }}</small></div>
</div>
</div>
<div class="capability-section-title"><b>链接</b><span>{{ knowledgeLinks.length }} 个</span></div>
<div class="knowledge-item-list">
<div v-for="item in knowledgeLinks" :key="item.name" class="knowledge-item">
- <span class="knowledge-type link"><LinkOutlined /></span>
+ <span class="knowledge-type link"><Link /></span>
<div><b>{{ item.name }}</b><small>{{ item.meta }}</small></div>
</div>
</div>
<div v-else class="domain-capability-body capability-empty-slot" :aria-label="`${card.title}内容占位`" />
<footer>
- <UiButton type="button" class="capability-edit-button" @click="editHandler(card)"><EditOutlined /> 编辑</UiButton>
- <UiButton @click="() => (preview = card.key)"><EyeOutlined /> 预览</UiButton>
- <UiButton @click="() => download(card.title)"><DownloadOutlined /> 下载配置 ZIP</UiButton>
+ <UiButton type="button" class="capability-edit-button" @click="editHandler(card)"><Edit /> 编辑</UiButton>
+ <UiButton @click="() => (preview = card.key)"><View /> 预览</UiButton>
+ <UiButton @click="() => download(card.title)"><Download /> 下载配置 ZIP</UiButton>
</footer>
</section>
</div>
<div v-else-if="preview === 'knowledge'" class="capability-knowledge-preview">
<section>
<h3>领域百科</h3>
- <div v-for="(name, i) in scene.wiki" :key="name"><SnippetsOutlined /><span><b>{{ name }}</b><small>{{ i === 0 ? "领域概念、工艺流程与管理指标" : "常用设备、参数与运行原理" }}</small></span></div>
+ <div v-for="(name, i) in scene.wiki" :key="name"><DocumentCopy /><span><b>{{ name }}</b><small>{{ i === 0 ? "领域概念、工艺流程与管理指标" : "常用设备、参数与运行原理" }}</small></span></div>
</section>
<section>
<h3>文档资料</h3>
</div>
</div>
<footer>
- <Button secondary @click="() => (preview = null)">关闭</Button>
- <Button @click="() => { download(previewTitle); preview = null; }"><DownloadOutlined /> 下载配置 ZIP</Button>
+ <ElButton secondary @click="() => (preview = null)">关闭</ElButton>
+ <ElButton @click="() => { download(previewTitle); preview = null; }"><Download /> 下载配置 ZIP</ElButton>
</footer>
</div>
</UiOverlay>
<div class="domain-maintain-note">资产、页面、知识与菜单均可分别进入预览后维护;本页不再采用分步确认流程。</div>
</div>
<footer>
- <Button secondary @click="() => (editing = false)">取消</Button>
- <Button @click="saveMeta">保存</Button>
+ <ElButton secondary @click="() => (editing = false)">取消</ElButton>
+ <ElButton @click="saveMeta">保存</ElButton>
</footer>
</div>
</UiOverlay>
</div>
</div>
<footer>
- <Button secondary @click="() => (packageOpen = false)">取消</Button>
- <Button @click="() => { download('完整领域方案'); packageOpen = false; }">确认下载 ZIP</Button>
+ <ElButton secondary @click="() => (packageOpen = false)">取消</ElButton>
+ <ElButton @click="() => { download('完整领域方案'); packageOpen = false; }">确认下载 ZIP</ElButton>
</footer>
</div>
</UiOverlay>
import UiTable from "../../ui/UiTable.vue";
import UiActions from "../../ui/UiActions.vue";
import UiTabbedRegion from "../../ui/UiTabbedRegion.vue";
-import Button from "../../components/PrimaryButton.vue";
+import ElButton from "../../components/PrimaryButton.vue";
import { buildMuLeaves } from "../../lib/api.js";
const props = defineProps({
</UiTable>
</div>
<div style="display: flex; gap: 8px; margin-top: 8px; align-items: center; flex-wrap: wrap">
- <Button @click="onAddPage">+ 添加 Cube 页面</Button>
+ <ElButton @click="onAddPage">+ 添加 Cube 页面</ElButton>
<label class="button secondary" style="cursor: pointer">
↑ 上传页面 Json<input type="file" accept=".json,application/json" hidden @change="(e) => { const f = e.target.files && e.target.files[0]; if (f) onUploadPageJson(f); e.target.value = ''; }" />
</label>
import Field from "../../components/Field.vue";
import UiInput from "../../ui/UiInput.vue";
import UiTable from "../../ui/UiTable.vue";
-import Button from "../../components/PrimaryButton.vue";
+import ElButton from "../../components/PrimaryButton.vue";
import Status from "../../components/StatusTag.vue";
import { fetchDb } from "../../lib/api.js";
import { useConfirm } from "./useConfirm.js";
props.notify("已上传知识文档");
props.reload();
} catch (e) {
- props.notify("上传失败:" + (e && e.message ? e.message : e));
+ props.notify("上传失败:" + (e && e.ElMessage ? e.ElMessage : e));
} finally {
busy.value = false;
}
urlVal.value = "";
props.reload();
} catch (e) {
- props.notify("添加失败:" + (e && e.message ? e.message : e));
+ props.notify("添加失败:" + (e && e.ElMessage ? e.ElMessage : e));
} finally {
busy.value = false;
}
props.notify("已删除");
props.reload();
} catch (e) {
- props.notify("删除失败:" + (e && e.message ? e.message : e));
+ props.notify("删除失败:" + (e && e.ElMessage ? e.ElMessage : e));
}
}
</script>
<div style="display: flex; flex-direction: column; gap: 8px">
<UiInput placeholder="站点名称,如:XX 行业标准" :value="urlName" @change="(e) => (urlName = e.target.value)" />
<UiInput placeholder="https://..." :value="urlVal" @change="(e) => (urlVal = e.target.value)" />
- <div><Button :disabled="busy" @click="addUrl">添加站点</Button></div>
+ <div><ElButton :disabled="busy" @click="addUrl">添加站点</ElButton></div>
</div>
</Panel>
<Panel :title="`已沉淀领域知识(${docs.length})`">
- <template #tools><Button secondary @click="reload">刷新</Button></template>
+ <template #tools><ElButton secondary @click="reload">刷新</ElButton></template>
<div v-if="docs.length === 0" class="empty-state">暂无领域知识,上传文档或配置站点后此处展示。</div>
<UiTable v-else class="data-table">
<thead><tr><th>名称</th><th>类型</th><th>大小</th><th>创建时间</th><th>操作</th></tr></thead>
<script setup>
import { ref, computed, watch } from "vue";
-import { Button as AntButton } from "ant-design-vue";
+import { ElButton as ElButton } from "element-plus";
import UiButton from "../../ui/UiButton.vue";
import UiInput from "../../ui/UiInput.vue";
import UiOverlay from "../../ui/UiOverlay.vue";
notify(`已根据修改意见重新生成「${props.page.name}」`);
onClose(true);
} catch (e) {
- notify("重新生成失败:" + (e && e.message ? e.message : e));
+ notify("重新生成失败:" + (e && e.ElMessage ? e.ElMessage : e));
} finally {
clearTimeout(watchdog);
refining.value = false;
placeholder="例如:把能耗图表改为按周维度对比,并增加告警列表"
/>
<pre v-if="refining" ref="consoleRef" class="modal-console-body">{{ refineLog || "(重新生成中…)" }}</pre>
- <AntButton :disabled="refining" @click="regenerate">{{ refining ? "重新生成中…" : "根据意见重新生成" }}</AntButton>
+ <ElButton :disabled="refining" @click="regenerate">{{ refining ? "重新生成中…" : "根据意见重新生成" }}</ElButton>
</div>
</div>
</div>
import { ref } from "vue";
import Panel from "../../components/Panel.vue";
import UiButton from "../../ui/UiButton.vue";
-import Button from "../../components/PrimaryButton.vue";
+import ElButton from "../../components/PrimaryButton.vue";
import StylePreview from "./StylePreview.vue";
import StyleEditorModal from "./StyleEditorModal.vue";
import { fetchDb } from "../../lib/api.js";
await fetchDb(`/page-styles/${s.id}`, { method: "DELETE" });
await afterChange(`已删除「${s.name}」`);
} catch (e) {
- props.notify("删除失败: " + (e && e.message ? e.message : e));
+ props.notify("删除失败: " + (e && e.ElMessage ? e.ElMessage : e));
}
}
</script>
<component :is="confirmEl" />
<StylePreview v-if="preview" :style="preview" :on-close="() => (preview = null)" />
<Panel title="全部页面样式">
- <template #tools><Button @click="() => (editor = { open: true })">+ 新增样式</Button></template>
+ <template #tools><ElButton @click="() => (editor = { open: true })">+ 新增样式</ElButton></template>
<p v-if="pageStyles.length === 0" class="hint">暂无页面样式,点击右上角「+ 新增样式」上传 JSON 与预览图即可创建。</p>
<div v-else class="style-grid">
<article v-for="s in pageStyles" :key="s.id" class="style-card">
import UiOverlay from "../../ui/UiOverlay.vue";
import Panel from "../../components/Panel.vue";
import Field from "../../components/Field.vue";
-import Button from "../../components/PrimaryButton.vue";
+import ElButton from "../../components/PrimaryButton.vue";
import Status from "../../components/StatusTag.vue";
import DomainSteps from "./DomainSteps.vue";
import DomainKnowledgeStep from "./DomainKnowledgeStep.vue";
}));
mu.value = d.mu && d.mu.length ? d.mu : [];
} catch (e) {
- props.notify("加载失败:" + (e && e.message ? e.message : e));
+ props.notify("加载失败:" + (e && e.ElMessage ? e.ElMessage : e));
} finally {
if (!silent) loading.value = false;
}
});
props.notify(msg);
} catch (e) {
- props.notify("保存失败:" + (e && e.message ? e.message : e));
+ props.notify("保存失败:" + (e && e.ElMessage ? e.ElMessage : e));
} finally {
saving.value = false;
}
patchM({ style_id: sid });
props.notify(sid ? "已应用页面样式" : "已清除页面样式");
} catch (e) {
- props.notify("应用样式失败:" + (e && e.message ? e.message : e));
+ props.notify("应用样式失败:" + (e && e.ElMessage ? e.ElMessage : e));
}
}
});
await patch({ supplement: newSup }, "AI 已写入补充描述");
} catch (e) {
- props.notify("AI 智能补全失败:" + (e && e.message ? e.message : e));
+ props.notify("AI 智能补全失败:" + (e && e.ElMessage ? e.ElMessage : e));
} finally {
clearTimeout(wd);
setBasicCompleting(false);
await load(true);
props.notify("AI 资产补全已完成" + (created.length ? `,并自动创建设备模板:${created.join("、")}` : ""));
} catch (e) {
- props.notify("AI 资产补全失败:" + (e && e.message ? e.message : e));
+ props.notify("AI 资产补全失败:" + (e && e.ElMessage ? e.ElMessage : e));
} finally {
clearTimeout(wd);
setAssetsCompleting(false);
if (!res.ok || !res.body) throw new Error("HTTP " + res.status);
await streamInto(res, cubesAi.setLog, (ev) => {
if (Array.isArray(ev.result)) items = ev.result;
- if (ev && ev.status === "failed") failed = ev.message || "AI 分析失败";
+ if (ev && ev.status === "failed") failed = ev.ElMessage || "AI 分析失败";
});
if (failed) {
props.notify("AI Cube 补全失败:" + failed);
});
added++;
} catch (e) {
- props.notify(`新增页面「${it.name || ""}」失败:${e.message}`);
+ props.notify(`新增页面「${it.name || ""}」失败:${e.ElMessage}`);
}
}
await load(true);
props.notify(items.length ? `AI Cube 页面补全已完成,已新增 ${added} 个页面` : "AI 未返回可用页面");
} catch (e) {
- props.notify("AI Cube 补全失败:" + (e && e.message ? e.message : e));
+ props.notify("AI Cube 补全失败:" + (e && e.ElMessage ? e.ElMessage : e));
} finally {
clearTimeout(wd);
cubesAi.setCompleting(false);
}
await load(true);
} catch (e) {
- props.notify("上传失败:" + (e && e.message ? e.message : e));
+ props.notify("上传失败:" + (e && e.ElMessage ? e.ElMessage : e));
}
}
async function savePageMeta(p) {
const res = await fetch(`/api/db/domain-templates/${props.id}/pages/${p.id}/generate`, { method: "POST" });
if (!res.ok || !res.body) throw new Error("HTTP " + res.status);
await streamInto(res, setGenLog, (ev) => {
- if (ev && ev.status === "failed") failed = ev.message || "生成失败";
+ if (ev && ev.status === "failed") failed = ev.ElMessage || "生成失败";
});
if (failed) props.notify(`「${p.name}」生成失败:${failed}`);
else props.notify(`「${p.name}」生成完成`);
} catch (e) {
- props.notify("生成失败:" + (e && e.message ? e.message : e));
+ props.notify("生成失败:" + (e && e.ElMessage ? e.ElMessage : e));
} finally {
const n2 = new Set(genIds.value);
n2.delete(p.id);
<Field label="补充描述" hint="可由右侧 AI 智能补全自动生成">
<UiInput textarea class="small" :value="m.supplement || ''" @change="(e) => patchM({ supplement: e.target.value })" />
</Field>
- <div style="grid-column: 1 / -1"><Button @click="saveMeta">保存方案信息</Button></div>
+ <div style="grid-column: 1 / -1"><ElButton @click="saveMeta">保存方案信息</ElButton></div>
</div>
</Panel>
<div class="stack">
<div v-if="saving"><span>保存</span><Status>保存中…</Status></div>
</div>
<Panel title="设备资产配置">
- <template #tools><Button @click="addAssetRow">+ 添加设备类型</Button></template>
+ <template #tools><ElButton @click="addAssetRow">+ 添加设备类型</ElButton></template>
<UiTable class="data-table editable">
<thead><tr><th>设备类型 *</th><th>数量 *</th><th>命名规则</th><th>操作</th></tr></thead>
<tbody>
</tr>
</tbody>
</UiTable>
- <div style="margin-top: 8px"><Button secondary @click="() => go('deviceTemplates')">查看全部设备模版</Button></div>
+ <div style="margin-top: 8px"><ElButton secondary @click="() => go('deviceTemplates')">查看全部设备模版</ElButton></div>
</Panel>
<Panel title="资产层级预览">
<div class="asset-tree-preview">
</div>
<div class="sticky-actions">
- <Button secondary @click="curIdx > 0 ? goStep(DOMAIN_STEPS[curIdx - 1].key) : onBack()">{{ curIdx > 0 ? "上一步" : "返回列表" }}</Button>
+ <ElButton secondary @click="curIdx > 0 ? goStep(DOMAIN_STEPS[curIdx - 1].key) : onBack()">{{ curIdx > 0 ? "上一步" : "返回列表" }}</ElButton>
<div>
- <Button secondary @click="() => notify('草稿已保存')">保存草稿</Button>
- <Button v-if="step === 'generate'" @click="() => (pkgOpen = true)">📦 打包下载</Button>
- <Button v-if="step !== 'generate' && curIdx < DOMAIN_STEPS.length - 1" @click="() => goStep(DOMAIN_STEPS[curIdx + 1].key)">下一步</Button>
- <Button v-if="curIdx === DOMAIN_STEPS.length - 1" @click="() => notify('方案已提交专家确认')">提交确认</Button>
+ <ElButton secondary @click="() => notify('草稿已保存')">保存草稿</ElButton>
+ <ElButton v-if="step === 'generate'" @click="() => (pkgOpen = true)">📦 打包下载</ElButton>
+ <ElButton v-if="step !== 'generate' && curIdx < DOMAIN_STEPS.length - 1" @click="() => goStep(DOMAIN_STEPS[curIdx + 1].key)">下一步</ElButton>
+ <ElButton v-if="curIdx === DOMAIN_STEPS.length - 1" @click="() => notify('方案已提交专家确认')">提交确认</ElButton>
</div>
</div>
<script setup>
import { ref, watch, onMounted } from "vue";
-import { UploadOutlined } from "@ant-design/icons-vue";
+import { Upload } from "@element-plus/icons-vue";
import Panel from "../../components/Panel.vue";
import Field from "../../components/Field.vue";
import UiInput from "../../ui/UiInput.vue";
import UiSelect from "../../ui/UiSelect.vue";
import UiActions from "../../ui/UiActions.vue";
-import Button from "../../components/PrimaryButton.vue";
+import ElButton from "../../components/PrimaryButton.vue";
import { fetchDb } from "../../lib/api.js";
import { friendlyDomainName } from "./domainCommon.js";
props.notify("领域方案导入完成,可继续维护各项能力");
props.onImported(Number(row.id));
} catch (e) {
- props.notify("导入失败:" + (e && e.message ? e.message : e));
+ props.notify("导入失败:" + (e && e.ElMessage ? e.ElMessage : e));
} finally {
busy.value = false;
}
<Panel title="导入领域方案">
<div class="domain-import-layout">
<label :class="'domain-import-drop' + (file ? ' selected' : '')">
- <UploadOutlined />
+ <Upload />
<b>{{ file ? file.name : "选择领域方案 ZIP 包" }}</b>
<span>{{ file ? `${(file.size / 1024).toFixed(1)} KB · 点击可重新选择` : "将客户现场已有的资产、页面、知识和菜单配置导入 StationBuilder" }}</span>
<input type="file" accept=".zip,application/zip" hidden @change="(e) => choose(e.target.files && e.target.files[0] ? e.target.files[0] : null)" />
<b>方案包内容</b><span>资产配置</span><span>仪表板与 SC 页面</span><span>百科与文档</span><span>菜单配置</span>
</div>
<UiActions class="domain-import-actions">
- <Button :disabled="busy || !file" @click="submit">{{ busy ? "正在导入…" : "开始导入" }}</Button>
+ <ElButton :disabled="busy || !file" @click="submit">{{ busy ? "正在导入…" : "开始导入" }}</ElButton>
</UiActions>
</Panel>
</div>
<script setup>
import UiOverlay from "../../ui/UiOverlay.vue";
import UiButton from "../../ui/UiButton.vue";
-import Button from "../../components/PrimaryButton.vue";
+import ElButton from "../../components/PrimaryButton.vue";
const props = defineProps({
name: { type: String, default: "" },
</div>
</div>
<footer>
- <Button secondary @click="onClose">取消</Button>
- <Button @click="() => { notify('方案已打包(示意),可交付给实施团队'); onClose(); }">确认打包下载</Button>
+ <ElButton secondary @click="onClose">取消</ElButton>
+ <ElButton @click="() => { notify('方案已打包(示意),可交付给实施团队'); onClose(); }">确认打包下载</ElButton>
</footer>
</div>
</UiOverlay>
import UiInput from "../../ui/UiInput.vue";
import Panel from "../../components/Panel.vue";
import Field from "../../components/Field.vue";
-import Button from "../../components/PrimaryButton.vue";
+import ElButton from "../../components/PrimaryButton.vue";
import { fetchDb } from "../../lib/api.js";
import { useConfirm } from "./useConfirm.js";
name.value = row.name || "";
jsonText.value = row.json_text || "";
} catch (e) {
- props.notify("加载样式失败: " + (e && e.message ? e.message : e));
+ props.notify("加载样式失败: " + (e && e.ElMessage ? e.ElMessage : e));
}
}
});
}
await props.afterChange(props.id ? "样式已更新" : "样式已新增");
} catch (e) {
- props.notify("保存失败: " + (e && e.message ? e.message : e));
+ props.notify("保存失败: " + (e && e.ElMessage ? e.ElMessage : e));
} finally {
saving.value = false;
}
await fetchDb(`/page-styles/${props.id}`, { method: "DELETE" });
await props.afterChange("样式已删除");
} catch (e) {
- props.notify("删除失败: " + (e && e.message ? e.message : e));
+ props.notify("删除失败: " + (e && e.ElMessage ? e.ElMessage : e));
}
}
</script>
</Field>
</div>
<footer>
- <Button v-if="id" secondary @click="remove" title="删除该样式">删除</Button>
+ <ElButton v-if="id" secondary @click="remove" title="删除该样式">删除</ElButton>
<span class="spacer" />
- <Button secondary @click="onClose">取消</Button>
- <Button :disabled="saving" @click="submit">{{ saving ? "保存中…" : "保存" }}</Button>
+ <ElButton secondary @click="onClose">取消</ElButton>
+ <ElButton :disabled="saving" @click="submit">{{ saving ? "保存中…" : "保存" }}</ElButton>
</footer>
</div>
</UiOverlay>
import UiActions from "../../ui/UiActions.vue";
export function useConfirm() {
- const state = ref({ open: false, message: "", resolve: null });
+ const state = ref({ open: false, ElMessage: "", resolve: null });
- const confirm = (message) =>
+ const confirm = (ElMessage) =>
new Promise((resolve) => {
- state.value = { open: true, message, resolve };
+ state.value = { open: true, ElMessage, resolve };
});
const close = (v) => {
"div",
{ class: "dlg-box", onClick: (e) => e.stopPropagation() },
[
- h("p", state.value.message),
+ h("p", state.value.ElMessage),
h(
UiActions,
{ class: "dlg-actions" },
--- /dev/null
+/* StationBuilder brand tokens — Vue3 / Element Plus edition.
+ *
+ * 历史:原 ant-design-vue 通过 ConfigProvider 在运行时向 :root 注入 --ant-* 变量,
+ * 全站自定义 CSS(globals.css / site-workspace.css / ui-system.css 等,约 2261 处)
+ * 均消费这些变量。切换到 Element Plus 后不再有 antd 运行时注入,故此处静态定义
+ * --ant-*(值取自 theme.json 品牌令牌),使现有自定义 CSS 零改动继续生效。
+ * 同时定义 Element Plus 的品牌覆盖(--el-*),让 Element Plus 组件继承品牌主色/圆角。
+ *
+ * 若日后要做第二轮清理,可把自定义 CSS 中的 --ant-* 统一改为 --el-* 后删除本文件上半段。
+ */
+
+:root {
+ /* ---- brand color tokens (暴露为 --ant-* 以兼容现有自定义 CSS) ---- */
+ --ant-color-primary: #008cd6;
+ --ant-color-primary-hover: #4dafe2;
+ --ant-color-primary-active: #0070ab;
+ --ant-color-primary-bg: #e6f2fb;
+ --ant-color-info: #008cd6;
+ --ant-color-success: #00cc66;
+ --ant-color-success-bg: #f6ffed;
+ --ant-color-success-border: #b7eb8f;
+ --ant-color-success-text: #389e0d;
+ --ant-color-error: #f55d3e;
+ --ant-color-error-bg: #fff2f0;
+ --ant-color-error-border: #ffccc7;
+ --ant-color-error-text: #cf1322;
+ --ant-color-warning: #ffa400;
+ --ant-color-warning-bg: #fffbe6;
+ --ant-color-warning-border: #ffe58f;
+ --ant-color-warning-text: #d48806;
+ --ant-color-text-base: #323233;
+ --ant-color-text: #323233;
+ --ant-color-text-secondary: #8c8c8c;
+ --ant-color-text-tertiary: #bfbfbf;
+ --ant-color-text-disabled: #d9d9d9;
+ --ant-color-text-light-solid: #ffffff;
+ --ant-color-bg-layout: #fafafa;
+ --ant-color-bg-container: #ffffff;
+ --ant-color-bg-elevated: #ffffff;
+ --ant-color-bg-mask: rgba(0, 0, 0, 0.45);
+ --ant-color-bg-container-disabled: rgba(0, 0, 0, 0.04);
+ --ant-color-border: #e1e1e1;
+ --ant-color-border-secondary: #f0f0f0;
+ --ant-color-split: #f0f0f0;
+ --ant-color-fill: rgba(0, 0, 0, 0.06);
+ --ant-color-fill-secondary: rgba(0, 0, 0, 0.09);
+ --ant-color-fill-tertiary: rgba(0, 0, 0, 0.10);
+ --ant-color-fill-quaternary: rgba(0, 0, 0, 0.04);
+ --ant-font-size: 16px;
+ --ant-font-size-sm: 14px;
+ --ant-font-size-lg: 16px;
+ --ant-font-size-heading-2: 30px;
+ --ant-font-size-heading-3: 24px;
+ --ant-font-size-heading-4: 20px;
+ --ant-border-radius: 4px;
+ --ant-border-radius-sm: 2px;
+ --ant-border-radius-lg: 4px;
+ --ant-padding: 16px;
+ --ant-padding-xs: 4px;
+ --ant-padding-sm: 8px;
+ --ant-padding-lg: 24px;
+ --ant-padding-xxs: 4px;
+ --ant-control-height: 32px;
+ --ant-line-height: 1.5;
+ --ant-line-height-sm: 1.4;
+ --ant-line-height-heading-4: 1.4;
+ --ant-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "PingFang SC", "Microsoft YaHei", sans-serif;
+ --ant-font-weight-strong: 600;
+ --ant-table-border-color: #f0f0f0;
+ --ant-table-header-color: #323233;
+ --ant-table-row-hover-bg: #fafafa;
+ --ant-table-cell-padding-block: 12px;
+ --ant-switch-track-min-width: 44px;
+ --ant-switch-track-height: 22px;
+ --ant-button-default-color: #323233;
+ --ant-button-default-border-color: #d9d9d9;
+ --ant-button-default-hover-color: #008cd6;
+ --ant-button-default-hover-border-color: #008cd6;
+
+ /* ---- Element Plus brand overrides ---- */
+ --el-color-primary: #008cd6;
+ --el-color-primary-light-3: #4dafe2;
+ --el-color-primary-light-5: #80c6eb;
+ --el-color-primary-light-7: #b3dcf3;
+ --el-color-primary-light-8: #cce7f7;
+ --el-color-primary-light-9: #e6f2fb;
+ --el-color-primary-dark-2: #0070ab;
+ --el-color-success: #00cc66;
+ --el-color-warning: #ffa400;
+ --el-color-error: #f55d3e;
+ --el-color-danger: #f55d3e;
+ --el-border-radius-base: 4px;
+ --el-border-radius-small: 2px;
+ --el-text-color-primary: #323233;
+ --el-text-color-regular: #595959;
+ --el-text-color-secondary: #8c8c8c;
+ --el-text-color-placeholder: #bfbfbf;
+ --el-text-color-disabled: #d9d9d9;
+ --el-border-color: #e1e1e1;
+ --el-border-color-light: #e1e1e1;
+ --el-border-color-lighter: #f0f0f0;
+ --el-border-color-extra-light: #f5f5f5;
+ --el-bg-color: #ffffff;
+ --el-bg-color-page: #fafafa;
+ --el-bg-color-overlay: #ffffff;
+ --el-fill-color: rgba(0, 0, 0, 0.06);
+ --el-fill-color-light: rgba(0, 0, 0, 0.09);
+ --el-fill-color-lighter: rgba(0, 0, 0, 0.10);
+ --el-fill-color-blank: #ffffff;
+}
-@import "tailwindcss";
*{box-sizing:border-box}html,body{margin:0;min-height:100%;font-family:var(--ant-font-family);color:var(--ant-color-text-base);font-size:var(--ant-font-size-sm);background:var(--ant-color-bg-layout)}button,input,select,textarea{font:inherit;color:inherit}button{cursor:pointer}.app-shell{min-height:100vh;padding-top:56px;padding-left:192px}.topbar{position:fixed;z-index:50;left:0;right:0;top:0;height:56px;border-top:3px solid transparent;background-image:linear-gradient(var(--ant-color-bg-container),var(--ant-color-bg-container)),linear-gradient(90deg,var(--ant-color-primary),var(--ant-color-primary));background-origin:border-box;background-clip:padding-box,border-box;border-bottom:1px solid var(--ant-color-border);display:flex;align-items:center;padding:0 12px;gap:8px;box-shadow:0 1px 3px color-mix(in srgb,var(--ant-color-text-base) 7%,transparent)}.hamburger{border:0;background:transparent;font-size:var(--ant-font-size);color:var(--ant-color-text-secondary)}.brand-mark,.login-logo{color:var(--ant-color-primary);font-size:var(--ant-font-size-heading-3);line-height:1;font-weight:600}.topbar>strong{font-size:var(--ant-font-size)}.divider{height:19px;border-left:1px solid var(--ant-color-border);margin:0 4px}.top-actions{margin-left:auto;display:flex;align-items:center;gap:16px;color:var(--ant-color-text-secondary)}.top-actions>span{font-size:var(--ant-font-size);position:relative}.top-actions i{position:absolute;width:5px;height:5px;background:var(--ant-color-error);border-radius:50%;right:-2px;top:-2px}.top-actions small{font-size:var(--ant-font-size-sm);line-height:1.2}.avatar{width:23px;height:23px;border-radius:50%;background:var(--ant-color-primary-bg);color:var(--ant-color-primary);display:grid!important;place-items:center;font-size:var(--ant-font-size-sm)!important}.site-switcher{margin:4px 8px 4px;height:30px;border:1px solid var(--ant-color-border);border-radius:var(--ant-border-radius);background:var(--ant-color-bg-container);color:var(--ant-color-text-base);font-size:var(--ant-font-size-sm);padding:0 8px;width:calc(100% - 18px)}.site-switcher:focus{outline:1px solid var(--ant-color-primary)}.side-nav{position:fixed;z-index:35;top:56px;bottom:0;left:0;width:190px;background:var(--ant-color-bg-layout);border-right:1px solid var(--ant-color-border);display:flex;flex-direction:column;padding:0}.side-title{height:48px;padding:16px 16px 0;font-weight:600;font-size:var(--ant-font-size)}.side-title span{float:right;color:var(--ant-color-text-secondary)}.side-nav>button{height:41px;border:0;background:transparent;text-align:left;padding:0 16px;display:flex;align-items:center;gap:12px;border-left:3px solid transparent;font-size:var(--ant-font-size)}.side-nav>button:hover{background:var(--ant-color-fill-quaternary);color:var(--ant-color-primary-hover)}.side-nav>button.active{background:var(--ant-color-primary-bg);color:var(--ant-color-primary);border-left-color:var(--ant-color-primary)}.side-nav>button b{width:18px;text-align:center}.side-nav>button em{font-style:normal;width:21px;height:21px;border:1px solid var(--ant-color-border);border-radius:50%;display:grid;place-items:center;font-size:var(--ant-font-size-sm);background:var(--ant-color-bg-container)}.side-nav>button.active em{background:var(--ant-color-primary);border-color:var(--ant-color-primary);color:var(--ant-color-bg-container)}.side-nav>small{font-size:var(--ant-font-size-sm);color:var(--ant-color-text-secondary);padding:20px 16px 8px;text-transform:uppercase;letter-spacing:.8px}.side-help{position:relative;margin:auto 12px 12px;border:1px solid var(--ant-color-border);background:var(--ant-color-bg-container);padding:12px;display:flex;flex-direction:column;gap:4px;font-size:var(--ant-font-size-sm)}.side-help span{color:var(--ant-color-text-secondary)}
.model-trigger{display:flex;align-items:center;justify-content:space-between;cursor:pointer;color:var(--ant-color-text-secondary)}
.model-trigger:hover{color:var(--ant-color-primary-hover)}
<script>
import { computed, h, cloneVNode, useSlots, useAttrs, Fragment } from "vue";
-import {
- Button as AntButton,
- Dropdown as AntDropdown,
- Menu as AntMenu,
-} from "ant-design-vue";
-import { EllipsisOutlined } from "@ant-design/icons-vue";
+import { ElButton, ElDropdown, ElDropdownMenu, ElDropdownItem } from "element-plus";
+import { MoreFilled } from "@element-plus/icons-vue";
import UiButton from "./UiButton.vue";
import { elements, textOf, classOf, childContent } from "./parse.js";
const parts = computed(() => elements(slots.default ? slots.default() : []));
const isActionType = (v) =>
- v.type === UiButton || v.type === AntButton || v.type === AntDropdown;
+ v.type === UiButton || v.type === ElButton || v.type === ElDropdown;
const businessAction = (v) => !!(v.type && v.type.uiPrimaryAction);
const actionable = (v) => isActionType(v) || businessAction(v);
rendered.value.slice(0, 2).forEach((a) => children.push(a));
const menuItems = rendered.value.slice(2).map((a, i) =>
h(
- AntMenu.Item,
+ ElDropdownItem,
{
key: String(i),
disabled: !!(a.props && a.props.disabled),
- onClick: (e) => triggerAction(a, e.domEvent || e),
+ onClick: (e) => triggerAction(a, e),
},
() => textOf(childContent(a))
)
);
children.push(
h(
- AntDropdown,
- { trigger: ["click"] },
+ ElDropdown,
+ { trigger: "click" },
{
- overlay: () => h(AntMenu, {}, { default: () => menuItems }),
default: () =>
h(
- AntButton,
- { "aria-label": "更多操作", icon: h(EllipsisOutlined) },
+ ElButton,
+ { "aria-label": "更多操作", icon: h(MoreFilled) },
() => "更多"
),
+ dropdown: () => h(ElDropdownMenu, {}, { default: () => menuItems }),
}
)
);
const isSecondary = computed(() => /(^|\s)secondary(\s|$)/.test(classStr.value));
const isLink = computed(() => /(^|\s)(link|text-button|domain-capability-back)(\s|$)/.test(classStr.value));
const isDanger = computed(() => /(^|\s)(danger|trash)(\s|$)/.test(classStr.value));
-const btnType = computed(() =>
- isLink.value ? "link" : isAction.value ? (isSecondary.value ? "default" : "primary") : "text"
+
+const elType = computed(() =>
+ isDanger.value ? "danger" : isAction.value ? (isSecondary.value ? "default" : "primary") : "default"
);
+const elText = computed(() => !isLink.value && !isAction.value && !isDanger.value);
+const elLink = computed(() => isLink.value);
+const elPlain = computed(() => isSecondary.value);
const passthrough = computed(() => ({
onClick: attrs.onClick,
</script>
<template>
- <a-switch
+ <el-switch
v-if="isSwitch"
- :checked="attrs['aria-checked'] === true"
+ :model-value="attrs['aria-checked'] === true"
:disabled="attrs.disabled"
:aria-label="attrs['aria-label']"
:class="attrs.class"
@change="() => attrs.onClick && attrs.onClick()"
/>
- <a-button
+ <el-button
v-else
- :html-type="type || 'button'"
- :type="btnType"
- :danger="isDanger"
+ :native-type="type || 'button'"
+ :type="elType"
+ :plain="elPlain"
+ :text="elText"
+ :link="elLink"
class="station-ui-button"
v-bind="passthrough"
>
<slot />
- </a-button>
+ </el-button>
</template>
onBlur: attrs.onBlur,
onKeydown: attrs.onKeydown,
onFocus: attrs.onFocus,
- onInput: attrs.onInput,
}));
const passthrough = computed(() => ({
class: attrs.class,
}));
-function onInput(e) {
- emit("change", e);
+// Element Plus emits the raw value (string/boolean) rather than an event, so wrap to
+// preserve the downstream `e.target.value` / `e.target.checked` contract used by callers.
+function emitValue(val) {
+ emit("change", { target: { value: val } });
+}
+function emitChecked(val) {
+ emit("change", { target: { checked: val } });
}
</script>
<template>
- <a-checkbox v-if="kind === 'checkbox'" :checked="checked" :value="value" v-bind="shared" @change="onInput">
+ <el-checkbox
+ v-if="kind === 'checkbox'"
+ :model-value="checked"
+ :value="value"
+ v-bind="shared"
+ @change="emitChecked"
+ >
<slot />
- </a-checkbox>
- <a-radio v-else-if="kind === 'radio'" :checked="checked" :value="value" v-bind="shared" @change="onInput">
+ </el-checkbox>
+ <el-radio
+ v-else-if="kind === 'radio'"
+ :model-value="checked"
+ :value="value"
+ v-bind="shared"
+ @change="emitChecked"
+ >
<slot />
- </a-radio>
- <a-textarea
+ </el-radio>
+ <el-input
v-else-if="kind === 'textarea'"
- :value="value"
+ type="textarea"
+ :model-value="value"
:placeholder="placeholder"
:rows="rows"
:maxlength="maxLength"
:readonly="readonly"
:autofocus="autofocus"
v-bind="passthrough"
- @change="onInput"
+ @input="emitValue"
>
- <slot />
- </a-textarea>
- <a-input
+ <template v-if="$slots.prefix" #prefix>
+ <slot name="prefix" />
+ </template>
+ </el-input>
+ <el-input
v-else
:type="type"
- :value="value"
+ :model-value="value"
:placeholder="placeholder"
:maxlength="maxLength"
:disabled="disabled"
:readonly="readonly"
:autofocus="autofocus"
v-bind="passthrough"
- @change="onInput"
+ @input="emitValue"
>
<template v-if="$slots.prefix" #prefix>
<slot name="prefix" />
</template>
- </a-input>
+ </el-input>
</template>
<script>
import { h, useSlots, useAttrs, Fragment } from "vue";
-import { Modal as AntModal } from "ant-design-vue";
+import { ElDialog } from "element-plus";
import UiActions from "./UiActions.vue";
import { elements, textOf, classOf, childContent } from "./parse.js";
if (close) close(e);
};
- const modalProps = {
- open: true,
- centered: true,
+ const dialogProps = {
+ modelValue: true,
+ alignCenter: true,
title: heading ? childContent(heading) : undefined,
width,
class: name + " station-ui-modal",
- closable: !!close,
- keyboard: !!close,
- maskClosable: !!close,
- onCancel: closeFn,
+ showClose: !!close,
+ closeOnClickModal: !!close,
+ closeOnPressEscape: !!close,
+ appendToBody: true,
+ "onUpdate:modelValue": closeFn,
};
- const modalSlots = { default: () => body };
+ const dialogSlots = { default: () => body };
if (footer) {
- modalSlots.footer = () =>
+ dialogSlots.footer = () =>
h(UiActions, { class: "station-modal-actions" }, () => childContent(footer));
- } else {
- modalProps.footer = null;
}
- return h(Fragment, {}, [h(AntModal, modalProps, modalSlots), ...extras]);
+ return h(Fragment, {}, [h(ElDialog, dialogProps, dialogSlots), ...extras]);
};
},
};
const attrs = useAttrs();
const emit = defineEmits(["change"]);
-// Port of React UiSelect: parse <option> children into antd Select options.
+// Port of React UiSelect: parse <option> children into Element Plus Select options.
const options = computed(() => {
const opts = elements(slots.default ? slots.default() : []).filter(
(o) => o.type === "option"
return String(props.defaultValue);
});
+const resolved = computed(() =>
+ modelValue.value !== undefined ? modelValue.value : defaultVal.value
+);
+
function onChange(selected) {
emit("change", {
target: { value: selected, name: props.name },
</script>
<template>
- <a-select
+ <el-select
:id="id"
:name="name"
:disabled="disabled"
:aria-label="props['aria-label']"
- :value="modelValue"
- :default-value="defaultVal"
+ :model-value="resolved"
:options="options"
:class="(typeof attrs.class === 'string' ? attrs.class : '') + ' station-ui-select'"
:style="attrs.style"
<script>
import { computed, h, useSlots } from "vue";
-import { Tabs as AntTabs, TabPane } from "ant-design-vue";
+import { ElTabs, ElTabPane } from "element-plus";
import { elements, classOf, childContent } from "./parse.js";
export default {
return () =>
h(
- AntTabs,
+ ElTabs,
{
class: navClass.value + " station-ui-tabs",
- activeKey: activeKey.value,
- onChange,
+ modelValue: activeKey.value,
+ onTabChange: onChange,
},
() =>
buttons.value.map((button, index) =>
h(
- TabPane,
+ ElTabPane,
{
key: String(button.key ?? index),
- tab: childContent(button),
+ name: String(button.key ?? index),
},
- { default: () => (slots.default ? slots.default() : null) }
+ {
+ label: () => childContent(button),
+ default: () => (slots.default ? slots.default() : null),
+ }
)
)
);
-<script setup>
-import { computed, useAttrs, defineOptions } from "vue";
+<script>
+import { h, computed, useAttrs } from "vue";
+import { ElTable, ElTableColumn } from "element-plus";
import { elements, classOf, childContent } from "./parse.js";
-defineOptions({ inheritAttrs: false });
+export default {
+ name: "UiTable",
+ props: {
+ className: { type: String, default: "" },
+ },
+ setup(props, { slots, attrs }) {
+ // Port of React UiTable: translate <thead>/<tbody> markup into an Element Plus Table.
+ const sections = computed(() => elements(slots.default ? slots.default() : []));
+ const head = computed(() => sections.value.find((s) => s.type === "thead"));
+ const body = computed(() => sections.value.find((s) => s.type === "tbody"));
+ const headerRow = computed(() => elements(head.value?.children)[0]);
+ const headers = computed(() => elements(headerRow.value?.children));
+ const cols = computed(() => {
+ const cg = sections.value.find((s) => s.type === "colgroup");
+ return elements(cg?.children);
+ });
-const props = defineProps({
- className: { type: String, default: "" },
-});
+ const data = computed(() =>
+ elements(body.value?.children).map((row, index) => ({
+ key: String(row.key ?? index),
+ cells: elements(row.children),
+ props: row.props || {},
+ }))
+ );
-const slots = defineSlots();
-const attrs = useAttrs();
+ const columns = computed(() =>
+ headers.value.map((header, index) => {
+ const hprops = header.props || {};
+ const colEl = cols.value[index];
+ const width =
+ colEl?.props?.style?.width ||
+ colEl?.props?.width ||
+ hprops.style?.width;
+ return {
+ key: String(index),
+ title: childContent(header),
+ width,
+ customRender: ({ index: ri }) => {
+ const cell = data.value[ri]?.cells[index];
+ return cell ? childContent(cell) : undefined;
+ },
+ };
+ })
+ );
-// Port of React UiTable: translate <thead>/<tbody> markup into antd Table columns/data.
-const sections = computed(() => elements(slots.default ? slots.default() : []));
-const head = computed(() =>
- sections.value.find((s) => s.type === "thead")
-);
-const body = computed(() =>
- sections.value.find((s) => s.type === "tbody")
-);
-const headerRow = computed(() => {
- const rows = elements(head.value?.children);
- return rows[0];
-});
-const headers = computed(() => elements(headerRow.value?.children));
-const cols = computed(() => {
- const cg = sections.value.find((s) => s.type === "colgroup");
- return elements(cg?.children);
-});
+ function spanMethod({ row, columnIndex }) {
+ const cell = row.cells?.[columnIndex];
+ const colSpan = cell?.props?.colSpan;
+ const rowSpan = row.props?.rowSpan;
+ return [
+ rowSpan && rowSpan > 1 ? rowSpan : 1,
+ colSpan && colSpan > 1 ? colSpan : 1,
+ ];
+ }
-const data = computed(() =>
- elements(body.value?.children).map((row, index) => ({
- key: String(row.key ?? index),
- cells: elements(row.children),
- props: row.props || {},
- }))
-);
+ function onRowClick(row, _column, event) {
+ if (row.props?.onClick) row.props.onClick(event);
+ }
-const columns = computed(() =>
- headers.value.map((header, index) => {
- const hprops = header.props || {};
- const hrest = { ...hprops };
- delete hrest.children;
- const colEl = cols.value[index];
- const width =
- colEl?.props?.style?.width ||
- colEl?.props?.width ||
- hprops.style?.width;
- return {
- key: String(index),
- title: childContent(header),
- width,
- customRender: ({ index: ri }) => {
- const cell = data.value[ri]?.cells[index];
- return cell ? childContent(cell) : undefined;
- },
- onHeaderCell: () => hrest,
- onCell: (record) => {
- const cell = record.cells[index];
- if (!cell) return { colSpan: 0 };
- const crest = { ...(cell.props || {}) };
- delete crest.children;
- return crest;
- },
+ return () => {
+ const cls =
+ (typeof attrs.class === "string" ? attrs.class : "") + " station-ui-table";
+ return h(
+ ElTable,
+ {
+ class: cls,
+ style: attrs.style,
+ data: data.value,
+ rowKey: (r) => r.key,
+ spanMethod,
+ onRowClick,
+ emptyText: "暂无数据",
+ },
+ {
+ default: () =>
+ columns.value.map((col) =>
+ h(
+ ElTableColumn,
+ {
+ key: col.key,
+ label: col.title,
+ width: col.width,
+ minWidth: col.width ? undefined : 100,
+ },
+ {
+ default: (scope) => col.customRender({ index: scope.$index }),
+ }
+ )
+ ),
+ }
+ );
};
- })
-);
-
-function customRow(record) {
- const rrest = { ...(record.props || {}) };
- delete rrest.children;
- return rrest;
-}
+ },
+};
</script>
-
-<template>
- <a-table
- :class="(typeof attrs.class === 'string' ? attrs.class : '') + ' station-ui-table'"
- :style="attrs.style"
- :columns="columns"
- :data-source="data"
- :pagination="false"
- :row-key="(record) => record.key"
- :custom-row="customRow"
- :locale="{ emptyText: '暂无数据' }"
- />
-</template>