import { useState, useEffect, useRef } from 'react' import { Terminal, Play, Pause, Trash2, ChevronDown } from 'lucide-react' interface LogEntry { type: string timestamp: string level: string message: string email?: string module?: string } const levelColors: Record = { success: 'text-green-400', error: 'text-red-400', warning: 'text-yellow-400', info: 'text-cyan-400', } const levelBgColors: Record = { success: 'bg-green-900/20', error: 'bg-red-900/20', warning: 'bg-yellow-900/20', info: 'bg-cyan-900/20', } interface LiveLogViewerProps { maxLogs?: number autoScroll?: boolean className?: string } export default function LiveLogViewer({ maxLogs = 200, autoScroll: initialAutoScroll = true, className = '', }: LiveLogViewerProps) { const [logs, setLogs] = useState([]) const [isConnected, setIsConnected] = useState(false) const [isPaused, setIsPaused] = useState(false) const [autoScroll, setAutoScroll] = useState(initialAutoScroll) const logContainerRef = useRef(null) const eventSourceRef = useRef(null) const pausedLogsRef = useRef([]) const isPausedRef = useRef(isPaused) isPausedRef.current = isPaused // 加载历史日志 const loadHistoryLogs = async () => { try { const res = await fetch('/api/logs') const data = await res.json() if (data.code === 0 && Array.isArray(data.data)) { // 转换为 LogEntry 格式 const historyLogs: LogEntry[] = data.data.map((log: { level: string; message: string; timestamp: string; email?: string; module?: string }) => ({ type: 'log', level: log.level || 'info', message: log.message || '', timestamp: log.timestamp ? new Date(log.timestamp).toLocaleTimeString('zh-CN') : '', email: log.email, module: log.module || 'system', })) setLogs(historyLogs.slice(-maxLogs)) } } catch (e) { console.error('Failed to load history logs:', e) } } // 组件挂载时先加载历史日志,再连接 SSE useEffect(() => { loadHistoryLogs() const es = new EventSource('/api/logs/stream') eventSourceRef.current = es es.onopen = () => { setIsConnected(true) } es.onmessage = (event) => { try { const data = JSON.parse(event.data) as LogEntry if (data.type === 'connected') { return } if (data.type === 'log') { if (isPausedRef.current) { pausedLogsRef.current.push(data) } else { setLogs((prev) => { const newLogs = [...prev, data] return newLogs.slice(-maxLogs) }) } } } catch (e) { console.error('Parse error:', e) } } es.onerror = () => { setIsConnected(false) } return () => { es.close() } // eslint-disable-next-line react-hooks/exhaustive-deps }, []) // 自动滚动 useEffect(() => { if (autoScroll && logContainerRef.current) { logContainerRef.current.scrollTop = logContainerRef.current.scrollHeight } }, [logs, autoScroll]) // 恢复暂停的日志 const handleResume = () => { setIsPaused(false) if (pausedLogsRef.current.length > 0) { setLogs((prev) => { const newLogs = [...prev, ...pausedLogsRef.current] pausedLogsRef.current = [] return newLogs.slice(-maxLogs) }) } } const handleClear = () => { setLogs([]) } return (
{/* Header */}
实时日志 {isConnected ? '已连接' : '连接中...'}
{logs.length} 条日志
{/* Log Content */}
{logs.length === 0 ? (
等待日志...
) : ( logs.map((log, index) => (
{log.timestamp} [{log.level}] [{log.module}] {log.email && ( {log.email} )} {log.message}
)) )}
{/* Paused indicator */} {isPaused && pausedLogsRef.current.length > 0 && (
已暂停 - 有 {pausedLogsRef.current.length} 条新日志等待显示
)}
) }