import { useQuery } from '@tanstack/react-query' import { createFileRoute, Link, useNavigate } from '@tanstack/react-router' import { createColumnHelper, flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table' import { ArrowLeft, Battery, BatteryCharging, BatteryLow, FilterX, Search, Zap } from 'lucide-react' import { useEffect, useMemo, useState } from 'react' import { z } from 'zod' import { orpc } from '@/client/orpc' import { MotionCardDiv, MotionHeader, MotionSection, MotionTableRow } from '@/components/motion' import { Badge, Button, Card, Input, SectionTitle, Select, SelectOption } from '@/components/ui' import type { BatteryInfo, BatteryListSort, PowerStatus } from '@/domain/battery' import { BATTERY_LIST_SORT, BATTERY_LIST_SORT_VALUES, POWER_STATUS, POWER_STATUS_VALUES } from '@/domain/battery' const pageSizeOptions = [20, 50, 100] as const type PageSizeOption = (typeof pageSizeOptions)[number] const firstPageCursor = '__FIRST_PAGE__' const allPowerStatusValue = 'all' 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(POWER_STATUS.NOT_CHARGING), z.literal(POWER_STATUS.CHARGING), z.literal(POWER_STATUS.FULL)]) .optional(), sort: z.enum(BATTERY_LIST_SORT_VALUES).optional().default(BATTERY_LIST_SORT.CREATED_AT_DESC), 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 = { [POWER_STATUS.NOT_CHARGING]: '未充电', [POWER_STATUS.CHARGING]: '充电中', [POWER_STATUS.FULL]: '已充满', } const powerStatusVariant: Record = { [POWER_STATUS.NOT_CHARGING]: 'muted', [POWER_STATUS.CHARGING]: 'info', [POWER_STATUS.FULL]: 'success', } 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 BATTERY_LIST_SORT_VALUES.find((option) => option === value) ?? BATTERY_LIST_SORT.CREATED_AT_DESC } function parsePowerStatus(value: string): PowerStatus | undefined { if (value === allPowerStatusValue) return undefined const parsed = Number(value) return POWER_STATUS_VALUES.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 hasActiveFilters = Boolean(search.search || search.lowPower || search.powerStatus !== undefined) const clearFilters = () => { setLocalSearch('') navigate({ search: (prev) => ({ ...prev, search: undefined, lowPower: undefined, powerStatus: undefined, cursor: undefined, cursors: [], }), }) } const handleNextPage = () => { if (!isPlaceholderData && data?.nextCursor) { const nextCursor = data.nextCursor navigate({ search: (prev) => ({ ...prev, cursor: nextCursor, cursors: [...(prev.cursors || []), prev.cursor || firstPageCursor].slice(-100), }), }) } } 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 ?? '-'}
} title="筛选设备" description="按设备名称、编号、电量与充电状态快速缩小排查范围。" /> {hasActiveFilters && ( )}
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} 台)` : ''}
) }