import { useCallback, useEffect, useMemo, useState } from "react"; import { Activity, BarChart2, Download, Filter, RefreshCw, ScrollText, Search, Users, X, } from "lucide-react"; import { Bar, BarChart, CartesianGrid, Cell, Line, LineChart, Pie, PieChart, ResponsiveContainer, Tooltip, XAxis, YAxis, } from "recharts"; import { AuditOperationResult } from "../../shared/api/enums"; import { useDictionaries } from "../dictionaries/dictionaryContext"; import { formatAuditDate, toUtcIso, } from "./auditLabels"; import { useAuditBreakdowns, useAuditLogs, useAuditTrend, type RemoteState, } from "./auditState"; import { AuditGranularity, type AuditActionStatisticsItem, type AuditLogQuery, type AuditTrendItem, type AuditUserStatisticsItem, } from "./auditTypes"; import { systemApi } from "../system/systemApi"; import { LOG_LEVELS, type SystemLogLevel, type SystemLogTail, } from "../system/systemTypes"; const NAVY = "var(--dms-deep)"; const BLUE = "var(--dms-bright)"; const WHITE = "#fff"; // 多彩配色(不再拘泥蓝色系):蓝为主色,辅以橙/绿/紫/红/青等,扇区按序取色。 // 类别色板是内容语义色(计划例外),不随界面主题换肤。 const COLORS = ["#3191eb", "#f5a623", "#34a853", "#a55eea", "#e74c3c", "#00bcd4", "#f8e71c", "#ff7043", "#7cb342", "#5c6bc0"]; const EMPTY = "—"; function ResourceState({ state, onRetry, label, }: { state: RemoteState; onRetry: () => void; label: string; }) { if (state.loading && !state.data) { return (
正在加载{label}…
); } if (state.error) { return (
{state.error.httpStatus === 403 ? `当前账号无权读取${label}` : state.error.message}
{state.error.requestId && (
错误编号:{state.error.requestId}
)}
); } if (Array.isArray(state.data) && state.data.length === 0) { return (
当前条件下暂无{label}
); } return null; } function StatsPanel({ title, icon, children, }: { title: string; icon: React.ReactNode; children: React.ReactNode; }) { return (
{icon}

{title}

{children}
); } /** 运行日志查看窗口(契约1.23,manageAuditLog 权限;读取服务端日志文件尾部)。 */ function RuntimeLogModal({ onClose }: { onClose: () => void }) { const [lines, setLines] = useState(500); const [level, setLevel] = useState(""); const [data, setData] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [downloading, setDownloading] = useState(false); const load = useCallback(async () => { setLoading(true); setError(null); try { setData(await systemApi.readLogTail({ lines, level })); } catch (loadError) { setError( loadError instanceof Error ? loadError.message : "运行日志读取失败", ); } finally { setLoading(false); } }, [lines, level]); useEffect(() => { void load(); }, [load]); const handleDownload = async () => { setDownloading(true); setError(null); try { const result = await systemApi.downloadLog(); const url = URL.createObjectURL(result.blob); const anchor = document.createElement("a"); anchor.href = url; anchor.download = "dms-runtime.log"; document.body.appendChild(anchor); anchor.click(); anchor.remove(); URL.revokeObjectURL(url); } catch (downloadError) { setError( downloadError instanceof Error ? downloadError.message : "日志下载失败", ); } finally { setDownloading(false); } }; const controlStyle = { borderColor: "rgba(var(--dms-bright-rgb),0.4)", color: NAVY, } as const; return (

运行日志

{error ? (
{error}
) : (
{data?.file ? ( <> 文件:{data.file} {data.truncated && "(级别过滤为全量扫描后取尾部,非完整结果)"} ) : ( "未配置运行日志文件(DMS_LOG_FILE),或文件尚未生成。" )}
              {data?.error
                ? `读取失败:${data.error}`
                : data && data.lines.length > 0
                  ? data.lines.join("\n")
                  : "(无日志内容)"}
            
)}
); } export function AuditDashboard() { const dictionaries = useDictionaries(); const [keyword, setKeyword] = useState(""); const [actionType, setActionType] = useState(""); const [operationResult, setOperationResult] = useState(""); const [targetType, setTargetType] = useState(""); const [userId, setUserId] = useState(""); const [organizationId, setOrganizationId] = useState(""); const [targetId, setTargetId] = useState(""); const [createdFromLocal, setCreatedFromLocal] = useState(""); const [createdToLocal, setCreatedToLocal] = useState(""); const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(20); const [sortDirection, setSortDirection] = useState<"asc" | "desc">("desc"); const [granularity, setGranularity] = useState(AuditGranularity.DAY); const [runtimeLogOpen, setRuntimeLogOpen] = useState(false); const createdFrom = toUtcIso(createdFromLocal); const createdTo = toUtcIso(createdToLocal); const query: AuditLogQuery = { keyword: keyword.trim() || undefined, actionType: actionType ? (actionType as AuditLogQuery["actionType"]) : undefined, operationResult: operationResult ? (operationResult as AuditLogQuery["operationResult"]) : undefined, targetType: targetType ? (targetType as AuditLogQuery["targetType"]) : undefined, userId: userId.trim() || undefined, organizationId: organizationId.trim() || undefined, targetId: targetId.trim() || undefined, createdFrom, createdTo, page, pageSize, sortDirection, }; const logs = useAuditLogs(query); const trend = useAuditTrend({ createdFrom, createdTo, granularity }); const breakdowns = useAuditBreakdowns( { createdFrom, createdTo }, { createdFrom, createdTo, limit: 6 }, ); const pieData = useMemo( () => (breakdowns.actions.data ?? []).map((item) => ({ name: dictionaries.label("auditActions", item.actionType), value: item.count, actionType: item.actionType, })), [breakdowns.actions.data, dictionaries.data], ); const activeUsers = useMemo( () => (breakdowns.users.data ?? []).map((item) => ({ ...item, label: item.realName ?? `用户 ${item.userId}`, })), [breakdowns.users.data], ); const resetPage = () => setPage(1); const clearFilters = () => { setKeyword(""); setActionType(""); setOperationResult(""); setTargetType(""); setUserId(""); setOrganizationId(""); setTargetId(""); setCreatedFromLocal(""); setCreatedToLocal(""); setSortDirection("desc"); setPage(1); }; return ( <>
操作日志 {logs.data?.total ?? 0} 条
{logs.loading && !logs.data && (
正在读取审计日志…
)} {logs.error && (
{logs.error.httpStatus === 403 ? "当前账号无权读取审计日志" : logs.error.message}
{logs.error.requestId && (
错误编号:{logs.error.requestId}
)}
)} {logs.data && logs.data.items.length === 0 && (
当前筛选条件下暂无日志
)} {logs.data && logs.data.items.length > 0 && (
{/* 对象类型 / 名称:弹性列吃剩余宽度 */} {/* 结果 / 失败原因:固定窄宽,容纳成功/失败徽标即可,失败原因换行展示 */} {/* 客户端IP:固定宽度保证完整显示 IPv4(最长 255.255.255.255) */} {[ "时间", "用户名 / 姓名", "组织", "动作", "对象类型 / 名称", "结果 / 失败原因", "客户端IP", ].map((heading) => ( ))} {logs.data.items.map((log, index) => "invalid" in log ? ( ) : ( ) )}
{heading}
该行数据不符合DMS接口契约,无法展示
{formatAuditDate(log.createdAt)}
{log.username ?? EMPTY}
{log.realName ?? EMPTY}
{log.organizationName ?? EMPTY} {dictionaries.label("auditActions", log.actionType)}
{log.targetType ? dictionaries.label("auditTargets", log.targetType) : EMPTY}
{log.targetName ?? EMPTY} {log.targetId ? `(${log.targetId})` : ""}
{dictionaries.label("auditResults", log.operationResult)}
{log.failureReason ?? EMPTY}
{log.clientIp ?? EMPTY}
)}
第 {logs.data?.page ?? page} / {logs.data?.totalPages ?? 0} 页
{runtimeLogOpen && setRuntimeLogOpen(false)} />} ); }