import { useState, useEffect, useCallback, useMemo } from 'react'; import { Search, Download, Filter, ChevronDown, ChevronRight, FileText, User, Globe, CheckCircle2, XCircle, AlertTriangle, BarChart3, Clock, RotateCcw, } from 'lucide-react'; import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid, PieChart, Pie, Cell, } from 'recharts'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import request from '../api/request'; /* ───────── Types ───────── */ interface AuditLog { id: number; user_id: number; username: string; action: string; target_type: string; target_id: number | null; detail: string | null; ip_address: string | null; result_status: 'success' | 'failed' | 'warning'; created_at: string | null; } interface AuditLogResponse { total: number; page: number; page_size: number; items: AuditLog[]; } /* ───────── Constants ───────── */ const ACTION_COLORS: Record = { CREATE_TICKET: 'bg-blue-500/15 text-blue-400', REVIEW_TICKET: 'bg-yellow-500/15 text-yellow-400', EXECUTE_TICKET: 'bg-green-500/15 text-green-400', CREATE_INSTANCE: 'bg-purple-500/15 text-purple-400', DELETE_INSTANCE: 'bg-red-500/15 text-red-400', CREATE_USER: 'bg-indigo-500/15 text-indigo-400', DELETE_USER: 'bg-red-500/15 text-red-400', QUERY: 'bg-cyan-500/15 text-cyan-400', LOGIN: 'bg-emerald-500/15 text-emerald-400', LOGOUT: 'bg-[var(--dg-text-muted)]/15 text-[var(--dg-text-muted)]', }; const ACTION_LABELS: Record = { CREATE_TICKET: '创建工单', REVIEW_TICKET: '审批工单', EXECUTE_TICKET: '执行工单', CREATE_INSTANCE: '创建实例', DELETE_INSTANCE: '删除实例', CREATE_USER: '创建用户', DELETE_USER: '删除用户', QUERY: '执行查询', LOGIN: '登录', LOGOUT: '登出', }; const STATUS_CONFIG: Record = { success: { icon: CheckCircle2, color: 'text-green-400', label: '成功' }, failed: { icon: XCircle, color: 'text-red-400', label: '失败' }, warning: { icon: AlertTriangle, color: 'text-yellow-400', label: '警告' }, }; const TARGET_ICONS: Record = { ticket: FileText, instance: Globe, user: User, query: Search, system: AlertTriangle, }; const CHART_COLORS = ['#6366f1', '#a855f7', '#ec4899', '#f43f5e', '#f97316', '#eab308', '#22c55e', '#06b6d4', '#3b82f6']; /* ───────── CSV Export Helper ───────── */ function exportLogsToCSV(logs: AuditLog[]) { const headers = ['ID', '操作人', '操作类型', '目标类型', '目标ID', '详情', 'IP地址', '结果', '时间']; const rows = logs.map(l => [ l.id, l.username, ACTION_LABELS[l.action] || l.action, l.target_type, l.target_id || '', l.detail || '', l.ip_address || '', l.result_status === 'success' ? '成功' : l.result_status === 'failed' ? '失败' : '警告', l.created_at || '', ]); const csv = [headers, ...rows].map(r => r.map(c => `"${String(c).replace(/"/g, '""')}"`).join(',')).join('\n'); const blob = new Blob(['\ufeff' + csv], { type: 'text/csv;charset=utf-8;' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `audit_logs_${new Date().toISOString().slice(0, 10)}.csv`; a.click(); URL.revokeObjectURL(url); } /* ───────── Main Component ───────── */ export default function AuditLogsPage() { const [logs, setLogs] = useState([]); const [, setTotal] = useState(0); const [page, setPage] = useState(1); const [loading, setLoading] = useState(true); const pageSize = 20; // Advanced filters const [filterAction, setFilterAction] = useState(''); const [filterTarget, setFilterTarget] = useState(''); const [filterStatus, setFilterStatus] = useState(''); const [filterDateRange, setFilterDateRange] = useState(''); const [searchQuery, setSearchQuery] = useState(''); const [showFilters, setShowFilters] = useState(false); // Detail expanded const [expandedId, setExpandedId] = useState(null); // Stats tab const [showStats, setShowStats] = useState(false); const fetchLogs = useCallback(async () => { setLoading(true); try { const params = new URLSearchParams({ page: String(page), page_size: String(pageSize) }); if (filterAction) params.set('action', filterAction); const res = await request.get(`/audit/logs?${params}`); const data = (res?.data ?? res ?? {}) as AuditLogResponse; const items = data.items || []; setLogs(items); setTotal(data.total || 0); } catch { setLogs([]); setTotal(0); } finally { setLoading(false); } }, [page, filterAction]); useEffect(() => { fetchLogs(); }, [fetchLogs]); // Client-side filtering for advanced filters const filteredLogs = useMemo(() => { return logs.filter(log => { if (filterTarget && log.target_type !== filterTarget) return false; if (filterStatus && log.result_status !== filterStatus) return false; if (filterDateRange && log.created_at) { const days = Math.floor((Date.now() - new Date(log.created_at).getTime()) / 86400000); if (filterDateRange === '1d' && days > 1) return false; if (filterDateRange === '7d' && days > 7) return false; if (filterDateRange === '30d' && days > 30) return false; } if (searchQuery) { const q = searchQuery.toLowerCase(); return ( log.username.toLowerCase().includes(q) || (log.detail || '').toLowerCase().includes(q) || (log.ip_address || '').toLowerCase().includes(q) || (ACTION_LABELS[log.action] || log.action).toLowerCase().includes(q) ); } return true; }); }, [logs, filterTarget, filterStatus, filterDateRange, searchQuery]); const totalPages = Math.max(1, Math.ceil(filteredLogs.length / pageSize)); const paginatedLogs = filteredLogs.slice((page - 1) * pageSize, page * pageSize); const hasActiveFilters = filterAction || filterTarget || filterStatus || filterDateRange || searchQuery; const clearFilters = () => { setFilterAction(''); setFilterTarget(''); setFilterStatus(''); setFilterDateRange(''); setSearchQuery(''); setPage(1); }; // Stats data const actionDistribution = useMemo(() => { const map: Record = {}; logs.forEach(l => { map[l.action] = (map[l.action] || 0) + 1; }); return Object.entries(map).map(([name, value], i) => ({ name: ACTION_LABELS[name] || name, value, color: CHART_COLORS[i % CHART_COLORS.length] })); }, [logs]); const statusDistribution = useMemo(() => { const map: Record = { success: 0, failed: 0, warning: 0 }; logs.forEach(l => { map[l.result_status] = (map[l.result_status] || 0) + 1; }); return [ { name: '成功', value: map.success, color: '#22c55e' }, { name: '失败', value: map.failed, color: '#f43f5e' }, { name: '警告', value: map.warning, color: '#eab308' }, ]; }, [logs]); const trendData = useMemo(() => { const map: Record = {}; logs.forEach(l => { if (l.created_at) { const d = new Date(l.created_at).toLocaleDateString('zh-CN', { month: '2-digit', day: '2-digit' }); map[d] = (map[d] || 0) + 1; } }); return Object.entries(map) .sort(([a], [b]) => new Date(a).getTime() - new Date(b).getTime()) .slice(-10) .map(([date, count]) => ({ date, count })); }, [logs]); const formatDate = (d: string | null) => { if (!d) return '-'; return new Date(d).toLocaleString('zh-CN'); }; return (
{/* Header */}

审计日志

追踪系统操作记录,满足合规审计需求

{/* Stats Panel */} {showStats && (
操作类型分布 `${name}: ${value}`}> {actionDistribution.map((entry, i) => )} 操作结果分布 `${name}: ${value}`}> {statusDistribution.map((entry, i) => )} 操作趋势(最近10天)
)} {/* Search & Filters */}
{ setSearchQuery(e.target.value); setPage(1); }} className="w-full pl-10 pr-4 py-2.5 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm placeholder:text-[var(--dg-text-muted)] focus:border-blue-400 focus:outline-none focus:ring-1 focus:ring-blue-400/20" placeholder="搜索操作人、IP、详情..." />
{hasActiveFilters && ( )} 共 {filteredLogs.length} 条记录
{showFilters && (
{/* Action type filter */} {/* Target type filter */} {/* Status filter */} {/* Date range */}
)}
{/* Quick Action Filters */}
{['', ...Object.keys(ACTION_LABELS)].map(action => ( ))}
{/* Table */}
{loading ? ( ) : paginatedLogs.length === 0 ? ( ) : ( paginatedLogs.map(log => { const isExpanded = expandedId === log.id; const statusConfig = STATUS_CONFIG[log.result_status] || STATUS_CONFIG.success; const StatusIcon = statusConfig.icon; const TargetIcon = TARGET_ICONS[log.target_type] || FileText; return ( <> setExpandedId(isExpanded ? null : log.id)} > {/* Expanded Detail */} {isExpanded && ( )} ); }) )}
操作 操作人 目标 IP地址 结果 时间
加载中...

暂无审计日志

{hasActiveFilters && ( )}
{isExpanded ? : } {ACTION_LABELS[log.action] || log.action}
{(log.username || '?').charAt(0).toUpperCase()}
{log.username || '-'}
{log.target_type} {log.target_id && #{log.target_id}}
{log.ip_address || '-'} {statusConfig.label} {formatDate(log.created_at)}
操作详情

{log.detail || '无详情'}

日志ID
#{log.id}
用户ID
#{log.user_id}
目标ID
{log.target_id || 'N/A'}
时间
{formatDate(log.created_at)}
{/* Pagination */} {totalPages > 1 && (
第 {page} / {totalPages} 页,共 {filteredLogs.length} 条
)}
); }