From b722799ca3e97d6c150af74be2cc4bde9154773e Mon Sep 17 00:00:00 2001 From: imbytecat Date: Mon, 11 May 2026 20:51:34 +0800 Subject: [PATCH] =?UTF-8?q?feat(ui):=20=E6=96=B0=E5=A2=9E=E7=94=B5?= =?UTF-8?q?=E6=B1=A0=E5=AE=9E=E6=97=B6=E7=8A=B6=E6=80=81=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/routeTree.gen.ts | 24 +++++++- src/routes/batteries.tsx | 119 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 140 insertions(+), 3 deletions(-) create mode 100644 src/routes/batteries.tsx diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index f50b926..2900279 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -10,6 +10,7 @@ import { Route as rootRouteImport } from './routes/__root' import { Route as HealthRouteImport } from './routes/health' +import { Route as BatteriesRouteImport } from './routes/batteries' import { Route as IndexRouteImport } from './routes/index' import { Route as ApiSplatRouteImport } from './routes/api/$' import { Route as ApiRpcSplatRouteImport } from './routes/api/rpc.$' @@ -19,6 +20,11 @@ const HealthRoute = HealthRouteImport.update({ path: '/health', getParentRoute: () => rootRouteImport, } as any) +const BatteriesRoute = BatteriesRouteImport.update({ + id: '/batteries', + path: '/batteries', + getParentRoute: () => rootRouteImport, +} as any) const IndexRoute = IndexRouteImport.update({ id: '/', path: '/', @@ -37,12 +43,14 @@ const ApiRpcSplatRoute = ApiRpcSplatRouteImport.update({ export interface FileRoutesByFullPath { '/': typeof IndexRoute + '/batteries': typeof BatteriesRoute '/health': typeof HealthRoute '/api/$': typeof ApiSplatRoute '/api/rpc/$': typeof ApiRpcSplatRoute } export interface FileRoutesByTo { '/': typeof IndexRoute + '/batteries': typeof BatteriesRoute '/health': typeof HealthRoute '/api/$': typeof ApiSplatRoute '/api/rpc/$': typeof ApiRpcSplatRoute @@ -50,20 +58,22 @@ export interface FileRoutesByTo { export interface FileRoutesById { __root__: typeof rootRouteImport '/': typeof IndexRoute + '/batteries': typeof BatteriesRoute '/health': typeof HealthRoute '/api/$': typeof ApiSplatRoute '/api/rpc/$': typeof ApiRpcSplatRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath - fullPaths: '/' | '/health' | '/api/$' | '/api/rpc/$' + fullPaths: '/' | '/batteries' | '/health' | '/api/$' | '/api/rpc/$' fileRoutesByTo: FileRoutesByTo - to: '/' | '/health' | '/api/$' | '/api/rpc/$' - id: '__root__' | '/' | '/health' | '/api/$' | '/api/rpc/$' + to: '/' | '/batteries' | '/health' | '/api/$' | '/api/rpc/$' + id: '__root__' | '/' | '/batteries' | '/health' | '/api/$' | '/api/rpc/$' fileRoutesById: FileRoutesById } export interface RootRouteChildren { IndexRoute: typeof IndexRoute + BatteriesRoute: typeof BatteriesRoute HealthRoute: typeof HealthRoute ApiSplatRoute: typeof ApiSplatRoute ApiRpcSplatRoute: typeof ApiRpcSplatRoute @@ -78,6 +88,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof HealthRouteImport parentRoute: typeof rootRouteImport } + '/batteries': { + id: '/batteries' + path: '/batteries' + fullPath: '/batteries' + preLoaderRoute: typeof BatteriesRouteImport + parentRoute: typeof rootRouteImport + } '/': { id: '/' path: '/' @@ -104,6 +121,7 @@ declare module '@tanstack/react-router' { const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, + BatteriesRoute: BatteriesRoute, HealthRoute: HealthRoute, ApiSplatRoute: ApiSplatRoute, ApiRpcSplatRoute: ApiRpcSplatRoute, diff --git a/src/routes/batteries.tsx b/src/routes/batteries.tsx new file mode 100644 index 0000000..6e41bac --- /dev/null +++ b/src/routes/batteries.tsx @@ -0,0 +1,119 @@ +import { useSuspenseQuery } from '@tanstack/react-query' +import { createFileRoute, Link } from '@tanstack/react-router' +import { orpc } from '@/client/orpc' +import type { BatteryInfo } from '@/domain/battery' + +export const Route = createFileRoute('/batteries')({ + component: BatteriesPage, + loader: async ({ context }) => { + await context.queryClient.ensureQueryData(orpc.battery.batteries.queryOptions({ input: {} })) + }, + errorComponent: ({ error }) => ( +
+
+

数据加载失败

+

{error.message}

+
+
+ ), +}) + +const powerStatusLabel: Record<0 | 1 | 2, string> = { + 0: '未充电', + 1: '充电中', + 2: '已充满', +} + +const powerStatusColor: Record<0 | 1 | 2, string> = { + 0: 'text-zinc-400', + 1: 'text-teal-400', + 2: 'text-emerald-400', +} + +function powerBarColor(power: number, isLowPower: boolean): string { + if (isLowPower || power <= 20) return 'bg-red-500' + if (power <= 50) return 'bg-amber-500' + return 'bg-teal-500' +} + +function DeviceCard({ item }: { item: BatteryInfo }) { + return ( +
+
+
+

{item.devName}

+

{item.mac}

+
+ {item.devModel} +
+ +
+
+ + {item.power} + % + + {powerStatusLabel[item.powerStatus]} +
+
+
+
+
+ +
+ 更新 {new Date(item.createTime).toLocaleString('zh-CN')} + {item.isLowPower && 低电量} +
+
+ ) +} + +function BatteriesPage() { + const { data } = useSuspenseQuery( + orpc.battery.batteries.queryOptions({ + input: {}, + refetchInterval: 30_000, + }), + ) + + return ( +
+
+
+
+

设备电池实时状态

+

更新 {new Date(data.updatedAt).toLocaleString('zh-CN')}

+
+ +
+ +
+
+
设备总数
+
{data.total}
+
+
+
低电量
+
{data.lowPower}
+
+
+
充电中
+
{data.charging}
+
+
+
+ +
+ {data.items.length > 0 ? ( + data.items.map((item) => ) + ) : ( +
暂无设备数据
+ )} +
+
+ ) +}