Browse Source

Initial commit: vaultody frontend (Vue 3 + Vite + Naive UI)

Made-with: Cursor
ALIEZ 2 tháng trước cách đây
commit
0c432d68f8
53 tập tin đã thay đổi với 5427 bổ sung0 xóa
  1. 4 0
      .env.development
  2. 4 0
      .env.example
  3. 4 0
      .env.test
  4. 24 0
      .gitignore
  5. 3 0
      .vscode/extensions.json
  6. 5 0
      README.md
  7. 19 0
      index.html
  8. 2385 0
      package-lock.json
  9. 32 0
      package.json
  10. 0 0
      public/favicon.svg
  11. 24 0
      public/icons.svg
  12. BIN
      public/log.jpg
  13. 24 0
      src/App.vue
  14. BIN
      src/assets/hero.png
  15. 0 0
      src/assets/vite.svg
  16. 1 0
      src/assets/vue.svg
  17. 14 0
      src/auto-imports.d.ts
  18. 36 0
      src/components.d.ts
  19. 7 0
      src/config/index.ts
  20. 39 0
      src/i18n/index.ts
  21. 383 0
      src/layouts/BasicLayout.vue
  22. 60 0
      src/locales/en-US.ts
  23. 56 0
      src/locales/zh-CN.ts
  24. 27 0
      src/main.ts
  25. 44 0
      src/router/guard.ts
  26. 32 0
      src/router/index.ts
  27. 13 0
      src/router/localMenu.ts
  28. 48 0
      src/router/routeRegistry.ts
  29. 31 0
      src/service/auth.ts
  30. 91 0
      src/service/http.ts
  31. 40 0
      src/service/salesExchange.ts
  32. 82 0
      src/service/vault.ts
  33. 132 0
      src/store/auth.ts
  34. 34 0
      src/store/permission.ts
  35. 68 0
      src/store/vault.ts
  36. 53 0
      src/style.css
  37. 52 0
      src/theme/naive.ts
  38. 5 0
      src/types/api.ts
  39. 26 0
      src/types/user.ts
  40. 24 0
      src/types/vault.ts
  41. 6 0
      src/utils/auth.ts
  42. 33 0
      src/utils/crypt.ts
  43. 38 0
      src/utils/menu.ts
  44. 4 0
      src/utils/menuKey.ts
  45. 52 0
      src/utils/session.ts
  46. 41 0
      src/utils/urlToken.ts
  47. 598 0
      src/views/Vault/vaultList.vue
  48. 620 0
      src/views/login/LoginView.vue
  49. 20 0
      src/vite-env.d.ts
  50. 20 0
      tsconfig.app.json
  51. 7 0
      tsconfig.json
  52. 26 0
      tsconfig.node.json
  53. 36 0
      vite.config.ts

+ 4 - 0
.env.development

@@ -0,0 +1,4 @@
+# 与 gypsy-crm-frontend-admin 开发环境默认 Host 对齐,可按环境修改
+# VITE_API_HOST82=https://ad.44a5c8109e4.com
+VITE_API_HOST82=http://103.158.191.66:9300
+VITE_API_HOST80=https://secure.44a5c8109e4.com

+ 4 - 0
.env.example

@@ -0,0 +1,4 @@
+VITE_API_HOST85=https://testad.example.com
+VITE_API_HOST80=https://secure.example.com
+
+# 打包测试环境:复制本文件为 .env.test 并改成测试地址,然后执行 npm run build:test

+ 4 - 0
.env.test

@@ -0,0 +1,4 @@
+# 测试环境(npm run build:test 时加载)
+# 把下面地址改成你们测试环境的 API 域名
+VITE_API_HOST85=https://testad.example.com
+# VITE_API_HOST82=103.158.191.66:9300

+ 24 - 0
.gitignore

@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?

+ 3 - 0
.vscode/extensions.json

@@ -0,0 +1,3 @@
+{
+  "recommendations": ["Vue.volar"]
+}

+ 5 - 0
README.md

