]> acesimba.cloud Git - StationBuilder-vue.git/commitdiff
feat: port StationBuilder Vue app from ant-design-vue to Element Plus
authorroot <root@localhost.localdomain>
Wed, 23 Sep 2026 09:29:04 +0000 (17:29 +0800)
committerroot <root@localhost.localdomain>
Wed, 23 Sep 2026 09:29:04 +0000 (17:29 +0800)
- Replace antd runtime theme injection with static brand-token CSS bridge
  (brand-tokens.css defines all --ant-* vars so 2261 legacy custom-CSS refs
  survive, plus --el-* brand overrides: primary #008cd6, radius 4px, etc.)
- Rewrite Ui* wrapper layer (UiTable/UiButton/UiInput/UiSelect/UiActions/
  UiTabbedRegion/UiOverlay) to Element Plus; App.vue uses ElConfigProvider
- Replace antd QRCode with local QrCode.vue (qrcode npm package)
- Rename antd icons to Element Plus equivalents; fix invalid exports
  (Apartment->OfficeBuilding, Copy->CopyDocument/DocumentCopy)
- Remove dead tailwind import; add less devDependency for .less build

50 files changed:
package-lock.json [new file with mode: 0644]
package.json
src-vue/App.vue
src-vue/Home.vue
src-vue/components/DeviceTemplateCard.js
src-vue/components/DropdownButton.vue
src-vue/components/Field.vue
src-vue/components/Pagination.vue
src-vue/components/Panel.vue
src-vue/components/PrimaryButton.vue
src-vue/components/QrCode.vue [new file with mode: 0644]
src-vue/components/SearchInput.vue
src-vue/components/SideMenu.vue
src-vue/components/StatusTag.vue
src-vue/main.js
src-vue/pages/Assets.vue
src-vue/pages/Basic.vue
src-vue/pages/DeviceTemplates.vue
src-vue/pages/DomainSettings.vue
src-vue/pages/Functions.vue
src-vue/pages/Generate.vue
src-vue/pages/ManualAssetConfiguration.vue
src-vue/pages/PageConfig.vue
src-vue/pages/Preview.vue
src-vue/pages/Projects.vue
src-vue/pages/Result.vue
src-vue/pages/SiteConfigurationContext.vue
src-vue/pages/StyleLibrary.vue
src-vue/pages/ToolSettings.vue
src-vue/pages/domain/CompanyDomainLibrary.vue
src-vue/pages/domain/DeviceTemplateCreate.vue
src-vue/pages/domain/DomainCapabilityDetail.vue
src-vue/pages/domain/DomainFunctionsStep.vue
src-vue/pages/domain/DomainKnowledgeStep.vue
src-vue/pages/domain/DomainPagePreviewModal.vue
src-vue/pages/domain/DomainStyleLibrary.vue
src-vue/pages/domain/DomainWizard.vue
src-vue/pages/domain/ImportDomainPackage.vue
src-vue/pages/domain/PackageModal.vue
src-vue/pages/domain/StyleEditorModal.vue
src-vue/pages/domain/useConfirm.js
src-vue/styles/brand-tokens.css [new file with mode: 0644]
src-vue/styles/globals.css
src-vue/ui/UiActions.vue
src-vue/ui/UiButton.vue
src-vue/ui/UiInput.vue
src-vue/ui/UiOverlay.vue
src-vue/ui/UiSelect.vue
src-vue/ui/UiTabbedRegion.vue
src-vue/ui/UiTable.vue

diff --git a/package-lock.json b/package-lock.json
new file mode 100644 (file)
index 0000000..5668385
--- /dev/null
@@ -0,0 +1,2051 @@
+{
+  "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"
+      }
+    }
+  }
+}
index 9b3850bf17b8086419286ec21f2ea5b20a576990..a042d038ff0c064a366e177c05f53ebb3d773e19 100644 (file)
     "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"
   }
 }
index 046d179f3d9b75793866664c0ae700874adb1fc7..a0eb0ab065aefde034a18b717988075a2f62b2ed 100644 (file)
@@ -1,54 +1,11 @@
 <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>
index ae86fccf380af06cee113a4843cdcab01c8074de..1e302512082f8361b0f6976fc3e33a914db92f56 100644 (file)
@@ -1,13 +1,7 @@
 <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";
@@ -59,7 +53,7 @@ import { meta, SITE_EDIT_STEPS, MODELS } from "./lib/meta.js";
 // 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 ----
@@ -136,13 +130,13 @@ const viewPrompt = async (cubeId, name, content) => {
       : 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; }
@@ -150,7 +144,7 @@ const viewCompletePrompt = async () => {
     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; }
@@ -158,7 +152,7 @@ const viewCompleteAssetsPrompt = async () => {
     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; }
