feat(ui): 新增电池实时状态页

This commit is contained in:
2026-05-11 20:51:34 +08:00
parent df7b58c2f8
commit b722799ca3
2 changed files with 140 additions and 3 deletions
+119
View File
@@ -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 }) => (
<main className="flex min-h-screen items-center justify-center bg-[#09090B]">
<div className="text-center">
<p className="text-lg text-red-400"></p>
<p className="mt-2 text-sm text-zinc-500">{error.message}</p>
</div>
</main>
),
})
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 (
<article className="rounded-lg border border-zinc-800 bg-zinc-950 p-4">
<header className="flex items-start justify-between">
<div>
<h3 className="font-medium text-zinc-100">{item.devName}</h3>
<p className="mt-0.5 text-xs text-zinc-500">{item.mac}</p>
</div>
<span className="rounded bg-zinc-900 px-2 py-0.5 text-xs text-zinc-400">{item.devModel}</span>
</header>
<div className="mt-4">
<div className="flex items-baseline justify-between">
<span className="font-semibold text-2xl text-zinc-100">
{item.power}
<span className="ml-0.5 font-normal text-sm text-zinc-500">%</span>
</span>
<span className={`text-xs ${powerStatusColor[item.powerStatus]}`}>{powerStatusLabel[item.powerStatus]}</span>
</div>
<div className="mt-2 h-1.5 overflow-hidden rounded-full bg-zinc-800">
<div className={`h-full ${powerBarColor(item.power, item.isLowPower)}`} style={{ width: `${item.power}%` }} />
</div>
</div>
<footer className="mt-4 flex items-center justify-between text-xs text-zinc-500">
<span> {new Date(item.createTime).toLocaleString('zh-CN')}</span>
{item.isLowPower && <span className="rounded bg-red-950 px-1.5 py-0.5 text-red-400"></span>}
</footer>
</article>
)
}
function BatteriesPage() {
const { data } = useSuspenseQuery(
orpc.battery.batteries.queryOptions({
input: {},
refetchInterval: 30_000,
}),
)
return (
<main className="min-h-screen bg-[#09090B] px-6 py-8 text-zinc-100">
<header className="mx-auto max-w-7xl">
<div className="flex items-end justify-between">
<div>
<h1 className="font-semibold text-2xl"></h1>
<p className="mt-1 text-sm text-zinc-500"> {new Date(data.updatedAt).toLocaleString('zh-CN')}</p>
</div>
<nav className="text-xs">
<Link to="/" className="text-zinc-500 hover:text-zinc-300">
SoH
</Link>
</nav>
</div>
<dl className="mt-6 grid grid-cols-3 gap-4">
<div className="rounded-lg border border-zinc-800 bg-zinc-950 p-4">
<dt className="text-xs text-zinc-500"></dt>
<dd className="mt-1 font-semibold text-2xl">{data.total}</dd>
</div>
<div className="rounded-lg border border-zinc-800 bg-zinc-950 p-4">
<dt className="text-xs text-zinc-500"></dt>
<dd className="mt-1 font-semibold text-2xl text-red-400">{data.lowPower}</dd>
</div>
<div className="rounded-lg border border-zinc-800 bg-zinc-950 p-4">
<dt className="text-xs text-zinc-500"></dt>
<dd className="mt-1 font-semibold text-2xl text-teal-400">{data.charging}</dd>
</div>
</dl>
</header>
<section className="mx-auto mt-8 grid max-w-7xl grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{data.items.length > 0 ? (
data.items.map((item) => <DeviceCard key={item.id} item={item} />)
) : (
<div className="col-span-full py-12 text-center text-zinc-500"></div>
)}
</section>
</main>
)
}