@@ -0,0 +1,5 @@
+# Vue 3 + TypeScript + Vite
+
+This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
+
+Learn more about the recommended Project Setup and IDE Support in the [Vue Docs TypeScript Guide](https://vuejs.org/guide/typescript/overview.html#project-setup).

+ 19 - 0
index.html

@@ -0,0 +1,19 @@
+<!doctype html>
+<html lang="zh-CN">
+  <head>
+    <meta charset="UTF-8" />
+    <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <link rel="preconnect" href="https://fonts.googleapis.com" />
+    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
+    <link
+      href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
+      rel="stylesheet"
+    />
+    <title>OnchainPay</title>
+  </head>
+  <body>
+    <div id="app"></div>
+    <script type="module" src="/src/main.ts"></script>
+  </body>
+</html>

+ 2385 - 0
package-lock.json

@@ -0,0 +1,2385 @@
+{
+  "name": "OnchainPay",
+  "version": "0.0.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "OnchainPay",
+      "version": "0.0.0",
+      "dependencies": {
+        "axios": "^1.14.0",
+        "crypto-js": "^4.2.0",
+        "naive-ui": "^2.44.1",
+        "pinia": "^3.0.4",
+        "vue": "^3.5.30",
+        "vue-i18n": "^9.14.5",
+        "vue-router": "^4.6.4"
+      },
+      "devDependencies": {
+        "@types/crypto-js": "^4.2.2",
+        "@types/node": "^24.12.0",
+        "@vitejs/plugin-vue": "^6.0.5",
+        "@vue/tsconfig": "^0.9.0",
+        "typescript": "~5.9.3",
+        "unplugin-auto-import": "^21.0.0",
+        "unplugin-vue-components": "^32.0.0",
+        "vite": "^8.0.1",
+        "vue-tsc": "^3.2.5"
+      }
+    },
+    "node_modules/@babel/helper-string-parser": {
+      "version": "7.27.1",
+      "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+      "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-validator-identifier": {
+      "version": "7.28.5",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+      "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/parser": {
+      "version": "7.29.2",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz",
+      "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/types": "^7.29.0"
+      },
+      "bin": {
+        "parser": "bin/babel-parser.js"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@babel/types": {
+      "version": "7.29.0",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
+      "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-string-parser": "^7.27.1",
+        "@babel/helper-validator-identifier": "^7.28.5"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@css-render/plugin-bem": {
+      "version": "0.15.14",
+      "resolved": "https://registry.npmjs.org/@css-render/plugin-bem/-/plugin-bem-0.15.14.tgz",
+      "integrity": "sha512-QK513CJ7yEQxm/P3EwsI+d+ha8kSOcjGvD6SevM41neEMxdULE+18iuQK6tEChAWMOQNQPLG/Rw3Khb69r5neg==",
+      "license": "MIT",
+      "peerDependencies": {
+        "css-render": "~0.15.14"
+      }
+    },
+    "node_modules/@css-render/vue3-ssr": {
+      "version": "0.15.14",
+      "resolved": "https://registry.npmjs.org/@css-render/vue3-ssr/-/vue3-ssr-0.15.14.tgz",
+      "integrity": "sha512-//8027GSbxE9n3QlD73xFY6z4ZbHbvrOVB7AO6hsmrEzGbg+h2A09HboUyDgu+xsmj7JnvJD39Irt+2D0+iV8g==",
+      "license": "MIT",
+      "peerDependencies": {
+        "vue": "^3.0.11"
+      }
+    },
+    "node_modules/@emnapi/core": {
+      "version": "1.9.1",
+      "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz",
+      "integrity": "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "peer": true,
+      "dependencies": {
+        "@emnapi/wasi-threads": "1.2.0",
+        "tslib": "^2.4.0"
+      }
+    },
+    "node_modules/@emnapi/runtime": {
+      "version": "1.9.1",
+      "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz",
+      "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "peer": true,
+      "dependencies": {
+        "tslib": "^2.4.0"
+      }
+    },
+    "node_modules/@emnapi/wasi-threads": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz",
+      "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "peer": true,
+      "dependencies": {
+        "tslib": "^2.4.0"
+      }
+    },
+    "node_modules/@emotion/hash": {
+      "version": "0.8.0",
+      "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz",
+      "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==",
+      "license": "MIT"
+    },
+    "node_modules/@intlify/core-base": {
+      "version": "9.14.5",
+      "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.14.5.tgz",
+      "integrity": "sha512-5ah5FqZG4pOoHjkvs8mjtv+gPKYU0zCISaYNjBNNqYiaITxW8ZtVih3GS/oTOqN8d9/mDLyrjD46GBApNxmlsA==",
+      "license": "MIT",
+      "dependencies": {
+        "@intlify/message-compiler": "9.14.5",
+        "@intlify/shared": "9.14.5"
+      },
+      "engines": {
+        "node": ">= 16"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/kazupon"
+      }
+    },
+    "node_modules/@intlify/message-compiler": {
+      "version": "9.14.5",
+      "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.14.5.tgz",
+      "integrity": "sha512-IHzgEu61/YIpQV5Pc3aRWScDcnFKWvQA9kigcINcCBXN8mbW+vk9SK+lDxA6STzKQsVJxUPg9ACC52pKKo3SVQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@intlify/shared": "9.14.5",
+        "source-map-js": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 16"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/kazupon"
+      }
+    },
+    "node_modules/@intlify/shared": {
+      "version": "9.14.5",
+      "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-9.14.5.tgz",
+      "integrity": "sha512-9gB+E53BYuAEMhbCAxVgG38EZrk59sxBtv3jSizNL2hEWlgjBjAw1AwpLHtNaeda12pe6W20OGEa0TwuMSRbyQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 16"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/kazupon"
+      }
+    },
+    "node_modules/@jridgewell/gen-mapping": {
+      "version": "0.3.13",
+      "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+      "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/sourcemap-codec": "^1.5.0",
+        "@jridgewell/trace-mapping": "^0.3.24"
+      }
+    },
+    "node_modules/@jridgewell/remapping": {
+      "version": "2.3.5",
+      "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+      "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/gen-mapping": "^0.3.5",
+        "@jridgewell/trace-mapping": "^0.3.24"
+      }
+    },
+    "node_modules/@jridgewell/resolve-uri": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+      "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@jridgewell/sourcemap-codec": {
+      "version": "1.5.5",
+      "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+      "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+      "license": "MIT"
+    },
+    "node_modules/@jridgewell/trace-mapping": {
+      "version": "0.3.31",
+      "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+      "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/resolve-uri": "^3.1.0",
+        "@jridgewell/sourcemap-codec": "^1.4.14"
+      }
+    },
+    "node_modules/@juggle/resize-observer": {
+      "version": "3.4.0",
+      "resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.4.0.tgz",
+      "integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==",
+      "license": "Apache-2.0"
+    },
+    "node_modules/@napi-rs/wasm-runtime": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.2.tgz",
+      "integrity": "sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "@tybys/wasm-util": "^0.10.1"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/Brooooooklyn"
+      },
+      "peerDependencies": {
+        "@emnapi/core": "^1.7.1",
+        "@emnapi/runtime": "^1.7.1"
+      }
+    },
+    "node_modules/@oxc-project/types": {
+      "version": "0.122.0",
+      "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz",
+      "integrity": "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==",
+      "dev": true,
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/Boshen"
+      }
+    },
+    "node_modules/@rolldown/binding-android-arm64": {
+      "version": "1.0.0-rc.12",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.12.tgz",
+      "integrity": "sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==",
+      "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.0-rc.12",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.12.tgz",
+      "integrity": "sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==",
+      "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.0-rc.12",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.12.tgz",
+      "integrity": "sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==",
+      "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.0-rc.12",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.12.tgz",
+      "integrity": "sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==",
+      "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.0-rc.12",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.12.tgz",
+      "integrity": "sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==",
+      "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.0-rc.12",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.12.tgz",
+      "integrity": "sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==",
+      "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.0-rc.12",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.12.tgz",
+      "integrity": "sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==",
+      "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.0-rc.12",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.12.tgz",
+      "integrity": "sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==",
+      "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.0-rc.12",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.12.tgz",
+      "integrity": "sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==",
+      "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.0-rc.12",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.12.tgz",
+      "integrity": "sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==",
+      "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.0-rc.12",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.12.tgz",
+      "integrity": "sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==",
+      "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.0-rc.12",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.12.tgz",
+      "integrity": "sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==",
+      "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.0-rc.12",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.12.tgz",
+      "integrity": "sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==",
+      "cpu": [
+        "wasm32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "@napi-rs/wasm-runtime": "^1.1.1"
+      },
+      "engines": {
+        "node": ">=14.0.0"
+      }
+    },
+    "node_modules/@rolldown/binding-win32-arm64-msvc": {
+      "version": "1.0.0-rc.12",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.12.tgz",
+      "integrity": "sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==",
+      "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.0-rc.12",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.12.tgz",
+      "integrity": "sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==",
+      "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.0-rc.2",
+      "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.2.tgz",
+      "integrity": "sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@tybys/wasm-util": {
+      "version": "0.10.1",
+      "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
+      "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "tslib": "^2.4.0"
+      }
+    },
+    "node_modules/@types/crypto-js": {
+      "version": "4.2.2",
+      "resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.2.2.tgz",
+      "integrity": "sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/estree": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+      "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/lodash": {
+      "version": "4.17.24",
+      "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.24.tgz",
+      "integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==",
+      "license": "MIT"
+    },
+    "node_modules/@types/lodash-es": {
+      "version": "4.17.12",
+      "resolved": "https://registry.npmjs.org/@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": "24.12.0",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.0.tgz",
+      "integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "undici-types": "~7.16.0"
+      }
+    },
+    "node_modules/@vitejs/plugin-vue": {
+      "version": "6.0.5",
+      "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.5.tgz",
+      "integrity": "sha512-bL3AxKuQySfk1iGcBsQnoRVexTPJq0Z/ixFVM8OhVJAP6ZXXXLtM7NFKWhLl30Kg7uTBqIaPXbh+nuQCuBDedg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@rolldown/pluginutils": "1.0.0-rc.2"
+      },
+      "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/@volar/language-core": {
+      "version": "2.4.28",
+      "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.28.tgz",
+      "integrity": "sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@volar/source-map": "2.4.28"
+      }
+    },
+    "node_modules/@volar/source-map": {
+      "version": "2.4.28",
+      "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.28.tgz",
+      "integrity": "sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@volar/typescript": {
+      "version": "2.4.28",
+      "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.28.tgz",
+      "integrity": "sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@volar/language-core": "2.4.28",
+        "path-browserify": "^1.0.1",
+        "vscode-uri": "^3.0.8"
+      }
+    },
+    "node_modules/@vue/compiler-core": {
+      "version": "3.5.31",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.31.tgz",
+      "integrity": "sha512-k/ueL14aNIEy5Onf0OVzR8kiqF/WThgLdFhxwa4e/KF/0qe38IwIdofoSWBTvvxQOesaz6riAFAUaYjoF9fLLQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.29.2",
+        "@vue/shared": "3.5.31",
+        "entities": "^7.0.1",
+        "estree-walker": "^2.0.2",
+        "source-map-js": "^1.2.1"
+      }
+    },
+    "node_modules/@vue/compiler-dom": {
+      "version": "3.5.31",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.31.tgz",
+      "integrity": "sha512-BMY/ozS/xxjYqRFL+tKdRpATJYDTTgWSo0+AJvJNg4ig+Hgb0dOsHPXvloHQ5hmlivUqw1Yt2pPIqp4e0v1GUw==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-core": "3.5.31",
+        "@vue/shared": "3.5.31"
+      }
+    },
+    "node_modules/@vue/compiler-sfc": {
+      "version": "3.5.31",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.31.tgz",
+      "integrity": "sha512-M8wpPgR9UJ8MiRGjppvx9uWJfLV7A/T+/rL8s/y3QG3u0c2/YZgff3d6SuimKRIhcYnWg5fTfDMlz2E6seUW8Q==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.29.2",
+        "@vue/compiler-core": "3.5.31",
+        "@vue/compiler-dom": "3.5.31",
+        "@vue/compiler-ssr": "3.5.31",
+        "@vue/shared": "3.5.31",
+        "estree-walker": "^2.0.2",
+        "magic-string": "^0.30.21",
+        "postcss": "^8.5.8",
+        "source-map-js": "^1.2.1"
+      }
+    },
+    "node_modules/@vue/compiler-ssr": {
+      "version": "3.5.31",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.31.tgz",
+      "integrity": "sha512-h0xIMxrt/LHOvJKMri+vdYT92BrK3HFLtDqq9Pr/lVVfE4IyKZKvWf0vJFW10Yr6nX02OR4MkJwI0c1HDa1hog==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-dom": "3.5.31",
+        "@vue/shared": "3.5.31"
+      }
+    },
+    "node_modules/@vue/devtools-api": {
+      "version": "7.7.9",
+      "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.9.tgz",
+      "integrity": "sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/devtools-kit": "^7.7.9"
+      }
+    },
+    "node_modules/@vue/devtools-kit": {
+      "version": "7.7.9",
+      "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.9.tgz",
+      "integrity": "sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/devtools-shared": "^7.7.9",
+        "birpc": "^2.3.0",
+        "hookable": "^5.5.3",
+        "mitt": "^3.0.1",
+        "perfect-debounce": "^1.0.0",
+        "speakingurl": "^14.0.1",
+        "superjson": "^2.2.2"
+      }
+    },
+    "node_modules/@vue/devtools-shared": {
+      "version": "7.7.9",
+      "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.9.tgz",
+      "integrity": "sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==",
+      "license": "MIT",
+      "dependencies": {
+        "rfdc": "^1.4.1"
+      }
+    },
+    "node_modules/@vue/language-core": {
+      "version": "3.2.6",
+      "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.2.6.tgz",
+      "integrity": "sha512-xYYYX3/aVup576tP/23sEUpgiEnujrENaoNRbaozC1/MA9I6EGFQRJb4xrt/MmUCAGlxTKL2RmT8JLTPqagCkg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@volar/language-core": "2.4.28",
+        "@vue/compiler-dom": "^3.5.0",
+        "@vue/shared": "^3.5.0",
+        "alien-signals": "^3.0.0",
+        "muggle-string": "^0.4.1",
+        "path-browserify": "^1.0.1",
+        "picomatch": "^4.0.2"
+      }
+    },
+    "node_modules/@vue/reactivity": {
+      "version": "3.5.31",
+      "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.31.tgz",
+      "integrity": "sha512-DtKXxk9E/KuVvt8VxWu+6Luc9I9ETNcqR1T1oW1gf02nXaZ1kuAx58oVu7uX9XxJR0iJCro6fqBLw9oSBELo5g==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/shared": "3.5.31"
+      }
+    },
+    "node_modules/@vue/runtime-core": {
+      "version": "3.5.31",
+      "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.31.tgz",
+      "integrity": "sha512-AZPmIHXEAyhpkmN7aWlqjSfYynmkWlluDNPHMCZKFHH+lLtxP/30UJmoVhXmbDoP1Ng0jG0fyY2zCj1PnSSA6Q==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/reactivity": "3.5.31",
+        "@vue/shared": "3.5.31"
+      }
+    },
+    "node_modules/@vue/runtime-dom": {
+      "version": "3.5.31",
+      "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.31.tgz",
+      "integrity": "sha512-xQJsNRmGPeDCJq/u813tyonNgWBFjzfVkBwDREdEWndBnGdHLHgkwNBQxLtg4zDrzKTEcnikUy1UUNecb3lJ6g==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/reactivity": "3.5.31",
+        "@vue/runtime-core": "3.5.31",
+        "@vue/shared": "3.5.31",
+        "csstype": "^3.2.3"
+      }
+    },
+    "node_modules/@vue/server-renderer": {
+      "version": "3.5.31",
+      "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.31.tgz",
+      "integrity": "sha512-GJuwRvMcdZX/CriUnyIIOGkx3rMV3H6sOu0JhdKbduaeCji6zb60iOGMY7tFoN24NfsUYoFBhshZtGxGpxO4iA==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-ssr": "3.5.31",
+        "@vue/shared": "3.5.31"
+      },
+      "peerDependencies": {
+        "vue": "3.5.31"
+      }
+    },
+    "node_modules/@vue/shared": {
+      "version": "3.5.31",
+      "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.31.tgz",
+      "integrity": "sha512-nBxuiuS9Lj5bPkPbWogPUnjxxWpkRniX7e5UBQDWl6Fsf4roq9wwV+cR7ezQ4zXswNvPIlsdj1slcLB7XCsRAw==",
+      "license": "MIT"
+    },
+    "node_modules/@vue/tsconfig": {
+      "version": "0.9.1",
+      "resolved": "https://registry.npmjs.org/@vue/tsconfig/-/tsconfig-0.9.1.tgz",
+      "integrity": "sha512-buvjm+9NzLCJL29KY1j1991YYJ5e6275OiK+G4jtmfIb+z4POywbdm0wXusT9adVWqe0xqg70TbI7+mRx4uU9w==",
+      "dev": true,
+      "license": "MIT",
+      "peerDependencies": {
+        "typescript": ">= 5.8",
+        "vue": "^3.4.0"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        },
+        "vue": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/acorn": {
+      "version": "8.16.0",
+      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
+      "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "acorn": "bin/acorn"
+      },
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/alien-signals": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.1.2.tgz",
+      "integrity": "sha512-d9dYqZTS90WLiU0I5c6DHj/HcKkF8ZyGN3G5x8wSbslulz70KOxaqCT0hQCo9KOyhVqzqGojvNdJXoTumZOtcw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/async-validator": {
+      "version": "4.2.5",
+      "resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz",
+      "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==",
+      "license": "MIT"
+    },
+    "node_modules/asynckit": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+      "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+      "license": "MIT"
+    },
+    "node_modules/axios": {
+      "version": "1.14.0",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-1.14.0.tgz",
+      "integrity": "sha512-3Y8yrqLSwjuzpXuZ0oIYZ/XGgLwUIBU3uLvbcpb0pidD9ctpShJd43KSlEEkVQg6DS0G9NKyzOvBfUtDKEyHvQ==",
+      "license": "MIT",
+      "dependencies": {
+        "follow-redirects": "^1.15.11",
+        "form-data": "^4.0.5",
+        "proxy-from-env": "^2.1.0"
+      }
+    },
+    "node_modules/birpc": {
+      "version": "2.9.0",
+      "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz",
+      "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      }
+    },
+    "node_modules/call-bind-apply-helpers": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+      "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/chokidar": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz",
+      "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "readdirp": "^5.0.0"
+      },
+      "engines": {
+        "node": ">= 20.19.0"
+      },
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
+    "node_modules/combined-stream": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+      "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+      "license": "MIT",
+      "dependencies": {
+        "delayed-stream": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/confbox": {
+      "version": "0.2.4",
+      "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz",
+      "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/copy-anything": {
+      "version": "4.0.5",
+      "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz",
+      "integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==",
+      "license": "MIT",
+      "dependencies": {
+        "is-what": "^5.2.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/mesqueeb"
+      }
+    },
+    "node_modules/crypto-js": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
+      "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
+      "license": "MIT"
+    },
+    "node_modules/css-render": {
+      "version": "0.15.14",
+      "resolved": "https://registry.npmjs.org/css-render/-/css-render-0.15.14.tgz",
+      "integrity": "sha512-9nF4PdUle+5ta4W5SyZdLCCmFd37uVimSjg1evcTqKJCyvCEEj12WKzOSBNak6r4im4J4iYXKH1OWpUV5LBYFg==",
+      "license": "MIT",
+      "dependencies": {
+        "@emotion/hash": "~0.8.0",
+        "csstype": "~3.0.5"
+      }
+    },
+    "node_modules/css-render/node_modules/csstype": {
+      "version": "3.0.11",
+      "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.11.tgz",
+      "integrity": "sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw==",
+      "license": "MIT"
+    },
+    "node_modules/csstype": {
+      "version": "3.2.3",
+      "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+      "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+      "license": "MIT"
+    },
+    "node_modules/date-fns": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
+      "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
+      "license": "MIT",
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/kossnocorp"
+      }
+    },
+    "node_modules/date-fns-tz": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/date-fns-tz/-/date-fns-tz-3.2.0.tgz",
+      "integrity": "sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ==",
+      "license": "MIT",
+      "peerDependencies": {
+        "date-fns": "^3.0.0 || ^4.0.0"
+      }
+    },
+    "node_modules/delayed-stream": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+      "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/detect-libc": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+      "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/dunder-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+      "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.2.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/entities": {
+      "version": "7.0.1",
+      "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
+      "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=0.12"
+      },
+      "funding": {
+        "url": "https://github.com/fb55/entities?sponsor=1"
+      }
+    },
+    "node_modules/es-define-property": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+      "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-errors": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+      "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-object-atoms": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+      "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-set-tostringtag": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+      "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.6",
+        "has-tostringtag": "^1.0.2",
+        "hasown": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/escape-string-regexp": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
+      "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/estree-walker": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
+      "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
+      "license": "MIT"
+    },
+    "node_modules/evtd": {
+      "version": "0.2.4",
+      "resolved": "https://registry.npmjs.org/evtd/-/evtd-0.2.4.tgz",
+      "integrity": "sha512-qaeGN5bx63s/AXgQo8gj6fBkxge+OoLddLniox5qtLAEY5HSnuSlISXVPxnSae1dWblvTh4/HoMIB+mbMsvZzw==",
+      "license": "MIT"
+    },
+    "node_modules/exsolve": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz",
+      "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/fdir": {
+      "version": "6.5.0",
+      "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+      "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "peerDependencies": {
+        "picomatch": "^3 || ^4"
+      },
+      "peerDependenciesMeta": {
+        "picomatch": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/follow-redirects": {
+      "version": "1.15.11",
+      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
+      "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://github.com/sponsors/RubenVerborgh"
+        }
+      ],
+      "license": "MIT",
+      "engines": {
+        "node": ">=4.0"
+      },
+      "peerDependenciesMeta": {
+        "debug": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/form-data": {
+      "version": "4.0.5",
+      "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
+      "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
+      "license": "MIT",
+      "dependencies": {
+        "asynckit": "^0.4.0",
+        "combined-stream": "^1.0.8",
+        "es-set-tostringtag": "^2.1.0",
+        "hasown": "^2.0.2",
+        "mime-types": "^2.1.12"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/fsevents": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmjs.org/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/function-bind": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-intrinsic": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+      "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "es-define-property": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.1.1",
+        "function-bind": "^1.1.2",
+        "get-proto": "^1.0.1",
+        "gopd": "^1.2.0",
+        "has-symbols": "^1.1.0",
+        "hasown": "^2.0.2",
+        "math-intrinsics": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+      "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+      "license": "MIT",
+      "dependencies": {
+        "dunder-proto": "^1.0.1",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/gopd": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+      "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-symbols": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+      "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-tostringtag": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+      "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+      "license": "MIT",
+      "dependencies": {
+        "has-symbols": "^1.0.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/hasown": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+      "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+      "license": "MIT",
+      "dependencies": {
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/highlight.js": {
+      "version": "11.11.1",
+      "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz",
+      "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==",
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=12.0.0"
+      }
+    },
+    "node_modules/hookable": {
+      "version": "5.5.3",
+      "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",
+      "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==",
+      "license": "MIT"
+    },
+    "node_modules/is-what": {
+      "version": "5.5.0",
+      "resolved": "https://registry.npmjs.org/is-what/-/is-what-5.5.0.tgz",
+      "integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/mesqueeb"
+      }
+    },
+    "node_modules/js-tokens": {
+      "version": "9.0.1",
+      "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz",
+      "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/lightningcss": {
+      "version": "1.32.0",
+      "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
+      "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
+      "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.32.0",
+        "lightningcss-darwin-arm64": "1.32.0",
+        "lightningcss-darwin-x64": "1.32.0",
+        "lightningcss-freebsd-x64": "1.32.0",
+        "lightningcss-linux-arm-gnueabihf": "1.32.0",
+        "lightningcss-linux-arm64-gnu": "1.32.0",
+        "lightningcss-linux-arm64-musl": "1.32.0",
+        "lightningcss-linux-x64-gnu": "1.32.0",
+        "lightningcss-linux-x64-musl": "1.32.0",
+        "lightningcss-win32-arm64-msvc": "1.32.0",
+        "lightningcss-win32-x64-msvc": "1.32.0"
+      }
+    },
+    "node_modules/lightningcss-android-arm64": {
+      "version": "1.32.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
+      "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
+      "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.32.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
+      "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
+      "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.32.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
+      "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
+      "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.32.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
+      "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
+      "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.32.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
+      "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
+      "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.32.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
+      "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
+      "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.32.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
+      "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
+      "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.32.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
+      "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
+      "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.32.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
+      "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
+      "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.32.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
+      "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
+      "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.32.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
+      "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
+      "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/local-pkg": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.2.tgz",
+      "integrity": "sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "mlly": "^1.7.4",
+        "pkg-types": "^2.3.0",
+        "quansync": "^0.2.11"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      }
+    },
+    "node_modules/lodash": {
+      "version": "4.17.23",
+      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
+      "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
+      "license": "MIT"
+    },
+    "node_modules/lodash-es": {
+      "version": "4.17.23",
+      "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz",
+      "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==",
+      "license": "MIT"
+    },
+    "node_modules/magic-string": {
+      "version": "0.30.21",
+      "resolved": "https://registry.npmjs.org/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/math-intrinsics": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+      "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/mime-db": {
+      "version": "1.52.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime-types": {
+      "version": "2.1.35",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-db": "1.52.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mitt": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
+      "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
+      "license": "MIT"
+    },
+    "node_modules/mlly": {
+      "version": "1.8.2",
+      "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz",
+      "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "acorn": "^8.16.0",
+        "pathe": "^2.0.3",
+        "pkg-types": "^1.3.1",
+        "ufo": "^1.6.3"
+      }
+    },
+    "node_modules/mlly/node_modules/confbox": {
+      "version": "0.1.8",
+      "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz",
+      "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/mlly/node_modules/pkg-types": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz",
+      "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "confbox": "^0.1.8",
+        "mlly": "^1.7.4",
+        "pathe": "^2.0.1"
+      }
+    },
+    "node_modules/muggle-string": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz",
+      "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/naive-ui": {
+      "version": "2.44.1",
+      "resolved": "https://registry.npmjs.org/naive-ui/-/naive-ui-2.44.1.tgz",
+      "integrity": "sha512-reo8Esw0p58liZwbUutC7meW24Xbn3EwNv91zReWKm2W4JPu+zfgJRn/F7aO0BFmvN+h2brA2M5lRvYqLq4kuA==",
+      "license": "MIT",
+      "dependencies": {
+        "@css-render/plugin-bem": "^0.15.14",
+        "@css-render/vue3-ssr": "^0.15.14",
+        "@types/lodash": "^4.17.20",
+        "@types/lodash-es": "^4.17.12",
+        "async-validator": "^4.2.5",
+        "css-render": "^0.15.14",
+        "csstype": "^3.1.3",
+        "date-fns": "^4.1.0",
+        "date-fns-tz": "^3.2.0",
+        "evtd": "^0.2.4",
+        "highlight.js": "^11.8.0",
+        "lodash": "^4.17.21",
+        "lodash-es": "^4.17.21",
+        "seemly": "^0.3.10",
+        "treemate": "^0.3.11",
+        "vdirs": "^0.1.8",
+        "vooks": "^0.2.12",
+        "vueuc": "^0.4.65"
+      },
+      "engines": {
+        "node": ">=20"
+      },
+      "peerDependencies": {
+        "vue": "^3.0.0"
+      }
+    },
+    "node_modules/nanoid": {
+      "version": "3.3.11",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+      "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+      "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/obug": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz",
+      "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==",
+      "dev": true,
+      "funding": [
+        "https://github.com/sponsors/sxzz",
+        "https://opencollective.com/debug"
+      ],
+      "license": "MIT"
+    },
+    "node_modules/path-browserify": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
+      "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/pathe": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
+      "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/perfect-debounce": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
+      "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==",
+      "license": "MIT"
+    },
+    "node_modules/picocolors": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+      "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+      "license": "ISC"
+    },
+    "node_modules/picomatch": {
+      "version": "4.0.4",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+      "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
+    "node_modules/pinia": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.4.tgz",
+      "integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/devtools-api": "^7.7.7"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/posva"
+      },
+      "peerDependencies": {
+        "typescript": ">=4.5.0",
+        "vue": "^3.5.11"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/pkg-types": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz",
+      "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "confbox": "^0.2.2",
+        "exsolve": "^1.0.7",
+        "pathe": "^2.0.3"
+      }
+    },
+    "node_modules/postcss": {
+      "version": "8.5.8",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
+      "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
+      "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.11",
+        "picocolors": "^1.1.1",
+        "source-map-js": "^1.2.1"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >=14"
+      }
+    },
+    "node_modules/proxy-from-env": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
+      "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/quansync": {
+      "version": "0.2.11",
+      "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz",
+      "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://github.com/sponsors/antfu"
+        },
+        {
+          "type": "individual",
+          "url": "https://github.com/sponsors/sxzz"
+        }
+      ],
+      "license": "MIT"
+    },
+    "node_modules/readdirp": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz",
+      "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 20.19.0"
+      },
+      "funding": {
+        "type": "individual",
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
+    "node_modules/rfdc": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
+      "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
+      "license": "MIT"
+    },
+    "node_modules/rolldown": {
+      "version": "1.0.0-rc.12",
+      "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.12.tgz",
+      "integrity": "sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@oxc-project/types": "=0.122.0",
+        "@rolldown/pluginutils": "1.0.0-rc.12"
+      },
+      "bin": {
+        "rolldown": "bin/cli.mjs"
+      },
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      },
+      "optionalDependencies": {
+        "@rolldown/binding-android-arm64": "1.0.0-rc.12",
+        "@rolldown/binding-darwin-arm64": "1.0.0-rc.12",
+        "@rolldown/binding-darwin-x64": "1.0.0-rc.12",
+        "@rolldown/binding-freebsd-x64": "1.0.0-rc.12",
+        "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.12",
+        "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.12",
+        "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.12",
+        "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.12",
+        "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.12",
+        "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.12",
+        "@rolldown/binding-linux-x64-musl": "1.0.0-rc.12",
+        "@rolldown/binding-openharmony-arm64": "1.0.0-rc.12",
+        "@rolldown/binding-wasm32-wasi": "1.0.0-rc.12",
+        "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.12",
+        "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.12"
+      }
+    },
+    "node_modules/rolldown/node_modules/@rolldown/pluginutils": {
+      "version": "1.0.0-rc.12",
+      "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.12.tgz",
+      "integrity": "sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/scule": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/scule/-/scule-1.3.0.tgz",
+      "integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/seemly": {
+      "version": "0.3.10",
+      "resolved": "https://registry.npmjs.org/seemly/-/seemly-0.3.10.tgz",
+      "integrity": "sha512-2+SMxtG1PcsL0uyhkumlOU6Qo9TAQ/WyH7tthnPIOQB05/12jz9naq6GZ6iZ6ApVsO3rr2gsnTf3++OV63kE1Q==",
+      "license": "MIT"
+    },
+    "node_modules/source-map-js": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/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/speakingurl": {
+      "version": "14.0.1",
+      "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz",
+      "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==",
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/strip-literal": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz",
+      "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "js-tokens": "^9.0.1"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      }
+    },
+    "node_modules/superjson": {
+      "version": "2.2.6",
+      "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz",
+      "integrity": "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==",
+      "license": "MIT",
+      "dependencies": {
+        "copy-anything": "^4"
+      },
+      "engines": {
+        "node": ">=16"
+      }
+    },
+    "node_modules/tinyglobby": {
+      "version": "0.2.15",
+      "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
+      "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "fdir": "^6.5.0",
+        "picomatch": "^4.0.3"
+      },
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/SuperchupuDev"
+      }
+    },
+    "node_modules/treemate": {
+      "version": "0.3.11",
+      "resolved": "https://registry.npmjs.org/treemate/-/treemate-0.3.11.tgz",
+      "integrity": "sha512-M8RGFoKtZ8dF+iwJfAJTOH/SM4KluKOKRJpjCMhI8bG3qB74zrFoArKZ62ll0Fr3mqkMJiQOmWYkdYgDeITYQg==",
+      "license": "MIT"
+    },
+    "node_modules/tslib": {
+      "version": "2.8.1",
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+      "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+      "dev": true,
+      "license": "0BSD",
+      "optional": true
+    },
+    "node_modules/typescript": {
+      "version": "5.9.3",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+      "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+      "devOptional": true,
+      "license": "Apache-2.0",
+      "bin": {
+        "tsc": "bin/tsc",
+        "tsserver": "bin/tsserver"
+      },
+      "engines": {
+        "node": ">=14.17"
+      }
+    },
+    "node_modules/ufo": {
+      "version": "1.6.3",
+      "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz",
+      "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/undici-types": {
+      "version": "7.16.0",
+      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
+      "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/unimport": {
+      "version": "5.7.0",
+      "resolved": "https://registry.npmjs.org/unimport/-/unimport-5.7.0.tgz",
+      "integrity": "sha512-njnL6sp8lEA8QQbZrt+52p/g4X0rw3bnGGmUcJnt1jeG8+iiqO779aGz0PirCtydAIVcuTBRlJ52F0u46z309Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "acorn": "^8.16.0",
+        "escape-string-regexp": "^5.0.0",
+        "estree-walker": "^3.0.3",
+        "local-pkg": "^1.1.2",
+        "magic-string": "^0.30.21",
+        "mlly": "^1.8.0",
+        "pathe": "^2.0.3",
+        "picomatch": "^4.0.3",
+        "pkg-types": "^2.3.0",
+        "scule": "^1.3.0",
+        "strip-literal": "^3.1.0",
+        "tinyglobby": "^0.2.15",
+        "unplugin": "^2.3.11",
+        "unplugin-utils": "^0.3.1"
+      },
+      "engines": {
+        "node": ">=18.12.0"
+      }
+    },
+    "node_modules/unimport/node_modules/estree-walker": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
+      "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/estree": "^1.0.0"
+      }
+    },
+    "node_modules/unplugin": {
+      "version": "2.3.11",
+      "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.11.tgz",
+      "integrity": "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/remapping": "^2.3.5",
+        "acorn": "^8.15.0",
+        "picomatch": "^4.0.3",
+        "webpack-virtual-modules": "^0.6.2"
+      },
+      "engines": {
+        "node": ">=18.12.0"
+      }
+    },
+    "node_modules/unplugin-auto-import": {
+      "version": "21.0.0",
+      "resolved": "https://registry.npmjs.org/unplugin-auto-import/-/unplugin-auto-import-21.0.0.tgz",
+      "integrity": "sha512-vWuC8SwqJmxZFYwPojhOhOXDb5xFhNNcEVb9K/RFkyk/3VnfaOjzitWN7v+8DEKpMjSsY2AEGXNgt6I0yQrhRQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "local-pkg": "^1.1.2",
+        "magic-string": "^0.30.21",
+        "picomatch": "^4.0.3",
+        "unimport": "^5.6.0",
+        "unplugin": "^2.3.11",
+        "unplugin-utils": "^0.3.1"
+      },
+      "engines": {
+        "node": ">=20.19.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      },
+      "peerDependencies": {
+        "@nuxt/kit": "^4.0.0",
+        "@vueuse/core": "*"
+      },
+      "peerDependenciesMeta": {
+        "@nuxt/kit": {
+          "optional": true
+        },
+        "@vueuse/core": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/unplugin-utils": {
+      "version": "0.3.1",
+      "resolved": "https://registry.npmjs.org/unplugin-utils/-/unplugin-utils-0.3.1.tgz",
+      "integrity": "sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "pathe": "^2.0.3",
+        "picomatch": "^4.0.3"
+      },
+      "engines": {
+        "node": ">=20.19.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sxzz"
+      }
+    },
+    "node_modules/unplugin-vue-components": {
+      "version": "32.0.0",
+      "resolved": "https://registry.npmjs.org/unplugin-vue-components/-/unplugin-vue-components-32.0.0.tgz",
+      "integrity": "sha512-uLdccgS7mf3pv1bCCP20y/hm+u1eOjAmygVkh+Oa70MPkzgl1eQv1L0CwdHNM3gscO8/GDMGIET98Ja47CBbZg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "chokidar": "^5.0.0",
+        "local-pkg": "^1.1.2",
+        "magic-string": "^0.30.21",
+        "mlly": "^1.8.2",
+        "obug": "^2.1.1",
+        "picomatch": "^4.0.3",
+        "tinyglobby": "^0.2.15",
+        "unplugin": "^3.0.0",
+        "unplugin-utils": "^0.3.1"
+      },
+      "engines": {
+        "node": ">=20.19.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      },
+      "peerDependencies": {
+        "@nuxt/kit": "^3.2.2 || ^4.0.0",
+        "vue": "^3.0.0"
+      },
+      "peerDependenciesMeta": {
+        "@nuxt/kit": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/unplugin-vue-components/node_modules/unplugin": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-3.0.0.tgz",
+      "integrity": "sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/remapping": "^2.3.5",
+        "picomatch": "^4.0.3",
+        "webpack-virtual-modules": "^0.6.2"
+      },
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/vdirs": {
+      "version": "0.1.8",
+      "resolved": "https://registry.npmjs.org/vdirs/-/vdirs-0.1.8.tgz",
+      "integrity": "sha512-H9V1zGRLQZg9b+GdMk8MXDN2Lva0zx72MPahDKc30v+DtwKjfyOSXWRIX4t2mhDubM1H09gPhWeth/BJWPHGUw==",
+      "license": "MIT",
+      "dependencies": {
+        "evtd": "^0.2.2"
+      },
+      "peerDependencies": {
+        "vue": "^3.0.11"
+      }
+    },
+    "node_modules/vite": {
+      "version": "8.0.3",
+      "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.3.tgz",
+      "integrity": "sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "lightningcss": "^1.32.0",
+        "picomatch": "^4.0.4",
+        "postcss": "^8.5.8",
+        "rolldown": "1.0.0-rc.12",
+        "tinyglobby": "^0.2.15"
+      },
+      "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.0",
+        "esbuild": "^0.27.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/vooks": {
+      "version": "0.2.12",
+      "resolved": "https://registry.npmjs.org/vooks/-/vooks-0.2.12.tgz",
+      "integrity": "sha512-iox0I3RZzxtKlcgYaStQYKEzWWGAduMmq+jS7OrNdQo1FgGfPMubGL3uGHOU9n97NIvfFDBGnpSvkWyb/NSn/Q==",
+      "license": "MIT",
+      "dependencies": {
+        "evtd": "^0.2.2"
+      },
+      "peerDependencies": {
+        "vue": "^3.0.0"
+      }
+    },
+    "node_modules/vscode-uri": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz",
+      "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/vue": {
+      "version": "3.5.31",
+      "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.31.tgz",
+      "integrity": "sha512-iV/sU9SzOlmA/0tygSmjkEN6Jbs3nPoIPFhCMLD2STrjgOU8DX7ZtzMhg4ahVwf5Rp9KoFzcXeB1ZrVbLBp5/Q==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-dom": "3.5.31",
+        "@vue/compiler-sfc": "3.5.31",
+        "@vue/runtime-dom": "3.5.31",
+        "@vue/server-renderer": "3.5.31",
+        "@vue/shared": "3.5.31"
+      },
+      "peerDependencies": {
+        "typescript": "*"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/vue-i18n": {
+      "version": "9.14.5",
+      "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.14.5.tgz",
+      "integrity": "sha512-0jQ9Em3ymWngyiIkj0+c/k7WgaPO+TNzjKSNq9BvBQaKJECqn9cd9fL4tkDhB5G1QBskGl9YxxbDAhgbFtpe2g==",
+      "deprecated": "v9 and v10 no longer supported. please migrate to v11. about maintenance status, see https://vue-i18n.intlify.dev/guide/maintenance.html",
+      "license": "MIT",
+      "dependencies": {
+        "@intlify/core-base": "9.14.5",
+        "@intlify/shared": "9.14.5",
+        "@vue/devtools-api": "^6.5.0"
+      },
+      "engines": {
+        "node": ">= 16"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/kazupon"
+      },
+      "peerDependencies": {
+        "vue": "^3.0.0"
+      }
+    },
+    "node_modules/vue-i18n/node_modules/@vue/devtools-api": {
+      "version": "6.6.4",
+      "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
+      "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
+      "license": "MIT"
+    },
+    "node_modules/vue-router": {
+      "version": "4.6.4",
+      "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz",
+      "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/devtools-api": "^6.6.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/posva"
+      },
+      "peerDependencies": {
+        "vue": "^3.5.0"
+      }
+    },
+    "node_modules/vue-router/node_modules/@vue/devtools-api": {
+      "version": "6.6.4",
+      "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
+      "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
+      "license": "MIT"
+    },
+    "node_modules/vue-tsc": {
+      "version": "3.2.6",
+      "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.2.6.tgz",
+      "integrity": "sha512-gYW/kWI0XrwGzd0PKc7tVB/qpdeAkIZLNZb10/InizkQjHjnT8weZ/vBarZoj4kHKbUTZT/bAVgoOr8x4NsQ/Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@volar/typescript": "2.4.28",
+        "@vue/language-core": "3.2.6"
+      },
+      "bin": {
+        "vue-tsc": "bin/vue-tsc.js"
+      },
+      "peerDependencies": {
+        "typescript": ">=5.0.0"
+      }
+    },
+    "node_modules/vueuc": {
+      "version": "0.4.65",
+      "resolved": "https://registry.npmjs.org/vueuc/-/vueuc-0.4.65.tgz",
+      "integrity": "sha512-lXuMl+8gsBmruudfxnMF9HW4be8rFziylXFu1VHVNbLVhRTXXV4njvpRuJapD/8q+oFEMSfQMH16E/85VoWRyQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@css-render/vue3-ssr": "^0.15.10",
+        "@juggle/resize-observer": "^3.3.1",
+        "css-render": "^0.15.10",
+        "evtd": "^0.2.4",
+        "seemly": "^0.3.6",
+        "vdirs": "^0.1.4",
+        "vooks": "^0.2.4"
+      },
+      "peerDependencies": {
+        "vue": "^3.0.11"
+      }
+    },
+    "node_modules/webpack-virtual-modules": {
+      "version": "0.6.2",
+      "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz",
+      "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==",
+      "dev": true,
+      "license": "MIT"
+    }
+  }
+}