@@ -166,23 +160,23 @@ const viewCompleteCubesPrompt = async () => {
     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) => {
@@ -213,14 +207,14 @@ const refineCube = async (cube, feedback) => {
     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(() => {
@@ -236,7 +230,7 @@ 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 () => {
@@ -244,7 +238,7 @@ 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; };
 
@@ -267,7 +261,7 @@ const deleteSites = async (ids) => {
     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;
@@ -297,7 +291,7 @@ const go = async (target, sid) => {
     }
     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;
@@ -322,19 +316,19 @@ const saveCurrentConfiguration = async () => {
   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;
@@ -365,26 +359,26 @@ const saveSiteBasic = async (patch) => {
     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) => {
@@ -396,7 +390,7 @@ 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();
@@ -409,13 +403,13 @@ const sendAi = async () => {
     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; }
 };
 
@@ -543,7 +537,7 @@ function onMenuOpenChange(keys) { resourceNavOpen.value = keys.includes("resourc
           <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 = ''; } }" />
@@ -594,11 +588,11 @@ function onMenuOpenChange(keys) { resourceNavOpen.value = keys.includes("resourc
           </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>
index 882c035c81bde4f4bded5cd3905f3663372a5c3c..b0fec417beb194704306dffe3f1d80287cf757a3 100644 (file)
@@ -1,11 +1,8 @@
 // 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";
@@ -16,8 +13,8 @@ import { fetchDb } from "../lib/api.js";
 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%"];
 
@@ -122,7 +119,7 @@ const DeviceTemplateCard = {
         p.notify("采集点已保存");
         p.onChange();
       } catch (e) {
-        p.notify("保存失败:" + (e && e.message ? e.message : e));
+        p.notify("保存失败:" + (e && e.ElMessage ? e.ElMessage : e));
       }
     };
 
@@ -150,7 +147,7 @@ const DeviceTemplateCard = {
         p.notify("属性已保存");
         p.onChange();
       } catch (e) {
-        p.notify("保存失败:" + (e && e.message ? e.message : e));
+        p.notify("保存失败:" + (e && e.ElMessage ? e.ElMessage : e));
       }
     };
 
@@ -234,7 +231,7 @@ const DeviceTemplateCard = {
           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;
@@ -336,8 +333,8 @@ const DeviceTemplateCard = {
           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" }, () => [
@@ -359,9 +356,9 @@ const DeviceTemplateCard = {
                       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) }),
                       ])),
                     ])
                   ),
@@ -369,18 +366,18 @@ const DeviceTemplateCard = {
           ]),
         ]),
         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
@@ -418,8 +415,8 @@ const DeviceTemplateCard = {
                     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" }, () => [
@@ -434,7 +431,7 @@ const DeviceTemplateCard = {
                           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) })),
                         ])
                       )),
                     ]),
@@ -444,19 +441,19 @@ const DeviceTemplateCard = {
               ])
             : 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" }, [
@@ -466,7 +463,7 @@ const DeviceTemplateCard = {
               : null,
           ])
         ),
-        h("div", { style: { marginTop: 8 } }, [h(Button, { onClick: save }, "保存设备")]),
+        h("div", { style: { marginTop: 8 } }, [h(ElButton, { onClick: save }, "保存设备")]),
       ]),
     ];
   },
index ec4964fd57161766fd59c0a8589434ea4ea5fe61..27625cd62a8d4445344dfe187518534a03f4208a 100644 (file)
@@ -15,11 +15,10 @@ const emit = defineEmits(["openChange"]);
 
 <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)"
     >
@@ -28,9 +27,9 @@ const emit = defineEmits(["openChange"]);
         <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>
index a9f5da3de4e668b0629c70038faecb9fc7604b58..56e791844bb5eef965e08229177ae37a1ca70c80 100644 (file)
@@ -1,5 +1,4 @@
 <script setup>
