feat(ui): 优化电池实时状态筛选体验
This commit is contained in:
+132
-69
@@ -1,9 +1,11 @@
|
||||
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, Database, FilterX, Search, Zap } from 'lucide-react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { z } from 'zod'
|
||||
import { orpc } from '@/client/orpc'
|
||||
import { Badge, Button, Card, Input, SectionTitle, Select } 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'
|
||||
|
||||
@@ -56,10 +58,10 @@ const powerStatusLabel: Record<PowerStatus, string> = {
|
||||
[POWER_STATUS.FULL]: '已充满',
|
||||
}
|
||||
|
||||
const powerStatusColor: Record<PowerStatus, string> = {
|
||||
[POWER_STATUS.NOT_CHARGING]: 'text-zinc-400',
|
||||
[POWER_STATUS.CHARGING]: 'text-teal-400',
|
||||
[POWER_STATUS.FULL]: 'text-emerald-400',
|
||||
const powerStatusVariant: Record<PowerStatus, 'muted' | 'info' | 'success'> = {
|
||||
[POWER_STATUS.NOT_CHARGING]: 'muted',
|
||||
[POWER_STATUS.CHARGING]: 'info',
|
||||
[POWER_STATUS.FULL]: 'success',
|
||||
}
|
||||
|
||||
function powerBarColor(power: number, isLowPower: boolean): string {
|
||||
@@ -158,7 +160,7 @@ function BatteriesPage() {
|
||||
header: '状态',
|
||||
cell: (info) => {
|
||||
const status = info.getValue()
|
||||
return <span className={`text-xs ${powerStatusColor[status]}`}>{powerStatusLabel[status]}</span>
|
||||
return <Badge variant={powerStatusVariant[status]}>{powerStatusLabel[status]}</Badge>
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor('createTime', {
|
||||
@@ -177,6 +179,21 @@ function BatteriesPage() {
|
||||
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
|
||||
@@ -222,52 +239,90 @@ function BatteriesPage() {
|
||||
|
||||
<div className="relative z-10 mx-auto max-w-7xl px-6 py-8">
|
||||
<header>
|
||||
<div className="flex items-end justify-between">
|
||||
<div className="flex flex-col gap-5 lg:flex-row lg:items-end lg:justify-between">
|
||||
<div>
|
||||
<h1 className="font-light text-3xl tracking-tight">设备电池实时状态</h1>
|
||||
<p className="mt-1 text-sm text-zinc-500">
|
||||
<Badge variant="info" className="mb-4">
|
||||
<Database className="size-3.5" /> MySQL 实时记录
|
||||
</Badge>
|
||||
<h1 className="text-3xl font-light tracking-tight text-white">设备电池实时状态</h1>
|
||||
<p className="mt-2 max-w-2xl text-sm leading-6 text-zinc-400">
|
||||
只展示 ls_battery_info 最新采集记录,筛选、排序和分页均通过 ORPC 服务端查询完成。
|
||||
</p>
|
||||
<p className="mt-2 text-xs tabular-nums text-zinc-500">
|
||||
{data ? `更新于 ${new Date(data.updatedAt).toLocaleString('zh-CN')}` : '加载中…'}
|
||||
</p>
|
||||
</div>
|
||||
<nav className="text-xs">
|
||||
<Link to="/" className="text-zinc-500 hover:text-teal-400 transition-colors">
|
||||
← 返回 SoH 看板
|
||||
<nav>
|
||||
<Link
|
||||
to="/"
|
||||
className="inline-flex items-center gap-2 rounded-lg border border-white/10 bg-white/[0.04] px-3 py-2 text-sm text-zinc-300 transition-colors hover:border-teal-400/30 hover:text-teal-300"
|
||||
>
|
||||
<ArrowLeft className="size-4" /> 返回 SoH 看板
|
||||
</Link>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<dl className="mt-8 grid grid-cols-1 gap-4 sm:grid-cols-3">
|
||||
<div className="rounded-xl border border-white/5 bg-white/[0.02] p-5">
|
||||
<dt className="text-xs font-medium text-zinc-500">设备总数</dt>
|
||||
<dd className="mt-2 font-light text-3xl tabular-nums">{data?.total ?? '-'}</dd>
|
||||
</div>
|
||||
<div className="rounded-xl border border-white/5 bg-white/[0.02] p-5">
|
||||
<dt className="text-xs font-medium text-zinc-500">低电量</dt>
|
||||
<dd className="mt-2 font-light text-3xl tabular-nums text-red-400">{data?.lowPower ?? '-'}</dd>
|
||||
</div>
|
||||
<div className="rounded-xl border border-white/5 bg-white/[0.02] p-5">
|
||||
<dt className="text-xs font-medium text-zinc-500">充电中</dt>
|
||||
<dd className="mt-2 font-light text-3xl tabular-nums text-teal-400">{data?.charging ?? '-'}</dd>
|
||||
</div>
|
||||
<Card className="p-5">
|
||||
<dt className="flex items-center gap-2 text-xs font-medium text-zinc-500">
|
||||
<Battery className="size-4 text-zinc-400" /> 设备总数
|
||||
</dt>
|
||||
<dd className="mt-3 text-3xl font-light tabular-nums text-white">{data?.total ?? '-'}</dd>
|
||||
</Card>
|
||||
<Card className="p-5">
|
||||
<dt className="flex items-center gap-2 text-xs font-medium text-zinc-500">
|
||||
<BatteryLow className="size-4 text-red-400" /> 低电量
|
||||
</dt>
|
||||
<dd className="mt-3 text-3xl font-light tabular-nums text-red-300">{data?.lowPower ?? '-'}</dd>
|
||||
</Card>
|
||||
<Card className="p-5">
|
||||
<dt className="flex items-center gap-2 text-xs font-medium text-zinc-500">
|
||||
<BatteryCharging className="size-4 text-teal-300" /> 充电中
|
||||
</dt>
|
||||
<dd className="mt-3 text-3xl font-light tabular-nums text-teal-300">{data?.charging ?? '-'}</dd>
|
||||
</Card>
|
||||
</dl>
|
||||
</header>
|
||||
|
||||
<section className="mt-10">
|
||||
{/* Controls */}
|
||||
<div className="mb-6 flex flex-wrap items-center gap-4">
|
||||
<div className="relative flex-1 min-w-[240px]">
|
||||
<input
|
||||
<Card className="mb-6 p-5">
|
||||
<div className="mb-5 flex flex-wrap items-center justify-between gap-3">
|
||||
<SectionTitle
|
||||
icon={<Search className="size-4" />}
|
||||
title="筛选设备"
|
||||
description="按真实采集字段筛选,不在前端伪造或补齐记录。"
|
||||
/>
|
||||
{hasActiveFilters && (
|
||||
<Button type="button" className="h-9 px-3 text-xs" onClick={clearFilters}>
|
||||
<FilterX className="size-3.5" /> 清除筛选
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-wrap items-end gap-4">
|
||||
<div className="flex flex-col gap-2 min-w-[260px] flex-1">
|
||||
<label htmlFor="search-input" className="text-xs font-medium text-zinc-500">
|
||||
设备名称 / MAC
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Search className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-zinc-600" />
|
||||
<Input
|
||||
id="search-input"
|
||||
type="text"
|
||||
placeholder="搜索设备名称或 MAC..."
|
||||
maxLength={100}
|
||||
className="w-full rounded-lg border border-white/10 bg-white/5 px-4 py-2 text-sm focus:border-teal-500/50 focus:outline-none focus:ring-1 focus:ring-teal-500/50"
|
||||
className="pl-9"
|
||||
value={localSearch}
|
||||
onChange={(e) => setLocalSearch(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<select
|
||||
className="rounded-lg border border-white/10 bg-white/5 px-3 py-2 text-sm focus:outline-none"
|
||||
<div className="flex flex-col gap-2">
|
||||
<label htmlFor="power-status-select" className="text-xs font-medium text-zinc-500">
|
||||
充电状态
|
||||
</label>
|
||||
<Select
|
||||
id="power-status-select"
|
||||
value={search.powerStatus ?? ''}
|
||||
onChange={(e) => {
|
||||
navigate({
|
||||
@@ -284,31 +339,15 @@ function BatteriesPage() {
|
||||
<option value={POWER_STATUS.NOT_CHARGING}>未充电</option>
|
||||
<option value={POWER_STATUS.CHARGING}>充电中</option>
|
||||
<option value={POWER_STATUS.FULL}>已充满</option>
|
||||
</select>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<label className="flex items-center gap-2 text-sm text-zinc-400 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="rounded border-white/10 bg-white/5 text-teal-500 focus:ring-teal-500/50"
|
||||
checked={search.lowPower ?? false}
|
||||
onChange={(e) =>
|
||||
navigate({
|
||||
search: (prev) => ({
|
||||
...prev,
|
||||
lowPower: e.target.checked || undefined,
|
||||
cursor: undefined,
|
||||
cursors: [],
|
||||
}),
|
||||
})
|
||||
}
|
||||
/>
|
||||
仅看低电量
|
||||
<div className="flex flex-col gap-2">
|
||||
<label htmlFor="sort-select" className="text-xs font-medium text-zinc-500">
|
||||
排序
|
||||
</label>
|
||||
|
||||
<div className="flex-1" />
|
||||
|
||||
<select
|
||||
className="rounded-lg border border-white/10 bg-white/5 px-3 py-2 text-sm focus:outline-none"
|
||||
<Select
|
||||
id="sort-select"
|
||||
value={search.sort}
|
||||
onChange={(e) => {
|
||||
navigate({
|
||||
@@ -325,10 +364,15 @@ function BatteriesPage() {
|
||||
<option value={BATTERY_LIST_SORT.CREATED_AT_ASC}>最早更新</option>
|
||||
<option value={BATTERY_LIST_SORT.POWER_DESC}>电量从高到低</option>
|
||||
<option value={BATTERY_LIST_SORT.POWER_ASC}>电量从低到高</option>
|
||||
</select>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<select
|
||||
className="rounded-lg border border-white/10 bg-white/5 px-3 py-2 text-sm focus:outline-none"
|
||||
<div className="flex flex-col gap-2">
|
||||
<label htmlFor="page-size-select" className="text-xs font-medium text-zinc-500">
|
||||
页大小
|
||||
</label>
|
||||
<Select
|
||||
id="page-size-select"
|
||||
value={search.pageSize}
|
||||
onChange={(e) => {
|
||||
navigate({
|
||||
@@ -344,10 +388,35 @@ function BatteriesPage() {
|
||||
<option value="20">20 条/页</option>
|
||||
<option value="50">50 条/页</option>
|
||||
<option value="100">100 条/页</option>
|
||||
</select>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border border-white/5 bg-white/[0.01] overflow-hidden">
|
||||
<label
|
||||
htmlFor="low-power-checkbox"
|
||||
className="inline-flex h-10 cursor-pointer items-center gap-2 rounded-lg border border-white/10 bg-white/[0.04] px-3 text-sm text-zinc-300 transition-colors hover:bg-white/[0.07]"
|
||||
>
|
||||
<input
|
||||
id="low-power-checkbox"
|
||||
type="checkbox"
|
||||
className="rounded border-white/10 bg-zinc-950 text-teal-500 focus:ring-teal-500/50"
|
||||
checked={search.lowPower ?? false}
|
||||
onChange={(e) =>
|
||||
navigate({
|
||||
search: (prev) => ({
|
||||
...prev,
|
||||
lowPower: e.target.checked || undefined,
|
||||
cursor: undefined,
|
||||
cursors: [],
|
||||
}),
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Zap className="size-4 text-amber-300" /> 仅看低电量
|
||||
</label>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<table className={`w-full border-collapse text-left text-sm ${isPlaceholderData ? 'opacity-60' : ''}`}>
|
||||
<thead>
|
||||
@@ -388,7 +457,7 @@ function BatteriesPage() {
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<div className="mt-6 flex items-center justify-between text-sm text-zinc-500">
|
||||
<div>
|
||||
@@ -396,22 +465,16 @@ function BatteriesPage() {
|
||||
{data?.total ? ` (共 ${data.total} 台)` : ''}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
className="rounded-lg border border-white/10 bg-white/5 px-4 py-2 hover:bg-white/10 disabled:opacity-30 disabled:hover:bg-white/5 transition-colors"
|
||||
onClick={handlePrevPage}
|
||||
disabled={isPlaceholderData || (!search.cursor && (!search.cursors || search.cursors.length === 0))}
|
||||
>
|
||||
上一页
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-lg border border-white/10 bg-white/5 px-4 py-2 hover:bg-white/10 disabled:opacity-30 disabled:hover:bg-white/5 transition-colors"
|
||||
onClick={handleNextPage}
|
||||
disabled={isPlaceholderData || !data?.nextCursor}
|
||||
>
|
||||
</Button>
|
||||
<Button type="button" onClick={handleNextPage} disabled={isPlaceholderData || !data?.nextCursor}>
|
||||
下一页
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user