+ 32 - 0
package.json

@@ -0,0 +1,32 @@
+{
+  "name": "OnchainPay",
+  "private": true,
+  "version": "0.0.0",
+  "type": "module",
+  "scripts": {
+    "dev": "vite",
+    "build": "vue-tsc -b && vite build",
+    "build:test": "vue-tsc -b && vite build --mode test",
+    "preview": "vite preview"
+  },
+  "dependencies": {
+    "axios": "^1.14.0",
+    "crypto-js": "^4.2.0",
+    "naive-ui": "^2.44.1",
+    "pinia": "^3.0.4",
+    "vue": "^3.5.30",
+    "vue-i18n": "^9.14.5",
+    "vue-router": "^4.6.4"
+  },
+  "devDependencies": {
+    "@types/crypto-js": "^4.2.2",
+    "@types/node": "^24.12.0",
+    "@vitejs/plugin-vue": "^6.0.5",
+    "@vue/tsconfig": "^0.9.0",
+    "typescript": "~5.9.3",
+    "unplugin-auto-import": "^21.0.0",
+    "unplugin-vue-components": "^32.0.0",
+    "vite": "^8.0.1",
+    "vue-tsc": "^3.2.5"
+  }
+}

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 0 - 0
public/favicon.svg


+ 24 - 0
public/icons.svg

@@ -0,0 +1,24 @@
+<svg xmlns="http://www.w3.org/2000/svg">
+  <symbol id="bluesky-icon" viewBox="0 0 16 17">
+    <g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
+    <defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
+  </symbol>
+  <symbol id="discord-icon" viewBox="0 0 20 19">
+    <path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
+  </symbol>
+  <symbol id="documentation-icon" viewBox="0 0 21 20">
+    <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
+    <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
+    <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
+  </symbol>
+  <symbol id="github-icon" viewBox="0 0 19 19">
+    <path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
+  </symbol>
+  <symbol id="social-icon" viewBox="0 0 20 20">
+    <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
+    <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
+  </symbol>
+  <symbol id="x-icon" viewBox="0 0 19 19">
+    <path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
+  </symbol>
+</svg>

BIN
public/log.jpg


+ 24 - 0
src/App.vue

@@ -0,0 +1,24 @@
+<script setup lang="ts">
+import { computed } from 'vue'
+import { dateEnUS, dateZhCN, enUS, zhCN } from 'naive-ui'
+import { themeOverrides } from '@/theme/naive'
+
+const { locale } = useI18n()
+
+const naiveLocale = computed(() => (locale.value === 'zh-CN' ? zhCN : enUS))
+const naiveDateLocale = computed(() => (locale.value === 'zh-CN' ? dateZhCN : dateEnUS))
+</script>
+
+<template>
+  <n-config-provider
+    :locale="naiveLocale"
+    :date-locale="naiveDateLocale"
+    :theme-overrides="themeOverrides"
+  >
+    <n-message-provider>
+      <n-dialog-provider>
+        <router-view />
+      </n-dialog-provider>
+    </n-message-provider>
+  </n-config-provider>
+</template>

BIN
src/assets/hero.png


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 0 - 0
src/assets/vite.svg


+ 1 - 0
src/assets/vue.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

+ 14 - 0
src/auto-imports.d.ts

@@ -0,0 +1,14 @@
+/* eslint-disable */
+/* prettier-ignore */
+// @ts-nocheck
+// noinspection JSUnusedGlobalSymbols
+// Generated by unplugin-auto-import
+// biome-ignore lint: disable
+export {}
+declare global {
+  const useDialog: typeof import('naive-ui').useDialog
+  const useI18n: typeof import('vue-i18n').useI18n
+  const useLoadingBar: typeof import('naive-ui').useLoadingBar
+  const useMessage: typeof import('naive-ui').useMessage
+  const useNotification: typeof import('naive-ui').useNotification
+}

+ 36 - 0
src/components.d.ts

@@ -0,0 +1,36 @@
+/* eslint-disable */
+// @ts-nocheck
+// biome-ignore lint: disable
+// oxlint-disable
+// ------
+// Generated by unplugin-vue-components
+// Read more: https://github.com/vuejs/core/pull/3399
+
+export {}
+
+/* prettier-ignore */
+declare module 'vue' {
+  export interface GlobalComponents {
+    NAvatar: typeof import('naive-ui')['NAvatar']
+    NButton: typeof import('naive-ui')['NButton']
+    NCard: typeof import('naive-ui')['NCard']
+    NConfigProvider: typeof import('naive-ui')['NConfigProvider']
+    NDataTable: typeof import('naive-ui')['NDataTable']
+    NDialogProvider: typeof import('naive-ui')['NDialogProvider']
+    NDropdown: typeof import('naive-ui')['NDropdown']
+    NEmpty: typeof import('naive-ui')['NEmpty']
+    NForm: typeof import('naive-ui')['NForm']
+    NFormItem: typeof import('naive-ui')['NFormItem']
+    NInput: typeof import('naive-ui')['NInput']
+    NLayout: typeof import('naive-ui')['NLayout']
+    NLayoutContent: typeof import('naive-ui')['NLayoutContent']
+    NLayoutHeader: typeof import('naive-ui')['NLayoutHeader']
+    NLayoutSider: typeof import('naive-ui')['NLayoutSider']
+    NMenu: typeof import('naive-ui')['NMenu']
+    NMessageProvider: typeof import('naive-ui')['NMessageProvider']
+    NSelect: typeof import('naive-ui')['NSelect']
+    NSpin: typeof import('naive-ui')['NSpin']
+    RouterLink: typeof import('vue-router')['RouterLink']
+    RouterView: typeof import('vue-router')['RouterView']
+  }
+}

+ 7 - 0
src/config/index.ts

@@ -0,0 +1,7 @@
+/** 与 gypsy-crm-frontend-admin config 中的 Code 对齐 */
+export const ApiCode = {
+  StatusOK: 200,
+  StatusFail: 400,
+  StatusSessionExpire: 600,
+  StatusSNotFound: 404,
+} as const

+ 39 - 0
src/i18n/index.ts

@@ -0,0 +1,39 @@
+import { createI18n } from 'vue-i18n'
+import enUS from '@/locales/en-US'
+import zhCN from '@/locales/zh-CN'
+
+const STORAGE_KEY = 'vaultody-lang'
+
+export type AppLocale = 'zh-CN' | 'en-US'
+
+export function getInitialLocale(): AppLocale {
+  try {
+    const s = localStorage.getItem(STORAGE_KEY)
+    if (s === 'zh-CN' || s === 'en-US') return s
+  } catch {
+    /* ignore */
+  }
+  if (typeof navigator !== 'undefined' && navigator.language.toLowerCase().startsWith('zh')) {
+    return 'zh-CN'
+  }
+  return 'en-US'
+}
+
+export function persistLocale(locale: AppLocale): void {
+  try {
+    localStorage.setItem(STORAGE_KEY, locale)
+    sessionStorage.setItem('lang', locale === 'zh-CN' ? 'zh-CN' : 'en')
+  } catch {
+    /* ignore */
+  }
+}
+
+export const i18n = createI18n({
+  legacy: false,
+  locale: getInitialLocale(),
+  fallbackLocale: 'en-US',
+  messages: {
+    'zh-CN': zhCN,
+    'en-US': enUS,
+  },
+})