-import { Form } from "ant-design-vue";
 const props = defineProps({
   label: { type: String, default: "" },
   required: { type: Boolean, default: false },
@@ -8,7 +7,8 @@ const props = defineProps({
 </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>
index a144b56d459ae4d0d06214442a567329754c39dc..fe76fc51f31d09a194aa66678befc89c016ddff1 100644 (file)
@@ -41,9 +41,9 @@ const atLast = computed(() => props.current >= props.totalPages);
         :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)"
       />
index 958e8948b838c364d2adb6beabe3fe81b31a2789..b18bed9ae36a1696275991b3c6fc16d59bba2683 100644 (file)
@@ -1,27 +1,19 @@
 <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>
index 0f54b97e4f4a6a83e27455b80c2bd6fa735b7320..a149e352afab913a4612e446ca02ee25b26dedb4 100644 (file)
@@ -1,7 +1,5 @@
 <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 },
@@ -11,14 +9,15 @@ const props = defineProps({
 </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>
diff --git a/src-vue/components/QrCode.vue b/src-vue/components/QrCode.vue
new file mode 100644 (file)
index 0000000..d296dea
--- /dev/null
@@ -0,0 +1,33 @@
+<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>
index bc60a6d6f9f48105d1fc193b8b2f701a286e73f8..3ce39bde0171308e8c4806618ab04bdebe86eb66 100644 (file)
@@ -10,18 +10,18 @@ const emit = defineEmits(["change"]);
 </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>
index 8e3828407eda86d1dcc4233d712b7af1ec4267e9..920830893eb33b5b30fc8f331aab1708e893375c 100644 (file)
@@ -1,30 +1,55 @@
-<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>
index 247c30f34a4e2220e309a65cdd31a9ee827a20d1..58298645a4b3eed84907d51e74ca5c97c2a5eed3 100644 (file)
@@ -1,17 +1,16 @@
 <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>
index 2dbd14a1767c8cfdbefe48c63beb953e6ff1778a..826847e3c0e3279bab10f6885d7a3bfad734dc91 100644 (file)
@@ -1,6 +1,6 @@
 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";
@@ -11,6 +11,7 @@ import "./styles/antd.less";
 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";
@@ -34,11 +35,11 @@ function showFatalError(title, detail) {
     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");
index 8127dc3a8dc2c493c073c057ce6da745501a4dab..54b944ea1f20c66814304b685d343b2a6bc2488e 100644 (file)
@@ -1,7 +1,7 @@
 <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";
@@ -48,7 +48,7 @@ const Status = (p, { slots }) => {
     : /生成中|校验中|告警|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) ----
@@ -134,7 +134,7 @@ const runCompleteAssets = async () => {
     }
     notify("AI 资产补全已完成");
   } catch (e) {
-    notify("AI 资产补全失败:" + (e?.message || e));
+    notify("AI 资产补全失败:" + (e?.ElMessage || e));
   } finally {
     clearTimeout(watchdog);
     completing.value = false;
@@ -237,7 +237,7 @@ const remove = (i) => change(rows.value.filter((_, idx) => idx !== i));
                       :class="'asset-eye-btn' + (matchedTemplate(row) ? '' : ' disabled')"
                       :title="matchedTemplate(row) ? `查看模板:${matchedTemplate(row).name}` : '请先选择设备模板'"
                       @click="() => onEye(row)"
-                    ><EyeOutlined /></UiButton>
+                    ><View /></UiButton>
                   </td>
                   <td>
                     <UiInput
index ea79aaee05c577b5df5700de349e2b39880ba50a..309874604f897bc5d40a9e1983e78fa7ce6e3497 100644 (file)
@@ -1,7 +1,7 @@
 <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";
@@ -88,7 +88,7 @@ const runComplete = async () => {
     }
     notify("AI 智能补全已完成,已写入补充描述");
   } catch (e) {
-    notify("AI 智能补全失败:" + (e?.message || e));
+    notify("AI 智能补全失败:" + (e?.ElMessage || e));
   } finally {
     clearTimeout(watchdog);
     completing.value = false;
@@ -171,13 +171,13 @@ onMounted(() => {
                 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">
@@ -188,17 +188,17 @@ onMounted(() => {
             <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>
@@ -229,15 +229,15 @@ onMounted(() => {
           </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>
index 2d9365ca137a2466d37d0c7cf853c212da81ac48..60c8fba18f5df1335d31b93c5d2ca2882480b943 100644 (file)
@@ -1,7 +1,7 @@
 <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";
@@ -41,7 +41,7 @@ async function load() {
   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;
   }
@@ -84,7 +84,7 @@ async function remove(row) {
     await load();
     props.notify("设备模板已删除");
   } catch (e) {
-    props.notify("删除失败:" + (e && e.message ? e.message : e));
+    props.notify("删除失败:" + (e && e.ElMessage ? e.ElMessage : e));
   }
 }
 
@@ -132,8 +132,8 @@ function back() {
 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)),
   };
@@ -190,7 +190,7 @@ function rowMenu(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>
@@ -225,13 +225,13 @@ function rowMenu(row) {
                 </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>
index 7120db36592ae6d89d94496a53e4a06839f91cbb..0b1aba370a6e54edcbbe35bd876e4b398a2337dc 100644 (file)
@@ -1,10 +1,7 @@
 <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";
@@ -19,9 +16,9 @@ import { MODELS } from "../lib/meta.js";
 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";
@@ -72,7 +69,7 @@ async function remove(row) {
     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));
   }
 }
 
@@ -116,7 +113,7 @@ const CreateTemplate = {
         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 () =>
@@ -145,7 +142,7 @@ const CreateTemplate = {
             "领域描述",
             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 }, "创建")]),
         ])
       );
   },
@@ -174,7 +171,7 @@ const DomainLibrary = {
       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;
       }
@@ -188,7 +185,7 @@ const DomainLibrary = {
         p.notify("已删除");
         load();
       } catch (e) {
-        p.notify("删除失败:" + (e && e.message ? e.message : e));
+        p.notify("删除失败:" + (e && e.ElMessage ? e.ElMessage : e));
       }
     };
 
