feat(ui): 新增电池实时状态页
This commit is contained in:
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user