1.45
This commit is contained in:
parent
9567a43418
commit
8131d4994c
@ -1,18 +1,29 @@
|
|||||||
/**
|
/**
|
||||||
* Monaco日志查看器组件
|
* Monaco日志查看器组件
|
||||||
* 基于Monaco Editor实现的高性能只读日志查看器
|
* 基于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 Editor, { Monaco } from '@monaco-editor/react';
|
||||||
import type { editor } from 'monaco-editor';
|
import type { editor } from 'monaco-editor';
|
||||||
import type { MonacoLogViewerProps, LogViewerAPI } from './types';
|
import type { MonacoLogViewerProps, LogViewerAPI } from './types';
|
||||||
|
|
||||||
|
// 默认最大行数限制
|
||||||
|
const DEFAULT_MAX_LINES = 10000;
|
||||||
|
// 批量更新间隔(ms)
|
||||||
|
const BATCH_INTERVAL = 50;
|
||||||
|
|
||||||
export const MonacoLogViewer: React.FC<MonacoLogViewerProps> = ({
|
export const MonacoLogViewer: React.FC<MonacoLogViewerProps> = ({
|
||||||
autoScroll = true,
|
autoScroll = true,
|
||||||
fontSize = 12,
|
fontSize = 12,
|
||||||
theme = 'vs-dark',
|
theme = 'vs-dark',
|
||||||
height = '100%',
|
height = '100%',
|
||||||
wordWrap = false,
|
wordWrap = false,
|
||||||
|
maxLines = DEFAULT_MAX_LINES,
|
||||||
onReady,
|
onReady,
|
||||||
className = '',
|
className = '',
|
||||||
style = {},
|
style = {},
|
||||||
@ -20,8 +31,12 @@ export const MonacoLogViewer: React.FC<MonacoLogViewerProps> = ({
|
|||||||
const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null);
|
const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null);
|
||||||
const monacoRef = useRef<Monaco | null>(null);
|
const monacoRef = useRef<Monaco | null>(null);
|
||||||
const autoScrollRef = useRef(autoScroll);
|
const autoScrollRef = useRef(autoScroll);
|
||||||
const [content, setContent] = useState('');
|
|
||||||
const lineCountRef = useRef(0);
|
const lineCountRef = useRef(0);
|
||||||
|
|
||||||
|
// 批量更新缓冲区
|
||||||
|
const pendingLogsRef = useRef<string[]>([]);
|
||||||
|
const batchTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
const isReadyRef = useRef(false);
|
||||||
|
|
||||||
// 更新自动滚动状态
|
// 更新自动滚动状态
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -37,17 +52,134 @@ export const MonacoLogViewer: React.FC<MonacoLogViewerProps> = ({
|
|||||||
}
|
}
|
||||||
}, [wordWrap]);
|
}, [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挂载完成
|
// Monaco Editor挂载完成
|
||||||
const handleEditorDidMount = (editor: editor.IStandaloneCodeEditor, monaco: Monaco) => {
|
const handleEditorDidMount = useCallback((editor: editor.IStandaloneCodeEditor, monaco: Monaco) => {
|
||||||
editorRef.current = editor;
|
editorRef.current = editor;
|
||||||
monacoRef.current = monaco;
|
monacoRef.current = monaco;
|
||||||
|
isReadyRef.current = true;
|
||||||
|
|
||||||
// 配置编辑器选项
|
// 配置编辑器选项
|
||||||
editor.updateOptions({
|
editor.updateOptions({
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
minimap: { enabled: false },
|
minimap: { enabled: false },
|
||||||
scrollBeyondLastLine: false,
|
scrollBeyondLastLine: false,
|
||||||
wordWrap: 'off',
|
wordWrap: wordWrap ? 'on' : 'off',
|
||||||
lineNumbers: 'on',
|
lineNumbers: 'on',
|
||||||
glyphMargin: false,
|
glyphMargin: false,
|
||||||
folding: false,
|
folding: false,
|
||||||
@ -55,64 +187,32 @@ export const MonacoLogViewer: React.FC<MonacoLogViewerProps> = ({
|
|||||||
lineNumbersMinChars: 6,
|
lineNumbersMinChars: 6,
|
||||||
renderLineHighlight: 'none',
|
renderLineHighlight: 'none',
|
||||||
automaticLayout: true,
|
automaticLayout: true,
|
||||||
|
// 性能优化选项
|
||||||
|
renderWhitespace: 'none',
|
||||||
|
renderControlCharacters: false,
|
||||||
|
guides: { indentation: false },
|
||||||
|
occurrencesHighlight: 'off',
|
||||||
|
selectionHighlight: false,
|
||||||
scrollbar: {
|
scrollbar: {
|
||||||
vertical: 'visible',
|
vertical: 'visible',
|
||||||
horizontal: 'visible',
|
horizontal: 'visible',
|
||||||
verticalScrollbarSize: 10,
|
verticalScrollbarSize: 10,
|
||||||
horizontalScrollbarSize: 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
|
// 暴露API
|
||||||
if (onReady) {
|
if (onReady) {
|
||||||
const api: LogViewerAPI = {
|
const api: LogViewerAPI = {
|
||||||
appendLog: (log: string) => {
|
appendLog: (log: string) => {
|
||||||
lineCountRef.current++;
|
queueLog(log);
|
||||||
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;
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
appendLogs: (logs: string[]) => {
|
appendLogs: (logs: string[]) => {
|
||||||
lineCountRef.current += logs.length;
|
logs.forEach(log => queueLog(log));
|
||||||
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);
|
|
||||||
},
|
},
|
||||||
|
clear: clearLogs,
|
||||||
|
scrollToBottom,
|
||||||
search: (text: string) => {
|
search: (text: string) => {
|
||||||
if (editorRef.current) {
|
if (editorRef.current) {
|
||||||
editorRef.current.trigger('', 'actions.find', { searchString: text });
|
editorRef.current.trigger('', 'actions.find', { searchString: text });
|
||||||
@ -132,14 +232,14 @@ export const MonacoLogViewer: React.FC<MonacoLogViewerProps> = ({
|
|||||||
};
|
};
|
||||||
onReady(api);
|
onReady(api);
|
||||||
}
|
}
|
||||||
};
|
}, [wordWrap, onReady, queueLog, clearLogs, scrollToBottom]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`w-full h-full relative ${className}`} style={{ height, ...style }}>
|
<div className={`w-full h-full relative ${className}`} style={{ height, ...style }}>
|
||||||
<Editor
|
<Editor
|
||||||
height="100%"
|
height="100%"
|
||||||
defaultLanguage="plaintext"
|
defaultLanguage="plaintext"
|
||||||
value={content}
|
defaultValue=""
|
||||||
theme={theme}
|
theme={theme}
|
||||||
options={{
|
options={{
|
||||||
fontSize,
|
fontSize,
|
||||||
@ -155,11 +255,17 @@ export const MonacoLogViewer: React.FC<MonacoLogViewerProps> = ({
|
|||||||
lineNumbersMinChars: 6,
|
lineNumbersMinChars: 6,
|
||||||
renderLineHighlight: 'none',
|
renderLineHighlight: 'none',
|
||||||
automaticLayout: true,
|
automaticLayout: true,
|
||||||
|
renderWhitespace: 'none',
|
||||||
|
renderControlCharacters: false,
|
||||||
|
guides: { indentation: false },
|
||||||
|
occurrencesHighlight: 'off',
|
||||||
|
selectionHighlight: false,
|
||||||
scrollbar: {
|
scrollbar: {
|
||||||
vertical: 'visible',
|
vertical: 'visible',
|
||||||
horizontal: 'visible',
|
horizontal: 'visible',
|
||||||
verticalScrollbarSize: 10,
|
verticalScrollbarSize: 10,
|
||||||
horizontalScrollbarSize: 10,
|
horizontalScrollbarSize: 10,
|
||||||
|
useShadows: false,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
onMount={handleEditorDidMount}
|
onMount={handleEditorDidMount}
|
||||||
|
|||||||
@ -162,6 +162,9 @@ export interface MonacoLogViewerProps {
|
|||||||
/** 自动换行 */
|
/** 自动换行 */
|
||||||
wordWrap?: boolean;
|
wordWrap?: boolean;
|
||||||
|
|
||||||
|
/** 最大行数限制(超过后自动删除旧日志,默认10000) */
|
||||||
|
maxLines?: number;
|
||||||
|
|
||||||
/** 就绪回调 */
|
/** 就绪回调 */
|
||||||
onReady?: (api: LogViewerAPI) => void;
|
onReady?: (api: LogViewerAPI) => void;
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user