diff --git a/.gitignore b/.gitignore
index 6cb294929..942e3a4bf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,4 +14,6 @@
*.tmp
*.swp
*~
-.codegraph
+*.codegraph
+*/node_modules
+*/dist
diff --git a/web/env.d.ts b/web/env.d.ts
new file mode 100644
index 000000000..11f02fe2a
--- /dev/null
+++ b/web/env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/web/index.html b/web/index.html
new file mode 100644
index 000000000..58d3af594
--- /dev/null
+++ b/web/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ Codex Proxy RS
+
+
+
+
+
+
diff --git a/web/package.json b/web/package.json
new file mode 100644
index 000000000..c0cac3f81
--- /dev/null
+++ b/web/package.json
@@ -0,0 +1,30 @@
+{
+ "name": "codex-proxy-rs-web",
+ "version": "0.1.0",
+ "private": true,
+ "type": "module",
+ "packageManager": "pnpm@11.7.0",
+ "scripts": {
+ "dev": "vite --host 0.0.0.0",
+ "build": "vue-tsc -b && vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@lucide/vue": "^1.18.0",
+ "axios": "^1.18.0",
+ "echarts": "^6.1.0",
+ "pinia": "^3.0.4",
+ "vue": "^3.5.38",
+ "vue-router": "^5.1.0"
+ },
+ "devDependencies": {
+ "@tailwindcss/vite": "^4.3.1",
+ "@types/node": "^25.9.3",
+ "@vitejs/plugin-vue": "^6.0.7",
+ "@vue/tsconfig": "^0.9.1",
+ "tailwindcss": "^4.3.1",
+ "typescript": "^6.0.3",
+ "vite": "^8.0.16",
+ "vue-tsc": "^3.3.5"
+ }
+}
diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml
new file mode 100644
index 000000000..094c9eda7
--- /dev/null
+++ b/web/pnpm-lock.yaml
@@ -0,0 +1,1691 @@
+lockfileVersion: '9.0'
+
+settings:
+ autoInstallPeers: true
+ excludeLinksFromLockfile: false
+
+importers:
+
+ .:
+ dependencies:
+ '@lucide/vue':
+ specifier: ^1.18.0
+ version: 1.18.0(vue@3.5.38(typescript@6.0.3))
+ axios:
+ specifier: ^1.18.0
+ version: 1.18.0
+ echarts:
+ specifier: ^6.1.0
+ version: 6.1.0
+ pinia:
+ specifier: ^3.0.4
+ version: 3.0.4(typescript@6.0.3)(vue@3.5.38(typescript@6.0.3))
+ vue:
+ specifier: ^3.5.38
+ version: 3.5.38(typescript@6.0.3)
+ vue-router:
+ specifier: ^5.1.0
+ version: 5.1.0(@vue/compiler-sfc@3.5.38)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.38(typescript@6.0.3)))(vite@8.0.16(@types/node@25.9.3)(jiti@2.7.0)(yaml@2.9.0))(vue@3.5.38(typescript@6.0.3))
+ devDependencies:
+ '@tailwindcss/vite':
+ specifier: ^4.3.1
+ version: 4.3.1(vite@8.0.16(@types/node@25.9.3)(jiti@2.7.0)(yaml@2.9.0))
+ '@types/node':
+ specifier: ^25.9.3
+ version: 25.9.3
+ '@vitejs/plugin-vue':
+ specifier: ^6.0.7
+ version: 6.0.7(vite@8.0.16(@types/node@25.9.3)(jiti@2.7.0)(yaml@2.9.0))(vue@3.5.38(typescript@6.0.3))
+ '@vue/tsconfig':
+ specifier: ^0.9.1
+ version: 0.9.1(typescript@6.0.3)(vue@3.5.38(typescript@6.0.3))
+ tailwindcss:
+ specifier: ^4.3.1
+ version: 4.3.1
+ typescript:
+ specifier: ^6.0.3
+ version: 6.0.3
+ vite:
+ specifier: ^8.0.16
+ version: 8.0.16(@types/node@25.9.3)(jiti@2.7.0)(yaml@2.9.0)
+ vue-tsc:
+ specifier: ^3.3.5
+ version: 3.3.5(typescript@6.0.3)
+
+packages:
+
+ '@babel/generator@8.0.0-rc.6':
+ resolution: {integrity: sha512-6mIzgVK8DgEzvIapoQwhXTMnnkuE4STQmVv9H03i/tZ2ml8oev3TRvZJgTenK2Bsq0YWNtzOrFdTyNzCMFtjJQ==}
+ engines: {node: ^22.18.0 || >=24.11.0}
+
+ '@babel/helper-string-parser@7.29.7':
+ resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-string-parser@8.0.0-rc.6':
+ resolution: {integrity: sha512-BCkFy+zN6kXQed3YOT7aJl93NfDSzQc3pBfsvTVPs9gU9X3V0aefEF5kwBT0E+mDWH9QgKaZstYUQN9VdQZT4g==}
+ engines: {node: ^22.18.0 || >=24.11.0}
+
+ '@babel/helper-validator-identifier@7.29.7':
+ resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-validator-identifier@8.0.0-rc.6':
+ resolution: {integrity: sha512-nVJ+1JcCgntv8d78rRo++o2wuODT0Irknx2BF8Np4Ft2CRgjLqIs4qzSZ8b66yGbBdMWGmZBO9WEZv1hhNiSpg==}
+ engines: {node: ^22.18.0 || >=24.11.0}
+
+ '@babel/parser@7.29.7':
+ resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==}
+ engines: {node: '>=6.0.0'}
+ hasBin: true
+
+ '@babel/parser@8.0.0-rc.6':
+ resolution: {integrity: sha512-rOS8IpdO7mQELkTPlCsTgPejO0bFuZdEDCGQJouYbYf9e1FLTym7Fei2pEjq8q7MWbX0ravcd7QQYKs1TxOuog==}
+ engines: {node: ^22.18.0 || >=24.11.0}
+ hasBin: true
+
+ '@babel/types@7.29.7':
+ resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/types@8.0.0-rc.6':
+ resolution: {integrity: sha512-p7/ABylAYlexb31wtRdIfH9L9A0Z2T/9H6zAqzqndkY2PLkvNNc580wGhp/gGKN4Sp9sQvSkhc6Oga8/O+wTyw==}
+ engines: {node: ^22.18.0 || >=24.11.0}
+
+ '@emnapi/core@1.10.0':
+ resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==}
+
+ '@emnapi/runtime@1.10.0':
+ resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==}
+
+ '@emnapi/wasi-threads@1.2.1':
+ resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==}
+
+ '@jridgewell/gen-mapping@0.3.13':
+ resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
+
+ '@jridgewell/remapping@2.3.5':
+ resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==}
+
+ '@jridgewell/resolve-uri@3.1.2':
+ resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
+ engines: {node: '>=6.0.0'}
+
+ '@jridgewell/sourcemap-codec@1.5.5':
+ resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
+
+ '@jridgewell/trace-mapping@0.3.31':
+ resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
+
+ '@lucide/vue@1.18.0':
+ resolution: {integrity: sha512-DmnUpDB85PlMZ+ofjZLcKq3JoJnaD1bk7SIj9xwUvqerfNqA6hCLa0/m3gIybH6rdrErABbqvTD8yYJdNqiZ3Q==}
+ peerDependencies:
+ vue: '>=3.0.1'
+
+ '@napi-rs/wasm-runtime@1.1.5':
+ resolution: {integrity: sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==}
+ peerDependencies:
+ '@emnapi/core': ^1.7.1
+ '@emnapi/runtime': ^1.7.1
+
+ '@oxc-project/types@0.133.0':
+ resolution: {integrity: sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==}
+
+ '@rolldown/binding-android-arm64@1.0.3':
+ resolution: {integrity: sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [android]
+
+ '@rolldown/binding-darwin-arm64@1.0.3':
+ resolution: {integrity: sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@rolldown/binding-darwin-x64@1.0.3':
+ resolution: {integrity: sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [darwin]
+
+ '@rolldown/binding-freebsd-x64@1.0.3':
+ resolution: {integrity: sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@rolldown/binding-linux-arm-gnueabihf@1.0.3':
+ resolution: {integrity: sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm]
+ os: [linux]
+
+ '@rolldown/binding-linux-arm64-gnu@1.0.3':
+ resolution: {integrity: sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rolldown/binding-linux-arm64-musl@1.0.3':
+ resolution: {integrity: sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ '@rolldown/binding-linux-ppc64-gnu@1.0.3':
+ resolution: {integrity: sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [ppc64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rolldown/binding-linux-s390x-gnu@1.0.3':
+ resolution: {integrity: sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [s390x]
+ os: [linux]
+ libc: [glibc]
+
+ '@rolldown/binding-linux-x64-gnu@1.0.3':
+ resolution: {integrity: sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rolldown/binding-linux-x64-musl@1.0.3':
+ resolution: {integrity: sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ '@rolldown/binding-openharmony-arm64@1.0.3':
+ resolution: {integrity: sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@rolldown/binding-wasm32-wasi@1.0.3':
+ resolution: {integrity: sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [wasm32]
+
+ '@rolldown/binding-win32-arm64-msvc@1.0.3':
+ resolution: {integrity: sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [win32]
+
+ '@rolldown/binding-win32-x64-msvc@1.0.3':
+ resolution: {integrity: sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [win32]
+
+ '@rolldown/pluginutils@1.0.1':
+ resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==}
+
+ '@tailwindcss/node@4.3.1':
+ resolution: {integrity: sha512-6NDaqRoAMSXD1mr/RXu0HBvNE9a2n5tHPsxu9XHLws8o4Twes5rBM2205SUUiJ9goAtadrN6xTGX0UDEwp/N4A==}
+
+ '@tailwindcss/oxide-android-arm64@4.3.1':
+ resolution: {integrity: sha512-SVlyf61g374l5cHyg8x9kf5xmLcOaxvOTsbsqDnSsDJaKOEFZ7GCvi84VAVGpxojYOs1+3K6M0UjXfqPU8vmOQ==}
+ engines: {node: '>= 20'}
+ cpu: [arm64]
+ os: [android]
+
+ '@tailwindcss/oxide-darwin-arm64@4.3.1':
+ resolution: {integrity: sha512-hVnWLwv+e/l7c4WKyVtHVrIPvYdqWHjRB3MDIqARynzFtnQg85kmQEFCbV9Ja0VVx4xXTIiDWY60Y7iz/iNoDA==}
+ engines: {node: '>= 20'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@tailwindcss/oxide-darwin-x64@4.3.1':
+ resolution: {integrity: sha512-Cf7abu0WVgbhU7ANgPUnSAvm7nCvMweusHb8FnaHlLfv/Caq4GYaEZg7ZImzzmjx4lIAfuS8q+eLIS7A7IzxIg==}
+ engines: {node: '>= 20'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@tailwindcss/oxide-freebsd-x64@4.3.1':
+ resolution: {integrity: sha512-ZZqzX2Y+GXtXXfqSfpJhDm60OoZfvLHLCgm+J7NVqgHHJjG/m9ugZI77RwTsVd4fnBJuCFP6Ae6kTJb71UdS8g==}
+ engines: {node: '>= 20'}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.1':
+ resolution: {integrity: sha512-/Ah/xik0LaMYfv9DZ0S/t4pBlBNYOcqtRwusjgovHkvT8ixueWCLyJjsaF5kQIckjb4IT8Q6K6p/iPmZMixYgg==}
+ engines: {node: '>= 20'}
+ cpu: [arm]
+ os: [linux]
+
+ '@tailwindcss/oxide-linux-arm64-gnu@4.3.1':
+ resolution: {integrity: sha512-gqdFoVJlw444GvpnheZLHmvTzSxI/cOUUh2KSNejQjTcYkW062SVD+En0rUgD+QV91bz1XGIGtt1HJd48xUGbQ==}
+ engines: {node: '>= 20'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ '@tailwindcss/oxide-linux-arm64-musl@4.3.1':
+ resolution: {integrity: sha512-Bwv9KwOvE0VKa86xPFif9b9c3Y1NxOV1P0gLti/IYaWEsQYZXDlxfGEtA8mdDZ7SG3wyNXAWYT5SIn3giL57oA==}
+ engines: {node: '>= 20'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ '@tailwindcss/oxide-linux-x64-gnu@4.3.1':
+ resolution: {integrity: sha512-Ymi8O8T15HYQdOUWUtTI6ldN0neHP85FC+Qz32xTcZ7iJXtem/x8ITev0o1e9e5rkqj4lONZfTRLvkmin1+tKg==}
+ engines: {node: '>= 20'}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ '@tailwindcss/oxide-linux-x64-musl@4.3.1':
+ resolution: {integrity: sha512-M+P/91qJ6uILLw4k2G93GMDRAXj61SMvFQYt39AqvUqYgExXpLL5aepfns7sj4HiAQeolirQF9E0lzRvdf4zPQ==}
+ engines: {node: '>= 20'}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ '@tailwindcss/oxide-wasm32-wasi@4.3.1':
+ resolution: {integrity: sha512-zsM8uOeqvVGHsAXsJxsT28ttosFahLJKCLOTUBqRAtKnVgGSRitds9T432QiT8b77Yga7JIBkulIRRlJPtYhRA==}
+ engines: {node: '>=14.0.0'}
+ cpu: [wasm32]
+ bundledDependencies:
+ - '@napi-rs/wasm-runtime'
+ - '@emnapi/core'
+ - '@emnapi/runtime'
+ - '@tybys/wasm-util'
+ - '@emnapi/wasi-threads'
+ - tslib
+
+ '@tailwindcss/oxide-win32-arm64-msvc@4.3.1':
+ resolution: {integrity: sha512-aiNvSq9BsVk8V513lDKlrCFAgf8qBMPZTpgEhInL+NwQqs97mYmupVMrPrgBBSL8Pv/0zXu9MrMF9rMun1ZeNg==}
+ engines: {node: '>= 20'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@tailwindcss/oxide-win32-x64-msvc@4.3.1':
+ resolution: {integrity: sha512-xDEyu1rg290472FEGaKHnzyDyh5QH+AlWvsU5hMoMtPpzmKlRI0jaYKCgSHDYtaQWZOYbMaduSyCwFwY4n1HmA==}
+ engines: {node: '>= 20'}
+ cpu: [x64]
+ os: [win32]
+
+ '@tailwindcss/oxide@4.3.1':
+ resolution: {integrity: sha512-yVPyo8RNkabVr3O2EhHEE0Rewu7YKzc1DhIqfL46LKveFrmu9XbDazNOJY7/GRuvw1h6u3utWnR29H/p5JPlgA==}
+ engines: {node: '>= 20'}
+
+ '@tailwindcss/vite@4.3.1':
+ resolution: {integrity: sha512-hItDHuIIlEV61R+faXu66s1K36aTurO/Qw0e45Vskz57gXl9pWOT6eg3zmcEui6CZXddbN7zd41bwmvag4JGwQ==}
+ peerDependencies:
+ vite: ^5.2.0 || ^6 || ^7 || ^8
+
+ '@tybys/wasm-util@0.10.2':
+ resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==}
+
+ '@types/jsesc@2.5.1':
+ resolution: {integrity: sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw==}
+
+ '@types/node@25.9.3':
+ resolution: {integrity: sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg==}
+
+ '@vitejs/plugin-vue@6.0.7':
+ resolution: {integrity: sha512-km+p+XdSz9Sxm5rqUbqcSfZYaAniKxWBj1KURl+Jr7UaPvvX7BmaWMdP69I5rrFDeQGyxAG7NXdc57vz+snhWg==}
+ 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
+
+ '@volar/language-core@2.4.28':
+ resolution: {integrity: sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==}
+
+ '@volar/source-map@2.4.28':
+ resolution: {integrity: sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==}
+
+ '@volar/typescript@2.4.28':
+ resolution: {integrity: sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==}
+
+ '@vue-macros/common@3.1.2':
+ resolution: {integrity: sha512-h9t4ArDdniO9ekYHAD95t9AZcAbb19lEGK+26iAjUODOIJKmObDNBSe4+6ELQAA3vtYiFPPBtHh7+cQCKi3Dng==}
+ engines: {node: '>=20.19.0'}
+ peerDependencies:
+ vue: ^2.7.0 || ^3.2.25
+ peerDependenciesMeta:
+ vue:
+ optional: true
+
+ '@vue/compiler-core@3.5.38':
+ resolution: {integrity: sha512-s99aGxWYig9ErHbct27KXEGhrBYlRI6c4MwAgXErOAbX9xiW37/uMa+XUDO69zLz83dng8UUZ70CTOJrLrYrEQ==}
+
+ '@vue/compiler-dom@3.5.38':
+ resolution: {integrity: sha512-JTqp25l8aFfJYF7/KmsXZjAxJz7T+SjmTJLoXVjHtc2BrSgSiW2n9Aem/cWq1OPe68A8JL06B3eVdhlP0H4TVw==}
+
+ '@vue/compiler-sfc@3.5.38':
+ resolution: {integrity: sha512-DuA2GiZawSEW442iw/9+Fkol8hTgb4Ke5KkhmSry65QA7YuyMbIdy8p0XZRMvNwJdgRz307W8g1CSzdvS4nuNg==}
+
+ '@vue/compiler-ssr@3.5.38':
+ resolution: {integrity: sha512-7s+W5Gc42FGxZMcuwl8H5B29T8BJPMdBT7KHFE+BbAuZ/iTEdTtv7z2XiMjiaUUw4w3ZcCEdHs36RuYJ2VA7bA==}
+
+ '@vue/devtools-api@7.7.9':
+ resolution: {integrity: sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==}
+
+ '@vue/devtools-api@8.1.3':
+ resolution: {integrity: sha512-73NMCvxXh8Hyozc/jiwqTFWVcCMyi11U1zmrq4DoukQJnuo8JHt6FsNu4HdeUDa8SpIp5vb7Q22GWgIq0efsXg==}
+
+ '@vue/devtools-kit@7.7.9':
+ resolution: {integrity: sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==}
+
+ '@vue/devtools-kit@8.1.3':
+ resolution: {integrity: sha512-cRn7GXiCQkMYU2Z3h3pM4YO/ndbx9FY1yLDAqIqPLcmIq4H6zAOJHein6tvZU3AfPwgrodqLiPBEF+YQaS8AxA==}
+
+ '@vue/devtools-shared@7.7.9':
+ resolution: {integrity: sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==}
+
+ '@vue/devtools-shared@8.1.3':
+ resolution: {integrity: sha512-CM3uIPL+v+lrJUk33+pxspYo0MhuMWlCvf7zC9fybifvCPyM2jUbYRPwoYEJgYbwRqPikm5HozbUhp60MF2QuA==}
+
+ '@vue/language-core@3.3.5':
+ resolution: {integrity: sha512-UkKu5nhX89fg4VhlG/FOeI10G3cj/7radKT/cy9BT4Q9qJmJlSTAc/dP63Xqs29aypN4f39xUV6PsLNk/dcD6g==}
+
+ '@vue/reactivity@3.5.38':
+ resolution: {integrity: sha512-pG6LV/NDNRbKizcUjFFLAfjaL8mcv4DmR9avNcUw2gDHBzZneuS2TWCmp633ynzxz9YYKNeEPK2I8Wraqy2HUQ==}
+
+ '@vue/runtime-core@3.5.38':
+ resolution: {integrity: sha512-iyW8WVfF1CpCXxncZY5Ei6rSd6oZr5DgEom//fUjRBRl56AXPD+s9ATvukRt77ZFTuYlnVA1bxY+dJB94tWVYw==}
+
+ '@vue/runtime-dom@3.5.38':
+ resolution: {integrity: sha512-apX2wt9sdfDshS+a2xueFZLVpt0GkRJZSoPmrW/SA4yzXTznhfcMVW59gr7h4YQeY0vJhdJkk2rsIDwgfFgC5A==}
+
+ '@vue/server-renderer@3.5.38':
+ resolution: {integrity: sha512-vue8vbf2QlV4quHqzwmJy6dWfmRhP1J8l4wtZg60CL6VoKqcPY2oe7may3+1d9qfpedjK5PRLFqd5k3Isj9mUw==}
+ peerDependencies:
+ vue: 3.5.38
+
+ '@vue/shared@3.5.38':
+ resolution: {integrity: sha512-FTW0AFZNaK5/mOqvGBwVfUlNLU38TiQn4+DQgIFUnrBBJQ1crMJ82yeGQLV5jyKFsO8yRukpbuP7x+nRbH6aug==}
+
+ '@vue/tsconfig@0.9.1':
+ resolution: {integrity: sha512-buvjm+9NzLCJL29KY1j1991YYJ5e6275OiK+G4jtmfIb+z4POywbdm0wXusT9adVWqe0xqg70TbI7+mRx4uU9w==}
+ peerDependencies:
+ typescript: '>= 5.8'
+ vue: ^3.4.0
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+ vue:
+ optional: true
+
+ acorn@8.17.0:
+ resolution: {integrity: sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==}
+ engines: {node: '>=0.4.0'}
+ hasBin: true
+
+ agent-base@6.0.2:
+ resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
+ engines: {node: '>= 6.0.0'}
+
+ alien-signals@3.2.1:
+ resolution: {integrity: sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g==}
+
+ ast-kit@2.2.0:
+ resolution: {integrity: sha512-m1Q/RaVOnTp9JxPX+F+Zn7IcLYMzM8kZofDImfsKZd8MbR+ikdOzTeztStWqfrqIxZnYWryyI9ePm3NGjnZgGw==}
+ engines: {node: '>=20.19.0'}
+
+ ast-walker-scope@0.9.0:
+ resolution: {integrity: sha512-IJdzo2vLiElBxKzwS36VsCue/62d6IdWjnPB2v3nuPKeWGynp6FF/CYoLa5i/3jXH/z97ZDdsXz6abpgM6w07A==}
+ engines: {node: '>=20.19.0'}
+
+ asynckit@0.4.0:
+ resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
+
+ axios@1.18.0:
+ resolution: {integrity: sha512-E32NzpYKp++W7XRe52rHiXV2ehxmh3wbdgO7MHeFM+vqxLBYHzt0ElkiImtOBxtOmyp0yoC8C6uESVV84Y2/hw==}
+
+ birpc@2.9.0:
+ resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==}
+
+ call-bind-apply-helpers@1.0.2:
+ resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
+ engines: {node: '>= 0.4'}
+
+ chokidar@5.0.0:
+ resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==}
+ engines: {node: '>= 20.19.0'}
+
+ combined-stream@1.0.8:
+ resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
+ engines: {node: '>= 0.8'}
+
+ confbox@0.1.8:
+ resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==}
+
+ confbox@0.2.4:
+ resolution: {integrity: sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==}
+
+ copy-anything@4.0.5:
+ resolution: {integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==}
+ engines: {node: '>=18'}
+
+ csstype@3.2.3:
+ resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
+
+ debug@4.4.3:
+ resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
+ engines: {node: '>=6.0'}
+ peerDependencies:
+ supports-color: '*'
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+
+ delayed-stream@1.0.0:
+ resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
+ engines: {node: '>=0.4.0'}
+
+ detect-libc@2.1.2:
+ resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
+ engines: {node: '>=8'}
+
+ dunder-proto@1.0.1:
+ resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
+ engines: {node: '>= 0.4'}
+
+ echarts@6.1.0:
+ resolution: {integrity: sha512-q0yaFPggC9FUdsWH4blavRWFmxdrIodbkoKNAjJudAI6CA9gNPxHtV2RcZNEepZVlk4yvBYkOkbk6HIVpIyHZA==}
+
+ enhanced-resolve@5.21.6:
+ resolution: {integrity: sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==}
+ engines: {node: '>=10.13.0'}
+
+ entities@7.0.1:
+ resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==}
+ engines: {node: '>=0.12'}
+
+ es-define-property@1.0.1:
+ resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
+ engines: {node: '>= 0.4'}
+
+ es-errors@1.3.0:
+ resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
+ engines: {node: '>= 0.4'}
+
+ es-object-atoms@1.1.2:
+ resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==}
+ engines: {node: '>= 0.4'}
+
+ es-set-tostringtag@2.1.0:
+ resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
+ engines: {node: '>= 0.4'}
+
+ estree-walker@2.0.2:
+ resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
+
+ exsolve@1.0.8:
+ resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==}
+
+ fdir@6.5.0:
+ resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
+ engines: {node: '>=12.0.0'}
+ peerDependencies:
+ picomatch: ^3 || ^4
+ peerDependenciesMeta:
+ picomatch:
+ optional: true
+
+ follow-redirects@1.16.0:
+ resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==}
+ engines: {node: '>=4.0'}
+ peerDependencies:
+ debug: '*'
+ peerDependenciesMeta:
+ debug:
+ optional: true
+
+ form-data@4.0.6:
+ resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==}
+ engines: {node: '>= 6'}
+
+ fsevents@2.3.3:
+ resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+ os: [darwin]
+
+ function-bind@1.1.2:
+ resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
+
+ get-intrinsic@1.3.0:
+ resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
+ engines: {node: '>= 0.4'}
+
+ get-proto@1.0.1:
+ resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
+ engines: {node: '>= 0.4'}
+
+ gopd@1.2.0:
+ resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
+ engines: {node: '>= 0.4'}
+
+ graceful-fs@4.2.11:
+ resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
+
+ has-symbols@1.1.0:
+ resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
+ engines: {node: '>= 0.4'}
+
+ has-tostringtag@1.0.2:
+ resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
+ engines: {node: '>= 0.4'}
+
+ hasown@2.0.4:
+ resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==}
+ engines: {node: '>= 0.4'}
+
+ hookable@5.5.3:
+ resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==}
+
+ https-proxy-agent@5.0.1:
+ resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==}
+ engines: {node: '>= 6'}
+
+ is-what@5.5.0:
+ resolution: {integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==}
+ engines: {node: '>=18'}
+
+ jiti@2.7.0:
+ resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==}
+ hasBin: true
+
+ jsesc@3.1.0:
+ resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
+ engines: {node: '>=6'}
+ hasBin: true
+
+ json5@2.2.3:
+ resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
+ engines: {node: '>=6'}
+ hasBin: true
+
+ lightningcss-android-arm64@1.32.0:
+ resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [android]
+
+ lightningcss-darwin-arm64@1.32.0:
+ resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [darwin]
+
+ lightningcss-darwin-x64@1.32.0:
+ resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [darwin]
+
+ lightningcss-freebsd-x64@1.32.0:
+ resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [freebsd]
+
+ lightningcss-linux-arm-gnueabihf@1.32.0:
+ resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm]
+ os: [linux]
+
+ lightningcss-linux-arm64-gnu@1.32.0:
+ resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ lightningcss-linux-arm64-musl@1.32.0:
+ resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ lightningcss-linux-x64-gnu@1.32.0:
+ resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ lightningcss-linux-x64-musl@1.32.0:
+ resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ lightningcss-win32-arm64-msvc@1.32.0:
+ resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [win32]
+
+ lightningcss-win32-x64-msvc@1.32.0:
+ resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [win32]
+
+ lightningcss@1.32.0:
+ resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==}
+ engines: {node: '>= 12.0.0'}
+
+ local-pkg@1.2.1:
+ resolution: {integrity: sha512-++gUqRDEvcnN6Zhqrr+y/CkVEHhlrR96vZn3nZZPYzMcBUyBtTKzB9NadClFIsIVSsu+3i9tfk/erqy9kAmt7Q==}
+ engines: {node: '>=14'}
+
+ magic-string-ast@1.0.3:
+ resolution: {integrity: sha512-CvkkH1i81zl7mmb94DsRiFeG9V2fR2JeuK8yDgS8oiZSFa++wWLEgZ5ufEOyLHbvSbD1gTRKv9NdX69Rnvr9JA==}
+ engines: {node: '>=20.19.0'}
+
+ magic-string@0.30.21:
+ resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
+
+ math-intrinsics@1.1.0:
+ resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
+ engines: {node: '>= 0.4'}
+
+ mime-db@1.52.0:
+ resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
+ engines: {node: '>= 0.6'}
+
+ mime-types@2.1.35:
+ resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
+ engines: {node: '>= 0.6'}
+
+ mitt@3.0.1:
+ resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==}
+
+ mlly@1.8.2:
+ resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==}
+
+ ms@2.1.3:
+ resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
+
+ muggle-string@0.4.1:
+ resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==}
+
+ nanoid@3.3.12:
+ resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==}
+ engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+ hasBin: true
+
+ path-browserify@1.0.1:
+ resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
+
+ pathe@2.0.3:
+ resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
+
+ perfect-debounce@1.0.0:
+ resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==}
+
+ perfect-debounce@2.1.0:
+ resolution: {integrity: sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==}
+
+ picocolors@1.1.1:
+ resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
+
+ picomatch@4.0.4:
+ resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==}
+ engines: {node: '>=12'}
+
+ pinia@3.0.4:
+ resolution: {integrity: sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==}
+ peerDependencies:
+ typescript: '>=4.5.0'
+ vue: ^3.5.11
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
+ pkg-types@1.3.1:
+ resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==}
+
+ pkg-types@2.3.1:
+ resolution: {integrity: sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==}
+
+ postcss@8.5.15:
+ resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==}
+ engines: {node: ^10 || ^12 || >=14}
+
+ proxy-from-env@2.1.0:
+ resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==}
+ engines: {node: '>=10'}
+
+ quansync@0.2.11:
+ resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==}
+
+ readdirp@5.0.0:
+ resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==}
+ engines: {node: '>= 20.19.0'}
+
+ rfdc@1.4.1:
+ resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==}
+
+ rolldown@1.0.3:
+ resolution: {integrity: sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ hasBin: true
+
+ scule@1.3.0:
+ resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==}
+
+ source-map-js@1.2.1:
+ resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
+ engines: {node: '>=0.10.0'}
+
+ speakingurl@14.0.1:
+ resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==}
+ engines: {node: '>=0.10.0'}
+
+ superjson@2.2.6:
+ resolution: {integrity: sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==}
+ engines: {node: '>=16'}
+
+ tailwindcss@4.3.1:
+ resolution: {integrity: sha512-hk+TB1m+K8CYNrP6rjQaq/Y+4Zylwpa87mLYBKCunwnnQ9p+fHb7kmSfGqyEJoxF/O6CDyABWVFEafNSYKll+Q==}
+
+ tapable@2.3.3:
+ resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==}
+ engines: {node: '>=6'}
+
+ tinyglobby@0.2.17:
+ resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==}
+ engines: {node: '>=12.0.0'}
+
+ tslib@2.3.0:
+ resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==}
+
+ tslib@2.8.1:
+ resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
+
+ typescript@6.0.3:
+ resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==}
+ engines: {node: '>=14.17'}
+ hasBin: true
+
+ ufo@1.6.4:
+ resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==}
+
+ undici-types@7.24.6:
+ resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==}
+
+ unplugin-utils@0.3.1:
+ resolution: {integrity: sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog==}
+ engines: {node: '>=20.19.0'}
+
+ unplugin@3.0.0:
+ resolution: {integrity: sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+
+ vite@8.0.16:
+ resolution: {integrity: sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ hasBin: true
+ peerDependencies:
+ '@types/node': ^20.19.0 || >=22.12.0
+ '@vitejs/devtools': ^0.1.18
+ esbuild: ^0.27.0 || ^0.28.0
+ jiti: '>=1.21.0'
+ less: ^4.0.0
+ sass: ^1.70.0
+ sass-embedded: ^1.70.0
+ stylus: '>=0.54.8'
+ sugarss: ^5.0.0
+ terser: ^5.16.0
+ tsx: ^4.8.1
+ yaml: ^2.4.2
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+ '@vitejs/devtools':
+ optional: true
+ esbuild:
+ optional: true
+ jiti:
+ optional: true
+ less:
+ optional: true
+ sass:
+ optional: true
+ sass-embedded:
+ optional: true
+ stylus:
+ optional: true
+ sugarss:
+ optional: true
+ terser:
+ optional: true
+ tsx:
+ optional: true
+ yaml:
+ optional: true
+
+ vscode-uri@3.1.0:
+ resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==}
+
+ vue-router@5.1.0:
+ resolution: {integrity: sha512-HAbiLzLEHQwxPgvsbOJDAwtavszEgLwri6XfyrsPECIFez8+59xc9LofWVdc/HEaSRT822lJ8H9Ns38VVond5g==}
+ peerDependencies:
+ '@pinia/colada': '>=0.21.2'
+ '@vue/compiler-sfc': ^3.5.34
+ pinia: ^3.0.4
+ vite: ^7.0.0 || ^8.0.0
+ vue: ^3.5.34
+ peerDependenciesMeta:
+ '@pinia/colada':
+ optional: true
+ '@vue/compiler-sfc':
+ optional: true
+ pinia:
+ optional: true
+ vite:
+ optional: true
+
+ vue-tsc@3.3.5:
+ resolution: {integrity: sha512-Rzh/G2MmNlMSAMTiQEjDrsb4dgB/jbtEM47rVN2NtidF1dfb/q4w4QvpQBtW5+y3y5H27Hjh7deVwk+YB02fNg==}
+ hasBin: true
+ peerDependencies:
+ typescript: '>=5.0.0'
+
+ vue@3.5.38:
+ resolution: {integrity: sha512-vAMKHfImQlYSy0C+PBue4s3ERZ2xGKfgZg5GXAsLInq1dyh2H78ILVP5sK0KPFPVW4kv+OGCIvBEondcjpZp7A==}
+ peerDependencies:
+ typescript: '*'
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
+ webpack-virtual-modules@0.6.2:
+ resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==}
+
+ yaml@2.9.0:
+ resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==}
+ engines: {node: '>= 14.6'}
+ hasBin: true
+
+ zrender@6.1.0:
+ resolution: {integrity: sha512-oEGMDB6pOP2S6OwRR4PdVv610zrjnA3Bh+JnSG12fYJlBKjtNAoEb5fSUoCOOINlH96I2fU38/A2UpRKs67xYQ==}
+
+snapshots:
+
+ '@babel/generator@8.0.0-rc.6':
+ dependencies:
+ '@babel/parser': 8.0.0-rc.6
+ '@babel/types': 8.0.0-rc.6
+ '@jridgewell/gen-mapping': 0.3.13
+ '@jridgewell/trace-mapping': 0.3.31
+ '@types/jsesc': 2.5.1
+ jsesc: 3.1.0
+
+ '@babel/helper-string-parser@7.29.7': {}
+
+ '@babel/helper-string-parser@8.0.0-rc.6': {}
+
+ '@babel/helper-validator-identifier@7.29.7': {}
+
+ '@babel/helper-validator-identifier@8.0.0-rc.6': {}
+
+ '@babel/parser@7.29.7':
+ dependencies:
+ '@babel/types': 7.29.7
+
+ '@babel/parser@8.0.0-rc.6':
+ dependencies:
+ '@babel/types': 8.0.0-rc.6
+
+ '@babel/types@7.29.7':
+ dependencies:
+ '@babel/helper-string-parser': 7.29.7
+ '@babel/helper-validator-identifier': 7.29.7
+
+ '@babel/types@8.0.0-rc.6':
+ dependencies:
+ '@babel/helper-string-parser': 8.0.0-rc.6
+ '@babel/helper-validator-identifier': 8.0.0-rc.6
+
+ '@emnapi/core@1.10.0':
+ dependencies:
+ '@emnapi/wasi-threads': 1.2.1
+ tslib: 2.8.1
+ optional: true
+
+ '@emnapi/runtime@1.10.0':
+ dependencies:
+ tslib: 2.8.1
+ optional: true
+
+ '@emnapi/wasi-threads@1.2.1':
+ dependencies:
+ tslib: 2.8.1
+ optional: true
+
+ '@jridgewell/gen-mapping@0.3.13':
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.5.5
+ '@jridgewell/trace-mapping': 0.3.31
+
+ '@jridgewell/remapping@2.3.5':
+ dependencies:
+ '@jridgewell/gen-mapping': 0.3.13
+ '@jridgewell/trace-mapping': 0.3.31
+
+ '@jridgewell/resolve-uri@3.1.2': {}
+
+ '@jridgewell/sourcemap-codec@1.5.5': {}
+
+ '@jridgewell/trace-mapping@0.3.31':
+ dependencies:
+ '@jridgewell/resolve-uri': 3.1.2
+ '@jridgewell/sourcemap-codec': 1.5.5
+
+ '@lucide/vue@1.18.0(vue@3.5.38(typescript@6.0.3))':
+ dependencies:
+ vue: 3.5.38(typescript@6.0.3)
+
+ '@napi-rs/wasm-runtime@1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)':
+ dependencies:
+ '@emnapi/core': 1.10.0
+ '@emnapi/runtime': 1.10.0
+ '@tybys/wasm-util': 0.10.2
+ optional: true
+
+ '@oxc-project/types@0.133.0': {}
+
+ '@rolldown/binding-android-arm64@1.0.3':
+ optional: true
+
+ '@rolldown/binding-darwin-arm64@1.0.3':
+ optional: true
+
+ '@rolldown/binding-darwin-x64@1.0.3':
+ optional: true
+
+ '@rolldown/binding-freebsd-x64@1.0.3':
+ optional: true
+
+ '@rolldown/binding-linux-arm-gnueabihf@1.0.3':
+ optional: true
+
+ '@rolldown/binding-linux-arm64-gnu@1.0.3':
+ optional: true
+
+ '@rolldown/binding-linux-arm64-musl@1.0.3':
+ optional: true
+
+ '@rolldown/binding-linux-ppc64-gnu@1.0.3':
+ optional: true
+
+ '@rolldown/binding-linux-s390x-gnu@1.0.3':
+ optional: true
+
+ '@rolldown/binding-linux-x64-gnu@1.0.3':
+ optional: true
+
+ '@rolldown/binding-linux-x64-musl@1.0.3':
+ optional: true
+
+ '@rolldown/binding-openharmony-arm64@1.0.3':
+ optional: true
+
+ '@rolldown/binding-wasm32-wasi@1.0.3':
+ dependencies:
+ '@emnapi/core': 1.10.0
+ '@emnapi/runtime': 1.10.0
+ '@napi-rs/wasm-runtime': 1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)
+ optional: true
+
+ '@rolldown/binding-win32-arm64-msvc@1.0.3':
+ optional: true
+
+ '@rolldown/binding-win32-x64-msvc@1.0.3':
+ optional: true
+
+ '@rolldown/pluginutils@1.0.1': {}
+
+ '@tailwindcss/node@4.3.1':
+ dependencies:
+ '@jridgewell/remapping': 2.3.5
+ enhanced-resolve: 5.21.6
+ jiti: 2.7.0
+ lightningcss: 1.32.0
+ magic-string: 0.30.21
+ source-map-js: 1.2.1
+ tailwindcss: 4.3.1
+
+ '@tailwindcss/oxide-android-arm64@4.3.1':
+ optional: true
+
+ '@tailwindcss/oxide-darwin-arm64@4.3.1':
+ optional: true
+
+ '@tailwindcss/oxide-darwin-x64@4.3.1':
+ optional: true
+
+ '@tailwindcss/oxide-freebsd-x64@4.3.1':
+ optional: true
+
+ '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.1':
+ optional: true
+
+ '@tailwindcss/oxide-linux-arm64-gnu@4.3.1':
+ optional: true
+
+ '@tailwindcss/oxide-linux-arm64-musl@4.3.1':
+ optional: true
+
+ '@tailwindcss/oxide-linux-x64-gnu@4.3.1':
+ optional: true
+
+ '@tailwindcss/oxide-linux-x64-musl@4.3.1':
+ optional: true
+
+ '@tailwindcss/oxide-wasm32-wasi@4.3.1':
+ optional: true
+
+ '@tailwindcss/oxide-win32-arm64-msvc@4.3.1':
+ optional: true
+
+ '@tailwindcss/oxide-win32-x64-msvc@4.3.1':
+ optional: true
+
+ '@tailwindcss/oxide@4.3.1':
+ optionalDependencies:
+ '@tailwindcss/oxide-android-arm64': 4.3.1
+ '@tailwindcss/oxide-darwin-arm64': 4.3.1
+ '@tailwindcss/oxide-darwin-x64': 4.3.1
+ '@tailwindcss/oxide-freebsd-x64': 4.3.1
+ '@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.1
+ '@tailwindcss/oxide-linux-arm64-gnu': 4.3.1
+ '@tailwindcss/oxide-linux-arm64-musl': 4.3.1
+ '@tailwindcss/oxide-linux-x64-gnu': 4.3.1
+ '@tailwindcss/oxide-linux-x64-musl': 4.3.1
+ '@tailwindcss/oxide-wasm32-wasi': 4.3.1
+ '@tailwindcss/oxide-win32-arm64-msvc': 4.3.1
+ '@tailwindcss/oxide-win32-x64-msvc': 4.3.1
+
+ '@tailwindcss/vite@4.3.1(vite@8.0.16(@types/node@25.9.3)(jiti@2.7.0)(yaml@2.9.0))':
+ dependencies:
+ '@tailwindcss/node': 4.3.1
+ '@tailwindcss/oxide': 4.3.1
+ tailwindcss: 4.3.1
+ vite: 8.0.16(@types/node@25.9.3)(jiti@2.7.0)(yaml@2.9.0)
+
+ '@tybys/wasm-util@0.10.2':
+ dependencies:
+ tslib: 2.8.1
+ optional: true
+
+ '@types/jsesc@2.5.1': {}
+
+ '@types/node@25.9.3':
+ dependencies:
+ undici-types: 7.24.6
+
+ '@vitejs/plugin-vue@6.0.7(vite@8.0.16(@types/node@25.9.3)(jiti@2.7.0)(yaml@2.9.0))(vue@3.5.38(typescript@6.0.3))':
+ dependencies:
+ '@rolldown/pluginutils': 1.0.1
+ vite: 8.0.16(@types/node@25.9.3)(jiti@2.7.0)(yaml@2.9.0)
+ vue: 3.5.38(typescript@6.0.3)
+
+ '@volar/language-core@2.4.28':
+ dependencies:
+ '@volar/source-map': 2.4.28
+
+ '@volar/source-map@2.4.28': {}
+
+ '@volar/typescript@2.4.28':
+ dependencies:
+ '@volar/language-core': 2.4.28
+ path-browserify: 1.0.1
+ vscode-uri: 3.1.0
+
+ '@vue-macros/common@3.1.2(vue@3.5.38(typescript@6.0.3))':
+ dependencies:
+ '@vue/compiler-sfc': 3.5.38
+ ast-kit: 2.2.0
+ local-pkg: 1.2.1
+ magic-string-ast: 1.0.3
+ unplugin-utils: 0.3.1
+ optionalDependencies:
+ vue: 3.5.38(typescript@6.0.3)
+
+ '@vue/compiler-core@3.5.38':
+ dependencies:
+ '@babel/parser': 7.29.7
+ '@vue/shared': 3.5.38
+ entities: 7.0.1
+ estree-walker: 2.0.2
+ source-map-js: 1.2.1
+
+ '@vue/compiler-dom@3.5.38':
+ dependencies:
+ '@vue/compiler-core': 3.5.38
+ '@vue/shared': 3.5.38
+
+ '@vue/compiler-sfc@3.5.38':
+ dependencies:
+ '@babel/parser': 7.29.7
+ '@vue/compiler-core': 3.5.38
+ '@vue/compiler-dom': 3.5.38
+ '@vue/compiler-ssr': 3.5.38
+ '@vue/shared': 3.5.38
+ estree-walker: 2.0.2
+ magic-string: 0.30.21
+ postcss: 8.5.15
+ source-map-js: 1.2.1
+
+ '@vue/compiler-ssr@3.5.38':
+ dependencies:
+ '@vue/compiler-dom': 3.5.38
+ '@vue/shared': 3.5.38
+
+ '@vue/devtools-api@7.7.9':
+ dependencies:
+ '@vue/devtools-kit': 7.7.9
+
+ '@vue/devtools-api@8.1.3':
+ dependencies:
+ '@vue/devtools-kit': 8.1.3
+
+ '@vue/devtools-kit@7.7.9':
+ dependencies:
+ '@vue/devtools-shared': 7.7.9
+ birpc: 2.9.0
+ hookable: 5.5.3
+ mitt: 3.0.1
+ perfect-debounce: 1.0.0
+ speakingurl: 14.0.1
+ superjson: 2.2.6
+
+ '@vue/devtools-kit@8.1.3':
+ dependencies:
+ '@vue/devtools-shared': 8.1.3
+ birpc: 2.9.0
+ hookable: 5.5.3
+ perfect-debounce: 2.1.0
+
+ '@vue/devtools-shared@7.7.9':
+ dependencies:
+ rfdc: 1.4.1
+
+ '@vue/devtools-shared@8.1.3': {}
+
+ '@vue/language-core@3.3.5':
+ dependencies:
+ '@volar/language-core': 2.4.28
+ '@vue/compiler-dom': 3.5.38
+ '@vue/shared': 3.5.38
+ alien-signals: 3.2.1
+ muggle-string: 0.4.1
+ path-browserify: 1.0.1
+ picomatch: 4.0.4
+
+ '@vue/reactivity@3.5.38':
+ dependencies:
+ '@vue/shared': 3.5.38
+
+ '@vue/runtime-core@3.5.38':
+ dependencies:
+ '@vue/reactivity': 3.5.38
+ '@vue/shared': 3.5.38
+
+ '@vue/runtime-dom@3.5.38':
+ dependencies:
+ '@vue/reactivity': 3.5.38
+ '@vue/runtime-core': 3.5.38
+ '@vue/shared': 3.5.38
+ csstype: 3.2.3
+
+ '@vue/server-renderer@3.5.38(vue@3.5.38(typescript@6.0.3))':
+ dependencies:
+ '@vue/compiler-ssr': 3.5.38
+ '@vue/shared': 3.5.38
+ vue: 3.5.38(typescript@6.0.3)
+
+ '@vue/shared@3.5.38': {}
+
+ '@vue/tsconfig@0.9.1(typescript@6.0.3)(vue@3.5.38(typescript@6.0.3))':
+ optionalDependencies:
+ typescript: 6.0.3
+ vue: 3.5.38(typescript@6.0.3)
+
+ acorn@8.17.0: {}
+
+ agent-base@6.0.2:
+ dependencies:
+ debug: 4.4.3
+ transitivePeerDependencies:
+ - supports-color
+
+ alien-signals@3.2.1: {}
+
+ ast-kit@2.2.0:
+ dependencies:
+ '@babel/parser': 7.29.7
+ pathe: 2.0.3
+
+ ast-walker-scope@0.9.0:
+ dependencies:
+ '@babel/parser': 7.29.7
+ '@babel/types': 7.29.7
+ ast-kit: 2.2.0
+
+ asynckit@0.4.0: {}
+
+ axios@1.18.0:
+ dependencies:
+ follow-redirects: 1.16.0
+ form-data: 4.0.6
+ https-proxy-agent: 5.0.1
+ proxy-from-env: 2.1.0
+ transitivePeerDependencies:
+ - debug
+ - supports-color
+
+ birpc@2.9.0: {}
+
+ call-bind-apply-helpers@1.0.2:
+ dependencies:
+ es-errors: 1.3.0
+ function-bind: 1.1.2
+
+ chokidar@5.0.0:
+ dependencies:
+ readdirp: 5.0.0
+
+ combined-stream@1.0.8:
+ dependencies:
+ delayed-stream: 1.0.0
+
+ confbox@0.1.8: {}
+
+ confbox@0.2.4: {}
+
+ copy-anything@4.0.5:
+ dependencies:
+ is-what: 5.5.0
+
+ csstype@3.2.3: {}
+
+ debug@4.4.3:
+ dependencies:
+ ms: 2.1.3
+
+ delayed-stream@1.0.0: {}
+
+ detect-libc@2.1.2: {}
+
+ dunder-proto@1.0.1:
+ dependencies:
+ call-bind-apply-helpers: 1.0.2
+ es-errors: 1.3.0
+ gopd: 1.2.0
+
+ echarts@6.1.0:
+ dependencies:
+ tslib: 2.3.0
+ zrender: 6.1.0
+
+ enhanced-resolve@5.21.6:
+ dependencies:
+ graceful-fs: 4.2.11
+ tapable: 2.3.3
+
+ entities@7.0.1: {}
+
+ es-define-property@1.0.1: {}
+
+ es-errors@1.3.0: {}
+
+ es-object-atoms@1.1.2:
+ dependencies:
+ es-errors: 1.3.0
+
+ es-set-tostringtag@2.1.0:
+ dependencies:
+ es-errors: 1.3.0
+ get-intrinsic: 1.3.0
+ has-tostringtag: 1.0.2
+ hasown: 2.0.4
+
+ estree-walker@2.0.2: {}
+
+ exsolve@1.0.8: {}
+
+ fdir@6.5.0(picomatch@4.0.4):
+ optionalDependencies:
+ picomatch: 4.0.4
+
+ follow-redirects@1.16.0: {}
+
+ form-data@4.0.6:
+ dependencies:
+ asynckit: 0.4.0
+ combined-stream: 1.0.8
+ es-set-tostringtag: 2.1.0
+ hasown: 2.0.4
+ mime-types: 2.1.35
+
+ fsevents@2.3.3:
+ optional: true
+
+ function-bind@1.1.2: {}
+
+ get-intrinsic@1.3.0:
+ dependencies:
+ call-bind-apply-helpers: 1.0.2
+ es-define-property: 1.0.1
+ es-errors: 1.3.0
+ es-object-atoms: 1.1.2
+ function-bind: 1.1.2
+ get-proto: 1.0.1
+ gopd: 1.2.0
+ has-symbols: 1.1.0
+ hasown: 2.0.4
+ math-intrinsics: 1.1.0
+
+ get-proto@1.0.1:
+ dependencies:
+ dunder-proto: 1.0.1
+ es-object-atoms: 1.1.2
+
+ gopd@1.2.0: {}
+
+ graceful-fs@4.2.11: {}
+
+ has-symbols@1.1.0: {}
+
+ has-tostringtag@1.0.2:
+ dependencies:
+ has-symbols: 1.1.0
+
+ hasown@2.0.4:
+ dependencies:
+ function-bind: 1.1.2
+
+ hookable@5.5.3: {}
+
+ https-proxy-agent@5.0.1:
+ dependencies:
+ agent-base: 6.0.2
+ debug: 4.4.3
+ transitivePeerDependencies:
+ - supports-color
+
+ is-what@5.5.0: {}
+
+ jiti@2.7.0: {}
+
+ jsesc@3.1.0: {}
+
+ json5@2.2.3: {}
+
+ lightningcss-android-arm64@1.32.0:
+ optional: true
+
+ lightningcss-darwin-arm64@1.32.0:
+ optional: true
+
+ lightningcss-darwin-x64@1.32.0:
+ optional: true
+
+ lightningcss-freebsd-x64@1.32.0:
+ optional: true
+
+ lightningcss-linux-arm-gnueabihf@1.32.0:
+ optional: true
+
+ lightningcss-linux-arm64-gnu@1.32.0:
+ optional: true
+
+ lightningcss-linux-arm64-musl@1.32.0:
+ optional: true
+
+ lightningcss-linux-x64-gnu@1.32.0:
+ optional: true
+
+ lightningcss-linux-x64-musl@1.32.0:
+ optional: true
+
+ lightningcss-win32-arm64-msvc@1.32.0:
+ optional: true
+
+ lightningcss-win32-x64-msvc@1.32.0:
+ optional: true
+
+ lightningcss@1.32.0:
+ dependencies:
+ detect-libc: 2.1.2
+ 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
+
+ local-pkg@1.2.1:
+ dependencies:
+ mlly: 1.8.2
+ pkg-types: 2.3.1
+ quansync: 0.2.11
+
+ magic-string-ast@1.0.3:
+ dependencies:
+ magic-string: 0.30.21
+
+ magic-string@0.30.21:
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.5.5
+
+ math-intrinsics@1.1.0: {}
+
+ mime-db@1.52.0: {}
+
+ mime-types@2.1.35:
+ dependencies:
+ mime-db: 1.52.0
+
+ mitt@3.0.1: {}
+
+ mlly@1.8.2:
+ dependencies:
+ acorn: 8.17.0
+ pathe: 2.0.3
+ pkg-types: 1.3.1
+ ufo: 1.6.4
+
+ ms@2.1.3: {}
+
+ muggle-string@0.4.1: {}
+
+ nanoid@3.3.12: {}
+
+ path-browserify@1.0.1: {}
+
+ pathe@2.0.3: {}
+
+ perfect-debounce@1.0.0: {}
+
+ perfect-debounce@2.1.0: {}
+
+ picocolors@1.1.1: {}
+
+ picomatch@4.0.4: {}
+
+ pinia@3.0.4(typescript@6.0.3)(vue@3.5.38(typescript@6.0.3)):
+ dependencies:
+ '@vue/devtools-api': 7.7.9
+ vue: 3.5.38(typescript@6.0.3)
+ optionalDependencies:
+ typescript: 6.0.3
+
+ pkg-types@1.3.1:
+ dependencies:
+ confbox: 0.1.8
+ mlly: 1.8.2
+ pathe: 2.0.3
+
+ pkg-types@2.3.1:
+ dependencies:
+ confbox: 0.2.4
+ exsolve: 1.0.8
+ pathe: 2.0.3
+
+ postcss@8.5.15:
+ dependencies:
+ nanoid: 3.3.12
+ picocolors: 1.1.1
+ source-map-js: 1.2.1
+
+ proxy-from-env@2.1.0: {}
+
+ quansync@0.2.11: {}
+
+ readdirp@5.0.0: {}
+
+ rfdc@1.4.1: {}
+
+ rolldown@1.0.3:
+ dependencies:
+ '@oxc-project/types': 0.133.0
+ '@rolldown/pluginutils': 1.0.1
+ optionalDependencies:
+ '@rolldown/binding-android-arm64': 1.0.3
+ '@rolldown/binding-darwin-arm64': 1.0.3
+ '@rolldown/binding-darwin-x64': 1.0.3
+ '@rolldown/binding-freebsd-x64': 1.0.3
+ '@rolldown/binding-linux-arm-gnueabihf': 1.0.3
+ '@rolldown/binding-linux-arm64-gnu': 1.0.3
+ '@rolldown/binding-linux-arm64-musl': 1.0.3
+ '@rolldown/binding-linux-ppc64-gnu': 1.0.3
+ '@rolldown/binding-linux-s390x-gnu': 1.0.3
+ '@rolldown/binding-linux-x64-gnu': 1.0.3
+ '@rolldown/binding-linux-x64-musl': 1.0.3
+ '@rolldown/binding-openharmony-arm64': 1.0.3
+ '@rolldown/binding-wasm32-wasi': 1.0.3
+ '@rolldown/binding-win32-arm64-msvc': 1.0.3
+ '@rolldown/binding-win32-x64-msvc': 1.0.3
+
+ scule@1.3.0: {}
+
+ source-map-js@1.2.1: {}
+
+ speakingurl@14.0.1: {}
+
+ superjson@2.2.6:
+ dependencies:
+ copy-anything: 4.0.5
+
+ tailwindcss@4.3.1: {}
+
+ tapable@2.3.3: {}
+
+ tinyglobby@0.2.17:
+ dependencies:
+ fdir: 6.5.0(picomatch@4.0.4)
+ picomatch: 4.0.4
+
+ tslib@2.3.0: {}
+
+ tslib@2.8.1:
+ optional: true
+
+ typescript@6.0.3: {}
+
+ ufo@1.6.4: {}
+
+ undici-types@7.24.6: {}
+
+ unplugin-utils@0.3.1:
+ dependencies:
+ pathe: 2.0.3
+ picomatch: 4.0.4
+
+ unplugin@3.0.0:
+ dependencies:
+ '@jridgewell/remapping': 2.3.5
+ picomatch: 4.0.4
+ webpack-virtual-modules: 0.6.2
+
+ vite@8.0.16(@types/node@25.9.3)(jiti@2.7.0)(yaml@2.9.0):
+ dependencies:
+ lightningcss: 1.32.0
+ picomatch: 4.0.4
+ postcss: 8.5.15
+ rolldown: 1.0.3
+ tinyglobby: 0.2.17
+ optionalDependencies:
+ '@types/node': 25.9.3
+ fsevents: 2.3.3
+ jiti: 2.7.0
+ yaml: 2.9.0
+
+ vscode-uri@3.1.0: {}
+
+ vue-router@5.1.0(@vue/compiler-sfc@3.5.38)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.38(typescript@6.0.3)))(vite@8.0.16(@types/node@25.9.3)(jiti@2.7.0)(yaml@2.9.0))(vue@3.5.38(typescript@6.0.3)):
+ dependencies:
+ '@babel/generator': 8.0.0-rc.6
+ '@vue-macros/common': 3.1.2(vue@3.5.38(typescript@6.0.3))
+ '@vue/devtools-api': 8.1.3
+ ast-walker-scope: 0.9.0
+ chokidar: 5.0.0
+ json5: 2.2.3
+ local-pkg: 1.2.1
+ magic-string: 0.30.21
+ mlly: 1.8.2
+ muggle-string: 0.4.1
+ pathe: 2.0.3
+ picomatch: 4.0.4
+ scule: 1.3.0
+ tinyglobby: 0.2.17
+ unplugin: 3.0.0
+ unplugin-utils: 0.3.1
+ vue: 3.5.38(typescript@6.0.3)
+ yaml: 2.9.0
+ optionalDependencies:
+ '@vue/compiler-sfc': 3.5.38
+ pinia: 3.0.4(typescript@6.0.3)(vue@3.5.38(typescript@6.0.3))
+ vite: 8.0.16(@types/node@25.9.3)(jiti@2.7.0)(yaml@2.9.0)
+
+ vue-tsc@3.3.5(typescript@6.0.3):
+ dependencies:
+ '@volar/typescript': 2.4.28
+ '@vue/language-core': 3.3.5
+ typescript: 6.0.3
+
+ vue@3.5.38(typescript@6.0.3):
+ dependencies:
+ '@vue/compiler-dom': 3.5.38
+ '@vue/compiler-sfc': 3.5.38
+ '@vue/runtime-dom': 3.5.38
+ '@vue/server-renderer': 3.5.38(vue@3.5.38(typescript@6.0.3))
+ '@vue/shared': 3.5.38
+ optionalDependencies:
+ typescript: 6.0.3
+
+ webpack-virtual-modules@0.6.2: {}
+
+ yaml@2.9.0: {}
+
+ zrender@6.1.0:
+ dependencies:
+ tslib: 2.3.0
diff --git a/web/pnpm-workspace.yaml b/web/pnpm-workspace.yaml
new file mode 100644
index 000000000..8e8049db6
--- /dev/null
+++ b/web/pnpm-workspace.yaml
@@ -0,0 +1,2 @@
+minimumReleaseAgeExclude:
+ - axios@1.18.0
diff --git a/web/public/favicon.svg b/web/public/favicon.svg
new file mode 100644
index 000000000..9a773b28f
--- /dev/null
+++ b/web/public/favicon.svg
@@ -0,0 +1,4 @@
+
diff --git a/web/src/App.vue b/web/src/App.vue
new file mode 100644
index 000000000..a78fb6fd3
--- /dev/null
+++ b/web/src/App.vue
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/web/src/api/index.ts b/web/src/api/index.ts
new file mode 100644
index 000000000..d13cb1509
--- /dev/null
+++ b/web/src/api/index.ts
@@ -0,0 +1,2 @@
+export * from './request'
+export type * from './types'
diff --git a/web/src/api/request.ts b/web/src/api/request.ts
new file mode 100644
index 000000000..7dcee361b
--- /dev/null
+++ b/web/src/api/request.ts
@@ -0,0 +1,70 @@
+import axios, { type AxiosHeaders, type AxiosRequestConfig, type AxiosResponse, type AxiosResponseHeaders } from 'axios'
+
+import type { ApiEnvelope } from './types'
+
+export class ApiError extends Error {
+ constructor(
+ message: string,
+ public readonly status: number,
+ public readonly requestId?: string,
+ ) {
+ super(message)
+ this.name = 'ApiError'
+ }
+}
+
+export const http = axios.create({
+ baseURL: '/',
+ withCredentials: true,
+ headers: {
+ Accept: 'application/json',
+ 'Content-Type': 'application/json',
+ },
+})
+
+type ResponseHeaders = AxiosResponseHeaders | Partial
+
+function headerValue(headers: ResponseHeaders | undefined, name: string) {
+ if (!headers) return undefined
+ const value = headers[name] ?? headers[name.toLowerCase()]
+ return typeof value === 'string' ? value : undefined
+}
+
+function isApiEnvelope(value: unknown): value is ApiEnvelope {
+ return typeof value === 'object'
+ && value !== null
+ && 'data' in value
+ && 'code' in value
+ && 'message' in value
+}
+
+export async function requestJson(
+ url: string,
+ config: Omit, 'url'> = {},
+): Promise {
+ try {
+ const response = await http.request | T, AxiosResponse | T>, D>({
+ url,
+ ...config,
+ headers: {
+ Accept: 'application/json',
+ 'Content-Type': 'application/json',
+ ...config.headers,
+ },
+ })
+
+ return isApiEnvelope(response.data) ? response.data.data : response.data
+ } catch (error) {
+ if (axios.isAxiosError>(error)) {
+ const requestId = headerValue(error.response?.headers, 'x-request-id')
+ ?? error.response?.data?.requestId
+ throw new ApiError(
+ error.response?.data?.message ?? error.message,
+ error.response?.status ?? 0,
+ requestId,
+ )
+ }
+
+ throw error
+ }
+}
diff --git a/web/src/api/types.ts b/web/src/api/types.ts
new file mode 100644
index 000000000..7a339a5ba
--- /dev/null
+++ b/web/src/api/types.ts
@@ -0,0 +1,6 @@
+export interface ApiEnvelope {
+ code: string
+ message: string
+ requestId: string
+ data: T
+}
diff --git a/web/src/components/base/BaseBadge.vue b/web/src/components/base/BaseBadge.vue
new file mode 100644
index 000000000..11d2fe8da
--- /dev/null
+++ b/web/src/components/base/BaseBadge.vue
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
diff --git a/web/src/components/base/BaseButton.vue b/web/src/components/base/BaseButton.vue
new file mode 100644
index 000000000..0519553c5
--- /dev/null
+++ b/web/src/components/base/BaseButton.vue
@@ -0,0 +1,55 @@
+
+
+
+
+
diff --git a/web/src/components/base/BaseCard.vue b/web/src/components/base/BaseCard.vue
new file mode 100644
index 000000000..6106552e6
--- /dev/null
+++ b/web/src/components/base/BaseCard.vue
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+ {{ title }}
+
+
+ {{ description }}
+
+
+
+
+
+
+
+
+
diff --git a/web/src/components/base/BaseEmpty.vue b/web/src/components/base/BaseEmpty.vue
new file mode 100644
index 000000000..722f841d1
--- /dev/null
+++ b/web/src/components/base/BaseEmpty.vue
@@ -0,0 +1,14 @@
+
+
+
+
+
+
{{ title ?? '暂无数据' }}
+
{{ description }}
+
+
diff --git a/web/src/components/base/BaseIconButton.vue b/web/src/components/base/BaseIconButton.vue
new file mode 100644
index 000000000..e84b98c59
--- /dev/null
+++ b/web/src/components/base/BaseIconButton.vue
@@ -0,0 +1,25 @@
+
+
+
+
+
diff --git a/web/src/components/base/BaseInput.vue b/web/src/components/base/BaseInput.vue
new file mode 100644
index 000000000..9a377ff27
--- /dev/null
+++ b/web/src/components/base/BaseInput.vue
@@ -0,0 +1,38 @@
+
+
+
+
+
diff --git a/web/src/components/base/BaseModal.vue b/web/src/components/base/BaseModal.vue
new file mode 100644
index 000000000..28642a1be
--- /dev/null
+++ b/web/src/components/base/BaseModal.vue
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
{{ title }}
+
+ {{ description }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/components/base/BaseSegmented.vue b/web/src/components/base/BaseSegmented.vue
new file mode 100644
index 000000000..22cc1968c
--- /dev/null
+++ b/web/src/components/base/BaseSegmented.vue
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
diff --git a/web/src/components/base/BaseSelect.vue b/web/src/components/base/BaseSelect.vue
new file mode 100644
index 000000000..7ce91fbf0
--- /dev/null
+++ b/web/src/components/base/BaseSelect.vue
@@ -0,0 +1,30 @@
+
+
+
+
+
diff --git a/web/src/components/base/BaseSpinner.vue b/web/src/components/base/BaseSpinner.vue
new file mode 100644
index 000000000..0382040bf
--- /dev/null
+++ b/web/src/components/base/BaseSpinner.vue
@@ -0,0 +1,14 @@
+
+
+
+
+
+ {{ label }}
+
+
diff --git a/web/src/components/base/BaseTable.vue b/web/src/components/base/BaseTable.vue
new file mode 100644
index 000000000..a99c5fda1
--- /dev/null
+++ b/web/src/components/base/BaseTable.vue
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+ {{ column.label }}
+
+
+
+
+
+ {{ cellValue(row, column.key) }}
+
+
+
+
+
+
diff --git a/web/src/components/base/BaseToast.vue b/web/src/components/base/BaseToast.vue
new file mode 100644
index 000000000..71e785225
--- /dev/null
+++ b/web/src/components/base/BaseToast.vue
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+ {{ title }}
+ {{ message }}
+
+
+
diff --git a/web/src/components/charts/BaseChart.vue b/web/src/components/charts/BaseChart.vue
new file mode 100644
index 000000000..b5222c43a
--- /dev/null
+++ b/web/src/components/charts/BaseChart.vue
@@ -0,0 +1,33 @@
+
+
+
+
+
diff --git a/web/src/composables/useECharts.ts b/web/src/composables/useECharts.ts
new file mode 100644
index 000000000..f45815ef3
--- /dev/null
+++ b/web/src/composables/useECharts.ts
@@ -0,0 +1,53 @@
+import { init, type EChartsType } from 'echarts/core'
+import type { EChartsOption } from 'echarts'
+import { onBeforeUnmount, shallowRef, watch, type ShallowRef } from 'vue'
+
+import '@/plugins/echarts'
+
+export function useECharts(
+ target: Readonly>,
+ option: Readonly>,
+) {
+ const chart = shallowRef()
+ const observer = shallowRef()
+
+ function resize() {
+ chart.value?.resize()
+ }
+
+ function dispose() {
+ observer.value?.disconnect()
+ chart.value?.dispose()
+ observer.value = undefined
+ chart.value = undefined
+ }
+
+ watch(
+ target,
+ (element) => {
+ dispose()
+ if (!element) return
+ chart.value = init(element, undefined, { renderer: 'canvas' })
+ chart.value.setOption(option.value)
+ observer.value = new ResizeObserver(resize)
+ observer.value.observe(element)
+ },
+ { immediate: true },
+ )
+
+ watch(
+ option,
+ (nextOption) => {
+ chart.value?.setOption(nextOption, true)
+ },
+ { deep: true },
+ )
+
+ onBeforeUnmount(dispose)
+
+ return {
+ chart,
+ resize,
+ dispose,
+ }
+}
diff --git a/web/src/layout/components/AppSidebar.vue b/web/src/layout/components/AppSidebar.vue
new file mode 100644
index 000000000..e026bd43a
--- /dev/null
+++ b/web/src/layout/components/AppSidebar.vue
@@ -0,0 +1,73 @@
+
+
+
+
+
diff --git a/web/src/layout/components/AppTopbar.vue b/web/src/layout/components/AppTopbar.vue
new file mode 100644
index 000000000..8d88bbf0e
--- /dev/null
+++ b/web/src/layout/components/AppTopbar.vue
@@ -0,0 +1,111 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ A
+
+ admin
+ 管理员
+
+
+
+ 在线
+
+
+
+
+
+ MFA
+ 已开启
+
+
+ 会话
+ 1 台
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/layout/index.vue b/web/src/layout/index.vue
new file mode 100644
index 000000000..5938599b9
--- /dev/null
+++ b/web/src/layout/index.vue
@@ -0,0 +1,14 @@
+
+
+
+
+
diff --git a/web/src/main.ts b/web/src/main.ts
new file mode 100644
index 000000000..672168ec8
--- /dev/null
+++ b/web/src/main.ts
@@ -0,0 +1,9 @@
+import { createApp } from 'vue'
+
+import App from './App.vue'
+import { router } from './router'
+import { pinia } from './stores'
+
+import './styles/index.css'
+
+createApp(App).use(pinia).use(router).mount('#app')
diff --git a/web/src/plugins/echarts.ts b/web/src/plugins/echarts.ts
new file mode 100644
index 000000000..936ca8fd5
--- /dev/null
+++ b/web/src/plugins/echarts.ts
@@ -0,0 +1,11 @@
+import { LineChart } from 'echarts/charts'
+import { GridComponent, TooltipComponent } from 'echarts/components'
+import { use } from 'echarts/core'
+import { CanvasRenderer } from 'echarts/renderers'
+
+use([
+ LineChart,
+ GridComponent,
+ TooltipComponent,
+ CanvasRenderer,
+])
diff --git a/web/src/router/index.ts b/web/src/router/index.ts
new file mode 100644
index 000000000..29d3acc69
--- /dev/null
+++ b/web/src/router/index.ts
@@ -0,0 +1,8 @@
+import { createRouter, createWebHistory } from 'vue-router'
+
+import { routes } from './routes'
+
+export const router = createRouter({
+ history: createWebHistory('/admin/'),
+ routes,
+})
diff --git a/web/src/router/routes.ts b/web/src/router/routes.ts
new file mode 100644
index 000000000..8d0137177
--- /dev/null
+++ b/web/src/router/routes.ts
@@ -0,0 +1,22 @@
+import type { RouteRecordRaw } from 'vue-router'
+
+import AdminLayout from '@/layout/index.vue'
+import DashboardView from '@/views/dashboard/DashboardView.vue'
+
+export const routes: RouteRecordRaw[] = [
+ {
+ path: '/',
+ component: AdminLayout,
+ children: [
+ {
+ path: '',
+ name: 'dashboard',
+ component: DashboardView,
+ },
+ ],
+ },
+ {
+ path: '/:pathMatch(.*)*',
+ redirect: '/',
+ },
+]
diff --git a/web/src/stores/index.ts b/web/src/stores/index.ts
new file mode 100644
index 000000000..c569ef7f2
--- /dev/null
+++ b/web/src/stores/index.ts
@@ -0,0 +1,3 @@
+import { createPinia } from 'pinia'
+
+export const pinia = createPinia()
diff --git a/web/src/stores/modules/app.ts b/web/src/stores/modules/app.ts
new file mode 100644
index 000000000..d3a3a8f6c
--- /dev/null
+++ b/web/src/stores/modules/app.ts
@@ -0,0 +1,15 @@
+import { defineStore } from 'pinia'
+import { shallowRef } from 'vue'
+
+export const useAppStore = defineStore('app', () => {
+ const sidebarCollapsed = shallowRef(false)
+
+ function toggleSidebar() {
+ sidebarCollapsed.value = !sidebarCollapsed.value
+ }
+
+ return {
+ sidebarCollapsed,
+ toggleSidebar,
+ }
+})
diff --git a/web/src/stores/modules/session.ts b/web/src/stores/modules/session.ts
new file mode 100644
index 000000000..3307f0ade
--- /dev/null
+++ b/web/src/stores/modules/session.ts
@@ -0,0 +1,17 @@
+import { defineStore } from 'pinia'
+
+export interface SessionUser {
+ name: string
+ role: string
+}
+
+export const useSessionStore = defineStore('session', () => {
+ const user: SessionUser = {
+ name: 'admin',
+ role: '管理员',
+ }
+
+ return {
+ user,
+ }
+})
diff --git a/web/src/styles/base.css b/web/src/styles/base.css
new file mode 100644
index 000000000..7ae759623
--- /dev/null
+++ b/web/src/styles/base.css
@@ -0,0 +1,38 @@
+* {
+ box-sizing: border-box;
+}
+
+html {
+ min-width: 360px;
+ background: var(--cp-bg-page);
+ color: var(--cp-text-primary);
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+body {
+ min-height: 100vh;
+ margin: 0;
+ background: var(--cp-bg-page);
+}
+
+button,
+input,
+select,
+textarea {
+ font: inherit;
+}
+
+button {
+ cursor: pointer;
+}
+
+button:disabled {
+ cursor: not-allowed;
+}
+
+#app {
+ min-height: 100vh;
+}
diff --git a/web/src/styles/index.css b/web/src/styles/index.css
new file mode 100644
index 000000000..a143158f2
--- /dev/null
+++ b/web/src/styles/index.css
@@ -0,0 +1,19 @@
+@import './tokens.css';
+@import './base.css';
+@import 'tailwindcss';
+
+@theme {
+ --font-sans: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
+ --font-mono: "JetBrains Mono", "SFMono-Regular", Consolas, "Liberation Mono", monospace;
+ --color-cp-page: var(--cp-bg-page);
+ --color-cp-surface: var(--cp-bg-surface);
+ --color-cp-subtle: var(--cp-bg-subtle);
+ --color-cp-muted: var(--cp-bg-muted);
+ --color-cp-primary: var(--cp-text-primary);
+ --color-cp-secondary: var(--cp-text-secondary);
+ --color-cp-info: var(--cp-info);
+ --color-cp-normal: var(--cp-normal);
+ --color-cp-success: var(--cp-success);
+ --color-cp-warning: var(--cp-warning);
+ --color-cp-danger: var(--cp-danger);
+}
diff --git a/web/src/styles/tokens.css b/web/src/styles/tokens.css
new file mode 100644
index 000000000..dd2678d0f
--- /dev/null
+++ b/web/src/styles/tokens.css
@@ -0,0 +1,106 @@
+:root {
+ color-scheme: light;
+
+ --cp-bg-page: #f6f8fb;
+ --cp-bg-surface: #ffffff;
+ --cp-bg-subtle: #f8fafc;
+ --cp-bg-muted: #f1f5f9;
+ --cp-bg-nav-active: #e9eef5;
+ --cp-bg-dark: #111827;
+ --cp-white: #ffffff;
+ --cp-transparent: #ffffff00;
+ --cp-overlay-scrim: #0e17264d;
+
+ --cp-text-primary: #0e1726;
+ --cp-text-strong: #111827;
+ --cp-text-secondary: #64748b;
+ --cp-text-muted: #94a3b8;
+
+ --cp-default-bg: #f8fafc;
+ --cp-default-bg-hover: #f1f5f9;
+ --cp-default-bg-active: #e9eef5;
+ --cp-default-border: #e2e8f0;
+ --cp-default-border-hover: #cbd5e1;
+ --cp-default-text: #0e1726;
+ --cp-default-text-hover: #111827;
+ --cp-default-text-active: #0b1220;
+
+ --cp-disabled-bg: #f1f5f9;
+ --cp-disabled-border: #e2e8f0;
+ --cp-disabled-text: #94a3b8;
+ --cp-disabled-icon: #cbd5e1;
+
+ --cp-info: #2563eb;
+ --cp-info-hover: #1d4ed8;
+ --cp-info-pressed: #1e40af;
+ --cp-info-bg: #eef6ff;
+ --cp-info-bg-hover: #dbeafe;
+ --cp-info-bg-active: #bfdbfe;
+ --cp-info-border: #93c5fd;
+ --cp-info-text: #2563eb;
+ --cp-info-on: #ffffff;
+
+ --cp-normal: #0f9f9a;
+ --cp-normal-hover: #0d8d88;
+ --cp-normal-pressed: #0b7370;
+ --cp-normal-bg: #ecfdfd;
+ --cp-normal-bg-hover: #ddf7f6;
+ --cp-normal-bg-active: #c7efee;
+ --cp-normal-border: #a7dfdd;
+ --cp-normal-text: #0b7370;
+ --cp-normal-on: #0b1220;
+
+ --cp-success: #12b981;
+ --cp-success-hover: #0ea371;
+ --cp-success-pressed: #0b865f;
+ --cp-success-bg: #ecfdf5;
+ --cp-success-bg-hover: #ddfbea;
+ --cp-success-bg-active: #c8f5dc;
+ --cp-success-border: #b7ead0;
+ --cp-success-text: #047857;
+ --cp-success-on: #0b1220;
+
+ --cp-warning: #f59e0b;
+ --cp-warning-hover: #d97706;
+ --cp-warning-pressed: #b45309;
+ --cp-warning-bg: #fffbeb;
+ --cp-warning-bg-hover: #fef3c7;
+ --cp-warning-bg-active: #fde68a;
+ --cp-warning-border: #fcd34d;
+ --cp-warning-text: #b45309;
+ --cp-warning-on: #0b1220;
+
+ --cp-danger: #ef4444;
+ --cp-danger-hover: #dc2626;
+ --cp-danger-pressed: #b91c1c;
+ --cp-danger-bg: #fef2f2;
+ --cp-danger-bg-hover: #fee2e2;
+ --cp-danger-bg-active: #fecaca;
+ --cp-danger-border: #fca5a5;
+ --cp-danger-text: #b91c1c;
+ --cp-danger-on: #0b1220;
+
+ --cp-control-height-small: 24px;
+ --cp-control-height-default: 32px;
+ --cp-control-height-large: 40px;
+ --cp-control-padding-x: 12px;
+ --cp-control-radius-small: 2px;
+ --cp-control-radius-base: 4px;
+ --cp-button-radius-small: 4px;
+ --cp-button-radius-base: 6px;
+ --cp-icon-button-radius: 8px;
+ --cp-card-radius: 18px;
+ --cp-panel-radius: 16px;
+ --cp-tag-radius: 7px;
+ --cp-radius-circle: 999px;
+
+ --cp-shadow-card: 0 10px 22px -18px #0e172614;
+ --cp-shadow-control: 0 8px 18px -16px #0e172614;
+ --cp-shadow-popover: 0 16px 34px -18px #0e172622;
+ --cp-shadow-sidebar: 2px 0 12px -12px #0e172607;
+
+ --cp-space-page-x: 28px;
+ --cp-space-page-y: 32px;
+ --cp-gap-card: 24px;
+ --cp-sidebar-width: 280px;
+}
diff --git a/web/src/views/dashboard/DashboardView.vue b/web/src/views/dashboard/DashboardView.vue
new file mode 100644
index 000000000..4add10f51
--- /dev/null
+++ b/web/src/views/dashboard/DashboardView.vue
@@ -0,0 +1,49 @@
+
+
+
+
+
diff --git a/web/src/views/dashboard/components/AccountOverviewCard.vue b/web/src/views/dashboard/components/AccountOverviewCard.vue
new file mode 100644
index 000000000..b522c5ef2
--- /dev/null
+++ b/web/src/views/dashboard/components/AccountOverviewCard.vue
@@ -0,0 +1,184 @@
+
+
+
+
+
+
+ 账号调度
+ 容量、并发与分配策略
+
+
+
可调度容量
+
+ 81 / 135
+ 60% 已分配
+
+
+
+
+
+
+
+
+ {{ stat.label }}
+ {{ stat.value }}
+
+
+
+
+ 分配策略
+ 最少使用优先
+
+
+
+
+ 活跃账号用量
+ 24h 请求排序
+
+
+
+
+ {{ account.name[0] }}
+
+
+
+ {{ account.name }}
+ {{ account.plan }}
+
+ {{ account.email }}
+
+ {{ account.requests }}
+ {{ account.tokens }}
+ {{ account.lastUsed }}
+
+
+
+
+
+
+
+
+
+
+
+
+ 状态分布
+ 不可用 7
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ row.label }}
+ {{ row.description }}
+
+ {{ row.value }}
+
+
+
+
+
+
diff --git a/web/src/views/dashboard/components/EventLogCard.vue b/web/src/views/dashboard/components/EventLogCard.vue
new file mode 100644
index 000000000..0aaf5b9bc
--- /dev/null
+++ b/web/src/views/dashboard/components/EventLogCard.vue
@@ -0,0 +1,91 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ column.label }}
+
+
+
+
+
+
+ {{ row.time }}
+ {{ row.level }}
+ {{ row.requestId }}
+ {{ row.route }}
+ {{ row.model }}
+ {{ row.statusCode }}
+ {{ row.latency }}
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/dashboard/components/MetricCard.vue b/web/src/views/dashboard/components/MetricCard.vue
new file mode 100644
index 000000000..0059f269c
--- /dev/null
+++ b/web/src/views/dashboard/components/MetricCard.vue
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+ {{ metric.title }}
+
+
+
+ {{ metric.value }}
+
+
+
+ {{ metric.details[0]?.label }}
+
+ {{ metric.details[0]?.value }}
+
+ {{ metric.details[1]?.label }}
+
+ {{ metric.details[1]?.value }}
+
+
+
+
diff --git a/web/src/views/dashboard/components/RequestTrendCard.vue b/web/src/views/dashboard/components/RequestTrendCard.vue
new file mode 100644
index 000000000..71c851c94
--- /dev/null
+++ b/web/src/views/dashboard/components/RequestTrendCard.vue
@@ -0,0 +1,95 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/dashboard/components/ServiceStatusCard.vue b/web/src/views/dashboard/components/ServiceStatusCard.vue
new file mode 100644
index 000000000..ab9c25260
--- /dev/null
+++ b/web/src/views/dashboard/components/ServiceStatusCard.vue
@@ -0,0 +1,44 @@
+
+
+
+
+ 服务状态
+
+
+
+
+
+
+ {{ item.label }}
+ {{ item.value }}
+ {{ item.detail }}
+
+
+
+
diff --git a/web/src/views/dashboard/composables/useDashboard.ts b/web/src/views/dashboard/composables/useDashboard.ts
new file mode 100644
index 000000000..d1a0a3cb5
--- /dev/null
+++ b/web/src/views/dashboard/composables/useDashboard.ts
@@ -0,0 +1,188 @@
+import {
+ Activity,
+ Boxes,
+ CloudCheck,
+ FileText,
+ MonitorCheck,
+ RefreshCw,
+ ScrollText,
+ Timer,
+ Users,
+} from '@lucide/vue'
+
+import type {
+ AccountUsageItem,
+ EventLogItem,
+ MetricCardItem,
+ ServiceStatusItem,
+ TrendPoint,
+ TrendSummaryItem,
+} from '../types'
+
+export function useDashboard() {
+ const metrics: MetricCardItem[] = [
+ {
+ title: '账号',
+ value: '33',
+ icon: Users,
+ tone: 'normal',
+ details: [
+ { label: '启用', value: '1', tone: 'normal' },
+ { label: '错误', value: '31', tone: 'danger' },
+ ],
+ },
+ {
+ title: '今日请求',
+ value: '0',
+ icon: Activity,
+ tone: 'info',
+ details: [
+ { label: '今日', value: '0', tone: 'info' },
+ { label: '总计', value: '3,363', tone: 'info' },
+ ],
+ },
+ {
+ title: '总 Token',
+ value: '401.29M',
+ icon: FileText,
+ tone: 'success',
+ details: [
+ { label: '今日', value: '0', tone: 'success' },
+ { label: '总计', value: '401.29M', tone: 'success' },
+ ],
+ },
+ {
+ title: '平均响应',
+ value: '21.71s',
+ icon: Timer,
+ tone: 'warning',
+ details: [
+ { label: '今日', value: '21.71s', tone: 'warning' },
+ { label: '目标', value: '< 3s', tone: 'warning' },
+ ],
+ },
+ ]
+
+ const trendPoints: TrendPoint[] = [
+ { time: '00', requests: 0, tokens: 0, errors: 0 },
+ { time: '04', requests: 0, tokens: 0, errors: 0 },
+ { time: '08', requests: 0, tokens: 0, errors: 0 },
+ { time: '12', requests: 0, tokens: 0, errors: 0 },
+ { time: '16', requests: 0, tokens: 0, errors: 0 },
+ { time: '20', requests: 0, tokens: 0, errors: 0 },
+ { time: '24', requests: 0, tokens: 0, errors: 0 },
+ ]
+
+ const trendSummary: TrendSummaryItem[] = [
+ { label: '成功率', value: '99.88%', tone: 'success' },
+ { label: '峰值', value: '15.2K', tone: 'info' },
+ { label: '慢请求', value: '42', tone: 'warning' },
+ ]
+
+ const accountUsage: AccountUsageItem[] = [
+ {
+ name: 'Amy Ops',
+ email: 'amy.ops@example.com',
+ plan: 'plus',
+ requests: '12.8K',
+ tokens: '382K',
+ lastUsed: '2分钟前',
+ tone: 'info',
+ loadWidth: 84,
+ },
+ {
+ name: 'Team Codex',
+ email: 'team-codex@example.com',
+ plan: 'pro',
+ requests: '9.4K',
+ tokens: '241K',
+ lastUsed: '8分钟前',
+ tone: 'normal',
+ loadWidth: 70,
+ },
+ {
+ name: 'Build Bot',
+ email: 'build@proxy.local',
+ plan: 'team',
+ requests: '7.1K',
+ tokens: '198K',
+ lastUsed: '16分钟前',
+ tone: 'success',
+ loadWidth: 58,
+ },
+ {
+ name: 'Reviewer',
+ email: 'reviewer@example.com',
+ plan: 'plus',
+ requests: '6.8K',
+ tokens: '176K',
+ lastUsed: '24分钟前',
+ tone: 'warning',
+ loadWidth: 48,
+ },
+ ]
+
+ const serviceStatuses: ServiceStatusItem[] = [
+ { label: '上游连接', value: '正常', detail: '23ms', tone: 'success', icon: CloudCheck },
+ { label: '模型目录', value: '已同步', detail: '58 个模型', tone: 'info', icon: Boxes },
+ { label: '自动刷新', value: '开启', detail: '2 并发', tone: 'success', icon: RefreshCw },
+ { label: '事件记录', value: '关闭', detail: '已存 1.8K', tone: 'warning', icon: ScrollText },
+ { label: '客户端版本', value: '可用', detail: '26.519', tone: 'normal', icon: MonitorCheck },
+ ]
+
+ const eventLogs: EventLogItem[] = [
+ {
+ id: 'evt_01',
+ time: '14:30:25',
+ level: '信息',
+ requestId: 'req_01HW9K7QY2',
+ route: '/v1/responses',
+ model: 'gpt-5.5',
+ statusCode: '200',
+ latency: '1.25s',
+ tone: 'info',
+ },
+ {
+ id: 'evt_02',
+ time: '14:30:15',
+ level: '警告',
+ requestId: 'req_01HW9K6N4F',
+ route: '/v1/chat/completions',
+ model: 'gpt-5.5',
+ statusCode: '429',
+ latency: '2.15s',
+ tone: 'warning',
+ },
+ {
+ id: 'evt_03',
+ time: '14:30:10',
+ level: '错误',
+ requestId: 'req_01HW9K5Z7A',
+ route: '/v1/responses',
+ model: 'gpt-5.5',
+ statusCode: '502',
+ latency: '5.32s',
+ tone: 'danger',
+ },
+ {
+ id: 'evt_04',
+ time: '14:29:56',
+ level: '信息',
+ requestId: 'req_01HW9K4C3Q',
+ route: '/v1/models',
+ model: 'catalog',
+ statusCode: '200',
+ latency: '0.84s',
+ tone: 'info',
+ },
+ ]
+
+ return {
+ metrics,
+ trendPoints,
+ trendSummary,
+ accountUsage,
+ serviceStatuses,
+ eventLogs,
+ }
+}
diff --git a/web/src/views/dashboard/types.ts b/web/src/views/dashboard/types.ts
new file mode 100644
index 000000000..1a1cac52a
--- /dev/null
+++ b/web/src/views/dashboard/types.ts
@@ -0,0 +1,61 @@
+import type { Component } from 'vue'
+
+export type SemanticTone = 'normal' | 'info' | 'success' | 'warning' | 'danger'
+
+export interface MetricDetail {
+ label: string
+ value: string
+ tone?: SemanticTone
+}
+
+export interface MetricCardItem {
+ title: string
+ value: string
+ icon: Component
+ tone: SemanticTone
+ details: MetricDetail[]
+}
+
+export interface TrendPoint {
+ time: string
+ requests: number
+ tokens: number
+ errors: number
+}
+
+export interface TrendSummaryItem {
+ label: string
+ value: string
+ tone: SemanticTone
+}
+
+export interface AccountUsageItem {
+ name: string
+ email: string
+ plan: string
+ requests: string
+ tokens: string
+ lastUsed: string
+ tone: SemanticTone
+ loadWidth: number
+}
+
+export interface ServiceStatusItem {
+ label: string
+ value: string
+ detail: string
+ tone: SemanticTone
+ icon: Component
+}
+
+export interface EventLogItem {
+ id: string
+ time: string
+ level: string
+ requestId: string
+ route: string
+ model: string
+ statusCode: string
+ latency: string
+ tone: SemanticTone
+}
diff --git a/web/tests/dashboard-design.test.mjs b/web/tests/dashboard-design.test.mjs
new file mode 100644
index 000000000..3628a0762
--- /dev/null
+++ b/web/tests/dashboard-design.test.mjs
@@ -0,0 +1,176 @@
+import assert from 'node:assert/strict'
+import { readFile } from 'node:fs/promises'
+import { dirname, resolve } from 'node:path'
+import { test } from 'node:test'
+import { fileURLToPath } from 'node:url'
+
+const root = resolve(dirname(fileURLToPath(import.meta.url)), '..')
+
+async function source(path) {
+ return readFile(resolve(root, path), 'utf8')
+}
+
+function assertTokens(sourceText, tokens) {
+ for (const token of tokens) {
+ assert.match(
+ sourceText,
+ new RegExp(token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')),
+ `Expected token: ${token}`,
+ )
+ }
+}
+
+function assertNoAbsoluteLayout(sourceText, label) {
+ assert.doesNotMatch(
+ sourceText,
+ /\babsolute\b/,
+ `${label} should use grid/flex/margin/padding flow layout, not absolute positioning`,
+ )
+}
+
+test('dashboard page recreates the Pencil canvas with flow layout spacing', async () => {
+ const view = await source('src/views/dashboard/DashboardView.vue')
+
+ assertNoAbsoluteLayout(view, 'DashboardView')
+ assertTokens(view, [
+ 'ml-7 w-[1584px] pt-[34px] pb-[60px]',
+ 'flex h-[68px] items-start justify-between',
+ 'mt-0 text-[34px] leading-[1.15] font-[800]',
+ 'mt-2.5 text-[15px] leading-[1.15] font-semibold',
+ 'mt-6 grid grid-cols-4 gap-6',
+ 'mt-6 grid grid-cols-[948px_608px] gap-7',
+ 'mt-6',
+ 'MetricCard',
+ 'RequestTrendCard',
+ 'ServiceStatusCard',
+ 'AccountOverviewCard',
+ 'EventLogCard',
+ ])
+})
+
+test('topbar controls are normal flex items and only the popover floats', async () => {
+ const topbar = await source('src/layout/components/AppTopbar.vue')
+
+ assert.doesNotMatch(topbar, /absolute left-\[/)
+ assertTokens(topbar, [
+ 'flex h-11 items-start gap-3.5',
+ 'h-11 w-[170px]',
+ 'size-11',
+ 'h-11 w-[216px]',
+ 'relative',
+ 'absolute right-0 top-14',
+ 'h-[304px] w-80',
+ 'CalendarDays',
+ 'RefreshCw',
+ 'ChevronUp',
+ 'User',
+ 'ShieldCheck',
+ 'ScrollText',
+ 'LogOut',
+ 'MFA',
+ '已开启',
+ '会话',
+ '1 台',
+ ])
+})
+
+test('dashboard cards use grid and flex primitives with exact Pencil dimensions', async () => {
+ const metric = await source('src/views/dashboard/components/MetricCard.vue')
+ const trend = await source('src/views/dashboard/components/RequestTrendCard.vue')
+ const service = await source('src/views/dashboard/components/ServiceStatusCard.vue')
+ const account = await source('src/views/dashboard/components/AccountOverviewCard.vue')
+ const events = await source('src/views/dashboard/components/EventLogCard.vue')
+
+ for (const [label, file] of [
+ ['MetricCard', metric],
+ ['RequestTrendCard', trend],
+ ['ServiceStatusCard', service],
+ ['AccountOverviewCard', account],
+ ['EventLogCard', events],
+ ]) {
+ assertNoAbsoluteLayout(file, label)
+ }
+
+ assertTokens(metric, [
+ 'h-[154px] w-[378px]',
+ 'px-6 pt-5',
+ 'flex items-start gap-3',
+ 'mt-2.5',
+ 'mt-[16.6px]',
+ 'h-[30px] w-[330px]',
+ ])
+
+ assertTokens(trend, [
+ 'h-[380px] w-[948px]',
+ 'px-7 pt-[22px]',
+ 'flex items-start justify-between',
+ 'h-[38px] w-[246px]',
+ 'mt-7 grid grid-cols-[682px_180px] gap-[30px]',
+ 'h-[268px] w-[682px]',
+ 'M0 150c70-14 96-20 136-38 48-24 74-20 108-58 42-42 78-22 116 6 44 32 82 18 124-10 44-24 82-16 120 30 18 22 34 12 46-6',
+ 'M0 70c80-2 122-8 170-6 56 2 116-14 166-8 62 6 94 18 142 2 48-16 108-2 172-10',
+ 'M0 44c72 4 116-8 174-4 58 2 110-6 166-2 66 8 118-8 174-4 60 6 100-4 136-8',
+ ])
+
+ assertTokens(service, [
+ 'h-[380px] w-[608px]',
+ 'px-7 pt-6',
+ 'mt-[23px] grid gap-2',
+ 'h-[50px] w-[552px]',
+ ])
+
+ assertTokens(account, [
+ 'h-[450px] w-[1584px]',
+ 'grid grid-cols-[360px_556px_556px] gap-7 px-7 pt-6',
+ 'h-[402px] w-[360px]',
+ 'h-[330px] w-[556px]',
+ 'h-[402px] w-[556px]',
+ '81 / 135',
+ '60% 已分配',
+ '最少使用优先',
+ '71.1%',
+ ])
+
+ assertTokens(events, [
+ 'h-[350px] w-[1584px]',
+ 'px-7 pt-6',
+ 'flex items-start justify-between',
+ 'h-9 w-[206px]',
+ 'mt-[26px] h-10 w-[1528px]',
+ 'mt-4 flex h-[184px] w-[1528px] justify-between overflow-hidden',
+ '请求 ID',
+ ])
+})
+
+test('dashboard data mirrors Pencil text values', async () => {
+ const data = await source('src/views/dashboard/composables/useDashboard.ts')
+
+ assertTokens(data, [
+ '启用',
+ '错误',
+ '3,363',
+ '< 3s',
+ '成功率',
+ '慢请求',
+ 'Amy Ops',
+ 'Team Codex',
+ 'Build Bot',
+ 'Reviewer',
+ 'team-codex@example.com',
+ '24分钟前',
+ '上游连接',
+ '模型目录',
+ '自动刷新',
+ '事件记录',
+ '客户端版本',
+ 'CloudCheck',
+ 'Boxes',
+ 'MonitorCheck',
+ 'req_01HW9K7QY2',
+ 'req_01HW9K6N4F',
+ 'req_01HW9K5Z7A',
+ 'req_01HW9K4C3Q',
+ '/v1/chat/completions',
+ 'catalog',
+ ])
+})
diff --git a/web/tests/sidebar-design.test.mjs b/web/tests/sidebar-design.test.mjs
new file mode 100644
index 000000000..aaf3d301d
--- /dev/null
+++ b/web/tests/sidebar-design.test.mjs
@@ -0,0 +1,63 @@
+import assert from 'node:assert/strict'
+import { readFile } from 'node:fs/promises'
+import { test } from 'node:test'
+import { fileURLToPath } from 'node:url'
+import { dirname, resolve } from 'node:path'
+
+const root = resolve(dirname(fileURLToPath(import.meta.url)), '..')
+const sidebarPath = resolve(root, 'src/layout/components/AppSidebar.vue')
+
+test('sidebar uses Pencil design icons and brand copy', async () => {
+ const source = await readFile(sidebarPath, 'utf8')
+
+ for (const token of [
+ 'SquareTerminal',
+ 'LayoutDashboard',
+ 'Users',
+ 'KeyRound',
+ 'ScrollText',
+ 'ChartColumn',
+ 'Box',
+ 'Settings',
+ 'Radar',
+ 'PanelLeftClose',
+ 'Proxy RS · v0.1.0',
+ ]) {
+ assert.match(source, new RegExp(token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')))
+ }
+})
+
+test('sidebar locks core Pencil dimensions and coordinates', async () => {
+ const source = await readFile(sidebarPath, 'utf8')
+
+ for (const token of [
+ 'w-[280px]',
+ 'basis-[280px]',
+ 'left-[28px]',
+ 'top-[31px]',
+ 'size-[46px]',
+ 'left-[84px]',
+ 'top-[34px]',
+ 'top-[57px]',
+ 'left-6',
+ 'w-[232px]',
+ 'h-[46px]',
+ 'left-[22px]',
+ 'top-[13px]',
+ 'left-[54px]',
+ 'top-[15px]',
+ 'top-[112px]',
+ 'top-[170px]',
+ 'top-[228px]',
+ 'top-[286px]',
+ 'top-[344px]',
+ 'top-[402px]',
+ 'top-[460px]',
+ 'top-[518px]',
+ 'left-[220px]',
+ 'bottom-8',
+ 'size-9',
+ ]) {
+ assert.match(source, new RegExp(token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')))
+ }
+})
diff --git a/web/tsconfig.app.json b/web/tsconfig.app.json
new file mode 100644
index 000000000..994988e0b
--- /dev/null
+++ b/web/tsconfig.app.json
@@ -0,0 +1,20 @@
+{
+ "extends": "@vue/tsconfig/tsconfig.dom.json",
+ "include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
+ "exclude": ["src/**/__tests__/*"],
+ "compilerOptions": {
+ "composite": true,
+ "target": "ES2024",
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "verbatimModuleSyntax": true,
+ "noEmit": true,
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
+ "paths": {
+ "@/*": ["./src/*"]
+ }
+ }
+}
diff --git a/web/tsconfig.json b/web/tsconfig.json
new file mode 100644
index 000000000..1ffef600d
--- /dev/null
+++ b/web/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "files": [],
+ "references": [
+ { "path": "./tsconfig.app.json" },
+ { "path": "./tsconfig.node.json" }
+ ]
+}
diff --git a/web/tsconfig.node.json b/web/tsconfig.node.json
new file mode 100644
index 000000000..b2564b73f
--- /dev/null
+++ b/web/tsconfig.node.json
@@ -0,0 +1,16 @@
+{
+ "include": ["vite.config.ts"],
+ "compilerOptions": {
+ "composite": true,
+ "target": "ES2024",
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "verbatimModuleSyntax": true,
+ "noEmit": true,
+ "types": ["node"]
+ }
+}
diff --git a/web/vite.config.ts b/web/vite.config.ts
new file mode 100644
index 000000000..5dc4104db
--- /dev/null
+++ b/web/vite.config.ts
@@ -0,0 +1,37 @@
+import tailwindcss from '@tailwindcss/vite'
+import vue from '@vitejs/plugin-vue'
+import { fileURLToPath, URL } from 'node:url'
+import { defineConfig } from 'vite'
+
+export default defineConfig({
+ base: '/admin/',
+ plugins: [vue(), tailwindcss()],
+ resolve: {
+ alias: {
+ '@': fileURLToPath(new URL('./src', import.meta.url)),
+ },
+ },
+ server: {
+ port: 5173,
+ proxy: {
+ '/api': 'http://127.0.0.1:8080',
+ '/auth': 'http://127.0.0.1:8080',
+ },
+ },
+ build: {
+ outDir: 'dist',
+ assetsDir: 'assets',
+ rolldownOptions: {
+ output: {
+ codeSplitting: {
+ groups: [
+ {
+ name: 'echarts',
+ test: /node_modules\/echarts|node_modules\/zrender/,
+ },
+ ],
+ },
+ },
+ },
+ },
+})