import React, { useCallback, useEffect, useMemo, useState } from 'react'; const LIMIT_OPTIONS = [100, 200, 500, 1000]; const ACTION_LABELS = { write_access_granted: '写操作放行', write_blocked_readonly: '只读拦截', write_auth_required: '未登录拦截', login_success: '登录成功', login_failed: '登录失败', logout: '退出登录', user_created: '创建用户', user_updated: '更新用户', license_uploaded: '上传授权文件', license_upload_failed: '上传授权失败', license_refreshed: '刷新授权状态', task_queued: '任务入队', batch_created: '创建任务批次', batch_marked_complete: '批次标记完成', batch_item_updated: '更新批次条目', dinsar_label_updated: '更新D-InSAR标签', }; const getActionLabel = (action) => ACTION_LABELS[action] || action || '未知动作'; const getOutcome = (action) => { const normalized = (action || '').toLowerCase(); if (normalized.includes('failed') || normalized.includes('blocked') || normalized.includes('required')) { return 'blocked'; } return 'ok'; }; const formatTime = (value) => { if (!value) return '-'; const date = new Date(value); if (Number.isNaN(date.getTime())) return '-'; return date.toLocaleString(); }; const formatDetail = (detail) => { if (detail === null || detail === undefined || detail === '') { return '-'; } if (typeof detail === 'string') { return detail; } try { return JSON.stringify(detail); } catch { return '[detail 解析失败]'; } }; const AuditLogPanel = ({ apiClient }) => { const [logs, setLogs] = useState([]); const [loading, setLoading] = useState(false); const [message, setMessage] = useState(''); const [limit, setLimit] = useState(200); const [keyword, setKeyword] = useState(''); const [actionFilter, setActionFilter] = useState('all'); const [outcomeFilter, setOutcomeFilter] = useState('all'); const [autoRefresh, setAutoRefresh] = useState(false); const [lastUpdated, setLastUpdated] = useState(null); const loadLogs = useCallback(async ({ silent = false } = {}) => { if (!silent) { setLoading(true); setMessage(''); } try { const response = await apiClient.get('/auth/audit-logs', { params: { limit } }); const data = Array.isArray(response.data) ? response.data : []; setLogs(data); setLastUpdated(new Date()); } catch (error) { setMessage(error.response?.data?.detail || '加载审计日志失败。'); } finally { if (!silent) { setLoading(false); } } }, [apiClient, limit]); useEffect(() => { loadLogs(); }, [loadLogs]); useEffect(() => { if (!autoRefresh) return undefined; const timer = setInterval(() => { loadLogs({ silent: true }); }, 15000); return () => clearInterval(timer); }, [autoRefresh, loadLogs]); const actionOptions = useMemo(() => { return Array.from(new Set(logs.map((item) => item.action).filter(Boolean))).sort(); }, [logs]); const filteredLogs = useMemo(() => { const normalizedKeyword = keyword.trim().toLowerCase(); return logs.filter((item) => { if (actionFilter !== 'all' && item.action !== actionFilter) { return false; } const outcome = getOutcome(item.action); if (outcomeFilter !== 'all' && outcome !== outcomeFilter) { return false; } if (!normalizedKeyword) { return true; } const detailText = formatDetail(item.detail).toLowerCase(); const searchText = [ item.username || '', item.action || '', item.resource || '', item.ip_address || '', detailText, ].join(' ').toLowerCase(); return searchText.includes(normalizedKeyword); }); }, [logs, actionFilter, outcomeFilter, keyword]); const summary = useMemo(() => { const total = filteredLogs.length; const blocked = filteredLogs.filter((item) => getOutcome(item.action) === 'blocked').length; const passed = total - blocked; return { total, blocked, passed }; }, [filteredLogs]); return (
审计日志

记录关键操作、权限拦截和鉴权行为,便于排查问题。

当前显示 {summary.total}
通过 {summary.passed}
拦截/失败 {summary.blocked}
更新时间 {lastUpdated ? lastUpdated.toLocaleTimeString() : '-'}
{filteredLogs.map((item) => { const outcome = getOutcome(item.action); return (
{outcome === 'ok' ? '通过' : '拦截/失败'} {getActionLabel(item.action)} {item.username || '匿名/未登录'}
{formatTime(item.created_at)}
资源:{item.resource || '-'} IP:{item.ip_address || '-'}
详情:{formatDetail(item.detail)}
); })} {!loading && filteredLogs.length === 0 && (
当前筛选条件下暂无日志。
)}
{message && (
{message}
)}
); }; export default AuditLogPanel;