+ 383 - 0
src/layouts/BasicLayout.vue

@@ -0,0 +1,383 @@
+<script setup lang="ts">
+import { computed, onMounted, ref } from 'vue'
+import { useRoute, useRouter } from 'vue-router'
+import type { DropdownOption } from 'naive-ui'
+import type { AppLocale } from '@/i18n'
+import { persistLocale } from '@/i18n'
+import { LOCAL_MENU_GROUPS } from '@/router/localMenu'
+import { filterDisplayByRegisteredRoutes } from '@/router/routeRegistry'
+import { useAuthStore } from '@/store/auth'
+import { useVaultStore } from '@/store/vault'
+import { createMenuTranslator, displayToMenuOptions } from '@/utils/menu'
+
+const route = useRoute()
+const router = useRouter()
+const { t, te, locale } = useI18n()
+const auth = useAuthStore()
+const vault = useVaultStore()
+const message = useMessage()
+const dialog = useDialog()
+
+const collapsed = ref(false)
+
+const menuOptions = computed(() => {
+  // 动态菜单(后端 user.display):后续恢复时改用 permission.menus
+  // const permission = usePermissionStore()
+  // const filtered = filterDisplayByRegisteredRoutes(permission.menus, router)
+  const filtered = filterDisplayByRegisteredRoutes(LOCAL_MENU_GROUPS, router)
+  const translate = createMenuTranslator(t, te)
+  return displayToMenuOptions(filtered, translate)
+})
+
+const selectedKey = computed(() => {
+  const p = route.path
+  return p === '/' ? '/vaultList' : p
+})
+
+const pageTitle = computed(() => {
+  const tk = route.meta.titleKey as string | undefined
+  if (tk) return t(tk)
+  return (route.meta.title as string) || t('page.workbench')
+})
+
+const initials = computed(() => {
+  const n = (auth.user?.name || 'User').trim()
+  const parts = n.split(/\s+/)
+  if (parts.length >= 2 && parts[0] && parts[1]) {
+    return (parts[0][0] + parts[1][0]).toUpperCase()
+  }
+  return n.slice(0, 2).toUpperCase() || 'U'
+})
+
+function onMenuUpdate(key: string) {
+  if (key.startsWith('group-')) return
+  if (key && key !== '#') {
+    router.push(key).catch(() => {})
+  }
+}
+
+function confirmLogout() {
+  dialog.warning({
+    title: t('dialog.logoutTitle'),
+    content: t('dialog.logoutConfirm'),
+    positiveText: t('dialog.logoutOk'),
+    negativeText: t('dialog.logoutCancel'),
+    onPositiveClick: async () => {
+      const ok = await auth.logout()
+      if (ok) {
+        message.success(t('dialog.logoutSuccess'))
+        await router.replace('/login')
+      } else {
+        message.error(t('dialog.logoutFail'))
+      }
+    },
+  })
+}
+
+const userDropdownOptions = computed<DropdownOption[]>(() => [
+  { key: 'logout', label: t('dialog.logout') },
+])
+
+function onUserSelect(key: string) {
+  if (key === 'logout') confirmLogout()
+}
+
+const langOptions = [
+  { label: '中文', value: 'zh-CN' as AppLocale },
+  { label: 'English', value: 'en-US' as AppLocale },
+]
+
+function onLocaleChange(v: AppLocale) {
+  locale.value = v
+  persistLocale(v)
+}
+
+function onVaultChange(v: string | number | null) {
+  vault.setCurrentVaultId(v)
+}
+
+onMounted(async () => {
+  const ok = await vault.loadVaults()
+  if (!ok) message.error(t('layout.vaultLoadFail'))
+})
+
+/** 右侧主区:纵向 flex,避免与侧栏一起撑高整页 */
+const mainLayoutContentStyle = {
+  display: 'flex',
+  flexDirection: 'column' as const,
+  flex: '1 1 auto',
+  minWidth: 0,
+  minHeight: 0,
+  overflow: 'hidden',
+}
+</script>
+
+<template>
+  <div class="layout-viewport">
+    <!-- Naive UI:带侧栏的根布局用 absolute 铺满父级,由父级固定高度,避免整页 document 滚动 -->
+    <n-layout
+      has-sider
+      position="absolute"
+      class="shell-root"
+      content-class="shell-root-scroll"
+    >
+      <n-layout-sider
+        v-model:collapsed="collapsed"
+        collapse-mode="width"
+        :collapsed-width="72"
+        :width="248"
+        show-trigger
+        class="shell-sider"
+      >
+        <div class="sider-inner">
+          <div class="brand">
+            <img class="brand-logo" src="/log.jpg" alt="Vaultody" />
+          </div>
+          <div class="sider-menu-wrap">
+            <n-menu
+              :value="selectedKey"
+              :options="menuOptions"
+              :collapsed="collapsed"
+              :collapsed-width="72"
+              :indent="18"
+              @update:value="onMenuUpdate"
+            />
+          </div>
+        </div>
+      </n-layout-sider>
+
+      <n-layout embedded class="shell-main" :content-style="mainLayoutContentStyle">
+        <n-layout-header class="shell-header">
+          <h1 class="shell-title">{{ pageTitle }}</h1>
+          <div class="shell-actions">
+            <n-select
+              class="vault-select"
+              size="small"
+              :value="vault.currentVaultId"
+              :options="vault.selectOptions"
+              :loading="vault.loading"
+              :placeholder="t('layout.vault')"
+              :consistent-menu-width="false"
+              :show-checkmark="false"
+              filterable
+              @update:value="onVaultChange"
+            />
+            <n-select
+              class="lang-select"
+              size="small"
+              :value="locale"
+              :options="langOptions"
+              :consistent-menu-width="false"
+              :show-checkmark="false"
+              @update:value="onLocaleChange"
+            />
+            <n-dropdown
+              trigger="click"
+              placement="bottom-end"
+              :options="userDropdownOptions"
+              @select="onUserSelect"
+            >
+              <n-button quaternary class="user-trigger" :focusable="false">
+                <n-avatar round size="small" class="user-avatar">{{ initials }}</n-avatar>
+                <span class="user-name">{{ auth.user?.name || 'User' }}</span>
+              </n-button>
+            </n-dropdown>
+          </div>
+        </n-layout-header>
+
+        <n-layout-content
+          class="shell-content"
+          :native-scrollbar="true"
+          content-class="shell-content-scroll"
+        >
+          <div class="shell-inner">
+            <router-view />
+          </div>
+        </n-layout-content>
+      </n-layout>
+    </n-layout>
+  </div>
+</template>
+
+<style scoped>
+/* 视口层:锁定高度,内部由 Naive Layout 分区滚动 */
+.layout-viewport {
+  position: relative;
+  width: 100%;
+  height: 100vh;
+  height: 100dvh;
+  overflow: hidden;
+  background: var(--vt-bg-page);
+}
+
+.shell-root {
+  background: var(--vt-bg-page);
+}
+
+/* 根布局:不整体纵向滚动,仅侧栏 / 主内容分区滚动 */
+.shell-root :deep(.shell-root-scroll) {
+  display: flex;
+  flex-direction: row;
+  align-items: stretch;
+  height: 100%;
+  overflow: hidden !important;
+}
+
+.shell-sider {
+  background: var(--vt-bg-sider) !important;
+  box-shadow: var(--vt-shadow-sider);
+}
+
+/* 侧栏:外层不滚;仅下方菜单区滚动 */
+.shell-sider :deep(.n-layout-sider-scroll-container) {
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+  max-height: 100%;
+  overflow: hidden !important;
+}
+
+.sider-inner {
+  flex: 1 1 auto;
+  min-height: 0;
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+}
+
+.sider-menu-wrap {
+  flex: 1 1 auto;
+  min-height: 0;
+  overflow-x: hidden;
+  overflow-y: auto;
+  -webkit-overflow-scrolling: touch;
+}
+
+.brand {
+  flex-shrink: 0;
+  height: 56px;
+  padding: 0 12px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  box-shadow: 0 1px 0 var(--vt-divider);
+}
+
+.brand-logo {
+  display: block;
+  height: 36px;
+  width: auto;
+  max-width: 100%;
+  object-fit: contain;
+  object-position: center;
+}
+
+/* 右侧:占满剩余宽度 */
+.shell-main {
+  flex: 1 1 auto !important;
+  min-width: 0 !important;
+  min-height: 0 !important;
+  background: var(--vt-bg-page);
+}
+
+/* 右侧内层:顶栏固定高度,下面内容区可滚 */
+.shell-main :deep(> .n-layout-scroll-container) {
+  display: flex;
+  flex-direction: column;
+  min-height: 0;
+  height: 100%;
+  overflow: hidden;
+}
+
+.shell-header {
+  flex-shrink: 0;
+  height: 56px;
+  padding: 0 24px;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 16px;
+  background: var(--vt-bg-elevated);
+  box-shadow: var(--vt-shadow-header);
+}
+
+.shell-title {
+  margin: 0;
+  font-size: 17px;
+  font-weight: 600;
+  letter-spacing: -0.02em;
+  color: var(--vt-text);
+}
+
+.shell-actions {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+}
+
+.lang-select {
+  width: 108px;
+}
+
+.vault-select {
+  min-width: 80px;
+  max-width: 130px;
+}
+
+.user-trigger {
+  height: 40px;
+  padding: 0 10px 0 6px;
+  border-radius: 10px;
+}
+
+.user-trigger:hover {
+  background: rgba(15, 23, 42, 0.05) !important;
+}
+
+.user-avatar {
+  background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%) !important;
+  color: #fff !important;
+  font-size: 12px;
+  font-weight: 600;
+}
+
+.user-name {
+  max-width: 160px;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  font-size: 14px;
+  font-weight: 500;
+  color: var(--vt-text);
+}
+
+/* 主内容:吃掉剩余高度,仅此处纵向滚动 */
+.shell-content {
+  flex: 1 1 auto;
+  min-height: 0;
+}
+
+.shell-content :deep(.shell-content-scroll) {
+  display: flex;
+  flex-direction: column;
+  min-height: 0;
+  height: 100%;
+  overflow: auto;
+}
+
+.shell-inner {
+  flex: 1 1 auto;
+  width: 100%;
+  min-height: 100%;
+  padding: 15px;
+  box-sizing: border-box;
+  display: flex;
+  flex-direction: column;
+}
+
+.user-trigger :deep(.n-button__content) {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+</style>

+ 60 - 0
src/locales/en-US.ts

@@ -0,0 +1,60 @@
+export default {
+  page: {
+    dashboard: 'Dashboard',
+    workbench: 'Workbench',
+  },
+  dash: {
+    greeting: 'Hello, {name}',
+    lead: 'This is your workspace. Use the menu on the left to open modules.',
+    cardOverview: 'Overview',
+    cardOverviewBody: 'Add metrics, tasks, or shortcuts here later.',
+    cardPerm: 'Permissions',
+    cardPermBody: 'Menus and actions follow your account settings and update when admins change them.',
+  },
+  layout: {
+    lang: 'Language',
+    vault: 'Vault',
+    vaultLoadFail: 'Failed to load vault list',
+  },
+  dialog: {
+    logout: 'Sign out',
+    logoutTitle: 'Sign out',
+    logoutConfirm: 'Are you sure you want to sign out?',
+    logoutOk: 'Sign out',
+    logoutCancel: 'Cancel',
+    logoutSuccess: 'Signed out',
+    logoutFail: 'Sign out failed. Try again later.',
+  },
+  menu: {
+    'R-Vault': 'Vault',
+    'R-VaultList': 'Vault List',
+  },
+  vaultTx: {
+    cardTitle: 'On-chain transactions',
+    cardSubtitle: 'Transaction history for the selected vault',
+    rowCountLabel: 'Rows',
+    selectVaultHint: 'Select a vault in the header first',
+    loadFail: 'Failed to load transactions',
+    export: 'Export',
+    exportSuccess: 'Download started',
+    exportFail: 'Export failed. Try again later.',
+    empty: 'No transactions',
+    boolYes: 'Yes',
+    boolNo: 'No',
+    colStatus: 'Status',
+    colCreated: 'Created',
+    colSenderAddr: 'Sender address',
+    colSenderInternal: 'Sender internal',
+    colSenderUnit: 'Sender asset',
+    colSenderAmount: 'Sender amount',
+    colRecipientAddr: 'Recipient address',
+    colRecipientInternal: 'Recipient internal',
+    colRecipientUnit: 'Recipient asset',
+    colRecipientAmount: 'Recipient amount',
+    colBlockchain: 'Blockchain',
+    colBlockHeight: 'Block height',
+    colFee: 'Network fee',
+    colFeeUnit: 'Fee asset',
+    colTxId: 'Transaction hash',
+  },
+}

+ 56 - 0
src/locales/zh-CN.ts

@@ -0,0 +1,56 @@
+export default {
+  dash: {
+    greeting: '你好,{name}',
+    lead: '这是你的工作台,可从左侧菜单进入各业务模块。',
+    cardOverview: '今日概览',
+    cardOverviewBody: '后续可在此接入关键指标、待办或快捷入口。',
+    cardPerm: '权限说明',
+    cardPermBody: '你能看到的菜单与操作由管理员配置,会随账号自动更新。',
+  },
+  layout: {
+    lang: '语言',
+    vault: '金库',
+    vaultLoadFail: '金库列表加载失败',
+  },
+  dialog: {
+    logout: '退出登录',
+    logoutTitle: '退出登录',
+    logoutConfirm: '确定要退出当前账号吗?',
+    logoutOk: '退出',
+    logoutCancel: '取消',
+    logoutSuccess: '已安全退出',
+    logoutFail: '退出失败,请稍后重试',
+  },
+  menu: {
+    'R-Vault': '保险柜',
+    'R-VaultList': '保险柜列表',
+  },
+  vaultTx: {
+    cardTitle: '链上交易',
+    cardSubtitle: '当前金库的交易流水',
+    rowCountLabel: '条数',
+    selectVaultHint: '请先在顶部选择金库',
+    loadFail: '交易列表加载失败',
+    export: '导出',
+    exportSuccess: '已开始下载导出文件',
+    exportFail: '导出失败,请稍后重试',
+    empty: '暂无交易记录',
+    boolYes: '是',
+    boolNo: '否',
+    colStatus: '交易状态',
+    colCreated: '创建时间',
+    colSenderAddr: '转出钱包地址',
+    colSenderInternal: '转出是否内部钱包',
+    colSenderUnit: '转出币种',
+    colSenderAmount: '转出金额',
+    colRecipientAddr: '收款钱包地址',
+    colRecipientInternal: '收款是否内部钱包',
+    colRecipientUnit: '收款币种',
+    colRecipientAmount: '收到金额',
+    colBlockchain: '所属区块链',
+    colBlockHeight: '区块高度',
+    colFee: '区块链手续费',
+    colFeeUnit: '手续费币种',
+    colTxId: '链上交易哈希',
+  },
+}

+ 27 - 0
src/main.ts

@@ -0,0 +1,27 @@
+import { createApp } from 'vue'
+import { createPinia } from 'pinia'
+import App from './App.vue'
+import { getInitialLocale, i18n, persistLocale } from './i18n'
+import router from './router'
+import { setSessionExpireHandler } from './service/http'
+import { useAuthStore } from './store/auth'
+
+import './style.css'
+
+persistLocale(getInitialLocale())
+
+const app = createApp(App)
+const pinia = createPinia()
+
+app.use(pinia)
+app.use(i18n)
+app.use(router)
+
+setSessionExpireHandler(() => {
+  const auth = useAuthStore()
+  auth.clearSession()
+  auth.setExpire(true)
+  void router.replace({ path: '/login' })
+})
+
+app.mount('#app')

+ 44 - 0
src/router/guard.ts

@@ -0,0 +1,44 @@
+import type { Router } from 'vue-router'
+import { useAuthStore } from '@/store/auth'
+// import { usePermissionStore } from '@/store/permission'
+import { loggedIn } from '@/utils/auth'
+import { urlLooksLikeTokenBootstrap } from '@/utils/urlToken'
+
+export function setupRouterGuard(router: Router): void {
+  router.beforeEach((to, _from, next) => {
+    const auth = useAuthStore()
+    auth.hydrateFromSession()
+    // 动态菜单 session 恢复(与 initFromUserDisplay 配套,恢复后端菜单时一并启用)
+    // usePermissionStore().hydrateFromSession()
+
+    if (to.path === '/login' && loggedIn() && !urlLooksLikeTokenBootstrap()) {
+      next({ path: '/' })
+      return
+    }
+
+    if (to.matched.some((r) => r.meta.requiresAuth)) {
+      if (!loggedIn()) {
+        next({ path: '/login', query: { redirect: to.fullPath } })
+        return
+      }
+    }
+    next()
+  })
+
+  /** 每次进入需登录的页面后拉取最新 /user/info,权限变更无需重新登录 */
+  router.afterEach((to) => {
+    if (!to.matched.some((r) => r.meta.requiresAuth)) return
+    if (!loggedIn()) return
+    void useAuthStore().refreshUserInfoFromServer()
+  })
+
+  if (typeof document !== 'undefined') {
+    document.addEventListener('visibilitychange', () => {
+      if (document.visibilityState !== 'visible') return
+      const to = router.currentRoute.value
+      if (!to.matched.some((r) => r.meta.requiresAuth)) return
+      if (!loggedIn()) return
+      void useAuthStore().refreshUserInfoFromServer()
+    })
+  }
+}

+ 32 - 0
src/router/index.ts

@@ -0,0 +1,32 @@
+import { createRouter, createWebHashHistory } from 'vue-router'
+import { setupRouterGuard } from './guard'
+
+const router = createRouter({
+  history: createWebHashHistory(import.meta.env.BASE_URL),
+  routes: [
+    {
+      path: '/login',
+      name: 'Login',
+      component: () => import('@/views/login/LoginView.vue'),
+      meta: { requiresAuth: false },
+    },
+    {
+      path: '/',
+      component: () => import('@/layouts/BasicLayout.vue'),
+      meta: { requiresAuth: true },
+      redirect: '/vaultList',
+      children: [
+        {
+          path: 'vaultList',
+          name: 'vaultList',
+          component: () => import('@/views/Vault/vaultList.vue'),
+          meta: { requiresAuth: true, titleKey: 'menu.R-VaultList' },
+        },
+      ],
+    },
+  ],
+})
+
+setupRouterGuard(router)
+
+export default router

+ 13 - 0
src/router/localMenu.ts

@@ -0,0 +1,13 @@
+import type { MenuGroupItem } from '@/types/user'
+
+/**
+ * 本地静态侧栏菜单(与 `router/index` 中已注册路由一致)。
+ * 恢复后端 user.display 驱动菜单时,改回使用 permission store,本文件可继续作为对照或合并进配置。
+ */
+export const LOCAL_MENU_GROUPS: MenuGroupItem[] = [
+  {
+    name: 'R-Vault',
+    show: true,
+    children: [{ name: 'R-VaultList', show: true, link: '/vaultList' }],
+  },
+]

+ 48 - 0
src/router/routeRegistry.ts

@@ -0,0 +1,48 @@
+import type { Router } from 'vue-router'
+import type { MenuGroupItem } from '@/types/user'
+
+/** 将后端菜单 link 规范为与 vue-router 一致的 path(hash 模式下仍用 pathname) */
+export function normalizeMenuLink(link: string): string {
+  let s = (link || '').trim()
+  if (!s) return '/'
+  if (s.startsWith('#')) s = s.slice(1)
+  if (!s.startsWith('/')) s = `/${s}`
+  const q = s.indexOf('?')
+  return q >= 0 ? s.slice(0, q) : s
+}
+
+/**
+ * 判断 link 是否对应本地已注册且具备页面组件的路由(避免点到空白页)。
+ */
+export function isRouteRegistered(router: Router, link: string): boolean {
+  const path = normalizeMenuLink(link)
+  if (path === '/login') return false
+  const resolved = router.resolve(path)
+  if (resolved.matched.length === 0) return false
+  const last = resolved.matched[resolved.matched.length - 1]
+  const comp = last.components?.default
+  return typeof comp === 'function' || (typeof comp === 'object' && comp !== null)
+}
+
+/**
+ * 仅保留本地有路由的子菜单;无可用子项的分组不展示。
+ */
+export function filterDisplayByRegisteredRoutes(
+  groups: MenuGroupItem[],
+  router: Router,
+): MenuGroupItem[] {
+  const out: MenuGroupItem[] = []
+  for (const g of groups) {
+    if (!g.show) continue
+    if (g.name === 'R-Shop') continue
+    const children =
+      g.children?.filter((c) => {
+        if (!c.show || c.name === 'R-System-QAList') return false
+        return isRouteRegistered(router, c.link)
+      }) ?? []
+    if (children.length) {
+      out.push({ ...g, children })
+    }
+  }
+  return out
+}

+ 31 - 0
src/service/auth.ts

@@ -0,0 +1,31 @@
+import { postJson } from './http'
+import type { UserInfo } from '@/types/user'
+
+export interface LoginBody {
+  loginName: string
+  password: string
+  gaCode?: string
+  emailCode?: string
+}
+
+/** 暂不使用:登录前校验是否需要二步(邮箱 / GA),对应 POST /user/login/valid */
+// export async function loginValid(body: Pick<LoginBody, 'loginName' | 'password'>) {
+//   return postJson<number>('/user/login/valid', body)
+// }
+
+export async function login(body: LoginBody) {
+  return postJson<string>('/user/login', body)
+}
+
+export async function fetchUserInfo() {
+  return postJson<UserInfo>('/user/info', {})
+}
+
+export async function logoutRequest() {
+  return postJson<unknown>('/user/logout', {})
+}
+
+/** 暂不使用:发送登录邮箱验证码,对应 POST /user/login/send/code */
+// export async function sendLoginCode(body: { loginName: string; password: string }) {
+//   return postJson<unknown>('/user/login/send/code', body)
+// }

+ 91 - 0
src/service/http.ts

@@ -0,0 +1,91 @@
+import axios, { type AxiosInstance, type AxiosResponse } from 'axios'
+import { ApiCode } from '@/config'
+import type { ApiResponse } from '@/types/api'
+
+let sessionExpireHandler: (() => void) | null = null
+
+export function setSessionExpireHandler(fn: () => void): void {
+  sessionExpireHandler = fn
+}
+
+function applyDefaultHeaders(instance: AxiosInstance): void {
+  const token = sessionStorage.getItem('access_token')
+  if (token) {
+    instance.defaults.headers.common['Access-Token'] = token
+  }
+  const lang = sessionStorage.getItem('lang')
+  if (lang) {
+    instance.defaults.headers.common['Language'] = lang
+  }
+  const client = sessionStorage.getItem('CLIENT')
+  if (client) {
+    instance.defaults.headers.common['CLIENT'] = client
+  }
+  instance.defaults.headers.common['X-System'] = 'A'
+}
+
+export const http = axios.create({
+  baseURL: import.meta.env.VITE_API_HOST82,
+  timeout: 120000,
+})
+
+applyDefaultHeaders(http)
+
+http.interceptors.request.use((config) => {
+  const token = sessionStorage.getItem('access_token')
+  if (token) {
+    config.headers['Access-Token'] = token
+  }
+  const lang = sessionStorage.getItem('lang')
+  if (lang) {
+    config.headers['Language'] = lang
+  }
+  const client = sessionStorage.getItem('CLIENT')
+  if (client) {
+    config.headers['CLIENT'] = client
+  }
+  config.headers['X-System'] = 'A'
+  return config
+})
+
+http.interceptors.response.use(
+  (response) => {
+    const data = response.data as ApiResponse
+    if (data?.code === ApiCode.StatusSessionExpire) {
+      sessionExpireHandler?.()
+      return Promise.reject(new Error('SESSION_EXPIRE'))
+    }
+    if (data?.code === ApiCode.StatusSNotFound) {
+      return {
+        ...response,
+        data: {
+          code: ApiCode.StatusFail,
+          msg: 'System error',
+          data: null,
+        } as ApiResponse,
+      }
+    }
+    return response
+  },
+  (error) =>
+    Promise.resolve({
+      data: {
+        code: ApiCode.StatusFail,
+        msg: 'System error',
+        data: null,
+      } as ApiResponse,
+      status: 200,
+      statusText: 'OK',
+      headers: {},
+      config: error?.config ?? ({} as never),
+    } as AxiosResponse<ApiResponse>)
+)
+
+export function syncTokenToHttpDefaults(): void {
+  applyDefaultHeaders(http)
+}
+
+export async function postJson<T>(url: string, body: object = {}): Promise<ApiResponse<T>> {
+  const res = await http.post<ApiResponse<T>>(url, body)
+  return res.data
+}

+ 40 - 0
src/service/salesExchange.ts

@@ -0,0 +1,40 @@
+/**
+ * 与 xsSigin.js 一致:用 URL 中的 access_token 调 Host80 接口换取正式 token。
+ */
+import axios from 'axios'
+import { ApiCode } from '@/config'
+import type { ApiResponse } from '@/types/api'
+
+interface ExchangeData {
+  accessToken: string
+}
+
+export async function exchangeSalesAccessToken(rawToken: string): Promise<ApiResponse<ExchangeData>> {
+  const url = `${import.meta.env.VITE_API_HOST82}/custom/sales/node/list`
+  const headers: Record<string, string> = {
+    'Access-Token': rawToken,
+    'X-System': 'A',
+  }
+  const lang = sessionStorage.getItem('lang')
+  if (lang) headers['Language'] = lang
+  const client = sessionStorage.getItem('CLIENT')
+  if (client) headers['CLIENT'] = client
+
+  try {
+    const response = await axios.post<ApiResponse<ExchangeData>>(url, {}, { headers })
+    const data = response.data
+    if (data.code === ApiCode.StatusSessionExpire) {
+      return { code: ApiCode.StatusSessionExpire, msg: data.msg, data: data.data }
+    }
+    if (data.code === ApiCode.StatusSNotFound) {
+      return { code: ApiCode.StatusFail, msg: 'System error', data: { accessToken: '' } }
+    }
+    return data
+  } catch {
+    return {
+      code: ApiCode.StatusFail,
+      msg: 'System error',
+      data: { accessToken: '' },
+    }
+  }
+}

+ 82 - 0
src/service/vault.ts

@@ -0,0 +1,82 @@
+import { ApiCode } from '@/config'
+import type { ApiResponse } from '@/types/api'
+import type { VaultListItem, VaultTransactionItem } from '@/types/vault'
+import { http, postJson } from './http'
+
+export function fetchVaultsList() {
+  return postJson<VaultListItem[]>('/vaultody/vaults/list', {})
+}
+
+export interface FetchVaultTransactionsBody {
+  vaultId?: string | number | null
+  /** 页码,从 1 开始 */
+  page?: number
+  pageSize?: number
+}
+
+export interface VaultTransactionsResult {
+  list: VaultTransactionItem[]
+  /** 总条数,用于分页;若后端暂未返回可先不传,前端会按当前页条数展示 */
+  total?: number
+}
+
+function buildVaultTransactionsPayload(body: FetchVaultTransactionsBody): Record<string, unknown> {
+  const payload: Record<string, unknown> = {}
+  if (body.vaultId != null) payload.vaultId = body.vaultId
+  if (body.page != null) payload.page = body.page
+  if (body.pageSize != null) payload.pageSize = body.pageSize
+  return payload
+}
+
+export function fetchVaultTransactions(body: FetchVaultTransactionsBody = {}) {
+  return postJson<VaultTransactionsResult>('/vaultody/vaults/transactions', buildVaultTransactionsPayload(body))
+}
+
+function parseFilenameFromContentDisposition(cd: string | undefined): string | null {
+  if (!cd) return null
+  const m = /filename\*=UTF-8''([^;\n]+)|filename="?([^";\n]+)"?/i.exec(cd)
+  const raw = m?.[1] ?? m?.[2]
+  if (!raw) return null
+  try {
+    return decodeURIComponent(raw.trim())
+  } catch {
+    return raw.trim()
+  }
+}
+
+async function tryParseJsonErrorBlob(blob: Blob): Promise<string | null> {
+  const text = await blob.text()
+  try {
+    const json = JSON.parse(text) as ApiResponse
+    if (typeof json.code === 'number' && json.code !== ApiCode.StatusOK) {
+      return json.msg || 'Export failed'
+    }
+  } catch {
+    /* not JSON */
+  }
+  return null
+}
+
+/** POST /vaultody/export,请求体与列表接口一致;成功返回文件 blob */
+export async function exportVaultTransactions(
+  body: FetchVaultTransactionsBody = {}
+): Promise<{ blob: Blob; filename: string | null } | { error: string }> {
+  const res = await http.post('/vaultody/export', buildVaultTransactionsPayload(body), {
+    responseType: 'blob',
+  })
+  const raw = res.data
+  if (!(raw instanceof Blob)) {
+    const api = raw as ApiResponse
+    return { error: api?.msg ?? 'Export failed' }
+  }
+  const blob = raw
+  const ct = String(res.headers['content-type'] ?? res.headers['Content-Type'] ?? '').toLowerCase()
+  if (ct.includes('application/json') || ct.includes('text/json')) {
+    const err = await tryParseJsonErrorBlob(blob)
+    return { error: err ?? 'Export failed' }
+  }
+  const filename = parseFilenameFromContentDisposition(
+    res.headers['content-disposition'] ?? res.headers['Content-Disposition']
+  )
+  return { blob, filename }
+}