@@ -335,7 +332,7 @@ const DeviceTemplateCreate = {
         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 () =>
@@ -347,7 +344,7 @@ const DeviceTemplateCreate = {
           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 }, "创建")]),
         ])
       );
   },
@@ -370,7 +367,7 @@ const DeviceTemplates = {
       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;
       }
@@ -400,7 +397,7 @@ const DeviceTemplates = {
           ]),
           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) })]),
@@ -481,7 +478,7 @@ const DeviceTemplatePicker = {
         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;
@@ -542,7 +539,7 @@ const PageRow = {
         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
index b4a4adce71b625da9dc2dfc0f5a9bc7c15686879..386335e2415f00c79b43e1cf0f89e489357def7f 100644 (file)
@@ -1,15 +1,7 @@
 <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";
@@ -229,7 +221,7 @@ function restoreDefault() {
           <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>
@@ -266,12 +258,12 @@ function restoreDefault() {
       <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">
@@ -294,8 +286,8 @@ function restoreDefault() {
                       :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" />
@@ -309,36 +301,36 @@ function restoreDefault() {
               <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>
@@ -350,7 +342,7 @@ function restoreDefault() {
         <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="编辑菜单"
@@ -378,12 +370,12 @@ function restoreDefault() {
       </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="菜单预览"
@@ -393,7 +385,7 @@ function restoreDefault() {
       <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>
@@ -407,8 +399,8 @@ function restoreDefault() {
               @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>
@@ -418,7 +410,7 @@ function restoreDefault() {
           <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>
@@ -426,8 +418,8 @@ function restoreDefault() {
           <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>
index b0795d6d30dca143b5c174098d8f9580ae78e1bd..40499292c9497c0b25d14e2d6a08867b18e9a291 100644 (file)
@@ -1,7 +1,7 @@
 <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";
 
@@ -48,12 +48,12 @@ const scope = computed(() => {
           <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>
index a282effdc656af551ffabc7e4c605693ec4313df..f2830641e782fa6408eeae864de76a5f43a01ef8 100644 (file)
@@ -1,11 +1,8 @@
 <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";
@@ -193,9 +190,9 @@ const loadGroup = async () => {
     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;
   }
@@ -493,7 +490,7 @@ const importBatchFile = async (file) => {
     batchTab.value = "online";
     notify(`已读取 ${rows.length} 行配置`);
   } catch (e) {
-    notify("读取文件失败:" + (e?.message || e));
+    notify("读取文件失败:" + (e?.ElMessage || e));
   }
 };
 
@@ -615,7 +612,7 @@ const importBindings = async (file) => {
     bindingImportOpen.value = false;
     notify(`已导入 ${rows.length} 个点位参数`);
   } catch (e) {
-    notify("导入失败:" + (e?.message || e));
+    notify("导入失败:" + (e?.ElMessage || e));
   }
 };
 
@@ -661,7 +658,7 @@ const onSaveClick = async () => {
     await saveAll();
     notify("资产组已保存");
   } catch (e) {
-    notify("保存失败:" + (e?.message || e));
+    notify("保存失败:" + (e?.ElMessage || e));
   }
 };
 
@@ -744,7 +741,7 @@ const AssetTreeBranch = defineComponent({
                       if (p.onAddChild) p.onAddChild(p.node);
                     },
                   },
-                  () => h(PlusOutlined)
+                  () => h(Plus)
                 ),
           ]
         ),
@@ -809,29 +806,29 @@ const AssetTreeBranch = defineComponent({
         <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>
 
@@ -880,7 +877,7 @@ const AssetTreeBranch = defineComponent({
       </div>
 
       <!-- node config drawer -->
-      <Drawer
+      <ElDrawer
         class="asset-node-drawer"
         title="节点配置"
         :width="'58vw'"
@@ -925,12 +922,12 @@ const AssetTreeBranch = defineComponent({
                 <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>
@@ -938,7 +935,7 @@ const AssetTreeBranch = defineComponent({
               </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>
 
@@ -967,8 +964,8 @@ const AssetTreeBranch = defineComponent({
                   <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>
@@ -990,7 +987,7 @@ const AssetTreeBranch = defineComponent({
                     <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>
@@ -998,7 +995,7 @@ const AssetTreeBranch = defineComponent({
             </section>
           </div>
         </UiTabbedRegion>
-      </Drawer>
+      </ElDrawer>
 
       <!-- binding import dialog -->
       <div v-if="bindingImportOpen" class="dlg-backdrop binding-import-backdrop" @mousedown="bindingImportOpen = false">
index 524b3313dea54243f8ff60530ab63ab5634b6efe..e05949ddd90d4bc1982745e05e894d5802258e4d 100644 (file)
@@ -1,7 +1,7 @@
 <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";
@@ -54,7 +54,7 @@ watch(
     } catch (error) {
       if (active) {
         catalogError.value = true;
-        props.notify("加载领域仪表板失败:" + error.message);
+        props.notify("加载领域仪表板失败:" + error.ElMessage);
       }
     } finally {
       if (active) catalogLoading.value = false;
@@ -160,7 +160,7 @@ async function changeMu(keys) {
   try {
     await props.onMuChange(keys);
   } catch (error) {
-    props.notify("保存页面选择失败:" + error.message);
+    props.notify("保存页面选择失败:" + error.ElMessage);
   } finally {
     saving.value = false;
   }
@@ -186,7 +186,7 @@ async function changeConfig(config) {
   try {
     await props.onPageConfigChange(config);
   } catch (error) {
-    props.notify("保存页面选择失败:" + error.message);
+    props.notify("保存页面选择失败:" + error.ElMessage);
   } finally {
     saving.value = false;
   }
@@ -199,7 +199,7 @@ async function changeDomainDashboard(page, selected) {
     catalog.value = result.catalog;
     props.onSiteChange && props.onSiteChange(result.site);
   } catch (error) {
-    props.notify("加载领域仪表板失败:" + error.message);
+    props.notify("加载领域仪表板失败:" + error.ElMessage);
   } finally {
     saving.value = false;
   }
@@ -279,9 +279,9 @@ function changeTwin(twin) {
               <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>
@@ -310,7 +310,7 @@ function changeTwin(twin) {
         </article>
       </div>
     </section>
-    <Drawer
+    <ElDrawer
       className="station-info-drawer station-dashboard-preview-drawer"
       :width="'80vw'"
       :title="preview?.name || '仪表板预览'"
@@ -318,6 +318,6 @@ function changeTwin(twin) {
       @close="() => (preview = null)"
     >
       <RenderVNode v-if="preview" :node="dashboardImage(preview, true)" />
-    </Drawer>
+    </ElDrawer>
   </div>
 </template>
index 772e5380fe8f054d67260ac500da9899c5643c82..ede984a11e9a7284706c0761e4ac352ea39f5613 100644 (file)
@@ -1,6 +1,6 @@
 <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";
@@ -37,7 +37,7 @@ watch(
         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;
       }
@@ -138,7 +138,7 @@ watch(
       </div>
 
       <div v-else class="preview-feedback">
-        <Button @click="() => setModal('feedback')">提出修改意见</Button>
+        <ElButton @click="() => setModal('feedback')">提出修改意见</ElButton>
       </div>
     </UiTabbedRegion>
   </div>
index 00d784611668b23a06b1122fb1648c604ae87763..dfcd62f63137e6f85a0952abe30d79e41b7907bc 100644 (file)
@@ -1,10 +1,7 @@
 <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";
@@ -149,7 +146,7 @@ const openEdit = async (id) => {
     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) => {
@@ -194,7 +191,7 @@ const submit = async (keepOpen = false) => {
     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;
   }
@@ -205,7 +202,7 @@ onMounted(() => {
     .then((d) => {
       templates.value = d || [];
     })
-    .catch((e) => notify("加载模板失败:" + (e?.message || e)))
+    .catch((e) => notify("加载模板失败:" + (e?.ElMessage || e)))
     .finally(() => {
       templatesLoading.value = false;
     });
@@ -231,7 +228,7 @@ onMounted(() => {
       </template>
       <section class="station-list-panel">
         <div class="station-hierarchy-note">
-          <ApartmentOutlined />
+          <OfficeBuilding />
           <span>先建立站点层级,再进入各站点配置。新建时可选择模板,暂不配置站点内部内容。</span>
           <span>顶级站点 <b>{{ topCount }}</b> · 子站点 <b>{{ subCount }}</b></span>
         </div>
@@ -242,9 +239,9 @@ onMounted(() => {
             <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">
@@ -269,8 +266,8 @@ onMounted(() => {
                         :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" />
@@ -284,20 +281,20 @@ onMounted(() => {
                   <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);
@@ -306,8 +303,8 @@ onMounted(() => {
                         },
                       }"
                     >
-                      <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>
@@ -327,7 +324,7 @@ onMounted(() => {
       </section>
     </UiTabbedRegion>
 
-    <Drawer
+    <ElDrawer
       class="station-info-drawer"
       :width="720"
       :title="draft?.id ? '编辑站点基本信息' : '新增站点'"
@@ -336,9 +333,9 @@ onMounted(() => {
     >
       <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">
@@ -437,6 +434,6 @@ onMounted(() => {
           </template>
         </section>
       </template>
-    </Drawer>
+    </ElDrawer>
   </div>
 </template>
index 00bf34d0e59b672d6d63a375110e6206d37fb68e..85e9ec50a614b0feaa5ec35a357b9092d5ffe13c 100644 (file)
@@ -1,12 +1,7 @@
 <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";
@@ -52,10 +47,10 @@ const actionMenu = {
 
 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 }) => {
@@ -100,7 +95,7 @@ const openPreview = async (cube) => {
     window.open(url, "_blank");
     setTimeout(() => URL.revokeObjectURL(url), 60000);
   } catch (e) {
-    props.notify("预览打开失败:" + (e?.message || e));
+    props.notify("预览打开失败:" + (e?.ElMessage || e));
   }
 };
 </script>
@@ -120,13 +115,13 @@ const openPreview = async (cube) => {
         <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>
 
@@ -152,10 +147,10 @@ const openPreview = async (cube) => {
                 <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>
index 4dfba62cfcb433ca4558e7ea6e610b1082d20968..c94920167f7751b67d14c6f6e938430be5efd7e3 100644 (file)
@@ -1,6 +1,6 @@
 <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({
@@ -29,7 +29,7 @@ const tagText = computed(() => {
     </div>
     <div>
       <span>站点模板</span><b>{{ site.template_name || "未选择" }}</b>
-      <Tag :color="tagColor">{{ tagText }}</Tag>
+      <ElTag :color="tagColor">{{ tagText }}</ElTag>
     </div>
   </div>
 </template>
index a4e8ee1c4d3c1741353adf8a427a8cafffb3b509..38a6d078c9ee857c4daa9d963ac9681829fbd926 100644 (file)
@@ -1,7 +1,7 @@
 <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";
@@ -49,14 +49,14 @@ async function remove(row) {
     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"
@@ -102,12 +102,12 @@ function rowMenu(row) {
           </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>
@@ -121,7 +121,7 @@ function rowMenu(row) {
       @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>
@@ -134,7 +134,7 @@ function rowMenu(row) {
         <dd>{{ preview.created_at || "—" }}</dd>
       </dl>
     </div>
-  </Drawer>
+  </ElDrawer>
   <StyleEditorModal
     v-if="editor.open"
     :id="editor.id"
index bd6d894d8ee8053d34674515aeeb22d7532baced..4d0d4c0a284bca69121d34f1b7bad02b513448a9 100644 (file)
@@ -8,8 +8,8 @@ import Field from "../components/Field.vue";
 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: "" },
@@ -43,7 +43,7 @@ const freeModels = MODELS.filter((item) => item.rate === "0.00x");
           <span>此处集中维护原侧栏底部的内部工具配置。保存后,新建的 AI 会话将使用所选默认模型。</span>
         </div>
         <UiActions className="settings-actions">
-          <Button @click="() => { onModelChange(draftModel); notify('内部工具配置已保存'); }">保存配置</Button>
+          <ElButton @click="() => { onModelChange(draftModel); notify('内部工具配置已保存'); }">保存配置</ElButton>
         </UiActions>
       </div>
     </Panel>
index 13b4188f3d757d95cc1a1017aa77880b85ab0a97..8ee9ccf0ed1fff30a0ffc146665917fbf16aaddb 100644 (file)
@@ -1,13 +1,7 @@
 <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";
@@ -31,7 +25,7 @@ const load = async () => {
   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;
   }
@@ -78,7 +72,7 @@ const statusText = (row) => (row.status === "available" ? "可用" : "不可用"
     </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)">
@@ -98,8 +92,8 @@ const statusText = (row) => (row.status === "available" ? "可用" : "不可用"
         <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>
@@ -111,10 +105,10 @@ const statusText = (row) => (row.status === "available" ? "可用" : "不可用"
           :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>
index d9b3d9a81f912564edb9184ca7d41f5f98041728..de5dddd25c8d50c97281644da24665dd0fb18028 100644 (file)
@@ -3,7 +3,7 @@ import { ref } from "vue";
 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";
 
@@ -48,7 +48,7 @@ async function add() {
     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>
@@ -62,7 +62,7 @@ async function add() {
       <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>
index 9e2e14452f28a12f0544521ac3f8b26f22ce2b6c..a7b91452ba22d11e00a8af3014959c5589a4abfd 100644 (file)
@@ -1,19 +1,7 @@
 <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";
@@ -22,7 +10,7 @@ import UiOverlay from "../../ui/UiOverlay.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";
@@ -58,7 +46,7 @@ const createFromTemplate = async () => {
   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;
@@ -80,7 +68,7 @@ async function load() {
     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;
   }
@@ -98,7 +86,7 @@ async function saveMeta() {
     editing.value = false;
     await load();
   } catch (e) {
-    props.notify("保存失败:" + (e && e.message ? e.message : e));
+    props.notify("保存失败:" + (e && e.ElMessage ? e.ElMessage : e));
   }
 }
 
@@ -137,10 +125,10 @@ const twinScenes = computed(() =>
 );
 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(() =>
@@ -170,14 +158,14 @@ function editHandler(card) {
             </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>
@@ -192,16 +180,16 @@ function editHandler(card) {
       <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>
 
@@ -230,7 +218,7 @@ function editHandler(card) {
                 :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>
@@ -262,7 +250,7 @@ function editHandler(card) {
               <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>
@@ -271,7 +259,7 @@ function editHandler(card) {
               <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>
@@ -281,9 +269,9 @@ function editHandler(card) {
           <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>
@@ -328,7 +316,7 @@ function editHandler(card) {
           <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>
@@ -340,8 +328,8 @@ function editHandler(card) {
           </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>
@@ -369,8 +357,8 @@ function editHandler(card) {
           <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>
@@ -385,8 +373,8 @@ function editHandler(card) {
           </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>
index 8ca92bc6f7d20537eb6842be0940f732b82d5aef..455021662278185a33602b09e023410e37b38441 100644 (file)
@@ -6,7 +6,7 @@ import UiInput from "../../ui/UiInput.vue";
 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({
@@ -127,7 +127,7 @@ function sourceBadge(src) {
               </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>
index e2fb1faba79469ce7c82f4638caae7fdb2def7ec..1a9152b0b750e50c82956cd656dfb08f6bc2a700 100644 (file)
@@ -4,7 +4,7 @@ import Panel from "../../components/Panel.vue";
 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";
@@ -45,7 +45,7 @@ async function uploadFiles(files) {
     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;
   }
@@ -77,7 +77,7 @@ async function addUrl() {
     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;
   }
@@ -90,7 +90,7 @@ async function removeDoc(docId) {
     props.notify("已删除");
     props.reload();
   } catch (e) {
-    props.notify("删除失败:" + (e && e.message ? e.message : e));
+    props.notify("删除失败:" + (e && e.ElMessage ? e.ElMessage : e));
   }
 }
 </script>
@@ -111,11 +111,11 @@ async function removeDoc(docId) {
       <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>
index 7cf55117673fdfad63e11d84488621b23865a11c..fd1ea8e20a931ee8619fa627d3c7eef39cfea310 100644 (file)
@@ -1,6 +1,6 @@
 <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";
@@ -54,7 +54,7 @@ const regenerate = async () => {
     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;
@@ -85,7 +85,7 @@ const regenerate = async () => {
             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>
index 9f998493a843c565998d5858b195cc1cf2f57a6d..5487660c484f116ce297de82a74a585ab024e41d 100644 (file)
@@ -2,7 +2,7 @@
 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";
@@ -32,7 +32,7 @@ async function remove(s) {
     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>
@@ -41,7 +41,7 @@ async function remove(s) {
   <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">
index 063d9d7dad2dccb72563661d1b5d5f5cdec9dd39..e103f65196b76f2e3b76ad0ca8237869c73dee41 100644 (file)
@@ -9,7 +9,7 @@ import UiTable from "../../ui/UiTable.vue";
 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";
@@ -121,7 +121,7 @@ async function load(silent = false) {
     }));
     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;
   }
@@ -145,7 +145,7 @@ async function patch(patchBody, msg = "已保存") {
     });
     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;
   }
@@ -178,7 +178,7 @@ async function selectStyle(sid) {
     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));
   }
 }
 
@@ -206,7 +206,7 @@ async function runBasicComplete() {
     });
     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);
@@ -254,7 +254,7 @@ async function runAssetsComplete() {
     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);
@@ -286,7 +286,7 @@ async function runCubesComplete() {
     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);
@@ -302,13 +302,13 @@ async function runCubesComplete() {
         });
         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);
@@ -389,7 +389,7 @@ async function uploadPageJson(file, replaceId) {
     }
     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) {
@@ -417,12 +417,12 @@ async function generatePage(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);
@@ -536,7 +536,7 @@ function updatePageField(i, patchRow) {
               <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">
@@ -582,7 +582,7 @@ function updatePageField(i, patchRow) {
               <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>
@@ -600,7 +600,7 @@ function updatePageField(i, patchRow) {
                   </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">
@@ -695,12 +695,12 @@ function updatePageField(i, patchRow) {
     </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>
 
index 67e2ad205fa673d822e695574e5ed65b54ebd04c..c4bdb0c246474e487ce1fd4d160ef054a9c5222e 100644 (file)
@@ -1,12 +1,12 @@
 <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";
 
@@ -71,7 +71,7 @@ async function submit() {
     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;
   }
@@ -83,7 +83,7 @@ async function submit() {
     <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)" />
@@ -104,7 +104,7 @@ async function submit() {
         <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>
index 3fadf85dfae5fe5f4c3bc051dbef3cb589389fee..a7b007e289795f3f796f57f8ab57275ca702d341 100644 (file)
@@ -1,7 +1,7 @@
 <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: "" },
@@ -48,8 +48,8 @@ const items = [
         </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>
index 6c0f831242ae016e1c6b0b65bbe79a42e6d408e2..3e361e701ee8a7c5a1f25f67fff5d86f9ff17e3e 100644 (file)
@@ -5,7 +5,7 @@ import UiButton from "../../ui/UiButton.vue";
 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";
 
@@ -48,7 +48,7 @@ onMounted(async () => {
       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));
     }
   }
 });
@@ -114,7 +114,7 @@ async function submit() {
     }
     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;
   }
@@ -127,7 +127,7 @@ async function remove() {
     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>
@@ -163,10 +163,10 @@ async function remove() {
         </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>
index dc0a1ccdd479a69b3b9fcf243f78968b95ffd471..40f53c451c65c2b7187634a0fe8ab0a9c776e09e 100644 (file)
@@ -6,11 +6,11 @@ import UiButton from "../../ui/UiButton.vue";
 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) => {
@@ -30,7 +30,7 @@ export function useConfirm() {
                   "div",
                   { class: "dlg-box", onClick: (e) => e.stopPropagation() },
                   [
-                    h("p", state.value.message),
+                    h("p", state.value.ElMessage),
                     h(
                       UiActions,
                       { class: "dlg-actions" },
diff --git a/src-vue/styles/brand-tokens.css b/src-vue/styles/brand-tokens.css
new file mode 100644 (file)
index 0000000..b9134db
--- /dev/null
@@ -0,0 +1,110 @@
+/* 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;
+}
index db3198a540b3e51ce03b1654c0bdc15ce8162f6b..ee46cabcbc937c5974d8434b347d702e16622439 100644 (file)
@@ -1,4 +1,3 @@
-@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)}
index 755dae60b8cba076b09a2735e514454995a939ac..bda2024fd30ee38139d54043bb8b0e8370026169 100644 (file)
@@ -1,11 +1,7 @@
 <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";
 
@@ -17,7 +13,7 @@ export default {
     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);
 
@@ -72,27 +68,27 @@ export default {
         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 }),
             }
           )
         );
index 49ca81bc607f6afc74d7846f792fbdbb63774915..b5a7e23b7c1d4685abb8d7fafc2f05ad3d46722d 100644 (file)
@@ -18,9 +18,13 @@ const isAction = computed(() => /(^|\s)(button|btn-ai|ai-send|sso-button)(\s|$)/
 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,
@@ -33,22 +37,24 @@ const passthrough = computed(() => ({
 </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>
index 9aabece49b27070373341ce4d0866cd8c5602716..6ec522009b242e19c56af83bb77d36ef1e4940f9 100644 (file)
@@ -35,7 +35,6 @@ const shared = computed(() => ({
   onBlur: attrs.onBlur,
   onKeydown: attrs.onKeydown,
   onFocus: attrs.onFocus,
-  onInput: attrs.onInput,
 }));
 
 const passthrough = computed(() => ({
@@ -43,21 +42,39 @@ 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"
@@ -65,24 +82,26 @@ function onInput(e) {
     :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>
index cb9f6af68027ddcde224096f678345192edfd50d..9af822aa818ecf3d8036200e578a0138f848ad39 100644 (file)
@@ -1,6 +1,6 @@
 <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";
 
@@ -69,27 +69,26 @@ export default {
         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]);
     };
   },
 };
index 65624aeecdb9c1d7a004ab32cef9dd0ef155fcd6..47ba972f07046e3e9cc0463f7eff1bfc02b1cba0 100644 (file)
@@ -17,7 +17,7 @@ const slots = useSlots();
 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"
@@ -39,6 +39,10 @@ const defaultVal = computed(() => {
   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 },
@@ -48,13 +52,12 @@ function onChange(selected) {
 </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"
index 5cb3c0205d6e3725c0c438c79344a44102edb3db..c3f80ac0e65afc2c694857394177b63bd824c2f2 100644 (file)
@@ -1,6 +1,6 @@
 <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 {
@@ -38,21 +38,24 @@ 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),
+              }
             )
           )
       );
index b5bf2c9d1ba4c295f705f69f3f994be2075fa902..57441790c69b76b482543c8f318cf04ad2bcec58 100644 (file)
-<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>