import { useState, useEffect, useRef, useMemo, memo } from 'react';
import { LogService, LogEntry } from '@esengine/editor-core';
import { LogLevel } from '@esengine/ecs-framework';
import { Trash2, AlertCircle, Info, AlertTriangle, XCircle, Bug, Search, Maximize2, ChevronRight, ChevronDown, Wifi } from 'lucide-react';
import { JsonViewer } from './JsonViewer';
import '../styles/ConsolePanel.css';
interface ConsolePanelProps {
logService: LogService;
}
interface ParsedLogData {
isJSON: boolean;
jsonStr?: string;
extracted?: { prefix: string; json: string; suffix: string } | null;
}
const LogEntryItem = memo(({
log,
isExpanded,
onToggleExpand,
onOpenJsonViewer,
parsedData
}: {
log: LogEntry;
isExpanded: boolean;
onToggleExpand: (id: number) => void;
onOpenJsonViewer: (jsonStr: string) => void;
parsedData: ParsedLogData;
}) => {
const getLevelIcon = (level: LogLevel) => {
switch (level) {
case LogLevel.Debug:
return ;
case LogLevel.Info:
return ;
case LogLevel.Warn:
return ;
case LogLevel.Error:
case LogLevel.Fatal:
return ;
default:
return ;
}
};
const getLevelClass = (level: LogLevel): string => {
switch (level) {
case LogLevel.Debug:
return 'log-entry-debug';
case LogLevel.Info:
return 'log-entry-info';
case LogLevel.Warn:
return 'log-entry-warn';
case LogLevel.Error:
case LogLevel.Fatal:
return 'log-entry-error';
default:
return '';
}
};
const formatTime = (date: Date): string => {
const hours = date.getHours().toString().padStart(2, '0');
const minutes = date.getMinutes().toString().padStart(2, '0');
const seconds = date.getSeconds().toString().padStart(2, '0');
const ms = date.getMilliseconds().toString().padStart(3, '0');
return `${hours}:${minutes}:${seconds}.${ms}`;
};
const formatMessage = (message: string, isExpanded: boolean, parsedData: ParsedLogData): JSX.Element => {
const MAX_PREVIEW_LENGTH = 200;
const { isJSON, jsonStr, extracted } = parsedData;
const shouldTruncate = message.length > MAX_PREVIEW_LENGTH && !isExpanded;
return (
{shouldTruncate ? (
<>
{extracted && extracted.prefix && {extracted.prefix} }
{message.substring(0, MAX_PREVIEW_LENGTH)}...
>
) : (
{message}
)}
{isJSON && jsonStr && (
)}
);
};
const shouldShowExpander = log.message.length > 200;
return (
{shouldShowExpander && (
onToggleExpand(log.id)}
>
{isExpanded ? : }
)}
{getLevelIcon(log.level)}
{formatTime(log.timestamp)}
[{log.source === 'remote' ? '🌐 Remote' : log.source}]
{log.clientId && (
{log.clientId}
)}
{formatMessage(log.message, isExpanded, parsedData)}
);
});
LogEntryItem.displayName = 'LogEntryItem';
const MAX_LOGS = 1000;
export function ConsolePanel({ logService }: ConsolePanelProps) {
const [logs, setLogs] = useState([]);
const [filter, setFilter] = useState('');
const [levelFilter, setLevelFilter] = useState>(new Set([
LogLevel.Debug,
LogLevel.Info,
LogLevel.Warn,
LogLevel.Error,
LogLevel.Fatal
]));
const [showRemoteOnly, setShowRemoteOnly] = useState(false);
const [autoScroll, setAutoScroll] = useState(true);
const [expandedLogs, setExpandedLogs] = useState>(new Set());
const [jsonViewerData, setJsonViewerData] = useState(null);
const logContainerRef = useRef(null);
useEffect(() => {
setLogs(logService.getLogs().slice(-MAX_LOGS));
const unsubscribe = logService.subscribe((entry) => {
setLogs((prev) => {
const newLogs = [...prev, entry];
if (newLogs.length > MAX_LOGS) {
return newLogs.slice(-MAX_LOGS);
}
return newLogs;
});
});
return unsubscribe;
}, [logService]);
useEffect(() => {
if (autoScroll && logContainerRef.current) {
logContainerRef.current.scrollTop = logContainerRef.current.scrollHeight;
}
}, [logs, autoScroll]);
const handleClear = () => {
logService.clear();
setLogs([]);
};
const handleScroll = () => {
if (logContainerRef.current) {
const { scrollTop, scrollHeight, clientHeight } = logContainerRef.current;
const isAtBottom = Math.abs(scrollHeight - scrollTop - clientHeight) < 10;
setAutoScroll(isAtBottom);
}
};
const toggleLevelFilter = (level: LogLevel) => {
const newFilter = new Set(levelFilter);
if (newFilter.has(level)) {
newFilter.delete(level);
} else {
newFilter.add(level);
}
setLevelFilter(newFilter);
};
// 使用ref保存缓存,避免每次都重新计算
const parsedLogsCacheRef = useRef