+ 132 - 0
src/store/auth.ts

@@ -0,0 +1,132 @@
+import { defineStore } from 'pinia'
+import { ApiCode } from '@/config'
+import { fetchUserInfo, login, logoutRequest, type LoginBody } from '@/service/auth'
+// import { loginValid } from '@/service/auth' // 恢复二步登录时取消注释
+import { syncTokenToHttpDefaults } from '@/service/http'
+import type { UserInfo } from '@/types/user'
+import { loggedIn } from '@/utils/auth'
+import { session } from '@/utils/session'
+import { usePermissionStore } from './permission'
+import { useVaultStore } from './vault'
+
+export const useAuthStore = defineStore('auth', {
+  state: () => ({
+    token: null as string | null,
+    user: null as UserInfo | null,
+    expire: false,
+    /** 防止多处同时触发 info 请求 */
+    userInfoRefreshInFlight: false,
+  }),
+  getters: {
+    isLoggedIn: (s) => !!s.user && session.IsExist('user'),
+  },
+  actions: {
+    hydrateFromSession(): void {
+      this.token = sessionStorage.getItem('access_token')
+      const raw = session.Get('user', true)
+      if (raw) {
+        try {
+          this.user = JSON.parse(raw) as UserInfo
+        } catch {
+          this.user = null
+        }
+      } else {
+        this.user = null
+      }
+      // 动态侧栏:由后端 user.display 写入 permission store(恢复时取消注释)
+      // if (this.user?.display) {
+      //   usePermissionStore().initFromUserDisplay(this.user.display)
+      // }
+      syncTokenToHttpDefaults()
+    },
+
+    setAccessToken(token: string): void {
+      this.token = token
+      sessionStorage.setItem('access_token', token)
+      syncTokenToHttpDefaults()
+    },
+
+    setUserInfo(user: UserInfo): void {
+      this.user = user
+      session.Set('user', JSON.stringify(user), true)
+      // if (user.display) {
+      //   usePermissionStore().initFromUserDisplay(user.display)
+      // }
+    },
+
+    setExpire(v: boolean): void {
+      this.expire = v
+    },
+
+    async fetchAndStoreUser(): Promise<boolean> {
+      try {
+        const res = await fetchUserInfo()
+        if (res.code === ApiCode.StatusOK && res.data) {
+          this.setUserInfo(res.data as UserInfo)
+          this.setExpire(false)
+          return true
+        }
+        return false
+      } catch {
+        return false
+      }
+    },
+
+    /**
+     * 全局静默同步用户信息。
+     * (恢复 display 驱动菜单时,setUserInfo 内 initFromUserDisplay 一并恢复。)
+     */
+    async refreshUserInfoFromServer(): Promise<void> {
+      if (!sessionStorage.getItem('access_token')) return
+      if (!loggedIn()) return
+      if (this.userInfoRefreshInFlight) return
+      this.userInfoRefreshInFlight = true
+      try {
+        await this.fetchAndStoreUser()
+      } finally {
+        this.userInfoRefreshInFlight = false
+      }
+    },
+
+    async doPasswordLogin(body: LoginBody): Promise<boolean> {
+      try {
+        const res = await login(body)
+        if (res.code === ApiCode.StatusOK && res.data) {
+          this.setAccessToken(res.data as string)
+          return await this.fetchAndStoreUser()
+        }
+        return false
+      } catch {
+        return false
+      }
+    },
+
+    /** 暂不使用:先 /user/login/valid 再弹二步;恢复时与 LoginView 二步流程一起启用 */
+    // async validateThenOpenSecondStep(loginName: string, password: string): Promise<1 | 0 | null> {
+    //   const res = await loginValid({ loginName, password })
+    //   if (res.code === ApiCode.StatusOK && res.data !== undefined && res.data !== null) {
+    //     return res.data === 1 ? 1 : 0
+    //   }
+    //   return null
+    // },
+
+    async logout(): Promise<boolean> {
+      const res = await logoutRequest()
+      if (res.code === ApiCode.StatusOK) {
+        this.clearSession()
+        return true
+      }
+      return false
+    },
+
+    clearSession(): void {
+      sessionStorage.clear()
+      this.token = null
+      this.user = null
+      this.expire = false
+      usePermissionStore().clear()
+      useVaultStore().clear()
+      syncTokenToHttpDefaults()
+    },
+  },
+})

+ 34 - 0
src/store/permission.ts

@@ -0,0 +1,34 @@
+import { defineStore } from 'pinia'
+import type { MenuGroupItem } from '@/types/user'
+import { session } from '@/utils/session'
+
+/** 预留:后端 user.display 驱动菜单;当前侧栏使用 `router/localMenu` 静态配置。 */
+export const usePermissionStore = defineStore('permission', {
+  state: () => ({
+    menus: [] as MenuGroupItem[],
+  }),
+  actions: {
+    initFromUserDisplay(display: MenuGroupItem[] | undefined | null): void {
+      this.menus = display ?? []
+      if (this.menus.length) {
+        session.Set('menus', JSON.stringify(this.menus), true)
+      }
+    },
+
+    hydrateFromSession(): void {
+      const raw = session.Get('menus', true)
+      if (raw) {
+        try {
+          this.menus = JSON.parse(raw) as MenuGroupItem[]
+        } catch {
+          this.menus = []
+        }
+      }
+    },
+
+    clear(): void {
+      this.menus = []
+      session.Del('menus')
+    },
+  },
+})

+ 68 - 0
src/store/vault.ts

