import { useEffect, useState } from "react"; import { listAudit, type AuditEntry } from "../api"; // 操作方法配色 const ACTION_STYLE: Record = { POST: "bg-emerald-50 text-emerald-600 border border-emerald-100", PUT: "bg-amber-50 text-amber-600 border border-amber-100", DELETE: "bg-rose-50 text-rose-600 border border-rose-100", PATCH: "bg-violet-50 text-violet-600 border border-violet-100", GET: "bg-gray-50 text-gray-600 border border-gray-100", }; const statusTone = (s: number) => (s < 300 ? "text-emerald-600" : s < 500 ? "text-amber-600" : "text-rose-500"); const shortId = (id: string) => (id && id.length > 10 ? `${id.slice(0, 4)}…${id.slice(-4)}` : id || "系统"); const fmt = (at: string) => new Date(at).toLocaleString("zh-CN", { hour12: false }); const LIMIT = 20; const ROUTE_OPTS = [ { value: "", label: "所有路由" }, { value: "/api/v1/admin", label: "管理员接口 (/admin)" }, { value: "/api/v1/prompts", label: "提示词接口 (/prompts)" }, { value: "/api/v1/auth", label: "鉴权接口 (/auth)" }, { value: "/api/v1/spaces", label: "空间接口 (/spaces)" }, ]; export function AuditPage() { const [audit, setAudit] = useState([]); const [loading, setLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); const [updatedAt, setUpdatedAt] = useState(null); const [err, setErr] = useState(""); // 筛选器状态 const [methodFilter, setMethodFilter] = useState(""); // 空 = 全部 const [routeFilter, setRouteFilter] = useState(""); const [searchQuery, setSearchQuery] = useState(""); // 分页状态 const [page, setPage] = useState(1); const [hasMore, setHasMore] = useState(true); const load = async (pageNum = page, showRef = false) => { if (showRef) setRefreshing(true); else setLoading(true); try { const offset = (pageNum - 1) * LIMIT; // 从后端载入比 LIMIT 稍微多一条,以此判断是否有下一页 const data = await listAudit(LIMIT + 1, offset); if (data.length > LIMIT) { setAudit(data.slice(0, LIMIT)); setHasMore(true); } else { setAudit(data); setHasMore(false); } setUpdatedAt(new Date()); setErr(""); } catch (er) { setErr((er as Error).message); } finally { setLoading(false); setRefreshing(false); } }; useEffect(() => { void load(page); // eslint-disable-next-line react-hooks/exhaustive-deps }, [page]); // 前端过滤(配合后端分页后的过滤,或按需做简单的前端实时匹配) const filteredAudit = audit.filter((a) => { if (methodFilter && a.action !== methodFilter) return false; if (routeFilter && !a.path.startsWith(routeFilter)) return false; if (searchQuery) { const q = searchQuery.toLowerCase(); const matchActor = a.actor.toLowerCase().includes(q); const matchIp = a.ip.toLowerCase().includes(q); const matchDetail = a.detail?.toLowerCase().includes(q) ?? false; const matchPath = a.path.toLowerCase().includes(q); if (!matchActor && !matchIp && !matchDetail && !matchPath) return false; } return true; }); const handlePrevPage = () => { if (page > 1) setPage(page - 1); }; const handleNextPage = () => { if (hasMore) setPage(page + 1); }; return (
{/* 顶栏 */}

敏感操作审计

敏感操作全链路留痕 · 只增不改,供合规审计与故障溯源

{updatedAt && 最近更新:{updatedAt.toLocaleTimeString("zh-CN", { hour12: false })}}
{/* 筛选工具栏 */}
{/* HTTP Method Button Group */}
{["", "GET", "POST", "PUT", "DELETE"].map((m) => ( ))}
{/* Route prefix select */} {/* Search Input */} { setSearchQuery(e.target.value); setPage(1); }} className="rounded-lg border border-gray-200 px-3 py-1.5 text-xs focus:outline-none focus:border-violet-400 w-56 shadow-sm" />
{/* 审计日志列表 */}

操作日志明细

当前页展示 {filteredAudit.length} 条
{loading ? (
正在载入审计记录…
) : err ? (
加载失败:{err}
) : filteredAudit.length === 0 ? (
没有匹配的审计记录
) : (
{filteredAudit.map((a) => ( ))}
方法 路由路径 状态 详情 操作人 客户端 IP 触发时间
{a.action} {a.path} {a.status} {a.detail || "—"} {shortId(a.actor)} {a.ip} {fmt(a.at)}
{/* Pagination controls */}
当前第 {page}
)}
); }