"use client" import { useEffect, useMemo, useState } from 'react' import Link from 'next/link' type AdminRecordItem = { id: string timestamp: string sampleCount: number } function toLocalInputValue(iso: string): string { const d = new Date(iso) if (Number.isNaN(d.getTime())) return '' const pad = (n: number) => n.toString().padStart(2, '0') const year = d.getFullYear() const month = pad(d.getMonth() + 1) const day = pad(d.getDate()) const hour = pad(d.getHours()) const minute = pad(d.getMinutes()) const second = pad(d.getSeconds()) return `${year}-${month}-${day}T${hour}:${minute}:${second}` } export default function AdminPage() { const [loading, setLoading] = useState(false) const [error, setError] = useState(null) const [data, setData] = useState([]) const [selected, setSelected] = useState>(new Set()) const [page, setPage] = useState(1) const [pageSize] = useState(50) const [total, setTotal] = useState(0) const [editingTimes, setEditingTimes] = useState>({}) const [dirty, setDirty] = useState>({}) const totalPages = useMemo(() => Math.max(1, Math.ceil(total / pageSize)), [total, pageSize]) useEffect(() => { const controller = new AbortController() async function run() { setLoading(true) setError(null) try { const params = new URLSearchParams() params.set('page', String(page)) params.set('pageSize', String(pageSize)) const res = await fetch(`/api/records?${params.toString()}`, { signal: controller.signal }) if (!res.ok) throw new Error(`HTTP ${res.status}`) const j = await res.json() const rows = j.data.map((r: any) => ({ id: r.id, timestamp: r.timestamp, sampleCount: r.sampleCount })) setData(rows) setTotal(j.pagination.total) setSelected(new Set()) setEditingTimes((prev) => { const next: Record = {} for (const r of rows) { const isoLocal = toLocalInputValue(r.timestamp) next[r.id] = prev[r.id] ?? isoLocal } return next }) setDirty({}) } catch (e: any) { if (e.name !== 'AbortError') setError(e.message || '加载失败') } finally { setLoading(false) } } run() return () => controller.abort() }, [page, pageSize]) function toggleSelect(id: string) { setSelected((prev) => { const next = new Set(prev) if (next.has(id)) next.delete(id) else next.add(id) return next }) } function selectAllCurrent() { setSelected(new Set(data.map((d) => d.id))) } function clearSelection() { setSelected(new Set()) } async function handleBatchDelete() { if (selected.size === 0) return if (!confirm(`确定要删除选中的 ${selected.size} 条记录吗?`)) return try { const res = await fetch('/api/records/batch', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ action: 'delete', ids: Array.from(selected) }), }) if (!res.ok) throw new Error('批量删除失败') const j = await res.json() const deletedIds: string[] = j.deletedIds || [] setData((prev) => prev.filter((r) => !deletedIds.includes(r.id))) setTotal((prev) => Math.max(0, prev - deletedIds.length)) clearSelection() } catch { alert('批量删除失败') } } function setNowFor(id: string) { const now = new Date() const isoLocal = toLocalInputValue(now.toISOString()) setEditingTimes((prev) => ({ ...prev, [id]: isoLocal })) setDirty((prev) => ({ ...prev, [id]: true })) } function shiftMinutes(id: string, minutes: number) { setEditingTimes((prev) => { const current = prev[id] const base = current ? new Date(current) : new Date() if (Number.isNaN(base.getTime())) return prev const shifted = new Date(base.getTime() + minutes * 60 * 1000) const isoLocal = toLocalInputValue(shifted.toISOString()) return { ...prev, [id]: isoLocal } }) setDirty((prev) => ({ ...prev, [id]: true })) } async function applyTime(id: string) { const input = editingTimes[id] if (!input) { alert('请输入时间') return } const dt = new Date(input) if (Number.isNaN(dt.getTime())) { alert('时间格式不正确') return } try { const res = await fetch('/api/records/batch', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ action: 'updateTimestamp', ids: [id], timestamp: dt.toISOString() }), }) if (!res.ok) throw new Error('修改时间失败') const j = await res.json() const updated: { id: string; timestamp: string }[] = j.updated || [] if (!updated.length) return const { timestamp } = updated[0] setData((prev) => prev.map((r) => (r.id === id ? { ...r, timestamp } : r))) setEditingTimes((prev) => ({ ...prev, [id]: toLocalInputValue(timestamp) })) setDirty((prev) => ({ ...prev, [id]: false })) } catch { alert('修改时间失败') } } return (

Admin 面板

批量删除记录,批量修改时间等维护操作

← 返回首页
{error &&
{error}
}
已选 {selected.size} 条
选中 ID 时间 / 编辑 采样点数
{loading && (
加载中...
)} {!loading && data.length === 0 && (
暂无数据
)}
{data.map((r) => (
toggleSelect(r.id)} className="h-4 w-4" /> {r.id}
{new Date(r.timestamp).toLocaleString()} {r.sampleCount}
详情
{ const v = e.target.value setEditingTimes((prev) => ({ ...prev, [r.id]: v })) const original = toLocalInputValue(r.timestamp) setDirty((prev) => ({ ...prev, [r.id]: v !== original })) }} className="w-full bg-transparent text-[11px] outline-none" />
当前:{new Date(r.timestamp).toLocaleString()} · 点数 {r.sampleCount}
))}
{page} / {totalPages}
) }