@@ -0,0 +1,68 @@
+import { defineStore } from 'pinia'
+import { ApiCode } from '@/config'
+import { fetchVaultsList } from '@/service/vault'
+import type { VaultListItem } from '@/types/vault'
+
+const STORAGE_KEY = 'vaultody-current-vault-id'
+
+function parseStoredId(raw: string): string | number {
+  const n = Number(raw)
+  if (raw !== '' && Number.isFinite(n) && String(n) === raw) return n
+  return raw
+}
+
+export const useVaultStore = defineStore('vault', {
+  state: () => ({
+    list: [] as VaultListItem[],
+    loading: false,
+    currentVaultId: null as string | number | null,
+  }),
+  getters: {
+    selectOptions: (state) =>
+      state.list.map((v) => ({ label: v.name, value: v.id })),
+  },
+  actions: {
+    hydrateFromSession(): void {
+      const raw = sessionStorage.getItem(STORAGE_KEY)
+      if (raw == null || raw === '') {
+        this.currentVaultId = null
+        return
+      }
+      this.currentVaultId = parseStoredId(raw)
+    },
+
+    setCurrentVaultId(id: string | number | null): void {
+      this.currentVaultId = id
+      if (id == null) sessionStorage.removeItem(STORAGE_KEY)
+      else sessionStorage.setItem(STORAGE_KEY, String(id))
+    },
+
+    async loadVaults(): Promise<boolean> {
+      this.loading = true
+      try {
+        const res = await fetchVaultsList()
+        if (res.code === ApiCode.StatusOK && Array.isArray(res.data)) {
+          this.list = res.data as VaultListItem[]
+          this.hydrateFromSession()
+          const valid =
+            this.currentVaultId != null &&
+            this.list.some((v) => String(v.id) === String(this.currentVaultId))
+          if (!valid) {
+            const first = this.list[0]
+            this.setCurrentVaultId(first != null ? first.id : null)
+          }
+          return true
+        }
+        return false
+      } finally {
+        this.loading = false
+      }
+    },
+
+    clear(): void {
+      this.list = []
+      this.currentVaultId = null
+      sessionStorage.removeItem(STORAGE_KEY)
+    },
+  },
+})

+ 53 - 0
src/style.css

