/** * 审计日志 Hooks * * contract §2.4:auditLogs * 仲裁 ARB-005:经 teacher-bff GraphQL 消费(不直连 Kafka) */ import { useCallback, useState } from "react"; import { useGraphQuery } from "./use-graphql"; import { AUDIT_LOGS_QUERY } from "@/lib/graphql-client"; import type { AuditLogViewModel, PaginatedResult, ListFilter, } from "@/types/view-models"; interface AuditLogsResponse { auditLogs: PaginatedResult; } interface AuditLogFilter extends Partial { action?: string; actorUserId?: string; startDate?: number; endDate?: number; } export function useAuditLogs(filter: AuditLogFilter) { const result = useGraphQuery(AUDIT_LOGS_QUERY, { filter }); return { ...result, data: result.data?.auditLogs ?? null, }; } export function useAuditLogFilter(initial?: Partial) { const [filter, setFilter] = useState({ page: 1, pageSize: 20, ...initial, }); const setPage = useCallback( (page: number) => setFilter((f) => ({ ...f, page })), [], ); const setSearch = useCallback( (search: string) => setFilter((f) => ({ ...f, search, page: 1 })), [], ); const setAction = useCallback( (action: string) => setFilter((f) => ({ ...f, action: action || undefined, page: 1 })), [], ); return { filter, setFilter, setPage, setSearch, setAction }; } /** 审计日志导出 CSV */ export function exportAuditLogsCsv(logs: AuditLogViewModel[]): void { const headers = [ "时间", "操作人", "操作", "资源类型", "资源ID", "IP", "追踪ID", ]; const rows = logs.map((l) => [ new Date(l.occurredAt).toLocaleString("zh-CN"), l.actorName, l.action, l.resourceType, l.resourceId, l.ip, l.traceId ?? "", ]); const csv = [headers, ...rows] .map((row) => row.map((cell) => `"${String(cell).replace(/"/g, '""')}"`).join(","), ) .join("\n"); const blob = new Blob([`\uFEFF${csv}`], { type: "text/csv;charset=utf-8;" }); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = `audit-logs-${new Date().toISOString().slice(0, 10)}.csv`; link.click(); URL.revokeObjectURL(url); }