feat: 替换 OpenBridge 组件并实现航海规范主题系统
- 使用 shadcn/ui 重新实现 TopBar、ThemeSidebar、AlertBadge 组件 - 解决 @oicl/openbridge-webcomponents ESM 模块解析问题 - 添加 OpenBridge 四种主题 CSS 变量 (day/bright/dusk/night) - Night 主题使用暗黄色文字保护夜视能力 - 更新 API 端点适配新的按模型分组数据结构
This commit is contained in:
69
bun.lock
69
bun.lock
@@ -12,6 +12,8 @@
|
||||
"@orpc/server": "^1.13.4",
|
||||
"@orpc/tanstack-query": "^1.13.4",
|
||||
"@orpc/zod": "^1.13.4",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@t3-oss/env-core": "^0.13.10",
|
||||
"@tanstack/react-query": "^5.90.18",
|
||||
"@tanstack/react-router": "^1.151.0",
|
||||
@@ -20,10 +22,14 @@
|
||||
"@tauri-apps/api": "^2.9.1",
|
||||
"@types/react": "^19.2.9",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"drizzle-orm": "^0.45.1",
|
||||
"drizzle-zod": "^0.8.3",
|
||||
"lucide-react": "^0.563.0",
|
||||
"react": "^19.2.3",
|
||||
"react-dom": "^19.2.3",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"zod": "^4.3.5",
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -42,6 +48,7 @@
|
||||
"nitro": "npm:nitro-nightly@latest",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"turbo": "^2.7.5",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^8.0.0-beta.8",
|
||||
"vite-tsconfig-paths": "^6.0.4",
|
||||
@@ -347,6 +354,40 @@
|
||||
|
||||
"@oxc-transform/binding-win32-x64-msvc": ["@oxc-transform/binding-win32-x64-msvc@0.108.0", "", { "os": "win32", "cpu": "x64" }, "sha512-k+7tuCMULfB7zr57jb68sVzxbyleZBasyr1h1Ieiu1U95XHYe64pbSrwHmlaSmiNHqV91ikM3809+ps68jZZhw=="],
|
||||
|
||||
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="],
|
||||
|
||||
"@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
|
||||
|
||||
"@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
|
||||
|
||||
"@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw=="],
|
||||
|
||||
"@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="],
|
||||
|
||||
"@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw=="],
|
||||
|
||||
"@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="],
|
||||
|
||||
"@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="],
|
||||
|
||||
"@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="],
|
||||
|
||||
"@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="],
|
||||
|
||||
"@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
||||
|
||||
"@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="],
|
||||
|
||||
"@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="],
|
||||
|
||||
"@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="],
|
||||
|
||||
"@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA=="],
|
||||
|
||||
"@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.1", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g=="],
|
||||
|
||||
"@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
|
||||
|
||||
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-beta.60", "", { "os": "android", "cpu": "arm64" }, "sha512-hOW6iQXtpG4uCW1zGK56+KhEXGttSkTp2ykncW/nkOIF/jOKTqbM944Q73HVeMXP1mPRvE2cZwNp3xeLIeyIGQ=="],
|
||||
|
||||
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-beta.60", "", { "os": "darwin", "cpu": "arm64" }, "sha512-vyDA4HXY2mP8PPtl5UE17uGPxUNG4m1wkfa3kAkR8JWrFbarV97UmLq22IWrNhtBPa89xqerzLK8KoVmz5JqCQ=="],
|
||||
@@ -597,6 +638,8 @@
|
||||
|
||||
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
||||
|
||||
"aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="],
|
||||
|
||||
"ast-types": ["ast-types@0.16.1", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg=="],
|
||||
|
||||
"babel-dead-code-elimination": ["babel-dead-code-elimination@1.0.12", "", { "dependencies": { "@babel/core": "^7.23.7", "@babel/parser": "^7.23.6", "@babel/traverse": "^7.23.7", "@babel/types": "^7.23.6" } }, "sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig=="],
|
||||
@@ -631,6 +674,8 @@
|
||||
|
||||
"chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
|
||||
|
||||
"class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="],
|
||||
|
||||
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||
|
||||
"consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="],
|
||||
@@ -661,6 +706,8 @@
|
||||
|
||||
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||
|
||||
"detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="],
|
||||
|
||||
"diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="],
|
||||
|
||||
"dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="],
|
||||
@@ -711,6 +758,8 @@
|
||||
|
||||
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
|
||||
|
||||
"get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="],
|
||||
|
||||
"get-tsconfig": ["get-tsconfig@4.13.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ=="],
|
||||
|
||||
"glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||
@@ -815,6 +864,8 @@
|
||||
|
||||
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
||||
|
||||
"lucide-react": ["lucide-react@0.563.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-8dXPB2GI4dI8jV4MgUDGBeLdGk8ekfqVZ0BdLcrRzocGgG75ltNEmWS+gE7uokKF/0oSUuczNDT+g9hFJ23FkA=="],
|
||||
|
||||
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
@@ -871,6 +922,12 @@
|
||||
|
||||
"react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="],
|
||||
|
||||
"react-remove-scroll": ["react-remove-scroll@2.7.2", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q=="],
|
||||
|
||||
"react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="],
|
||||
|
||||
"react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],
|
||||
|
||||
"readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
|
||||
|
||||
"recast": ["recast@0.23.11", "", { "dependencies": { "ast-types": "^0.16.1", "esprima": "~4.0.0", "source-map": "~0.6.1", "tiny-invariant": "^1.3.3", "tslib": "^2.0.1" } }, "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA=="],
|
||||
@@ -911,6 +968,8 @@
|
||||
|
||||
"tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="],
|
||||
|
||||
"tailwind-merge": ["tailwind-merge@3.4.0", "", {}, "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g=="],
|
||||
|
||||
"tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="],
|
||||
|
||||
"tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
|
||||
@@ -943,6 +1002,8 @@
|
||||
|
||||
"turbo-windows-arm64": ["turbo-windows-arm64@2.7.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-G377Gxn6P42RnCzfMyDvsqQV7j69kVHKlhz9J4RhtJOB5+DyY4yYh/w0oTIxZQ4JRMmhjwLu3w9zncMoQ6nNDw=="],
|
||||
|
||||
"tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="],
|
||||
|
||||
"type-fest": ["type-fest@5.4.1", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-xygQcmneDyzsEuKZrFbRMne5HDqMs++aFzefrJTgEIKjQ3rekM+RPfFCVq2Gp1VIDqddoYeppCj4Pcb+RZW0GQ=="],
|
||||
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
@@ -961,6 +1022,10 @@
|
||||
|
||||
"update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
|
||||
|
||||
"use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="],
|
||||
|
||||
"use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="],
|
||||
|
||||
"use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="],
|
||||
|
||||
"uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="],
|
||||
@@ -991,6 +1056,10 @@
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="],
|
||||
|
||||
"@radix-ui/react-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||
|
||||
"@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="],
|
||||
|
||||
22
components.json
Normal file
22
components.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "src/styles.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"iconLibrary": "lucide",
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"registries": {}
|
||||
}
|
||||
@@ -27,6 +27,8 @@
|
||||
"@orpc/server": "^1.13.4",
|
||||
"@orpc/tanstack-query": "^1.13.4",
|
||||
"@orpc/zod": "^1.13.4",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@t3-oss/env-core": "^0.13.10",
|
||||
"@tanstack/react-query": "^5.90.18",
|
||||
"@tanstack/react-router": "^1.151.0",
|
||||
@@ -35,10 +37,14 @@
|
||||
"@tauri-apps/api": "^2.9.1",
|
||||
"@types/react": "^19.2.9",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"drizzle-orm": "^0.45.1",
|
||||
"drizzle-zod": "^0.8.3",
|
||||
"lucide-react": "^0.563.0",
|
||||
"react": "^19.2.3",
|
||||
"react-dom": "^19.2.3",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"zod": "^4.3.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -57,6 +63,7 @@
|
||||
"nitro": "npm:nitro-nightly@latest",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"turbo": "^2.7.5",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^8.0.0-beta.8",
|
||||
"vite-tsconfig-paths": "^6.0.4"
|
||||
|
||||
@@ -117,7 +117,7 @@ export const HealthRing = ({
|
||||
<span className="text-2xl font-bold" style={{ color: ringColor }}>
|
||||
{percentage}%
|
||||
</span>
|
||||
<span className="text-sm text-[var(--on-container-color)] opacity-70">
|
||||
<span className="text-sm text-[var(--element-inactive-color,#707070)]">
|
||||
{countdown.formatted}
|
||||
</span>
|
||||
</div>
|
||||
@@ -126,13 +126,13 @@ export const HealthRing = ({
|
||||
{/* 底部标签 */}
|
||||
<div className="text-center">
|
||||
<div
|
||||
className="text-sm font-medium truncate max-w-[140px] text-[var(--on-container-color)]"
|
||||
className="text-sm font-medium truncate max-w-[140px] text-[var(--element-active-color,#3d3d3d)]"
|
||||
title={displayName || model}
|
||||
>
|
||||
{displayName || 'Claude Opus 4.5'}
|
||||
</div>
|
||||
<div
|
||||
className="text-xs truncate max-w-[140px] text-[var(--on-container-color)] opacity-50"
|
||||
className="text-xs truncate max-w-[140px] text-[var(--element-inactive-color,#707070)]"
|
||||
title={account}
|
||||
>
|
||||
{account}
|
||||
|
||||
@@ -2,64 +2,25 @@
|
||||
* TokenUsageDashboard 组件
|
||||
*
|
||||
* 主仪表盘,展示多个账户的 claude-opus-4-5-thinking 配额使用情况。
|
||||
* 使用 OpenBridge 设计系统的 TopBar 和 Alert 组件。
|
||||
* 使用自定义组件替代 OpenBridge 组件,基于 shadcn/ui 实现。
|
||||
*
|
||||
* 特性:
|
||||
* - 多账户配额可视化 (根据 API 返回的账户数量动态显示)
|
||||
* - 实时告警通知 (低于 20% 警告,低于 5% 紧急)
|
||||
* - 支持 OpenBridge 四种主题切换 (day/bright/dusk/night)
|
||||
* - OpenBridge 组件懒加载以避免 SSR 问题
|
||||
* - 支持四种主题切换 (day/bright/dusk/night)
|
||||
*/
|
||||
import '@oicl/openbridge-webcomponents/dist/icons/icon-palette-day.js'
|
||||
import '@oicl/openbridge-webcomponents/dist/icons/icon-palette-day-bright.js'
|
||||
import '@oicl/openbridge-webcomponents/dist/icons/icon-palette-dusk.js'
|
||||
import '@oicl/openbridge-webcomponents/dist/icons/icon-palette-night.js'
|
||||
import { AlertType } from '@oicl/openbridge-webcomponents/dist/types'
|
||||
import { lazy, Suspense, useCallback, useMemo, useState } from 'react'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { HealthRing } from '@/components/HealthRing'
|
||||
import { type ObcTheme, useTheme } from '@/hooks/useTheme'
|
||||
import {
|
||||
AlertBadge,
|
||||
AlertNotification,
|
||||
AlertType,
|
||||
} from '@/components/ui/AlertBadge'
|
||||
import { ThemeSidebar } from '@/components/ui/ThemeSidebar'
|
||||
import { TopBar } from '@/components/ui/TopBar'
|
||||
import { useTheme } from '@/hooks/useTheme'
|
||||
import type { ModelUsage } from '@/orpc/contracts/usage'
|
||||
|
||||
// ============================================================================
|
||||
// 懒加载 OpenBridge 组件(避免 SSR 问题)
|
||||
// ============================================================================
|
||||
|
||||
const ObcTopBar = lazy(() =>
|
||||
import(
|
||||
'@oicl/openbridge-webcomponents-react/components/top-bar/top-bar'
|
||||
).then((mod) => ({ default: mod.ObcTopBar })),
|
||||
)
|
||||
|
||||
const ObcAlertTopbarElement = lazy(() =>
|
||||
import(
|
||||
'@oicl/openbridge-webcomponents-react/components/alert-topbar-element/alert-topbar-element'
|
||||
).then((mod) => ({ default: mod.ObcAlertTopbarElement })),
|
||||
)
|
||||
|
||||
const ObcNotificationMessageItem = lazy(() =>
|
||||
import(
|
||||
'@oicl/openbridge-webcomponents-react/components/notification-message-item/notification-message-item'
|
||||
).then((mod) => ({ default: mod.ObcNotificationMessageItem })),
|
||||
)
|
||||
|
||||
const ObcAlertIcon = lazy(() =>
|
||||
import(
|
||||
'@oicl/openbridge-webcomponents-react/components/alert-icon/alert-icon'
|
||||
).then((mod) => ({ default: mod.ObcAlertIcon })),
|
||||
)
|
||||
|
||||
const ObcNavigationMenu = lazy(() =>
|
||||
import(
|
||||
'@oicl/openbridge-webcomponents-react/components/navigation-menu/navigation-menu'
|
||||
).then((mod) => ({ default: mod.ObcNavigationMenu })),
|
||||
)
|
||||
|
||||
const ObcNavigationItem = lazy(() =>
|
||||
import(
|
||||
'@oicl/openbridge-webcomponents-react/components/navigation-item/navigation-item'
|
||||
).then((mod) => ({ default: mod.ObcNavigationItem })),
|
||||
)
|
||||
|
||||
// ============================================================================
|
||||
// 类型定义
|
||||
// ============================================================================
|
||||
@@ -171,15 +132,6 @@ const getHighestAlertType = (alerts: AlertInfo[]): AlertType => {
|
||||
return AlertType.Caution
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 子组件
|
||||
// ============================================================================
|
||||
|
||||
/** TopBar 加载占位符 (SSR 时显示) */
|
||||
const TopBarFallback = () => (
|
||||
<div className="h-14 bg-[var(--container-background-color)] border-b border-[var(--divider-color)]" />
|
||||
)
|
||||
|
||||
// ============================================================================
|
||||
// 主组件
|
||||
// ============================================================================
|
||||
@@ -211,149 +163,41 @@ export const TokenUsageDashboard = ({ data }: TokenUsageDashboardProps) => {
|
||||
setMenuOpen((prev) => !prev)
|
||||
}, [])
|
||||
|
||||
/** 关闭侧边菜单 */
|
||||
const closeMenu = useCallback(() => {
|
||||
setMenuOpen(false)
|
||||
}, [])
|
||||
|
||||
/** 切换主题并关闭菜单 */
|
||||
const handleThemeChange = useCallback(
|
||||
(newTheme: ObcTheme) => {
|
||||
setTheme(newTheme)
|
||||
setMenuOpen(false)
|
||||
},
|
||||
[setTheme],
|
||||
)
|
||||
|
||||
// ========== 渲染 ==========
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col bg-[var(--container-background-color)] text-[var(--on-container-color)]">
|
||||
<div className="min-h-screen flex flex-col bg-[var(--container-background-color,#f7f7f7)] text-[var(--element-active-color,#3d3d3d)]">
|
||||
{/* 顶部导航栏 */}
|
||||
<header className="sticky top-0 z-50">
|
||||
<Suspense fallback={<TopBarFallback />}>
|
||||
<ObcTopBar
|
||||
<TopBar
|
||||
appTitle="Token Usage Viewer"
|
||||
pageName=""
|
||||
onMenuButtonClicked={
|
||||
handleMenuButtonClick as unknown as EventListener
|
||||
}
|
||||
menuButtonActivated={menuOpen}
|
||||
>
|
||||
{/* 右侧告警面板 */}
|
||||
<div slot="alerts">
|
||||
<ObcAlertTopbarElement
|
||||
nAlerts={alerts.length}
|
||||
onMenuButtonClick={handleMenuButtonClick}
|
||||
rightSlot={
|
||||
<>
|
||||
<AlertBadge
|
||||
count={alerts.length}
|
||||
alertType={alertType}
|
||||
alertMuted={alertMuted}
|
||||
showAck={false}
|
||||
onMuteclick={handleMuteClick as unknown as EventListener}
|
||||
>
|
||||
muted={alertMuted}
|
||||
onMuteClick={handleMuteClick}
|
||||
/>
|
||||
{topAlert && (
|
||||
<ObcNotificationMessageItem time="">
|
||||
<span slot="icon">
|
||||
<ObcAlertIcon
|
||||
name={
|
||||
topAlert.type === AlertType.Alarm
|
||||
? 'alarm-unack'
|
||||
: 'warning-unack'
|
||||
<AlertNotification
|
||||
account={extractUsername(topAlert.account)}
|
||||
remainingPercent={Math.round(topAlert.remainingFraction * 100)}
|
||||
alertType={topAlert.type}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</span>
|
||||
<span slot="message">
|
||||
{extractUsername(topAlert.account)}: 剩余{' '}
|
||||
{Math.round(topAlert.remainingFraction * 100)}%
|
||||
</span>
|
||||
</ObcNotificationMessageItem>
|
||||
)}
|
||||
</ObcAlertTopbarElement>
|
||||
</div>
|
||||
</ObcTopBar>
|
||||
</Suspense>
|
||||
</header>
|
||||
|
||||
{/* 侧边导航菜单 */}
|
||||
{menuOpen && (
|
||||
<aside className="fixed top-14 left-0 z-40 h-[calc(100vh-3.5rem)] w-64 bg-[var(--container-background-color)] border-r border-[var(--divider-color)] shadow-lg">
|
||||
<Suspense fallback={null}>
|
||||
<ObcNavigationMenu>
|
||||
<div slot="main">
|
||||
{/* 白天模式选项 */}
|
||||
<ObcNavigationItem
|
||||
label="白天模式"
|
||||
checked={theme === 'day'}
|
||||
onClick={() => handleThemeChange('day')}
|
||||
>
|
||||
<span
|
||||
slot="icon"
|
||||
// biome-ignore lint: custom element
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: '<obi-palette-day></obi-palette-day>',
|
||||
}}
|
||||
{/* 主题切换侧边栏 */}
|
||||
<ThemeSidebar
|
||||
open={menuOpen}
|
||||
onOpenChange={setMenuOpen}
|
||||
currentTheme={theme}
|
||||
onThemeChange={setTheme}
|
||||
/>
|
||||
</ObcNavigationItem>
|
||||
|
||||
{/* 明亮模式选项 */}
|
||||
<ObcNavigationItem
|
||||
label="明亮模式"
|
||||
checked={theme === 'bright'}
|
||||
onClick={() => handleThemeChange('bright')}
|
||||
>
|
||||
<span
|
||||
slot="icon"
|
||||
// biome-ignore lint: custom element
|
||||
dangerouslySetInnerHTML={{
|
||||
__html:
|
||||
'<obi-palette-day-bright></obi-palette-day-bright>',
|
||||
}}
|
||||
/>
|
||||
</ObcNavigationItem>
|
||||
|
||||
{/* 黄昏模式选项 */}
|
||||
<ObcNavigationItem
|
||||
label="黄昏模式"
|
||||
checked={theme === 'dusk'}
|
||||
onClick={() => handleThemeChange('dusk')}
|
||||
>
|
||||
<span
|
||||
slot="icon"
|
||||
// biome-ignore lint: custom element
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: '<obi-palette-dusk></obi-palette-dusk>',
|
||||
}}
|
||||
/>
|
||||
</ObcNavigationItem>
|
||||
|
||||
{/* 夜间模式选项 */}
|
||||
<ObcNavigationItem
|
||||
label="夜间模式"
|
||||
checked={theme === 'night'}
|
||||
onClick={() => handleThemeChange('night')}
|
||||
>
|
||||
<span
|
||||
slot="icon"
|
||||
// biome-ignore lint: custom element
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: '<obi-palette-night></obi-palette-night>',
|
||||
}}
|
||||
/>
|
||||
</ObcNavigationItem>
|
||||
</div>
|
||||
</ObcNavigationMenu>
|
||||
</Suspense>
|
||||
</aside>
|
||||
)}
|
||||
|
||||
{/* 点击遮罩关闭菜单 */}
|
||||
{menuOpen && (
|
||||
<button
|
||||
type="button"
|
||||
aria-label="关闭菜单"
|
||||
className="fixed inset-0 z-30 bg-black/20 cursor-default"
|
||||
onClick={closeMenu}
|
||||
onKeyDown={(e) => e.key === 'Escape' && closeMenu()}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 主内容区 - 配额圆环展示 */}
|
||||
<main className="flex-1 flex flex-col items-center justify-center p-8">
|
||||
|
||||
140
src/components/ui/AlertBadge.tsx
Normal file
140
src/components/ui/AlertBadge.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* AlertBadge 组件
|
||||
*
|
||||
* 告警徽章组件,显示告警数量和状态。
|
||||
* 支持三种告警类型:Alarm(紧急)、Warning(警告)、Caution(注意)
|
||||
*/
|
||||
import { AlertTriangle, Bell, BellOff, XCircle } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
/** 告警类型枚举 */
|
||||
export enum AlertType {
|
||||
Caution = 'caution',
|
||||
Warning = 'warning',
|
||||
Alarm = 'alarm',
|
||||
}
|
||||
|
||||
export interface AlertBadgeProps {
|
||||
/** 告警数量 */
|
||||
count: number
|
||||
/** 告警类型 */
|
||||
alertType: AlertType
|
||||
/** 是否静音 */
|
||||
muted?: boolean
|
||||
/** 静音按钮点击事件 */
|
||||
onMuteClick?: () => void
|
||||
/** 额外的 className */
|
||||
className?: string
|
||||
}
|
||||
|
||||
/** 告警类型对应的颜色配置 - 使用 OpenBridge CSS 变量 */
|
||||
const ALERT_COLORS: Record<
|
||||
AlertType,
|
||||
{ bgVar: string; textClass: string; icon: typeof AlertTriangle }
|
||||
> = {
|
||||
[AlertType.Alarm]: {
|
||||
bgVar: 'var(--alert-alarm-color, rgb(227, 0, 25))',
|
||||
textClass: 'text-white',
|
||||
icon: XCircle,
|
||||
},
|
||||
[AlertType.Warning]: {
|
||||
bgVar: 'var(--alert-warning-color, rgb(254, 148, 19))',
|
||||
textClass: 'text-white',
|
||||
icon: AlertTriangle,
|
||||
},
|
||||
[AlertType.Caution]: {
|
||||
bgVar: 'var(--alert-caution-color, rgb(255, 219, 55))',
|
||||
textClass: 'text-gray-900',
|
||||
icon: AlertTriangle,
|
||||
},
|
||||
}
|
||||
|
||||
export const AlertBadge = ({
|
||||
count,
|
||||
alertType,
|
||||
muted = false,
|
||||
onMuteClick,
|
||||
className,
|
||||
}: AlertBadgeProps) => {
|
||||
const { bgVar, textClass, icon: Icon } = ALERT_COLORS[alertType]
|
||||
|
||||
if (count === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('flex items-center gap-1', className)}>
|
||||
{/* 告警徽章 */}
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center gap-1.5 px-2.5 py-1 rounded-full',
|
||||
textClass,
|
||||
)}
|
||||
style={{ backgroundColor: bgVar }}
|
||||
>
|
||||
<Icon className="size-4" />
|
||||
<span className="text-sm font-medium">{count}</span>
|
||||
</div>
|
||||
|
||||
{/* 静音按钮 */}
|
||||
{onMuteClick && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
onClick={onMuteClick}
|
||||
className={cn(
|
||||
'hover:bg-[var(--element-hover-color,rgba(0,0,0,0.12))]',
|
||||
muted && 'opacity-50',
|
||||
)}
|
||||
aria-label={muted ? '取消静音' : '静音告警'}
|
||||
>
|
||||
{muted ? (
|
||||
<BellOff className="size-4 text-[var(--element-inactive-color,#707070)]" />
|
||||
) : (
|
||||
<Bell className="size-4 text-[var(--element-active-color,#3d3d3d)]" />
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export interface AlertNotificationProps {
|
||||
/** 账户名 */
|
||||
account: string
|
||||
/** 剩余百分比 */
|
||||
remainingPercent: number
|
||||
/** 告警类型 */
|
||||
alertType: AlertType
|
||||
}
|
||||
|
||||
/** 获取告警类型对应的 CSS 变量颜色 */
|
||||
const getAlertColor = (alertType: AlertType): string => {
|
||||
switch (alertType) {
|
||||
case AlertType.Alarm:
|
||||
return 'var(--alert-alarm-color, rgb(227, 0, 25))'
|
||||
case AlertType.Warning:
|
||||
return 'var(--alert-warning-color, rgb(254, 148, 19))'
|
||||
case AlertType.Caution:
|
||||
return 'var(--alert-caution-color, rgb(255, 219, 55))'
|
||||
}
|
||||
}
|
||||
|
||||
export const AlertNotification = ({
|
||||
account,
|
||||
remainingPercent,
|
||||
alertType,
|
||||
}: AlertNotificationProps) => {
|
||||
const { icon: Icon } = ALERT_COLORS[alertType]
|
||||
const alertColor = getAlertColor(alertType)
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2 px-3 py-2 text-sm">
|
||||
<Icon className="size-4 shrink-0" style={{ color: alertColor }} />
|
||||
<span className="text-[var(--element-active-color,#3d3d3d)]">
|
||||
{account}: 剩余 {remainingPercent}%
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
130
src/components/ui/ThemeSidebar.tsx
Normal file
130
src/components/ui/ThemeSidebar.tsx
Normal file
@@ -0,0 +1,130 @@
|
||||
/**
|
||||
* ThemeSidebar 组件
|
||||
*
|
||||
* 主题切换侧边栏,使用 shadcn Sheet 组件实现。
|
||||
* 支持 OpenBridge 四种主题:day/bright/dusk/night
|
||||
*/
|
||||
import { Check, Moon, Sun, SunDim, Sunrise } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
} from '@/components/ui/sheet'
|
||||
import type { ObcTheme } from '@/hooks/useTheme'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export interface ThemeSidebarProps {
|
||||
/** 是否打开 */
|
||||
open: boolean
|
||||
/** 关闭事件 */
|
||||
onOpenChange: (open: boolean) => void
|
||||
/** 当前主题 */
|
||||
currentTheme: ObcTheme
|
||||
/** 主题切换事件 */
|
||||
onThemeChange: (theme: ObcTheme) => void
|
||||
}
|
||||
|
||||
/** 主题配置 */
|
||||
const THEME_OPTIONS: Array<{
|
||||
value: ObcTheme
|
||||
label: string
|
||||
icon: typeof Sun
|
||||
description: string
|
||||
}> = [
|
||||
{
|
||||
value: 'day',
|
||||
label: '白天模式',
|
||||
icon: Sun,
|
||||
description: '明亮的白色背景',
|
||||
},
|
||||
{
|
||||
value: 'bright',
|
||||
label: '明亮模式',
|
||||
icon: SunDim,
|
||||
description: '高对比度明亮主题',
|
||||
},
|
||||
{
|
||||
value: 'dusk',
|
||||
label: '黄昏模式',
|
||||
icon: Sunrise,
|
||||
description: '柔和的暖色调',
|
||||
},
|
||||
{
|
||||
value: 'night',
|
||||
label: '夜间模式',
|
||||
icon: Moon,
|
||||
description: '深色背景护眼',
|
||||
},
|
||||
]
|
||||
|
||||
export const ThemeSidebar = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
currentTheme,
|
||||
onThemeChange,
|
||||
}: ThemeSidebarProps) => {
|
||||
const handleThemeSelect = (theme: ObcTheme) => {
|
||||
onThemeChange(theme)
|
||||
onOpenChange(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={onOpenChange}>
|
||||
<SheetContent
|
||||
side="left"
|
||||
className="w-72 bg-[var(--container-background-color,#fcfcfc)] border-[var(--divider-color,#e0e0e0)]"
|
||||
showCloseButton={false}
|
||||
>
|
||||
<SheetHeader className="border-b border-[var(--divider-color,#e0e0e0)] pb-4">
|
||||
<SheetTitle className="text-[var(--on-container-color,#1a1a1a)]">
|
||||
外观设置
|
||||
</SheetTitle>
|
||||
</SheetHeader>
|
||||
|
||||
<nav className="flex flex-col gap-1 py-4">
|
||||
{THEME_OPTIONS.map(({ value, label, icon: Icon, description }) => {
|
||||
const isActive = currentTheme === value
|
||||
|
||||
return (
|
||||
<Button
|
||||
key={value}
|
||||
variant="ghost"
|
||||
onClick={() => handleThemeSelect(value)}
|
||||
className={cn(
|
||||
'w-full justify-start gap-3 h-14 px-3',
|
||||
'hover:bg-[var(--element-hover-color,rgba(0,0,0,0.12))]',
|
||||
isActive &&
|
||||
'bg-[var(--element-focused-color,rgba(0,0,0,0.08))]',
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'size-10 rounded-lg flex items-center justify-center',
|
||||
'bg-[var(--container-section-color,#f0f0f0)]',
|
||||
)}
|
||||
>
|
||||
<Icon className="size-5 text-[var(--element-active-color,#1a1a1a)]" />
|
||||
</div>
|
||||
|
||||
<div className="flex-1 text-left">
|
||||
<div className="text-sm font-medium text-[var(--element-active-color,#1a1a1a)]">
|
||||
{label}
|
||||
</div>
|
||||
<div className="text-xs text-[var(--element-inactive-color,#666)]">
|
||||
{description}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isActive && (
|
||||
<Check className="size-5 text-[var(--alert-running-color,#34C759)]" />
|
||||
)}
|
||||
</Button>
|
||||
)
|
||||
})}
|
||||
</nav>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
)
|
||||
}
|
||||
84
src/components/ui/TopBar.tsx
Normal file
84
src/components/ui/TopBar.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* TopBar 组件
|
||||
*
|
||||
* 基于 OpenBridge 设计系统的顶部导航栏,使用 shadcn/ui 重新实现。
|
||||
* 特性:汉堡菜单按钮、应用标题、右侧告警/操作区域
|
||||
*/
|
||||
import { Menu } from 'lucide-react'
|
||||
import type { ReactNode } from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export interface TopBarProps {
|
||||
/** 应用标题 */
|
||||
appTitle: string
|
||||
/** 页面名称(可选,显示在标题右侧) */
|
||||
pageName?: string
|
||||
/** 菜单按钮是否激活 */
|
||||
menuButtonActivated?: boolean
|
||||
/** 菜单按钮点击事件 */
|
||||
onMenuButtonClick?: () => void
|
||||
/** 右侧插槽内容(告警、操作按钮等) */
|
||||
rightSlot?: ReactNode
|
||||
/** 额外的 className */
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const TopBar = ({
|
||||
appTitle,
|
||||
pageName,
|
||||
menuButtonActivated = false,
|
||||
onMenuButtonClick,
|
||||
rightSlot,
|
||||
className,
|
||||
}: TopBarProps) => {
|
||||
return (
|
||||
<header
|
||||
className={cn(
|
||||
'h-12 flex items-center justify-between px-2',
|
||||
'bg-[var(--container-background-color,#fcfcfc)]',
|
||||
'border-b border-[var(--divider-color,#e0e0e0)]',
|
||||
'shadow-[0px_2px_4px_0px_rgba(0,0,0,0.1)]',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{/* 左侧:汉堡菜单按钮 + 标题 */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={onMenuButtonClick}
|
||||
className={cn(
|
||||
'size-8 rounded-md',
|
||||
'hover:bg-[var(--element-focused-color,rgba(0,0,0,0.08))]',
|
||||
menuButtonActivated &&
|
||||
'bg-[var(--element-active-color,rgba(0,0,0,0.12))]',
|
||||
)}
|
||||
aria-label="打开菜单"
|
||||
aria-expanded={menuButtonActivated}
|
||||
>
|
||||
<Menu className="size-5 text-[var(--on-container-color,#1a1a1a)]" />
|
||||
</Button>
|
||||
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-base font-medium text-[var(--on-container-color,#1a1a1a)]">
|
||||
{appTitle}
|
||||
</span>
|
||||
{pageName && (
|
||||
<>
|
||||
<span className="text-[var(--on-container-low-color,#666)]">
|
||||
/
|
||||
</span>
|
||||
<span className="text-sm text-[var(--on-container-low-color,#666)]">
|
||||
{pageName}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 右侧:告警/操作区域 */}
|
||||
{rightSlot && <div className="flex items-center gap-1">{rightSlot}</div>}
|
||||
</header>
|
||||
)
|
||||
}
|
||||
64
src/components/ui/button.tsx
Normal file
64
src/components/ui/button.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import { Slot } from '@radix-ui/react-slot'
|
||||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
import type * as React from 'react'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
||||
destructive:
|
||||
'bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
|
||||
outline:
|
||||
'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
|
||||
secondary:
|
||||
'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||
ghost:
|
||||
'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
|
||||
link: 'text-primary underline-offset-4 hover:underline',
|
||||
},
|
||||
size: {
|
||||
default: 'h-9 px-4 py-2 has-[>svg]:px-3',
|
||||
xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
|
||||
sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
|
||||
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
|
||||
icon: 'size-9',
|
||||
'icon-xs': "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3",
|
||||
'icon-sm': 'size-8',
|
||||
'icon-lg': 'size-10',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
function Button({
|
||||
className,
|
||||
variant = 'default',
|
||||
size = 'default',
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<'button'> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean
|
||||
}) {
|
||||
const Comp = asChild ? Slot : 'button'
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="button"
|
||||
data-variant={variant}
|
||||
data-size={size}
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Button, buttonVariants }
|
||||
141
src/components/ui/sheet.tsx
Normal file
141
src/components/ui/sheet.tsx
Normal file
@@ -0,0 +1,141 @@
|
||||
import * as SheetPrimitive from '@radix-ui/react-dialog'
|
||||
import { XIcon } from 'lucide-react'
|
||||
import type * as React from 'react'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
|
||||
return <SheetPrimitive.Root data-slot="sheet" {...props} />
|
||||
}
|
||||
|
||||
function SheetTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
|
||||
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
|
||||
}
|
||||
|
||||
function SheetClose({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Close>) {
|
||||
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
|
||||
}
|
||||
|
||||
function SheetPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
|
||||
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
|
||||
}
|
||||
|
||||
function SheetOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
|
||||
return (
|
||||
<SheetPrimitive.Overlay
|
||||
data-slot="sheet-overlay"
|
||||
className={cn(
|
||||
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SheetContent({
|
||||
className,
|
||||
children,
|
||||
side = 'right',
|
||||
showCloseButton = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
|
||||
side?: 'top' | 'right' | 'bottom' | 'left'
|
||||
showCloseButton?: boolean
|
||||
}) {
|
||||
return (
|
||||
<SheetPortal>
|
||||
<SheetOverlay />
|
||||
<SheetPrimitive.Content
|
||||
data-slot="sheet-content"
|
||||
className={cn(
|
||||
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500',
|
||||
side === 'right' &&
|
||||
'data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm',
|
||||
side === 'left' &&
|
||||
'data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm',
|
||||
side === 'top' &&
|
||||
'data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b',
|
||||
side === 'bottom' &&
|
||||
'data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
|
||||
<XIcon className="size-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</SheetPrimitive.Close>
|
||||
)}
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
||||
)
|
||||
}
|
||||
|
||||
function SheetHeader({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sheet-header"
|
||||
className={cn('flex flex-col gap-1.5 p-4', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SheetFooter({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sheet-footer"
|
||||
className={cn('mt-auto flex flex-col gap-2 p-4', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SheetTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Title>) {
|
||||
return (
|
||||
<SheetPrimitive.Title
|
||||
data-slot="sheet-title"
|
||||
className={cn('text-foreground font-semibold', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SheetDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Description>) {
|
||||
return (
|
||||
<SheetPrimitive.Description
|
||||
data-slot="sheet-description"
|
||||
className={cn('text-muted-foreground text-sm', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Sheet,
|
||||
SheetTrigger,
|
||||
SheetClose,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetFooter,
|
||||
SheetTitle,
|
||||
SheetDescription,
|
||||
}
|
||||
@@ -9,8 +9,8 @@ import { dirname, join } from 'node:path'
|
||||
import { createEnv } from '@t3-oss/env-core'
|
||||
import { z } from 'zod'
|
||||
|
||||
/** Token 使用量 API 的默认地址 */
|
||||
const DEFAULT_TOKEN_USAGE_URL = 'http://10.0.1.1:8318/usage'
|
||||
/** Token 使用量 API 的默认地址 (按模型分组) */
|
||||
const DEFAULT_TOKEN_USAGE_URL = 'http://10.0.1.1:8318/api/usage/model'
|
||||
|
||||
/** 服务器端口默认值 */
|
||||
const DEFAULT_SERVER_PORT = '3000'
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import { type ClassValue, clsx } from 'clsx'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
|
||||
@@ -8,17 +8,24 @@ import { env } from '@/env'
|
||||
import { dbProvider } from '@/orpc/middlewares'
|
||||
import { os } from '@/orpc/server'
|
||||
|
||||
/** 远程 API 响应中的模型数据结构 */
|
||||
interface RemoteModelData {
|
||||
model: string
|
||||
displayName?: string
|
||||
/** 远程 API 响应中的账户数据结构 */
|
||||
interface RemoteAccountData {
|
||||
accountName: string
|
||||
remainingFraction: number
|
||||
resetTime?: string
|
||||
status?: string
|
||||
}
|
||||
|
||||
/** 远程 API 响应中的模型数据结构 (按模型分组) */
|
||||
interface RemoteModelData {
|
||||
modelId: string
|
||||
displayName?: string
|
||||
accounts: RemoteAccountData[]
|
||||
}
|
||||
|
||||
/** 远程 API 响应结构 */
|
||||
interface RemoteResponse {
|
||||
result: Record<string, RemoteModelData[]>
|
||||
data: RemoteModelData[]
|
||||
}
|
||||
|
||||
export const getUsage = os.usage.getUsage
|
||||
@@ -31,7 +38,7 @@ export const getUsage = os.usage.getUsage
|
||||
}
|
||||
const data = (await response.json()) as RemoteResponse
|
||||
|
||||
// 2. 解析并筛选每个账户的 claude-opus-4-5-thinking 模型
|
||||
// 2. 找到 claude-opus-4-5-thinking 模型,然后从其 accounts 中提取数据
|
||||
const opusModels: Array<{
|
||||
account: string
|
||||
model: string
|
||||
@@ -40,21 +47,22 @@ export const getUsage = os.usage.getUsage
|
||||
resetTime?: string
|
||||
}> = []
|
||||
|
||||
for (const [accountFile, models] of Object.entries(data.result)) {
|
||||
const account = accountFile.replace('.json', '')
|
||||
|
||||
// 只找 claude-opus-4-5-thinking 模型
|
||||
const opusModel = models.find(
|
||||
(m) => m.model === 'claude-opus-4-5-thinking',
|
||||
// 新 API 按模型分组,找到目标模型
|
||||
const opusModelData = data.data.find(
|
||||
(m) => m.modelId === 'claude-opus-4-5-thinking',
|
||||
)
|
||||
|
||||
if (opusModel) {
|
||||
if (opusModelData) {
|
||||
// 遍历该模型下的所有账户
|
||||
for (const accountData of opusModelData.accounts) {
|
||||
const account = accountData.accountName.replace('.json', '')
|
||||
|
||||
opusModels.push({
|
||||
account,
|
||||
model: opusModel.model,
|
||||
displayName: opusModel.displayName,
|
||||
remainingFraction: opusModel.remainingFraction,
|
||||
resetTime: opusModel.resetTime,
|
||||
model: opusModelData.modelId,
|
||||
displayName: opusModelData.displayName,
|
||||
remainingFraction: accountData.remainingFraction,
|
||||
resetTime: accountData.resetTime,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
291
src/styles.css
291
src/styles.css
@@ -1 +1,292 @@
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
/* ============================================================================
|
||||
* OpenBridge 航海设计系统 - 四种光照条件主题
|
||||
*
|
||||
* 符合 IMO 航海规范,确保在各种光照条件下的可读性:
|
||||
* - Day: 日间正常光照
|
||||
* - Bright: 强光/阳光直射
|
||||
* - Dusk: 黄昏/低光照
|
||||
* - Night: 夜间模式(保护夜视能力)
|
||||
* ============================================================================ */
|
||||
|
||||
/* Day 主题 - 日间正常光照 */
|
||||
:root[data-obc-theme='day'] {
|
||||
/* 容器/背景颜色 */
|
||||
--container-global-color: rgb(255, 255, 255);
|
||||
--container-background-color: rgb(247, 247, 247);
|
||||
--container-section-color: rgb(240, 240, 240);
|
||||
--container-backdrop-color: rgb(224, 224, 224);
|
||||
|
||||
/* 文字/元素颜色 - 深色文字确保可读性 */
|
||||
--element-active-color: rgb(61, 61, 61);
|
||||
--element-neutral-color: rgb(83, 83, 83);
|
||||
--element-inactive-color: rgb(112, 112, 112);
|
||||
--element-disabled-color: rgb(202, 202, 202);
|
||||
|
||||
/* 兼容旧变量名 */
|
||||
--on-container-color: rgb(61, 61, 61);
|
||||
--on-container-low-color: rgb(112, 112, 112);
|
||||
|
||||
/* 分割线/边框颜色 */
|
||||
--border-outline-color: rgb(221, 221, 221);
|
||||
--border-divider-color: rgb(221, 221, 221);
|
||||
--border-silhouette-color: rgb(247, 247, 247);
|
||||
--divider-color: rgb(221, 221, 221);
|
||||
|
||||
/* 交互状态 */
|
||||
--element-focused-color: rgba(0, 0, 0, 0.08);
|
||||
--element-hover-color: rgba(0, 0, 0, 0.12);
|
||||
|
||||
/* 告警颜色 */
|
||||
--alert-alarm-color: rgb(227, 0, 25);
|
||||
--alert-warning-color: rgb(254, 148, 19);
|
||||
--alert-caution-color: rgb(255, 219, 55);
|
||||
--alert-running-color: rgb(0, 131, 0);
|
||||
|
||||
/* 按钮/强调色 */
|
||||
--primary-color: rgb(51, 84, 131);
|
||||
--border-focus-color: rgb(51, 84, 131);
|
||||
}
|
||||
|
||||
/* Bright 主题 - 强光/阳光直射(高对比度) */
|
||||
:root[data-obc-theme='bright'] {
|
||||
/* 容器/背景颜色 - 纯白最大亮度 */
|
||||
--container-global-color: rgb(255, 255, 255);
|
||||
--container-background-color: rgb(255, 255, 255);
|
||||
--container-section-color: rgb(255, 255, 255);
|
||||
--container-backdrop-color: rgb(255, 255, 255);
|
||||
|
||||
/* 文字/元素颜色 - 最深黑色确保阳光下可读 */
|
||||
--element-active-color: rgb(26, 26, 26);
|
||||
--element-neutral-color: rgb(61, 61, 61);
|
||||
--element-inactive-color: rgb(83, 83, 83);
|
||||
--element-disabled-color: rgb(202, 202, 202);
|
||||
|
||||
/* 兼容旧变量名 */
|
||||
--on-container-color: rgb(26, 26, 26);
|
||||
--on-container-low-color: rgb(83, 83, 83);
|
||||
|
||||
/* 分割线/边框颜色 - 加粗边框增强对比 */
|
||||
--border-outline-color: rgb(142, 142, 142);
|
||||
--border-divider-color: rgb(142, 142, 142);
|
||||
--border-silhouette-color: rgb(255, 255, 255);
|
||||
--divider-color: rgb(142, 142, 142);
|
||||
|
||||
/* 交互状态 */
|
||||
--element-focused-color: rgba(0, 0, 0, 0.12);
|
||||
--element-hover-color: rgba(0, 0, 0, 0.18);
|
||||
|
||||
/* 告警颜色 - 饱和度更高 */
|
||||
--alert-alarm-color: rgb(235, 0, 20);
|
||||
--alert-warning-color: rgb(254, 148, 19);
|
||||
--alert-caution-color: rgb(228, 186, 2);
|
||||
--alert-running-color: rgb(6, 131, 6);
|
||||
|
||||
/* 按钮/强调色 - 更深的蓝色 */
|
||||
--primary-color: rgb(34, 60, 97);
|
||||
--border-focus-color: rgb(34, 60, 97);
|
||||
}
|
||||
|
||||
/* Dusk 主题 - 黄昏/低光照 */
|
||||
:root[data-obc-theme='dusk'] {
|
||||
/* 容器/背景颜色 - 深灰色减少眩光 */
|
||||
--container-global-color: rgb(36, 36, 36);
|
||||
--container-background-color: rgb(26, 26, 26);
|
||||
--container-section-color: rgb(18, 18, 18);
|
||||
--container-backdrop-color: rgb(3, 3, 3);
|
||||
|
||||
/* 文字/元素颜色 - 浅灰色文字 */
|
||||
--element-active-color: rgb(198, 198, 198);
|
||||
--element-neutral-color: rgb(165, 165, 165);
|
||||
--element-inactive-color: rgb(130, 130, 130);
|
||||
--element-disabled-color: rgb(58, 58, 58);
|
||||
|
||||
/* 兼容旧变量名 */
|
||||
--on-container-color: rgb(198, 198, 198);
|
||||
--on-container-low-color: rgb(130, 130, 130);
|
||||
|
||||
/* 分割线/边框颜色 */
|
||||
--border-outline-color: rgb(0, 0, 0);
|
||||
--border-divider-color: rgba(255, 255, 255, 0.09);
|
||||
--border-silhouette-color: rgb(0, 0, 0);
|
||||
--divider-color: rgba(255, 255, 255, 0.09);
|
||||
|
||||
/* 交互状态 */
|
||||
--element-focused-color: rgba(255, 255, 255, 0.08);
|
||||
--element-hover-color: rgba(255, 255, 255, 0.12);
|
||||
|
||||
/* 告警颜色 */
|
||||
--alert-alarm-color: rgb(203, 19, 29);
|
||||
--alert-warning-color: rgb(254, 155, 41);
|
||||
--alert-caution-color: rgb(233, 195, 0);
|
||||
--alert-running-color: rgb(18, 119, 15);
|
||||
|
||||
/* 按钮/强调色 - 亮蓝色 */
|
||||
--primary-color: rgb(133, 167, 216);
|
||||
--border-focus-color: rgb(133, 167, 216);
|
||||
}
|
||||
|
||||
/* Night 主题 - 夜间模式(保护夜视能力)
|
||||
* 使用红色/黄色光谱,避免蓝光影响夜视适应 */
|
||||
:root[data-obc-theme='night'] {
|
||||
/* 容器/背景颜色 - 纯黑背景 */
|
||||
--container-global-color: rgb(10, 10, 0);
|
||||
--container-background-color: rgb(0, 0, 0);
|
||||
--container-section-color: rgb(0, 0, 0);
|
||||
--container-backdrop-color: rgb(5, 5, 0);
|
||||
|
||||
/* 文字/元素颜色 - 暗黄色文字(保护夜视) */
|
||||
--element-active-color: rgb(71, 71, 0);
|
||||
--element-neutral-color: rgba(71, 71, 0, 0.65);
|
||||
--element-inactive-color: rgba(71, 71, 0, 0.45);
|
||||
--element-disabled-color: rgba(71, 71, 0, 0.25);
|
||||
|
||||
/* 兼容旧变量名 */
|
||||
--on-container-color: rgb(71, 71, 0);
|
||||
--on-container-low-color: rgba(71, 71, 0, 0.65);
|
||||
|
||||
/* 分割线/边框颜色 */
|
||||
--border-outline-color: rgba(71, 71, 0, 0.2);
|
||||
--border-divider-color: rgba(71, 71, 0, 0.2);
|
||||
--border-silhouette-color: rgb(0, 0, 0);
|
||||
--divider-color: rgba(71, 71, 0, 0.2);
|
||||
|
||||
/* 交互状态 */
|
||||
--element-focused-color: rgba(71, 71, 0, 0.15);
|
||||
--element-hover-color: rgba(71, 71, 0, 0.25);
|
||||
|
||||
/* 告警颜色 - 低亮度红/黄/绿 */
|
||||
--alert-alarm-color: rgb(77, 0, 5);
|
||||
--alert-warning-color: rgb(92, 44, 0);
|
||||
--alert-caution-color: rgb(92, 69, 0);
|
||||
--alert-running-color: rgb(23, 56, 0);
|
||||
|
||||
/* 按钮/强调色 - 暗绿色 */
|
||||
--primary-color: rgb(0, 51, 21);
|
||||
--border-focus-color: rgb(71, 71, 0);
|
||||
}
|
||||
|
||||
/* ============================================================================ */
|
||||
|
||||
@theme inline {
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--radius-2xl: calc(var(--radius) + 8px);
|
||||
--radius-3xl: calc(var(--radius) + 12px);
|
||||
--radius-4xl: calc(var(--radius) + 16px);
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
}
|
||||
|
||||
:root {
|
||||
--radius: 0.625rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--primary: oklch(0.205 0 0);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.97 0 0);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
--muted: oklch(0.97 0 0);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.205 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.97 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.145 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.205 0 0);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.205 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.922 0 0);
|
||||
--primary-foreground: oklch(0.205 0 0);
|
||||
--secondary: oklch(0.269 0 0);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.269 0 0);
|
||||
--muted-foreground: oklch(0.708 0 0);
|
||||
--accent: oklch(0.269 0 0);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.556 0 0);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.205 0 0);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.269 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.556 0 0);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user