feat(ui): 重构电池列表分页表格
This commit is contained in:
+349
-60
@@ -1,15 +1,53 @@
|
|||||||
import { useQuery } from '@tanstack/react-query'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
import { createFileRoute, Link } from '@tanstack/react-router'
|
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 { orpc } from '@/client/orpc'
|
||||||
import type { BatteryInfo } from '@/domain/battery'
|
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')({
|
export const Route = createFileRoute('/batteries')({
|
||||||
|
validateSearch: (search) => searchSchema.parse(search),
|
||||||
component: BatteriesPage,
|
component: BatteriesPage,
|
||||||
errorComponent: ({ error }) => (
|
errorComponent: () => (
|
||||||
<main className="flex min-h-screen items-center justify-center bg-[#09090B]">
|
<main className="flex min-h-screen items-center justify-center bg-[#09090B]">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<p className="text-lg text-red-400">数据加载失败</p>
|
<p className="text-lg text-red-400">数据加载失败</p>
|
||||||
<p className="mt-2 text-sm text-zinc-500">{error.message}</p>
|
<p className="mt-2 text-sm text-zinc-500">请检查筛选条件后重试。</p>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
),
|
),
|
||||||
@@ -33,103 +71,354 @@ function powerBarColor(power: number, isLowPower: boolean): string {
|
|||||||
return 'bg-teal-500'
|
return 'bg-teal-500'
|
||||||
}
|
}
|
||||||
|
|
||||||
function DeviceCard({ item }: { item: BatteryInfo }) {
|
const columnHelper = createColumnHelper<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">
|
function parseSort(value: string): BatteryListSort {
|
||||||
<div className="flex items-baseline justify-between">
|
return sortOptions.find((option) => option === value) ?? 'createdAtDesc'
|
||||||
<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">
|
function parsePowerStatus(value: string): PowerStatusFilter | undefined {
|
||||||
<span>更新 {new Date(item.createTime).toLocaleString('zh-CN')}</span>
|
const parsed = Number(value)
|
||||||
{item.isLowPower && <span className="rounded bg-red-950 px-1.5 py-0.5 text-red-400">低电量</span>}
|
|
||||||
</footer>
|
return powerStatusOptions.find((option) => option === parsed)
|
||||||
</article>
|
}
|
||||||
)
|
|
||||||
|
function parsePageSize(value: string): PageSizeOption {
|
||||||
|
const parsed = Number(value)
|
||||||
|
|
||||||
|
return pageSizeOptions.find((option) => option === parsed) ?? 50
|
||||||
}
|
}
|
||||||
|
|
||||||
function BatteriesPage() {
|
function BatteriesPage() {
|
||||||
const { data, error, isPending } = useQuery(
|
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({
|
orpc.battery.batteries.queryOptions({
|
||||||
input: {},
|
input: {
|
||||||
|
search: search.search,
|
||||||
|
lowPower: search.lowPower,
|
||||||
|
powerStatus: search.powerStatus,
|
||||||
|
sort: search.sort,
|
||||||
|
pageSize: search.pageSize,
|
||||||
|
cursor: search.cursor,
|
||||||
|
},
|
||||||
refetchInterval: 30_000,
|
refetchInterval: 30_000,
|
||||||
|
placeholderData: (prev) => prev,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const columns = useMemo(
|
||||||
|
() => [
|
||||||
|
columnHelper.accessor('devName', {
|
||||||
|
header: '设备名称',
|
||||||
|
cell: (info) => (
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="font-medium text-zinc-100">{info.getValue()}</span>
|
||||||
|
<span className="text-[10px] text-zinc-500 tabular-nums">{info.row.original.mac}</span>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
columnHelper.accessor('devModel', {
|
||||||
|
header: '型号',
|
||||||
|
cell: (info) => <span className="text-zinc-400">{info.getValue()}</span>,
|
||||||
|
}),
|
||||||
|
columnHelper.accessor('power', {
|
||||||
|
header: '电量',
|
||||||
|
cell: (info) => {
|
||||||
|
const power = info.getValue()
|
||||||
|
const isLow = info.row.original.isLowPower
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<span className="w-8 text-right tabular-nums font-medium">{power}%</span>
|
||||||
|
<div className="h-1.5 w-16 overflow-hidden rounded-full bg-zinc-800">
|
||||||
|
<div className={`h-full ${powerBarColor(power, isLow)}`} style={{ width: `${power}%` }} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.accessor('powerStatus', {
|
||||||
|
header: '状态',
|
||||||
|
cell: (info) => {
|
||||||
|
const status = info.getValue()
|
||||||
|
return <span className={`text-xs ${powerStatusColor[status]}`}>{powerStatusLabel[status]}</span>
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.accessor('createTime', {
|
||||||
|
header: '最后更新',
|
||||||
|
cell: (info) => (
|
||||||
|
<span className="text-zinc-500 tabular-nums">{new Date(info.getValue()).toLocaleString('zh-CN')}</span>
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
|
||||||
|
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) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<main className="flex min-h-screen items-center justify-center bg-[#09090B]">
|
<main className="flex min-h-screen items-center justify-center bg-[#09090B]">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<p className="text-lg text-red-400">数据加载失败</p>
|
<p className="text-lg text-red-400">数据加载失败</p>
|
||||||
<p className="mt-2 text-sm text-zinc-500">{error.message}</p>
|
<p className="mt-2 text-sm text-zinc-500">请稍后重试,或联系管理员检查数据源连接。</p>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isPending || !data) {
|
|
||||||
return (
|
return (
|
||||||
<main className="flex min-h-screen items-center justify-center bg-[#09090B]">
|
<main className="min-h-screen bg-[#09090B] text-zinc-100">
|
||||||
<p className="text-zinc-500">加载中…</p>
|
{/* Background gradient */}
|
||||||
</main>
|
<div className="pointer-events-none fixed inset-0 z-0 flex justify-center">
|
||||||
)
|
<div className="h-[600px] w-[1000px] -translate-y-1/2 rounded-full bg-teal-900/5 blur-[100px]" />
|
||||||
}
|
</div>
|
||||||
|
|
||||||
return (
|
<div className="relative z-10 mx-auto max-w-7xl px-6 py-8">
|
||||||
<main className="min-h-screen bg-[#09090B] px-6 py-8 text-zinc-100">
|
<header>
|
||||||
<header className="mx-auto max-w-7xl">
|
|
||||||
<div className="flex items-end justify-between">
|
<div className="flex items-end justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="font-semibold text-2xl">设备电池实时状态</h1>
|
<h1 className="font-light text-3xl tracking-tight">设备电池实时状态</h1>
|
||||||
<p className="mt-1 text-sm text-zinc-500">更新 {new Date(data.updatedAt).toLocaleString('zh-CN')}</p>
|
<p className="mt-1 text-sm text-zinc-500">
|
||||||
|
{data ? `更新于 ${new Date(data.updatedAt).toLocaleString('zh-CN')}` : '加载中…'}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<nav className="text-xs">
|
<nav className="text-xs">
|
||||||
<Link to="/" className="text-zinc-500 hover:text-zinc-300">
|
<Link to="/" className="text-zinc-500 hover:text-teal-400 transition-colors">
|
||||||
← 返回 SoH 看板
|
← 返回 SoH 看板
|
||||||
</Link>
|
</Link>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<dl className="mt-6 grid grid-cols-3 gap-4">
|
<dl className="mt-8 grid grid-cols-1 gap-4 sm:grid-cols-3">
|
||||||
<div className="rounded-lg border border-zinc-800 bg-zinc-950 p-4">
|
<div className="rounded-xl border border-white/5 bg-white/[0.02] p-5">
|
||||||
<dt className="text-xs text-zinc-500">设备总数</dt>
|
<dt className="text-xs font-medium text-zinc-500">设备总数</dt>
|
||||||
<dd className="mt-1 font-semibold text-2xl">{data.total}</dd>
|
<dd className="mt-2 font-light text-3xl tabular-nums">{data?.total ?? '-'}</dd>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-lg border border-zinc-800 bg-zinc-950 p-4">
|
<div className="rounded-xl border border-white/5 bg-white/[0.02] p-5">
|
||||||
<dt className="text-xs text-zinc-500">低电量</dt>
|
<dt className="text-xs font-medium text-zinc-500">低电量</dt>
|
||||||
<dd className="mt-1 font-semibold text-2xl text-red-400">{data.lowPower}</dd>
|
<dd className="mt-2 font-light text-3xl tabular-nums text-red-400">{data?.lowPower ?? '-'}</dd>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-lg border border-zinc-800 bg-zinc-950 p-4">
|
<div className="rounded-xl border border-white/5 bg-white/[0.02] p-5">
|
||||||
<dt className="text-xs text-zinc-500">充电中</dt>
|
<dt className="text-xs font-medium text-zinc-500">充电中</dt>
|
||||||
<dd className="mt-1 font-semibold text-2xl text-teal-400">{data.charging}</dd>
|
<dd className="mt-2 font-light text-3xl tabular-nums text-teal-400">{data?.charging ?? '-'}</dd>
|
||||||
</div>
|
</div>
|
||||||
</dl>
|
</dl>
|
||||||
</header>
|
</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">
|
<section className="mt-10">
|
||||||
{data.items.length > 0 ? (
|
{/* Controls */}
|
||||||
data.items.map((item) => <DeviceCard key={item.id} item={item} />)
|
<div className="mb-6 flex flex-wrap items-center gap-4">
|
||||||
|
<div className="relative flex-1 min-w-[240px]">
|
||||||
|
<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"
|
||||||
|
value={localSearch}
|
||||||
|
onChange={(e) => setLocalSearch(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<select
|
||||||
|
className="rounded-lg border border-white/10 bg-white/5 px-3 py-2 text-sm focus:outline-none"
|
||||||
|
value={search.powerStatus ?? ''}
|
||||||
|
onChange={(e) => {
|
||||||
|
navigate({
|
||||||
|
search: (prev) => ({
|
||||||
|
...prev,
|
||||||
|
powerStatus: parsePowerStatus(e.target.value),
|
||||||
|
cursor: undefined,
|
||||||
|
cursors: [],
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="">所有充电状态</option>
|
||||||
|
<option value="0">未充电</option>
|
||||||
|
<option value="1">充电中</option>
|
||||||
|
<option value="2">已充满</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<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: [],
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
仅看低电量
|
||||||
|
</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"
|
||||||
|
value={search.sort}
|
||||||
|
onChange={(e) => {
|
||||||
|
navigate({
|
||||||
|
search: (prev) => ({
|
||||||
|
...prev,
|
||||||
|
sort: parseSort(e.target.value),
|
||||||
|
cursor: undefined,
|
||||||
|
cursors: [],
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="createdAtDesc">最新更新</option>
|
||||||
|
<option value="createdAtAsc">最早更新</option>
|
||||||
|
<option value="powerDesc">电量从高到低</option>
|
||||||
|
<option value="powerAsc">电量从低到高</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select
|
||||||
|
className="rounded-lg border border-white/10 bg-white/5 px-3 py-2 text-sm focus:outline-none"
|
||||||
|
value={search.pageSize}
|
||||||
|
onChange={(e) => {
|
||||||
|
navigate({
|
||||||
|
search: (prev) => ({
|
||||||
|
...prev,
|
||||||
|
pageSize: parsePageSize(e.target.value),
|
||||||
|
cursor: undefined,
|
||||||
|
cursors: [],
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="20">20 条/页</option>
|
||||||
|
<option value="50">50 条/页</option>
|
||||||
|
<option value="100">100 条/页</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="rounded-xl border border-white/5 bg-white/[0.01] overflow-hidden">
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className={`w-full border-collapse text-left text-sm ${isPlaceholderData ? 'opacity-60' : ''}`}>
|
||||||
|
<thead>
|
||||||
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
|
<tr key={headerGroup.id} className="border-b border-white/5 bg-white/[0.02]">
|
||||||
|
{headerGroup.headers.map((header) => (
|
||||||
|
<th key={header.id} className="px-6 py-4 font-medium text-zinc-500 whitespace-nowrap">
|
||||||
|
{flexRender(header.column.columnDef.header, header.getContext())}
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-white/[0.02]">
|
||||||
|
{isPending && !isPlaceholderData ? (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={columns.length} className="h-32 text-center text-zinc-500">
|
||||||
|
加载中…
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : data?.items.length === 0 ? (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={columns.length} className="h-32 text-center text-zinc-500">
|
||||||
|
暂无匹配设备
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
) : (
|
) : (
|
||||||
<div className="col-span-full py-12 text-center text-zinc-500">暂无设备数据</div>
|
table.getRowModel().rows.map((row) => (
|
||||||
|
<tr key={row.id} className="transition-colors hover:bg-white/[0.02]">
|
||||||
|
{row.getVisibleCells().map((cell) => (
|
||||||
|
<td key={cell.id} className="px-6 py-4 whitespace-nowrap">
|
||||||
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
))
|
||||||
)}
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 flex items-center justify-between text-sm text-zinc-500">
|
||||||
|
<div>
|
||||||
|
显示 {data?.items.length ?? 0} 台设备
|
||||||
|
{data?.total ? ` (共 ${data.total} 台)` : ''}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -145,7 +145,11 @@ function Dashboard() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs tabular-nums text-[#71717A]">数据更新时间: {updatedAt}</p>
|
<p className="text-xs tabular-nums text-[#71717A]">数据更新时间: {updatedAt}</p>
|
||||||
<Link to="/batteries" className="text-xs text-teal-400 hover:text-teal-300">
|
<Link
|
||||||
|
to="/batteries"
|
||||||
|
search={{ pageSize: 50, sort: 'createdAtDesc', cursors: [] }}
|
||||||
|
className="text-xs text-teal-400 hover:text-teal-300"
|
||||||
|
>
|
||||||
设备电池实时状态 →
|
设备电池实时状态 →
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user