import { useEffect, useState, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { Card } from '@/components/ui/Card'; import { Button } from '@/components/ui/Button'; import { EmptyState } from '@/components/ui/EmptyState'; import { useNotificationStore, useAuthStore } from '@/stores'; import { logsApi } from '@/services/api/logs'; import styles from './LogsPage.module.scss'; interface ErrorLogItem { name: string; size?: number; modified?: number; } // 限制显示的最大日志行数,防止渲染过多导致卡死 const MAX_DISPLAY_LINES = 500; export function LogsPage() { const { t } = useTranslation(); const { showNotification } = useNotificationStore(); const connectionStatus = useAuthStore((state) => state.connectionStatus); const [logLines, setLogLines] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); const [autoRefresh, setAutoRefresh] = useState(false); const [errorLogs, setErrorLogs] = useState([]); const [loadingErrors, setLoadingErrors] = useState(false); const logViewerRef = useRef(null); const pendingScrollToBottomRef = useRef(false); // 保存最新时间戳用于增量获取 const latestTimestampRef = useRef(0); const disableControls = connectionStatus !== 'connected'; const isNearBottom = (node: HTMLPreElement | null) => { if (!node) return true; const threshold = 24; return node.scrollHeight - node.scrollTop - node.clientHeight <= threshold; }; const scrollToBottom = () => { const node = logViewerRef.current; if (!node) return; node.scrollTop = node.scrollHeight; }; const isWarningLine = (line: string) => /\bwarn(?:ing)?\b/i.test(line) || line.includes('警告'); const loadLogs = async (incremental = false) => { if (connectionStatus !== 'connected') { setLoading(false); return; } if (!incremental) { setLoading(true); } setError(''); try { pendingScrollToBottomRef.current = !incremental || isNearBottom(logViewerRef.current); const params = incremental && latestTimestampRef.current > 0 ? { after: latestTimestampRef.current } : {}; const data = await logsApi.fetchLogs(params); // 更新时间戳 if (data['latest-timestamp']) { latestTimestampRef.current = data['latest-timestamp']; } const newLines = Array.isArray(data.lines) ? data.lines : []; if (incremental && newLines.length > 0) { // 增量更新:追加新日志并限制总行数 setLogLines(prev => { const combined = [...prev, ...newLines]; return combined.slice(-MAX_DISPLAY_LINES); }); } else if (!incremental) { // 全量加载:只取最后 MAX_DISPLAY_LINES 行 setLogLines(newLines.slice(-MAX_DISPLAY_LINES)); } } catch (err: any) { console.error('Failed to load logs:', err); if (!incremental) { setError(err?.message || t('logs.load_error')); } } finally { if (!incremental) { setLoading(false); } } }; const clearLogs = async () => { if (!window.confirm(t('logs.clear_confirm'))) return; try { await logsApi.clearLogs(); setLogLines([]); latestTimestampRef.current = 0; showNotification(t('logs.clear_success'), 'success'); } catch (err: any) { showNotification(`${t('notification.delete_failed')}: ${err?.message || ''}`, 'error'); } }; const downloadLogs = () => { const text = logLines.join('\n'); const blob = new Blob([text], { type: 'text/plain' }); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'logs.txt'; a.click(); window.URL.revokeObjectURL(url); showNotification(t('logs.download_success'), 'success'); }; const loadErrorLogs = async () => { if (connectionStatus !== 'connected') { setLoadingErrors(false); return; } setLoadingErrors(true); try { const res = await logsApi.fetchErrorLogs(); // API 返回 { files: [...] } const files = (res as any)?.files; const list: ErrorLogItem[] = Array.isArray(files) ? files.map((f: any) => ({ name: f.name, size: f.size, modified: f.modified })) : []; setErrorLogs(list); } catch (err: any) { console.error('Failed to load error logs:', err); // 静默失败,不影响主日志显示 setErrorLogs([]); } finally { setLoadingErrors(false); } }; const downloadErrorLog = async (name: string) => { try { const response = await logsApi.downloadErrorLog(name); const blob = new Blob([response.data], { type: 'text/plain' }); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = name; a.click(); window.URL.revokeObjectURL(url); showNotification(t('logs.error_log_download_success'), 'success'); } catch (err: any) { showNotification(`${t('notification.download_failed')}: ${err?.message || ''}`, 'error'); } }; useEffect(() => { if (connectionStatus === 'connected') { latestTimestampRef.current = 0; loadLogs(false); loadErrorLogs(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [connectionStatus]); useEffect(() => { if (!autoRefresh || connectionStatus !== 'connected') { return; } const id = window.setInterval(() => { loadLogs(true); }, 8000); return () => window.clearInterval(id); // eslint-disable-next-line react-hooks/exhaustive-deps }, [autoRefresh, connectionStatus]); useEffect(() => { if (!pendingScrollToBottomRef.current) return; if (loading) return; if (!logViewerRef.current) return; scrollToBottom(); pendingScrollToBottomRef.current = false; }, [loading, logLines]); const logsText = logLines.join('\n'); return (

{t('logs.title')}

} > {error &&
{error}
} {loading ? (
{t('logs.loading')}
) : logsText ? (
            {logLines.map((line, index) => (
              
                {line}
              
            ))}
          
) : ( )} {t('common.refresh')} } > {errorLogs.length === 0 ? (
{t('logs.error_logs_empty')}
) : (
{errorLogs.map((item) => (
{item.name}
{item.size ? `${(item.size / 1024).toFixed(1)} KB` : ''}{' '} {item.modified ? new Date(item.modified).toLocaleString() : ''}
))}
)}
); }