import { useQuery } from '@tanstack/react-query' import { createFileRoute, Link, useNavigate } from '@tanstack/react-router' import { createColumnHelper, flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table' import { useEffect, useMemo, useState } from 'react' import { z } from 'zod' import { orpc } from '@/client/orpc' import type { BatteryInfo } from '@/domain/battery' const sortOptions = ['createdAtDesc', 'createdAtAsc', 'powerDesc', 'powerAsc'] as const type BatteryListSort = (typeof sortOptions)[number] const powerStatusOptions = [0, 1, 2] as const type PowerStatusFilter = (typeof powerStatusOptions)[number] const pageSizeOptions = [20, 50, 100] as const type PageSizeOption = (typeof pageSizeOptions)[number] const firstPageCursor = '__FIRST_PAGE__' const searchFilterSchema = z.preprocess( (value) => (typeof value === 'string' ? value.trim() || undefined : value), z.string().min(1).max(100).optional(), ) const cursorSchema = z.preprocess( (value) => (typeof value === 'string' ? value.trim() || undefined : value), z.string().min(1).max(1024).optional(), ) const searchSchema = z.object({ search: searchFilterSchema, lowPower: z.boolean().optional(), powerStatus: z.union([z.literal(0), z.literal(1), z.literal(2)]).optional(), sort: z.enum(sortOptions).optional().default('createdAtDesc'), pageSize: z.coerce .number() .pipe(z.union([z.literal(20), z.literal(50), z.literal(100)])) .optional() .default(50), cursor: cursorSchema, cursors: z.array(z.string().min(1).max(1024)).max(100).optional().default([]), }) export const Route = createFileRoute('/batteries')({ validateSearch: (search) => searchSchema.parse(search), component: BatteriesPage, errorComponent: () => (

数据加载失败

请检查筛选条件后重试。

), }) 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' } const columnHelper = createColumnHelper() function parseSort(value: string): BatteryListSort { return sortOptions.find((option) => option === value) ?? 'createdAtDesc' } function parsePowerStatus(value: string): PowerStatusFilter | undefined { const parsed = Number(value) return powerStatusOptions.find((option) => option === parsed) } function parsePageSize(value: string): PageSizeOption { const parsed = Number(value) return pageSizeOptions.find((option) => option === parsed) ?? 50 } function BatteriesPage() { const search = Route.useSearch() const navigate = useNavigate({ from: Route.fullPath }) const [localSearch, setLocalSearch] = useState(search.search || '') useEffect(() => { setLocalSearch(search.search ?? '') }, [search.search]) useEffect(() => { const timer = setTimeout(() => { const trimmedSearch = localSearch.trim() const nextSearch = trimmedSearch || undefined if (nextSearch !== search.search) { navigate({ search: (prev) => ({ ...prev, search: nextSearch, cursor: undefined, cursors: [] }), }) } }, 500) return () => clearTimeout(timer) }, [localSearch, navigate, search.search]) const { data, error, isPending, isPlaceholderData } = useQuery( orpc.battery.batteries.queryOptions({ input: { search: search.search, lowPower: search.lowPower, powerStatus: search.powerStatus, sort: search.sort, pageSize: search.pageSize, cursor: search.cursor, }, refetchInterval: 30_000, placeholderData: (prev) => prev, }), ) const columns = useMemo( () => [ columnHelper.accessor('devName', { header: '设备名称', cell: (info) => (
{info.getValue()} {info.row.original.mac}
), }), columnHelper.accessor('devModel', { header: '型号', cell: (info) => {info.getValue()}, }), columnHelper.accessor('power', { header: '电量', cell: (info) => { const power = info.getValue() const isLow = info.row.original.isLowPower return (
{power}%
) }, }), columnHelper.accessor('powerStatus', { header: '状态', cell: (info) => { const status = info.getValue() return {powerStatusLabel[status]} }, }), columnHelper.accessor('createTime', { header: '最后更新', cell: (info) => ( {new Date(info.getValue()).toLocaleString('zh-CN')} ), }), ], [], ) const table = useReactTable({ data: data?.items ?? [], columns, getCoreRowModel: getCoreRowModel(), }) const handleNextPage = () => { if (!isPlaceholderData && data?.nextCursor) { const nextCursor = data.nextCursor navigate({ search: (prev) => ({ ...prev, cursor: nextCursor, cursors: [...(prev.cursors || []), prev.cursor || firstPageCursor], }), }) } } const handlePrevPage = () => { const newCursors = [...(search.cursors || [])] const lastCursor = newCursors.pop() navigate({ search: (prev) => ({ ...prev, cursor: lastCursor === firstPageCursor ? undefined : lastCursor, cursors: newCursors, }), }) } if (error) { return (

数据加载失败

请稍后重试,或联系管理员检查数据源连接。

) } return (
{/* Background gradient */}

设备电池实时状态

{data ? `更新于 ${new Date(data.updatedAt).toLocaleString('zh-CN')}` : '加载中…'}

设备总数
{data?.total ?? '-'}
低电量
{data?.lowPower ?? '-'}
充电中
{data?.charging ?? '-'}
{/* Controls */}
setLocalSearch(e.target.value)} />
{table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => ( ))} ))} {isPending && !isPlaceholderData ? ( ) : data?.items.length === 0 ? ( ) : ( table.getRowModel().rows.map((row) => ( {row.getVisibleCells().map((cell) => ( ))} )) )}
{flexRender(header.column.columnDef.header, header.getContext())}
加载中…
暂无匹配设备
{flexRender(cell.column.columnDef.cell, cell.getContext())}
显示 {data?.items.length ?? 0} 台设备 {data?.total ? ` (共 ${data.total} 台)` : ''}
) }