import { FileCheck2, ShieldCheck } from "lucide-react"; import { useCallback, useEffect, useMemo, useState } from "react"; import { useSearchParams } from "react-router-dom"; import { adminApi } from "../../api/client"; import type { AdminAuditAction, AuditLogEntry, AuditQuery, SortOrder, } from "../../api/types"; import { DataTable, type DataColumn } from "../../components/data-table"; import { DateRangeControl } from "../../components/date-range-control"; import { FilterControl } from "../../components/filter-control"; import { Badge, Button, Card, ErrorState, LoadingState, PageHeader, } from "../../components/primitives"; import { TableToolbar } from "../../components/table-toolbar"; import { useCursorPage } from "../../hooks/use-cursor-page"; import { formatDateTime, statusLabel } from "../../lib/format"; export function AuditPage() { const [searchParams, setSearchParams] = useSearchParams(); const [from, setFrom] = useState(searchParams.get("from") ?? ""); const [until, setUntil] = useState(searchParams.get("until") ?? ""); const [action, setAction] = useState<"" | AdminAuditAction>( (searchParams.get("action") as AdminAuditAction | null) ?? "", ); const [result, setResult] = useState<"" | AuditLogEntry["result"]>( (searchParams.get("result") as AuditLogEntry["result"] | null) ?? "", ); const [order, setOrder] = useState( searchParams.get("order") === "asc" ? "asc" : "desc", ); const auditQuery = useMemo( () => ({ from: from || undefined, until: until || undefined, action: action || undefined, result: result || undefined, sort: "createdAt", order, limit: 50, }), [action, from, order, result, until], ); const fetchAudit = useCallback((value: AuditQuery) => adminApi.auditLogs(value), []); const { items, nextCursor, loading, loadingMore, error, loadMore, reload, } = useCursorPage(auditQuery, fetchAudit); useEffect(() => { const params = new URLSearchParams(); if (from) params.set("from", from); if (until) params.set("until", until); if (action) params.set("action", action); if (result) params.set("result", result); params.set("sort", "createdAt"); params.set("order", order); setSearchParams(params, { replace: true }); }, [action, from, order, result, setSearchParams, until]); const columns = useMemo[]>( () => [ { accessorKey: "createdAt", header: "时间", cell: ({ getValue }) => ( {formatDateTime(String(getValue()))} ), }, { accessorKey: "operatorName", header: "操作员", cell: ({ getValue }) => {String(getValue())}, }, { accessorKey: "action", header: "操作", cell: ({ getValue }) => ( {String(getValue())} ), }, { id: "target", header: "目标", cell: ({ row }) => ( {row.original.targetType} {row.original.targetId} ), }, { accessorKey: "requestId", header: "请求 ID", cell: ({ getValue }) => ( {String(getValue() || "—")} ), }, { accessorKey: "result", header: "结果", cell: ({ getValue }) => { const result = String(getValue()); return ( {statusLabel(result)} ); }, }, ], [], ); return (

不可变审计保护

管理员变更、积分赠送和安全操作都会记录请求标识与处理结果。

已加载 {items.length} 条
{ setFrom(""); setUntil(""); setAction(""); setResult(""); setOrder("desc"); }} > { setFrom(value.from ?? ""); setUntil(value.until ?? ""); }} /> {error ? (
) : loading ? ( ) : ( setOrder(nextOrder)} footer={ nextCursor ? (
) : null } /> )}
); }