1.45
This commit is contained in:
parent
9567a43418
commit
8131d4994c
@ -1,18 +1,29 @@
|
||||
/**
|
||||
* Monaco日志查看器组件
|
||||
* 基于Monaco Editor实现的高性能只读日志查看器
|
||||
*
|
||||
* 性能优化:
|
||||
* - 直接操作Monaco Model,避免React状态更新
|
||||
* - 批量追加日志,减少更新频率
|
||||
* - 限制最大行数,防止内存溢出
|
||||
*/
|
||||
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,9 +31,13 @@ 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(() => {
|
||||
autoScrollRef.current = autoScroll;
|
||||
@ -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}
|
||||
|
||||
@ -162,6 +162,9 @@ export interface MonacoLogViewerProps {
|
||||
/** 自动换行 */
|
||||
wordWrap?: boolean;
|
||||
|
||||
/** 最大行数限制(超过后自动删除旧日志,默认10000) */
|
||||
maxLines?: number;
|
||||
|
||||
/** 就绪回调 */
|
||||
onReady?: (api: LogViewerAPI) => void;
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user