This commit is contained in:
dengqichen 2025-12-30 15:34:50 +08:00
parent 9567a43418
commit 8131d4994c
2 changed files with 158 additions and 49 deletions

View File

@ -1,18 +1,29 @@
/**
* Monaco日志查看器组件
* Monaco Editor实现的高性能只读日志查看器
*
*
* - Monaco ModelReact状态更新
* -
* -
*/
import { useEffect, useRef, useState } from 'react';
import { useEffect, useRef, useCallback } from 'react';
import Editor, { Monaco } from '@monaco-editor/react';
import type { editor } from 'monaco-editor';
import type { MonacoLogViewerProps, LogViewerAPI } from './types';
// 默认最大行数限制
const DEFAULT_MAX_LINES = 10000;
// 批量更新间隔(ms)
const BATCH_INTERVAL = 50;
export const MonacoLogViewer: React.FC<MonacoLogViewerProps> = ({
autoScroll = true,
fontSize = 12,
theme = 'vs-dark',
height = '100%',
wordWrap = false,
maxLines = DEFAULT_MAX_LINES,
onReady,
className = '',
style = {},
@ -20,8 +31,12 @@ export const MonacoLogViewer: React.FC<MonacoLogViewerProps> = ({
const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null);
const monacoRef = useRef<Monaco | null>(null);
const autoScrollRef = useRef(autoScroll);
const [content, setContent] = useState('');
const lineCountRef = useRef(0);
// 批量更新缓冲区
const pendingLogsRef = useRef<string[]>([]);
const batchTimerRef = useRef<NodeJS.Timeout | null>(null);
const isReadyRef = useRef(false);
// 更新自动滚动状态
useEffect(() => {
@ -37,17 +52,134 @@ export const MonacoLogViewer: React.FC<MonacoLogViewerProps> = ({
}
}, [wordWrap]);
// 清理定时器
useEffect(() => {
return () => {
if (batchTimerRef.current) {
clearTimeout(batchTimerRef.current);
}
};
}, []);
// 检查是否在底部附近
const isNearBottom = useCallback(() => {
if (!editorRef.current) return true;
const scrollTop = editorRef.current.getScrollTop();
const scrollHeight = editorRef.current.getScrollHeight();
const clientHeight = editorRef.current.getLayoutInfo().height;
return scrollHeight - scrollTop - clientHeight < 50;
}, []);
// 滚动到底部
const scrollToBottom = useCallback(() => {
if (!editorRef.current) return;
const model = editorRef.current.getModel();
if (model) {
const lineCount = model.getLineCount();
editorRef.current.revealLine(lineCount);
}
}, []);
// 直接追加文本到Monaco Model高性能
const appendToModel = useCallback((text: string) => {
if (!editorRef.current) return;
const model = editorRef.current.getModel();
if (!model) return;
const shouldScroll = autoScrollRef.current && isNearBottom();
const lineCount = model.getLineCount();
const lastLineLength = model.getLineLength(lineCount);
// 使用 applyEdits 直接追加文本,避免重新解析整个文档
const prefix = lineCount === 1 && lastLineLength === 0 ? '' : '\n';
model.applyEdits([{
range: {
startLineNumber: lineCount,
startColumn: lastLineLength + 1,
endLineNumber: lineCount,
endColumn: lastLineLength + 1,
},
text: prefix + text,
}]);
// 更新行数
lineCountRef.current = model.getLineCount();
// 检查是否超过最大行数,删除旧日志
if (maxLines && lineCountRef.current > maxLines) {
const linesToRemove = lineCountRef.current - maxLines;
model.applyEdits([{
range: {
startLineNumber: 1,
startColumn: 1,
endLineNumber: linesToRemove + 1,
endColumn: 1,
},
text: '',
}]);
lineCountRef.current = model.getLineCount();
}
// 自动滚动到底部
if (shouldScroll) {
requestAnimationFrame(scrollToBottom);
}
}, [isNearBottom, scrollToBottom, maxLines]);
// 批量刷新日志
const flushPendingLogs = useCallback(() => {
if (pendingLogsRef.current.length === 0) return;
const logs = pendingLogsRef.current;
pendingLogsRef.current = [];
batchTimerRef.current = null;
// 合并所有待处理日志
const text = logs.join('\n');
appendToModel(text);
}, [appendToModel]);
// 添加日志到缓冲区(批量处理)
const queueLog = useCallback((log: string) => {
pendingLogsRef.current.push(log);
// 如果没有定时器,启动批量更新
if (!batchTimerRef.current) {
batchTimerRef.current = setTimeout(flushPendingLogs, BATCH_INTERVAL);
}
}, [flushPendingLogs]);
// 清空日志
const clearLogs = useCallback(() => {
if (!editorRef.current) return;
const model = editorRef.current.getModel();
if (model) {
model.setValue('');
lineCountRef.current = 0;
}
// 清空缓冲区
pendingLogsRef.current = [];
if (batchTimerRef.current) {
clearTimeout(batchTimerRef.current);
batchTimerRef.current = null;
}
}, []);
// Monaco Editor挂载完成
const handleEditorDidMount = (editor: editor.IStandaloneCodeEditor, monaco: Monaco) => {
const handleEditorDidMount = useCallback((editor: editor.IStandaloneCodeEditor, monaco: Monaco) => {
editorRef.current = editor;
monacoRef.current = monaco;
isReadyRef.current = true;
// 配置编辑器选项
editor.updateOptions({
readOnly: true,
minimap: { enabled: false },
scrollBeyondLastLine: false,
wordWrap: 'off',
wordWrap: wordWrap ? 'on' : 'off',
lineNumbers: 'on',
glyphMargin: false,
folding: false,
@ -55,64 +187,32 @@ export const MonacoLogViewer: React.FC<MonacoLogViewerProps> = ({
lineNumbersMinChars: 6,
renderLineHighlight: 'none',
automaticLayout: true,
// 性能优化选项
renderWhitespace: 'none',
renderControlCharacters: false,
guides: { indentation: false },
occurrencesHighlight: 'off',
selectionHighlight: false,
scrollbar: {
vertical: 'visible',
horizontal: 'visible',
verticalScrollbarSize: 10,
horizontalScrollbarSize: 10,
useShadows: false,
},
});
// 检查是否在底部附近
const isNearBottom = () => {
if (!editorRef.current) return true;
const scrollTop = editorRef.current.getScrollTop();
const scrollHeight = editorRef.current.getScrollHeight();
const clientHeight = editorRef.current.getLayoutInfo().height;
return scrollHeight - scrollTop - clientHeight < 50;
};
// 暴露API
if (onReady) {
const api: LogViewerAPI = {
appendLog: (log: string) => {
lineCountRef.current++;
const shouldScroll = autoScrollRef.current && isNearBottom();
setContent((prev) => {
const newContent = prev ? `${prev}\n${log}` : log;
// 只有在底部时才自动滚动
if (shouldScroll) {
setTimeout(() => {
const lineCount = editorRef.current?.getModel()?.getLineCount() || 0;
editorRef.current?.revealLine(lineCount);
}, 0);
}
return newContent;
});
queueLog(log);
},
appendLogs: (logs: string[]) => {
lineCountRef.current += logs.length;
const shouldScroll = autoScrollRef.current && isNearBottom();
setContent((prev) => {
const newContent = prev ? `${prev}\n${logs.join('\n')}` : logs.join('\n');
// 只有在底部时才自动滚动
if (shouldScroll) {
setTimeout(() => {
const lineCount = editorRef.current?.getModel()?.getLineCount() || 0;
editorRef.current?.revealLine(lineCount);
}, 0);
}
return newContent;
});
},
clear: () => {
setContent('');
lineCountRef.current = 0;
},
scrollToBottom: () => {
const lineCount = editorRef.current?.getModel()?.getLineCount() || 0;
editorRef.current?.revealLine(lineCount);
logs.forEach(log => queueLog(log));
},
clear: clearLogs,
scrollToBottom,
search: (text: string) => {
if (editorRef.current) {
editorRef.current.trigger('', 'actions.find', { searchString: text });
@ -132,14 +232,14 @@ export const MonacoLogViewer: React.FC<MonacoLogViewerProps> = ({
};
onReady(api);
}
};
}, [wordWrap, onReady, queueLog, clearLogs, scrollToBottom]);
return (
<div className={`w-full h-full relative ${className}`} style={{ height, ...style }}>
<Editor
height="100%"
defaultLanguage="plaintext"
value={content}
defaultValue=""
theme={theme}
options={{
fontSize,
@ -155,11 +255,17 @@ export const MonacoLogViewer: React.FC<MonacoLogViewerProps> = ({
lineNumbersMinChars: 6,
renderLineHighlight: 'none',
automaticLayout: true,
renderWhitespace: 'none',
renderControlCharacters: false,
guides: { indentation: false },
occurrencesHighlight: 'off',
selectionHighlight: false,
scrollbar: {
vertical: 'visible',
horizontal: 'visible',
verticalScrollbarSize: 10,
horizontalScrollbarSize: 10,
useShadows: false,
},
}}
onMount={handleEditorDidMount}

View File

@ -162,6 +162,9 @@ export interface MonacoLogViewerProps {
/** 自动换行 */
wordWrap?: boolean;
/** 最大行数限制超过后自动删除旧日志默认10000 */
maxLines?: number;
/** 就绪回调 */
onReady?: (api: LogViewerAPI) => void;