From d22a0f8d69313fbe88a56ef854c9ddbde7c0cb77 Mon Sep 17 00:00:00 2001 From: MAO Dongyang Date: Mon, 26 Jan 2026 21:17:56 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9B=BF=E6=8D=A2=20OpenBridge=20?= =?UTF-8?q?=E7=BB=84=E4=BB=B6=E5=B9=B6=E5=AE=9E=E7=8E=B0=E8=88=AA=E6=B5=B7?= =?UTF-8?q?=E8=A7=84=E8=8C=83=E4=B8=BB=E9=A2=98=E7=B3=BB=E7=BB=9F=20-=20?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=20shadcn/ui=20=E9=87=8D=E6=96=B0=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0=20TopBar=E3=80=81ThemeSidebar=E3=80=81AlertBadge=20?= =?UTF-8?q?=E7=BB=84=E4=BB=B6=20-=20=E8=A7=A3=E5=86=B3=20@oicl/openbridge-?= =?UTF-8?q?webcomponents=20ESM=20=E6=A8=A1=E5=9D=97=E8=A7=A3=E6=9E=90?= =?UTF-8?q?=E9=97=AE=E9=A2=98=20-=20=E6=B7=BB=E5=8A=A0=20OpenBridge=20?= =?UTF-8?q?=E5=9B=9B=E7=A7=8D=E4=B8=BB=E9=A2=98=20CSS=20=E5=8F=98=E9=87=8F?= =?UTF-8?q?=20(day/bright/dusk/night)=20-=20Night=20=E4=B8=BB=E9=A2=98?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=E6=9A=97=E9=BB=84=E8=89=B2=E6=96=87=E5=AD=97?= =?UTF-8?q?=E4=BF=9D=E6=8A=A4=E5=A4=9C=E8=A7=86=E8=83=BD=E5=8A=9B=20-=20?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=20API=20=E7=AB=AF=E7=82=B9=E9=80=82=E9=85=8D?= =?UTF-8?q?=E6=96=B0=E7=9A=84=E6=8C=89=E6=A8=A1=E5=9E=8B=E5=88=86=E7=BB=84?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bun.lock | 69 ++++++ components.json | 22 ++ package.json | 7 + src/components/HealthRing.tsx | 6 +- src/components/TokenUsageDashboard.tsx | 238 ++++---------------- src/components/ui/AlertBadge.tsx | 140 ++++++++++++ src/components/ui/ThemeSidebar.tsx | 130 +++++++++++ src/components/ui/TopBar.tsx | 84 +++++++ src/components/ui/button.tsx | 64 ++++++ src/components/ui/sheet.tsx | 141 ++++++++++++ src/env.ts | 4 +- src/lib/utils.ts | 6 + src/orpc/handlers/usage.ts | 42 ++-- src/styles.css | 291 +++++++++++++++++++++++++ 14 files changed, 1025 insertions(+), 219 deletions(-) create mode 100644 components.json create mode 100644 src/components/ui/AlertBadge.tsx create mode 100644 src/components/ui/ThemeSidebar.tsx create mode 100644 src/components/ui/TopBar.tsx create mode 100644 src/components/ui/button.tsx create mode 100644 src/components/ui/sheet.tsx diff --git a/bun.lock b/bun.lock index 765b013..e6e5b14 100644 --- a/bun.lock +++ b/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=="], diff --git a/components.json b/components.json new file mode 100644 index 0000000..d6ff923 --- /dev/null +++ b/components.json @@ -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": {} +} diff --git a/package.json b/package.json index 0dcde47..cb0e420 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/src/components/HealthRing.tsx b/src/components/HealthRing.tsx index 119a791..6c0f60a 100644 --- a/src/components/HealthRing.tsx +++ b/src/components/HealthRing.tsx @@ -117,7 +117,7 @@ export const HealthRing = ({ {percentage}% - + {countdown.formatted} @@ -126,13 +126,13 @@ export const HealthRing = ({ {/* 底部标签 */}
{displayName || 'Claude Opus 4.5'}
{account} diff --git a/src/components/TokenUsageDashboard.tsx b/src/components/TokenUsageDashboard.tsx index 68a8428..ea165c3 100644 --- a/src/components/TokenUsageDashboard.tsx +++ b/src/components/TokenUsageDashboard.tsx @@ -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 = () => ( -
-) - // ============================================================================ // 主组件 // ============================================================================ @@ -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 ( -
+
{/* 顶部导航栏 */} -
- }> - - {/* 右侧告警面板 */} -
- - {topAlert && ( - - - - - - {extractUsername(topAlert.account)}: 剩余{' '} - {Math.round(topAlert.remainingFraction * 100)}% - - - )} - -
-
-
-
+ + + {topAlert && ( + + )} + + } + /> - {/* 侧边导航菜单 */} - {menuOpen && ( - - )} - - {/* 点击遮罩关闭菜单 */} - {menuOpen && ( - + )} +
+ ) +} + +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 ( +
+ + + {account}: 剩余 {remainingPercent}% + +
+ ) +} diff --git a/src/components/ui/ThemeSidebar.tsx b/src/components/ui/ThemeSidebar.tsx new file mode 100644 index 0000000..8e357a9 --- /dev/null +++ b/src/components/ui/ThemeSidebar.tsx @@ -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 ( + + + + + 外观设置 + + + + + + + ) +} diff --git a/src/components/ui/TopBar.tsx b/src/components/ui/TopBar.tsx new file mode 100644 index 0000000..db5b72f --- /dev/null +++ b/src/components/ui/TopBar.tsx @@ -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 ( +
+ {/* 左侧:汉堡菜单按钮 + 标题 */} +
+ + +
+ + {appTitle} + + {pageName && ( + <> + + / + + + {pageName} + + + )} +
+
+ + {/* 右侧:告警/操作区域 */} + {rightSlot &&
{rightSlot}
} +
+ ) +} diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx new file mode 100644 index 0000000..37bf89b --- /dev/null +++ b/src/components/ui/button.tsx @@ -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 & { + asChild?: boolean + }) { + const Comp = asChild ? Slot : 'button' + + return ( + + ) +} + +export { Button, buttonVariants } diff --git a/src/components/ui/sheet.tsx b/src/components/ui/sheet.tsx new file mode 100644 index 0000000..8a900d7 --- /dev/null +++ b/src/components/ui/sheet.tsx @@ -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) { + return +} + +function SheetTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function SheetClose({ + ...props +}: React.ComponentProps) { + return +} + +function SheetPortal({ + ...props +}: React.ComponentProps) { + return +} + +function SheetOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function SheetContent({ + className, + children, + side = 'right', + showCloseButton = true, + ...props +}: React.ComponentProps & { + side?: 'top' | 'right' | 'bottom' | 'left' + showCloseButton?: boolean +}) { + return ( + + + + {children} + {showCloseButton && ( + + + Close + + )} + + + ) +} + +function SheetHeader({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ) +} + +function SheetFooter({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ) +} + +function SheetTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function SheetDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + Sheet, + SheetTrigger, + SheetClose, + SheetContent, + SheetHeader, + SheetFooter, + SheetTitle, + SheetDescription, +} diff --git a/src/env.ts b/src/env.ts index 648f686..636cd6d 100644 --- a/src/env.ts +++ b/src/env.ts @@ -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' diff --git a/src/lib/utils.ts b/src/lib/utils.ts index e69de29..d32b0fe 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { type ClassValue, clsx } from 'clsx' +import { twMerge } from 'tailwind-merge' + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/src/orpc/handlers/usage.ts b/src/orpc/handlers/usage.ts index 281f073..001f60e 100644 --- a/src/orpc/handlers/usage.ts +++ b/src/orpc/handlers/usage.ts @@ -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 + 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', '') + // 新 API 按模型分组,找到目标模型 + const opusModelData = data.data.find( + (m) => m.modelId === 'claude-opus-4-5-thinking', + ) - // 只找 claude-opus-4-5-thinking 模型 - const opusModel = models.find( - (m) => m.model === 'claude-opus-4-5-thinking', - ) + if (opusModelData) { + // 遍历该模型下的所有账户 + for (const accountData of opusModelData.accounts) { + const account = accountData.accountName.replace('.json', '') - if (opusModel) { 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, }) } } diff --git a/src/styles.css b/src/styles.css index f1d8c73..3f02efe 100644 --- a/src/styles.css +++ b/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; + } +}