@@ -0,0 +1,53 @@
+*,
+*::before,
+*::after {
+  box-sizing: border-box;
+}
+
+:root {
+  color-scheme: light;
+  --vt-font-sans: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
+  /* 页面底:略偏冷灰,与侧栏/卡片靠明暗区分,少画线 */
+  --vt-bg-page: #f3f4f6;
+  --vt-bg-elevated: #ffffff;
+  /* 与 public 内 logo 图白底一致,避免侧栏灰底与 JPEG 白边发灰 */
+  --vt-bg-sider: #ffffff;
+  --vt-text: #0f172a;
+  --vt-text-muted: #64748b;
+  /* 表单等仍需要可见描边时用 */
+  --vt-border: #e5e7eb;
+  /* 结构分割:极浅,避免「线框感」 */
+  --vt-divider: rgba(15, 23, 42, 0.06);
+  --vt-brand: #4f46e5;
+  --vt-brand-soft: rgba(79, 70, 229, 0.12);
+  --vt-sidebar: #0f172a;
+  --vt-shadow-header: 0 1px 0 var(--vt-divider);
+  --vt-shadow-sider: 1px 0 0 var(--vt-divider);
+  --vt-shadow-card: 0 1px 2px rgba(15, 23, 42, 0.04), 0 4px 12px rgba(15, 23, 42, 0.04);
+}
+
+html,
+body,
+#app {
+  margin: 0;
+  min-height: 100%;
+}
+
+#app {
+  width: 100%;
+}
+
+body {
+  font-family: var(--vt-font-sans);
+  font-size: 14px;
+  line-height: 1.5;
+  color: var(--vt-text);
+  background: var(--vt-bg-page);
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+
+::selection {
+  background: var(--vt-brand-soft);
+  color: var(--vt-text);
+}

+ 52 - 0
src/theme/naive.ts

@@ -0,0 +1,52 @@
+import type { GlobalThemeOverrides } from 'naive-ui'
+
+/** 与全局 CSS 变量 `--vt-*` 对齐的 Naive UI 主题覆盖 */
+export const themeOverrides: GlobalThemeOverrides = {
+  common: {
+    primaryColor: '#4f46e5',
+    primaryColorHover: '#6366f1',
+    primaryColorPressed: '#4338ca',
+    primaryColorSuppl: '#6366f1',
+    borderRadius: '10px',
+    fontSize: '14px',
+    fontSizeMedium: '14px',
+    heightMedium: '40px',
+    lineHeight: '1.5',
+    borderColor: '#e8eaed',
+  },
+  Button: {
+    fontWeight: '500',
+    borderRadiusMedium: '10px',
+  },
+  Card: {
+    borderRadius: '14px',
+    paddingMedium: '22px',
+    borderColor: 'transparent',
+    color: '#ffffff',
+    titleTextColor: '#0f172a',
+    textColor: '#64748b',
+    boxShadow: '0 1px 2px rgba(15, 23, 42, 0.04), 0 6px 16px rgba(15, 23, 42, 0.05)',
+  },
+  Input: {
+    borderRadius: '10px',
+    heightMedium: '40px',
+  },
+  Layout: {
+    color: '#f3f4f6',
+    textColor: '#0f172a',
+    headerBorderColor: 'transparent',
+    siderBorderColor: 'transparent',
+  },
+  Menu: {
+    itemColorActive: 'rgba(79, 70, 229, 0.1)',
+    itemColorActiveHover: 'rgba(79, 70, 229, 0.14)',
+    itemTextColorActive: '#4338ca',
+    itemIconColorActive: '#4f46e5',
+    itemTextColorHover: '#0f172a',
+    itemIconColorHover: '#475569',
+    itemColorHover: 'rgba(15, 23, 42, 0.05)',
+  },
+  Modal: {
+    borderRadius: '14px',
+  },
+}

+ 5 - 0
src/types/api.ts

@@ -0,0 +1,5 @@
+export interface ApiResponse<T = unknown> {
+  code: number
+  msg?: string
+  data: T
+}

+ 26 - 0
src/types/user.ts

@@ -0,0 +1,26 @@
+/** 菜单/权限树节点(与旧系统 user.display 结构兼容) */
+export interface MenuBtnMap {
+  [routeBtnKey: string]: { show: boolean }
+}
+
+export interface MenuChildItem {
+  name: string
+  show: boolean
+  link: string
+  icon?: string
+  btns?: MenuBtnMap
+}
+
+export interface MenuGroupItem {
+  name: string
+  show: boolean
+  link?: string
+  icon?: string
+  children?: MenuChildItem[]
+}
+
+export interface UserInfo {
+  name?: string
+  display: MenuGroupItem[]
+  [key: string]: unknown
+}

+ 24 - 0
src/types/vault.ts

@@ -0,0 +1,24 @@
+export interface VaultListItem {
+  id: string | number
+  name: string
+}
+
+/** `/vaultody/vaults/transactions` 列表项 */
+export interface VaultTransactionItem {
+  status?: string
+  createdTimestamp?: string | number
+  senderAddress?: string
+  senderIsVaultAddress?: boolean
+  senderAmountUnit?: string
+  senderAmount?: string | number
+  recipientAddress?: string
+  recipientIsVaultAddress?: boolean
+  recipientAmountUnit?: string
+  recipientAmount?: string | number
+  blockchain?: string
+  minedInBlockHeight?: string | number
+  feeAmount?: string | number
+  feeAmountUnit?: string
+  transactionId?: string
+  list?: []
+}

+ 6 - 0
src/utils/auth.ts

@@ -0,0 +1,6 @@
+import { session } from './session'
+
+/** 与旧系统 router loggedIn() 一致:存在加密 user 即视为已登录 */
+export function loggedIn(): boolean {
+  return session.IsExist('user')
+}

+ 33 - 0
src/utils/crypt.ts

@@ -0,0 +1,33 @@
+/**
+ * 与 gypsy-crm-frontend-admin 的 crypt.js 保持一致,保证 session 中 user 加解密互通。
+ */
+import CryptoJS from 'crypto-js'
+
+class CryptToJS {
+  private crypt = CryptoJS
+  private readonly secret = 'Believe in yourself.'
+
+  Encrypt(text: string): string {
+    return this.crypt.AES.encrypt(text, this.secret).toString()
+  }
+
+  Decrypt(text: string | null | undefined): string {
+    if (text == null || text.length === 0) {
+      return ''
+    }
+    try {
+      const decrypted = this.crypt.AES.decrypt(text, this.secret)
+      const result = decrypted.toString(CryptoJS.enc.Utf8)
+      if (result && result.length > 0) {
+        return result
+      }
+      console.warn('Decryption failed or resulted in empty string')
+      return ''
+    } catch (error) {
+      console.error('Decryption error:', error)
+      return ''
+    }
+  }
+}
+
+export const crypt = new CryptToJS()

+ 38 - 0
src/utils/menu.ts

@@ -0,0 +1,38 @@
+import type { MenuOption } from 'naive-ui'
+import { normalizeMenuLink } from '@/router/routeRegistry'
+import type { MenuGroupItem } from '@/types/user'
+import { encodeMenuNameForI18n } from '@/utils/menuKey'
+
+export function displayToMenuOptions(
+  groups: MenuGroupItem[],
+  translate: (rawName: string) => string,
+): MenuOption[] {
+  const out: MenuOption[] = []
+  for (const g of groups) {
+    if (!g.show) continue
+    if (g.name === 'R-Shop') continue
+    const children = g.children?.filter((c) => c.show && c.name !== 'R-System-QAList') ?? []
+    if (children.length) {
+      out.push({
+        label: translate(g.name),
+        key: `group-${g.name}`,
+        children: children.map((c) => ({
+          label: translate(c.name),
+          key: normalizeMenuLink(c.link),
+        })),
+      })
+    }
+  }
+  return out
+}
+
+/** 在组件内传入 useI18n 的 t / te */
+export function createMenuTranslator(
+  t: (key: string) => string,
+  te: (key: string) => boolean,
+): (rawName: string) => string {
+  return (rawName: string) => {
+    const key = `menu.${encodeMenuNameForI18n(rawName)}`
+    return te(key) ? t(key) : rawName
+  }
+}

+ 4 - 0
src/utils/menuKey.ts

@@ -0,0 +1,4 @@
+/** 菜单名中的 `.` 与 vue-i18n 路径冲突,编码为单层 key(与 locales/menu 下字段一致) */
+export function encodeMenuNameForI18n(name: string): string {
+  return name.replace(/\./g, '_')
+}

+ 52 - 0
src/utils/session.ts

@@ -0,0 +1,52 @@
+/**
+ * 与 gypsy-crm-frontend-admin 的 session.js 行为一致(加密存储 user)。
+ */
+import { crypt } from './crypt'
+
+class SessionStorage {
+  private Data = sessionStorage
+
+  Get(key: string, flag = false): string | null {
+    try {
+      let res: string | null = null
+      if (flag) {
+        const raw = this.Data[key]
+        res = raw != null ? crypt.Decrypt(raw) : null
+      } else {
+        res = this.Data[key]
+      }
+      return this.IsExist(key) ? res : null
+    } catch (error) {
+      console.error(`Error getting session data for key "${key}":`, error)
+      return null
+    }
+  }
+
+  Set(key: string, val: string, flag = false): void {
+    try {
+      let res: string
+      if (flag) {
+        res = crypt.Encrypt(String(val))
+      } else {
+        res = String(val)
+      }
+      this.Data[key] = res
+    } catch (error) {
+      console.error(`Error setting session data for key "${key}":`, error)
+    }
+  }
+
+  Del(key: string): void {
+    this.Data.removeItem(key)
+  }
+
+  IsExist(key: string): boolean {
+    return this.Data.getItem(key) != null && this.Data.getItem(key) !== ''
+  }
+
+  Clear(): void {
+    this.Data.clear()
+  }
+}
+
+export const session = new SessionStorage()

+ 41 - 0
src/utils/urlToken.ts

@@ -0,0 +1,41 @@
+/**
+ * 从 hash 或 search 解析参数(兼容旧系统 SignIn 的 URL 带 token 行为)。
+ */
+export function getQueryStringFromLocation(): string {
+  if (window.location.hash && window.location.hash.includes('?')) {
+    return window.location.hash.slice(window.location.hash.indexOf('?'))
+  }
+  return window.location.search
+}
+
+export function parseQueryParam(query: string, key: string): string | null {
+  const re = new RegExp(`[?&]${key}=([^&]*)`)
+  const match = query.match(re)
+  if (match?.[1]) {
+    let raw = match[1]
+    raw = raw.replace(/%2B/gi, '+')
+    try {
+      return decodeURIComponent(raw)
+    } catch {
+      return raw
+    }
+  }
+  return null
+}
+
+/** 是否可能正在通过 URL 完成登录(避免路由守卫在登录页误跳) */
+export function urlLooksLikeTokenBootstrap(): boolean {
+  const q = getQueryStringFromLocation()
+  return (
+    parseQueryParam(q, 'accessToken') != null ||
+    parseQueryParam(q, 'access_token') != null
+  )
+}
+
+export function applyLangFromUrl(): void {
+  const q = getQueryStringFromLocation()
+  const lang = parseQueryParam(q, 'lang')
+  if (lang) {
+    sessionStorage.setItem('sales_lang', lang)
+  }
+}

+ 598 - 0
src/views/Vault/vaultList.vue

@@ -0,0 +1,598 @@
+<script setup lang="ts">
+import { computed, h, onMounted, reactive, ref, watch } from 'vue'
+import type { DataTableColumns } from 'naive-ui'
+import { ApiCode } from '@/config'
+import { exportVaultTransactions, fetchVaultTransactions } from '@/service/vault'
+import { useVaultStore } from '@/store/vault'
+import type { VaultTransactionItem } from '@/types/vault'
+
+const { t } = useI18n()
+const message = useMessage()
+const vault = useVaultStore()
+
+const loading = ref(false)
+const exporting = ref(false)
+const rows = ref<VaultTransactionItem[]>([])
+
+const pagination = reactive({
+  page: 1,
+  pageSize: 10,
+  itemCount: 0,
+  showSizePicker: true,
+  pageSizes: [10, 20, 50],
+  onUpdatePage: (p: number) => {
+    pagination.page = p
+    void load()
+  },
+  onUpdatePageSize: (size: number) => {
+    pagination.pageSize = size
+    pagination.page = 1
+    void load()
+  },
+})
+
+function formatDash(v: unknown): string {
+  if (v == null || v === '') return '—'
+  return String(v)
+}
+
+function formatAmount(v: string | number | undefined): string {
+  if (v == null || v === '') return '—'
+  return String(v)
+}
+
+/** 兼容秒级/毫秒级时间戳与 ISO 字符串 */
+function formatTimestamp(v: string | number | undefined): string {
+  if (v == null || v === '') return '—'
+  if (typeof v === 'string' && Number.isNaN(Number(v))) {
+    const d = new Date(v)
+    return Number.isNaN(d.getTime()) ? v : d.toLocaleString()
+  }
+  const n = typeof v === 'number' ? v : Number(v)
+  if (Number.isNaN(n)) return String(v)
+  const ms = n < 1e12 ? n * 1000 : n
+  return new Date(ms).toLocaleString()
+}
+
+function boolText(v: boolean | undefined): string {
+  if (v === true) return t('vaultTx.boolYes')
+  if (v === false) return t('vaultTx.boolNo')
+  return '—'
+}
+
+/** 各列 width 之和,供 scroll-x 使用,保证超出视口时可横向滚动看全列 */
+const VAULT_TX_COL_WIDTHS = [
+  120, 186, 200, 120, 96, 140, 200, 120, 96, 140, 120, 112, 128, 96, 260,
+] as const
+
+const tableScrollX = VAULT_TX_COL_WIDTHS.reduce((a, w) => a + w, 0) + 120
+
+/** 表头与单元格均居中(titleAlign / align: center) */
+const columns = computed<DataTableColumns<VaultTransactionItem>>(() => [
+  {
+    title: () => t('vaultTx.colStatus'),
+    key: 'status',
+    width: 120,
+    titleAlign: 'center',
+    align: 'center',
+    ellipsis: { tooltip: true },
+    render: (row) =>
+      h('span', { class: 'vault-tx__cell-text vault-tx__cell-status' }, formatDash(row.status)),
+  },
+  {
+    title: () => t('vaultTx.colCreated'),
+    key: 'createdTimestamp',
+    width: 186,
+    titleAlign: 'center',
+    align: 'center',
+    render: (row) =>
+      h('span', { class: 'vault-tx__cell-text vault-tx__cell-mono' }, formatTimestamp(row.createdTimestamp)),
+  },
+  {
+    title: () => t('vaultTx.colSenderAddr'),
+    key: 'senderAddress',
+    width: 200,
+    titleAlign: 'center',
+    align: 'center',
+    ellipsis: { tooltip: true },
+    render: (row) =>
+      h('span', { class: 'vault-tx__cell-text vault-tx__cell-mono' }, formatDash(row.senderAddress)),
+  },
+  {
+    title: () => t('vaultTx.colSenderInternal'),
+    key: 'senderIsVaultAddress',
+    width: 120,
+    titleAlign: 'center',
+    align: 'center',
+    render: (row) =>
+      h('span', { class: 'vault-tx__cell-text' }, boolText(row.senderIsVaultAddress)),
+  },
+  {
+    title: () => t('vaultTx.colSenderUnit'),
+    key: 'senderAmountUnit',
+    width: 96,
+    titleAlign: 'center',
+    align: 'center',
+    ellipsis: { tooltip: true },
+    render: (row) =>
+      h('span', { class: 'vault-tx__cell-text vault-tx__cell-unit' }, formatDash(row.senderAmountUnit)),
+  },
+  {
+    title: () => t('vaultTx.colSenderAmount'),
+    key: 'senderAmount',
+    width: 140,
+    titleAlign: 'center',
+    align: 'center',
+    render: (row) =>
+      h('span', { class: 'vault-tx__cell-num' }, formatAmount(row.senderAmount)),
+  },
+  {
+    title: () => t('vaultTx.colRecipientAddr'),
+    key: 'recipientAddress',
+    width: 200,
+    titleAlign: 'center',
+    align: 'center',
+    ellipsis: { tooltip: true },
+    render: (row) =>
+      h('span', { class: 'vault-tx__cell-text vault-tx__cell-mono' }, formatDash(row.recipientAddress)),
+  },
+  {
+    title: () => t('vaultTx.colRecipientInternal'),
+    key: 'recipientIsVaultAddress',
+    width: 120,
+    titleAlign: 'center',
+    align: 'center',
+    render: (row) =>
+      h('span', { class: 'vault-tx__cell-text' }, boolText(row.recipientIsVaultAddress)),
+  },
+  {
+    title: () => t('vaultTx.colRecipientUnit'),
+    key: 'recipientAmountUnit',
+    width: 96,
+    titleAlign: 'center',
+    align: 'center',
+    ellipsis: { tooltip: true },
+    render: (row) =>
+      h('span', { class: 'vault-tx__cell-text vault-tx__cell-unit' }, formatDash(row.recipientAmountUnit)),
+  },
+  {
+    title: () => t('vaultTx.colRecipientAmount'),
+    key: 'recipientAmount',
+    width: 140,
+    titleAlign: 'center',
+    align: 'center',
+    render: (row) =>
+      h('span', { class: 'vault-tx__cell-num' }, formatAmount(row.recipientAmount)),
+  },
+  {
+    title: () => t('vaultTx.colBlockchain'),
+    key: 'blockchain',
+    width: 120,
+    titleAlign: 'center',
+    align: 'center',
+    ellipsis: { tooltip: true },
+    render: (row) =>
+      h('span', { class: 'vault-tx__cell-text' }, formatDash(row.blockchain)),
+  },
+  {
+    title: () => t('vaultTx.colBlockHeight'),
+    key: 'minedInBlockHeight',
+    width: 112,
+    titleAlign: 'center',
+    align: 'center',
+    render: (row) =>
+      h('span', { class: 'vault-tx__cell-num' }, formatDash(row.minedInBlockHeight)),
+  },
+  {
+    title: () => t('vaultTx.colFee'),
+    key: 'feeAmount',
+    width: 128,
+    titleAlign: 'center',
+    align: 'center',
+    render: (row) =>
+      h('span', { class: 'vault-tx__cell-num' }, formatAmount(row.feeAmount)),
+  },
+  {
+    title: () => t('vaultTx.colFeeUnit'),
+    key: 'feeAmountUnit',
+    width: 96,
+    titleAlign: 'center',
+    align: 'center',
+    ellipsis: { tooltip: true },
+    render: (row) =>
+      h('span', { class: 'vault-tx__cell-text vault-tx__cell-unit' }, formatDash(row.feeAmountUnit)),
+  },
+  {
+    title: () => t('vaultTx.colTxId'),
+    key: 'transactionId',
+    width: 260,
+    titleAlign: 'center',
+    align: 'center',
+    ellipsis: { tooltip: true },
+    render: (row) =>
+      h('span', { class: 'vault-tx__cell-text vault-tx__cell-mono' }, formatDash(row.transactionId)),
+  },
+])
+
+async function load(): Promise<void> {
+  if (vault.currentVaultId == null) {
+    rows.value = []
+    pagination.itemCount = 0
+    return
+  }
+  loading.value = true
+  try {
+    const res = await fetchVaultTransactions({
+      vaultId: vault.currentVaultId,
+      page: pagination.page,
+      pageSize: pagination.pageSize,
+    })
+    if (res.code === ApiCode.StatusOK && Array.isArray(res.data?.list)) {
+      rows.value = res.data.list
+      const total = res.data.total
+      pagination.itemCount =
+        typeof total === 'number' && Number.isFinite(total) ? total : res.data.list.length
+    } else {
+      rows.value = []
+      pagination.itemCount = 0
+      message.error(res.msg || t('vaultTx.loadFail'))
+    }
+  } finally {
+    loading.value = false
+  }
+}
+
+watch(
+  () => vault.currentVaultId,
+  () => {
+    pagination.page = 1
+    void load()
+  }
+)
+
+onMounted(() => {
+  void load()
+})
+
+function triggerFileDownload(blob: Blob, filename: string): void {
+  const url = URL.createObjectURL(blob)
+  const a = document.createElement('a')
+  a.href = url
+  a.download = filename
+  a.rel = 'noopener'
+  a.click()
+  URL.revokeObjectURL(url)
+}
+
+async function onExport(): Promise<void> {
+  if (vault.currentVaultId == null) return
+  exporting.value = true
+  try {
+    const res = await exportVaultTransactions({
+      vaultId: vault.currentVaultId,
+      page: pagination.page,
+      pageSize: pagination.pageSize,
+    })
+    if ('error' in res) {
+      message.error(res.error || t('vaultTx.exportFail'))
+      return
+    }
+    const name = res.filename?.trim() || `vault-transactions-${pagination.page}.xlsx`
+    triggerFileDownload(res.blob, name)
+    message.success(t('vaultTx.exportSuccess'))
+  } catch {
+    message.error(t('vaultTx.exportFail'))
+  } finally {
+    exporting.value = false
+  }
+}
+</script>
+
+<template>
+  <div class="vault-list">
+    <n-card class="vault-panel" :segmented="{ content: true }" size="medium">
+      <template #header>
+        <!-- <div class="vault-panel__header">
+          <div class="vault-panel__titles">
+            <h2 class="vault-panel__title">{{ t('vaultTx.cardTitle') }}</h2>
+            <p class="vault-panel__desc">{{ t('vaultTx.cardSubtitle') }}</p>
+          </div>
+          <div v-if="vault.currentVaultId != null" class="vault-panel__meta">
+            <span class="vault-panel__meta-label">{{ t('vaultTx.rowCountLabel') }}</span>
+            <span class="vault-panel__meta-value">{{ pagination.itemCount }}</span>
+          </div>
+        </div> -->
+      </template>
+
+      <n-spin :show="loading" class="vault-panel__spin">
+        <div v-if="vault.currentVaultId == null" class="vault-panel__empty-wrap">
+          <n-empty size="large" :description="t('vaultTx.selectVaultHint')" />
+        </div>
+        <div v-else class="vault-panel__body">
+          <div class="vault-panel__toolbar">
+            <n-button type="primary" secondary :loading="exporting" @click="onExport">
+              {{ t('vaultTx.export') }}
+            </n-button>
+          </div>
+          <div class="vault-panel__table-frame">
+            <n-data-table
+              class="vault-panel__table"
+              :columns="columns"
+              :data="rows"
+              :pagination="pagination"
+              :bordered="true"
+              striped
+              bottom-bordered
+              :single-line="false"
+              size="medium"
+              :scroll-x="tableScrollX"
+              :scrollbar-props="{ trigger: 'none' }"
+              :empty-description="t('vaultTx.empty')"
+            />
+          </div>
+          <!-- 仅占位:让白卡片铺满剩余高度,表格本身不纵向拉伸 -->
+          <div class="vault-panel__tail-spacer" aria-hidden="true" />
+        </div>
+      </n-spin>
+    </n-card>
+  </div>
+</template>
+
+<style scoped>
+.vault-list {
+  width: 100%;
+  flex: 1;
+  min-height: 0;
+  display: flex;
+  flex-direction: column;
+}
+
+.vault-panel {
+  flex: 1;
+  min-height: 0;
+  max-width: 100%;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+  background: var(--vt-bg-elevated) !important;
+  border: 1px solid var(--vt-divider) !important;
+  box-shadow: var(--vt-shadow-card);
+  --n-padding-top: 0;
+  --n-padding-bottom: 0;
+  --n-padding-left: 0;
+  --n-padding-right: 0;
+}
+
+.vault-panel :deep(.n-card-header) {
+  padding: 20px 22px 16px;
+  border-bottom: 1px solid var(--vt-divider);
+}
+
+/* 卡片内容区铺满卡片剩余高度;内边距 15px */
+.vault-panel :deep(.n-card-content) {
+  flex: 1;
+  min-height: 0;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+  padding: 15px !important;
+  box-sizing: border-box;
+}
+
+.vault-panel__header {
+  display: flex;
+  align-items: flex-start;
+  justify-content: space-between;
+  gap: 16px;
+}
+
+.vault-panel__titles {
+  min-width: 0;
+}
+
+.vault-panel__title {
+  margin: 0;
+  font-size: 17px;
+  font-weight: 600;
+  letter-spacing: -0.02em;
+  color: var(--vt-text);
+  line-height: 1.3;
+}
+
+.vault-panel__desc {
+  margin: 6px 0 0;
+  font-size: 13px;
+  line-height: 1.45;
+  color: var(--vt-text-muted);
+}
+
+.vault-panel__meta {
+  flex-shrink: 0;
+  display: inline-flex;
+  align-items: baseline;
+  gap: 6px;
+  padding: 6px 12px;
+  border-radius: 999px;
+  background: var(--vt-brand-soft);
+  border: 1px solid rgba(79, 70, 229, 0.15);
+}
+
+.vault-panel__meta-label {
+  font-size: 12px;
+  font-weight: 500;
+  color: var(--vt-text-muted);
+}
+
+.vault-panel__meta-value {
+  font-size: 15px;
+  font-weight: 700;
+  font-variant-numeric: tabular-nums;
+  color: var(--vt-brand);
+}
+
+.vault-panel__spin {
+  flex: 1;
+  min-height: 0;
+  display: flex;
+  flex-direction: column;
+}
+
+.vault-panel__spin :deep(.n-spin-content) {
+  flex: 1;
+  min-height: 0;
+  display: flex;
+  flex-direction: column;
+}
+
+.vault-panel__empty-wrap {
+  flex: 1;
+  min-height: 0;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 24px 0;
+}
+
+.vault-panel__body {
+  flex: 1;
+  min-height: 0;
+  display: flex;
+  flex-direction: column;
+  align-items: stretch;
+}
+
+.vault-panel__toolbar {
+  flex-shrink: 0;
+  display: flex;
+  justify-content: flex-end;
+  padding: 0 14px 10px;
+  box-sizing: border-box;
+}
+
+.vault-panel__table-frame {
+  flex: 0 0 auto;
+  width: 100%;
+  min-width: 0;
+  padding: 12px 14px 10px;
+  box-sizing: border-box;
+  /* border: 1px solid var(--vt-border); */
+  /* border-radius: 12px; */
+  /* background: #fafbfc; */
+  overflow-x: auto;
+  overflow-y: visible;
+}
+
+.vault-panel__tail-spacer {
+  flex: 1 1 auto;
+  min-height: 0;
+  min-width: 0;
+}
+
+.vault-panel__table {
+  width: 100%;
+  max-width: 100%;
+  min-width: 0;
+}
+
+/* 表格:表头、分隔、对齐与数字字体 */
+.vault-panel__table :deep(.n-data-table) {
+  --n-th-padding: 11px 14px;
+  --n-td-padding: 11px 14px;
+}
+
+.vault-panel__table :deep(.n-data-table-wrapper) {
+  min-width: 0;
+  border-radius: 10px;
+  overflow: hidden;
+  border: 1px solid #e5e7eb;
+  background: #fff;
+  box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
+}
+
+.vault-panel__table :deep(.n-data-table-base-table-header) {
+  border-radius: 10px 10px 0 0;
+}
+
+.vault-panel__table :deep(.n-data-table-th) {
+  text-align: center !important;
+  font-weight: 600;
+  font-size: 12px;
+  line-height: 1.35;
+  letter-spacing: 0.01em;
+  color: #475569 !important;
+  background: #f1f5f9 !important;
+  border-color: #e2e8f0 !important;
+  border-bottom: 1px solid #e2e8f0 !important;
+}
+
+.vault-panel__table :deep(.n-data-table-th__title-wrapper) {
+  justify-content: center;
+}
+
+.vault-panel__table :deep(.n-data-table-td) {
+  text-align: center !important;
+  font-size: 13px;
+  line-height: 1.45;
+  color: var(--vt-text);
+  border-color: #eef2f7 !important;
+  vertical-align: middle;
+}
+
+.vault-panel__table :deep(.n-data-table-tr:not(.n-data-table-tr--summary):hover .n-data-table-td) {
+  background: #f8fafc !important;
+}
+
+.vault-panel__table :deep(.n-data-table__pagination) {
+  padding: 12px 4px 4px;
+  margin-top: 2px;
+  border-top: 1px solid #eef2f7;
+  justify-content: flex-end;
+}
+
+.vault-panel__table :deep(.n-data-table-empty) {
+  padding: 40px 16px 48px;
+  background: #fff;
+}
+
+.vault-panel__table :deep(.n-data-table-empty .n-empty__description) {
+  margin-top: 12px;
+  font-size: 14px;
+  color: var(--vt-text-muted);
+}
+
+/* 单元格内层:与表头一致居中 */
+.vault-panel__table :deep(.vault-tx__cell-text),
+.vault-panel__table :deep(.vault-tx__cell-num) {
+  display: inline-block;
+  max-width: 100%;
+  vertical-align: middle;
+  text-align: center;
+}
+
+.vault-panel__table :deep(.vault-tx__cell-mono) {
+  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
+  font-size: 12.5px;
+  letter-spacing: -0.01em;
+}
+
+.vault-panel__table :deep(.vault-tx__cell-num) {
+  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
+  font-size: 13px;
+  font-variant-numeric: tabular-nums;
+  font-feature-settings: 'tnum' 1;
+  letter-spacing: -0.02em;
+}
+
+.vault-panel__table :deep(.vault-tx__cell-unit) {
+  font-weight: 600;
+  font-size: 12px;
+  color: #64748b;
+  text-transform: uppercase;
+}
+
+.vault-panel__table :deep(.vault-tx__cell-status) {
+  font-weight: 500;
+  font-size: 12.5px;
+  color: #0f172a;
+}
+</style>

+ 620 - 0
src/views/login/LoginView.vue

@@ -0,0 +1,620 @@
+<script setup lang="ts">
+import { onMounted, reactive, ref } from 'vue'
+// import { onUnmounted, watch } from 'vue' // 恢复二步登录时启用
+import { useRoute, useRouter } from 'vue-router'
+import type { FormInst, FormRules } from 'naive-ui'
+import { ApiCode } from '@/config'
+// import { sendLoginCode } from '@/service/auth' // 恢复二步登录时启用
+import { exchangeSalesAccessToken } from '@/service/salesExchange'
+import { useAuthStore } from '@/store/auth'
+import {
+  applyLangFromUrl,
+  getQueryStringFromLocation,
+  parseQueryParam,
+} from '@/utils/urlToken'
+
+const message = useMessage()
+const route = useRoute()
+const router = useRouter()
+const auth = useAuthStore()
+
+const loading = ref(false)
+const processingRouteParams = ref(false)
+const processingSystemToken = ref(false)
+
+const formRef = ref<FormInst | null>(null)
+const form = reactive({
+  loginName: '',
+  password: '',
+})
+
+const rules: FormRules = {
+  loginName: [{ required: true, message: '请输入账号', trigger: 'blur' }],
+  password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
+}
+
+/* ---------- 二步验证码登录(暂不使用:valid + send/code);恢复时取消本段注释并改用下方 submitLogin 为 openSecondStep ----------
+const secondOpen = ref(false)
+const secondType = ref<0 | 1>(0)
+const second = reactive({
+  emailCode: '',
+  gaCode: '',
+})
+
+const emailCodeCountdown = ref(0)
+const sendingCode = ref(false)
+let emailCountdownTimer: ReturnType<typeof setInterval> | null = null
+
+function clearEmailCountdown() {
+  if (emailCountdownTimer) {
+    clearInterval(emailCountdownTimer)
+    emailCountdownTimer = null
+  }
+  emailCodeCountdown.value = 0
+}
+
+function startEmailCountdown() {
+  clearEmailCountdown()
+  emailCodeCountdown.value = 60
+  emailCountdownTimer = setInterval(() => {
+    if (emailCodeCountdown.value <= 1) {
+      emailCodeCountdown.value = 0
+      if (emailCountdownTimer) {
+        clearInterval(emailCountdownTimer)
+        emailCountdownTimer = null
+      }
+    } else {
+      emailCodeCountdown.value -= 1
+    }
+  }, 1000)
+}
+
+watch(secondOpen, (open) => {
+  if (!open) clearEmailCountdown()
+})
+
+onUnmounted(() => {
+  clearEmailCountdown()
+})
+
+async function resendEmailCode() {
+  if (emailCodeCountdown.value > 0) return
+  sendingCode.value = true
+  try {
+    await sendLoginCode({ loginName: form.loginName, password: form.password })
+    message.success('验证码已发送')
+    startEmailCountdown()
+  } finally {
+    sendingCode.value = false
+  }
+}
+---------- */
+
+async function afterLoginSuccess() {
+  const redirect = (route.query.redirect as string) || '/'
+  await router.replace(redirect)
+}
+
+async function submitLogin() {
+  try {
+    await formRef.value?.validate()
+  } catch {
+    return
+  }
+  loading.value = true
+  try {
+    const ok = await auth.doPasswordLogin({
+      loginName: form.loginName,
+      password: form.password,
+    })
+    if (ok) {
+      message.success('登录成功')
+      await afterLoginSuccess()
+    } else {
+      message.error('登录失败')
+    }
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 与 SignIn getCurrentSystemToken:URL 中 accessToken 直接作为 CRM token */
+async function trySystemAccessTokenFromUrl(): Promise<boolean> {
+  const q = getQueryStringFromLocation()
+  const accessToken = parseQueryParam(q, 'accessToken')
+  if (!accessToken) return false
+
+  processingSystemToken.value = true
+  try {
+    auth.setAccessToken(accessToken)
+    const ok = await auth.fetchAndStoreUser()
+    if (ok) {
+      message.success('登录成功')
+      await afterLoginSuccess()
+      return true
+    }
+    message.error('获取用户信息失败')
+    return false
+  } finally {
+    processingSystemToken.value = false
+  }
+}
+
+/** 与 SignIn getRouteParams:access_token 换票后再拉用户信息 */
+async function trySalesAccessTokenFromUrl(): Promise<boolean> {
+  const q = getQueryStringFromLocation()
+  let raw = parseQueryParam(q, 'access_token')
+  if (!raw && route.query.access_token) {
+    raw = String(route.query.access_token)
+  }
+  if (!raw) return false
+
+  processingRouteParams.value = true
+  try {
+    const res = await exchangeSalesAccessToken(raw)
+    if (res.code === ApiCode.StatusOK && res.data?.accessToken) {
+      auth.setAccessToken(res.data.accessToken)
+      sessionStorage.setItem('sales_token', res.data.accessToken)
+      const ok = await auth.fetchAndStoreUser()
+      if (ok) {
+        message.success(res.msg || '登录成功')
+        await afterLoginSuccess()
+        return true
+      }
+    }
+    if (res.msg) message.error(res.msg)
+    return false
+  } finally {
+    processingRouteParams.value = false
+  }
+}
+
+async function bootstrapUrlTokens(): Promise<void> {
+  applyLangFromUrl()
+  if (await trySystemAccessTokenFromUrl()) return
+  await trySalesAccessTokenFromUrl()
+}
+
+/* ---------- 二步流程(与 validateThenOpenSecondStep / sendLoginCode 配套,暂不使用)----------
+async function openSecondStep() {
+  try {
+    await formRef.value?.validate()
+  } catch {
+    return
+  }
+  loading.value = true
+  try {
+    const flag = await auth.validateThenOpenSecondStep(form.loginName, form.password)
+    if (flag === null) {
+      message.error('校验失败')
+      return
+    }
+    secondType.value = flag === 1 ? 1 : 0
+    second.emailCode = ''
+    second.gaCode = ''
+    secondOpen.value = true
+    if (secondType.value === 0) {
+      await sendLoginCode({ loginName: form.loginName, password: form.password })
+      message.success('验证码已发送')
+      startEmailCountdown()
+    }
+  } finally {
+    loading.value = false
+  }
+}
+
+async function submitSecondStep() {
+  if (secondType.value === 1 && !second.gaCode.trim()) {
+    message.warning('请输入 GA 验证码')
+    return
+  }
+  if (secondType.value === 0 && !second.emailCode.trim()) {
+    message.warning('请输入邮箱验证码')
+    return
+  }
+  loading.value = true
+  try {
+    const body =
+      secondType.value === 1
+        ? {
+            loginName: form.loginName,
+            password: form.password,
+            gaCode: second.gaCode.trim(),
+          }
+        : {
+            loginName: form.loginName,
+            password: form.password,
+            emailCode: second.emailCode.trim(),
+          }
+    const ok = await auth.doPasswordLogin(body)
+    if (ok) {
+      message.success('登录成功')
+      secondOpen.value = false
+      await afterLoginSuccess()
+    } else {
+      message.error('登录失败')
+    }
+  } finally {
+    loading.value = false
+  }
+}
+---------- */
+
+onMounted(() => {
+  auth.hydrateFromSession()
+  void bootstrapUrlTokens()
+})
+</script>
+
+<template>
+  <div class="login-page">
+    <div v-if="processingRouteParams || processingSystemToken" class="fullscreen-loading">
+      <n-spin size="large">
+        <template #description>正在通过链接登录…</template>
+      </n-spin>
+    </div>
+
+    <template v-else>
+      <div class="login-page-backdrop" aria-hidden="true">
+        <div class="login-bg-base" />
+        <div class="login-bg-effects">
+          <div class="login-bg-aurora" />
+          <div class="login-bg-grid" />
+          <div class="login-bg-orbit" />
+        </div>
+      </div>
+
+      <main class="login-main">
+        <div class="login-card">
+          <header class="login-card-head">
+            <img class="login-brand-logo" src="/log.jpg" alt="OnchainPay" />
+            <h1 class="login-title">欢迎回来</h1>
+            <p class="login-subtitle">使用账号登录以继续</p>
+          </header>
+
+          <n-form ref="formRef" :model="form" :rules="rules" label-placement="top" size="large">
+            <n-form-item path="loginName" label="账号">
+              <n-input
+                v-model:value="form.loginName"
+                placeholder="邮箱或账号"
+                autocomplete="username"
+                @keyup.enter="submitLogin"
+              />
+            </n-form-item>
+            <n-form-item path="password" label="密码">
+              <n-input
+                v-model:value="form.password"
+                type="password"
+                show-password-on="click"
+                placeholder="输入密码"
+                autocomplete="current-password"
+                @keyup.enter="submitLogin"
+              />
+            </n-form-item>
+            <n-button type="primary" block class="login-submit" :loading="loading" @click="submitLogin">
+              登录
+            </n-button>
+          </n-form>
+        </div>
+        <p class="login-footnote">登录即表示你理解并遵守组织的安全与合规要求。</p>
+      </main>
+    </template>
+
+    <!-- 二步验证码弹窗(与 valid/send/code 配套,暂不使用)
+    <n-modal
+      v-model:show="secondOpen"
+      preset="dialog"
+      :title="secondType === 1 ? 'Google 身份验证' : '邮箱验证'"
+      :mask-closable="false"
+    >
+      <n-form v-if="secondType === 0" label-placement="top">
+        <n-form-item label="验证码">
+          <div class="email-code-row">
+            <n-input
+              v-model:value="second.emailCode"
+              placeholder="请输入邮件中的验证码"
+              maxlength="12"
+              class="email-code-input"
+            />
+            <n-button
+              quaternary
+              type="primary"
+              class="email-code-action"
+              :disabled="emailCodeCountdown > 0"
+              :loading="sendingCode"
+              @click="resendEmailCode"
+            >
+              {{ emailCodeCountdown > 0 ? `${emailCodeCountdown}s` : '获取验证码' }}
+            </n-button>
+          </div>
+        </n-form-item>
+      </n-form>
+      <n-form v-else label-placement="top">
+        <n-form-item label="动态口令">
+          <n-input v-model:value="second.gaCode" placeholder="请输入 Google Authenticator 中的 6 位数字" maxlength="8" />
+        </n-form-item>
+      </n-form>
+      <template #action>
+        <n-button quaternary @click="secondOpen = false">取消</n-button>
+        <n-button type="primary" :loading="loading" @click="submitSecondStep">登录</n-button>
+      </template>
+    </n-modal>
+    -->
+  </div>
+</template>
+
+<style scoped>
+/* 全屏深色背景 + 登录区水平垂直居中 */
+.login-page {
+  position: relative;
+  width: 100%;
+  min-height: 100dvh;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  padding: clamp(24px, 4vw, 48px) 20px;
+  box-sizing: border-box;
+  overflow-x: hidden;
+}
+
+.login-page-backdrop {
+  position: absolute;
+  inset: 0;
+  z-index: 0;
+  overflow: hidden;
+  pointer-events: none;
+}
+
+.fullscreen-loading {
+  position: fixed;
+  inset: 0;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background: rgba(248, 250, 252, 0.92);
+  backdrop-filter: blur(6px);
+  z-index: 1000;
+}
+
+.login-bg-base {
+  position: absolute;
+  inset: 0;
+  z-index: 0;
+  background:
+    radial-gradient(120% 80% at 15% 0%, rgba(99, 102, 241, 0.5) 0%, transparent 52%),
+    radial-gradient(100% 70% at 100% 100%, rgba(124, 58, 237, 0.38) 0%, transparent 48%),
+    linear-gradient(165deg, #0b1120 0%, #1e1b4b 42%, #0f172a 100%);
+}
+
+/* 全屏背景:轻量科技感装饰(纯 CSS) */
+.login-bg-effects {
+  position: absolute;
+  inset: 0;
+  z-index: 1;
+  pointer-events: none;
+  overflow: hidden;
+}
+
+.login-bg-aurora {
+  position: absolute;
+  inset: -25%;
+  background:
+    radial-gradient(ellipse 55% 45% at 18% 22%, rgba(99, 102, 241, 0.22) 0%, transparent 58%),
+    radial-gradient(ellipse 50% 42% at 88% 78%, rgba(124, 58, 237, 0.18) 0%, transparent 55%),
+    radial-gradient(ellipse 40% 50% at 55% 48%, rgba(56, 189, 248, 0.06) 0%, transparent 50%);
+  animation: login-bg-aurora-drift 22s ease-in-out infinite alternate;
+  will-change: transform;
+}
+
+.login-bg-grid {
+  position: absolute;
+  inset: 0;
+  opacity: 0.075;
+  background-image:
+    linear-gradient(rgba(148, 163, 184, 0.35) 1px, transparent 1px),
+    linear-gradient(90deg, rgba(148, 163, 184, 0.35) 1px, transparent 1px);
+  background-size: 56px 56px;
+  mask-image: radial-gradient(ellipse 85% 75% at 50% 45%, #000 20%, transparent 72%);
+  animation: login-bg-grid-drift 48s linear infinite;
+}
+
+.login-bg-orbit {
+  position: absolute;
+  inset: 0;
+  background:
+    radial-gradient(circle 1.5px at 12% 28%, rgba(226, 232, 240, 0.35), transparent 100%),
+    radial-gradient(circle 1.5px at 78% 18%, rgba(226, 232, 240, 0.28), transparent 100%),
+    radial-gradient(circle 1.5px at 42% 72%, rgba(226, 232, 240, 0.22), transparent 100%),
+    radial-gradient(circle 1.5px at 88% 62%, rgba(226, 232, 240, 0.2), transparent 100%);
+  animation: login-bg-orbit-pulse 14s ease-in-out infinite;
+}
+
+@keyframes login-bg-aurora-drift {
+  0% {
+    transform: translate(-4%, -2%) rotate(-2deg) scale(1);
+  }
+  100% {
+    transform: translate(5%, 4%) rotate(2deg) scale(1.06);
+  }
+}
+
+@keyframes login-bg-grid-drift {
+  0% {
+    background-position: 0 0, 0 0;
+  }
+  100% {
+    background-position: 56px 56px, 56px 56px;
+  }
+}
+
+@keyframes login-bg-orbit-pulse {
+  0%,
+  100% {
+    opacity: 0.65;
+  }
+  50% {
+    opacity: 0.88;
+  }
+}
+
+@media (prefers-reduced-motion: reduce) {
+  .login-bg-aurora,
+  .login-bg-grid,
+  .login-bg-orbit {
+    animation: none;
+  }
+
+  .login-bg-grid {
+    background-position: 0 0, 0 0;
+  }
+
+  .login-bg-orbit {
+    opacity: 0.75;
+  }
+}
+
+.login-brand-logo {
+  display: block;
+  height: clamp(48px, 12vw, 64px);
+  width: auto;
+  max-width: 100%;
+  margin: 0 auto 20px;
+  object-fit: contain;
+  object-position: center;
+}
+
+.login-tagline {
+  margin: 0 0 8px;
+  font-size: 15px;
+  font-weight: 500;
+  color: #cbd5e1;
+}
+
+.login-lead {
+  margin: 0;
+  font-size: 14px;
+  line-height: 1.6;
+  color: #94a3b8;
+  max-width: 36em;
+}
+
+/* 占满左栏剩余高度;无图时仍保留一块与背景一体的区域 */
+.login-hero {
+  flex: 1 1 0%;
+  min-height: 0;
+  margin-top: clamp(20px, 3vh, 36px);
+  width: 100%;
+  border-radius: 14px;
+  overflow: hidden;
+  border: 1px solid rgba(148, 163, 184, 0.18);
+  background: rgba(15, 23, 42, 0.45);
+  box-shadow: 0 20px 50px rgba(0, 0, 0, 0.28);
+  position: relative;
+}
+
+.login-hero-img {
+  position: absolute;
+  inset: 0;
+  display: block;
+  width: 100%;
+  height: 100%;
+  object-fit: cover;
+  object-position: center center;
+  opacity: 0.97;
+}
+
+.login-main {
+  position: relative;
+  z-index: 1;
+  width: 100%;
+  max-width: 400px;
+  min-width: 0;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  box-sizing: border-box;
+}
+
+.login-card {
+  width: 100%;
+  padding: 36px 32px 32px;
+  border-radius: 20px;
+  background: var(--vt-bg-elevated);
+  border: 1px solid var(--vt-border);
+  box-shadow:
+    0 1px 2px rgba(15, 23, 42, 0.04),
+    0 12px 40px rgba(15, 23, 42, 0.06);
+}
+
+.login-card-head {
+  margin-bottom: 28px;
+  text-align: left;
+}
+
+.login-title {
+  margin: 0 0 6px;
+  font-size: 22px;
+  font-weight: 700;
+  letter-spacing: -0.03em;
+  color: var(--vt-text);
+}
+
+.login-subtitle {
+  margin: 0;
+  font-size: 14px;
+  color: var(--vt-text-muted);
+}
+
+.login-submit {
+  margin-top: 8px;
+  height: 44px;
+  font-weight: 600;
+  font-size: 15px;
+}
+
+.login-footnote {
+  margin: 24px 0 0;
+  width: 100%;
+  text-align: center;
+  font-size: 12px;
+  line-height: 1.5;
+  color: rgba(226, 232, 240, 0.55);
+}
+
+.login-card :deep(.n-form-item-label) {
+  font-weight: 500;
+  color: #334155;
+}
+
+.login-card :deep(.n-input) {
+  --n-border: 1px solid var(--vt-border) !important;
+}
+
+.login-card :deep(.n-input:hover) {
+  --n-border-hover: 1px solid #cbd5e1 !important;
+}
+
+.login-card :deep(.n-input--focus) {
+  --n-border-focus: 1px solid #a5b4fc !important;
+  --n-box-shadow-focus: 0 0 0 3px rgba(99, 102, 241, 0.2) !important;
+}
+
+.email-code-row {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  width: 100%;
+}
+
+.email-code-input {
+  flex: 1;
+  min-width: 0;
+}
+
+.email-code-action {
+  flex-shrink: 0;
+  min-width: 96px;
+}
+</style>

+ 20 - 0
src/vite-env.d.ts

@@ -0,0 +1,20 @@
+/// <reference types="vite/client" />
+
+import 'vue-router'
+
+declare module 'vue-router' {
+  interface RouteMeta {
+    titleKey?: string
+    title?: string
+    requiresAuth?: boolean
+  }
+}
+
+interface ImportMetaEnv {
+  readonly VITE_API_HOST85: string
+  readonly VITE_API_HOST82: string
+}
+
+interface ImportMeta {
+  readonly env: ImportMetaEnv
+}

+ 20 - 0
tsconfig.app.json

@@ -0,0 +1,20 @@
+{
+  "extends": "@vue/tsconfig/tsconfig.dom.json",
+  "compilerOptions": {
+    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
+    "types": ["vite/client"],
+    "baseUrl": ".",
+    "paths": {
+      "@/*": ["./src/*"]
+    },
+
+    /* Linting */
+    "strict": true,
+    "noUnusedLocals": true,
+    "noUnusedParameters": true,
+    "erasableSyntaxOnly": true,
+    "noFallthroughCasesInSwitch": true,
+    "noUncheckedSideEffectImports": true
+  },
+  "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "src/**/*.d.ts"]
+}

+ 7 - 0
tsconfig.json

@@ -0,0 +1,7 @@
+{
+  "files": [],
+  "references": [
+    { "path": "./tsconfig.app.json" },
+    { "path": "./tsconfig.node.json" }
+  ]
+}

+ 26 - 0
tsconfig.node.json

@@ -0,0 +1,26 @@
+{
+  "compilerOptions": {
+    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
+    "target": "ES2023",
+    "lib": ["ES2023"],
+    "module": "ESNext",
+    "types": ["node"],
+    "skipLibCheck": true,
+
+    /* Bundler mode */
+    "moduleResolution": "bundler",
+    "allowImportingTsExtensions": true,
+    "verbatimModuleSyntax": true,
+    "moduleDetection": "force",
+    "noEmit": true,
+
+    /* Linting */
+    "strict": true,
+    "noUnusedLocals": true,
+    "noUnusedParameters": true,
+    "erasableSyntaxOnly": true,
+    "noFallthroughCasesInSwitch": true,
+    "noUncheckedSideEffectImports": true
+  },
+  "include": ["vite.config.ts"]
+}

+ 36 - 0
vite.config.ts

@@ -0,0 +1,36 @@
+import { fileURLToPath, URL } from 'node:url'
+import { defineConfig } from 'vite'
+import vue from '@vitejs/plugin-vue'
+import AutoImport from 'unplugin-auto-import/vite'
+import Components from 'unplugin-vue-components/vite'
+import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'
+
+// https://vite.dev/config/
+export default defineConfig({
+  plugins: [
+    vue(),
+    AutoImport({
+      imports: [
+        'vue-i18n',
+        {
+          'naive-ui': [
+            'useMessage',
+            'useDialog',
+            'useNotification',
+            'useLoadingBar',
+          ],
+        },
+      ],
+      dts: 'src/auto-imports.d.ts',
+    }),
+    Components({
+      resolvers: [NaiveUiResolver()],
+      dts: 'src/components.d.ts',
+    }),
+  ],
+  resolve: {
+    alias: {
+      '@': fileURLToPath(new URL('./src', import.meta.url)),
+    },
+  },
+})

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác