1.45
This commit is contained in:
parent
8131d4994c
commit
ee18a6220b
@ -6,16 +6,24 @@
|
|||||||
* - 直接操作Monaco Model,避免React状态更新
|
* - 直接操作Monaco Model,避免React状态更新
|
||||||
* - 批量追加日志,减少更新频率
|
* - 批量追加日志,减少更新频率
|
||||||
* - 限制最大行数,防止内存溢出
|
* - 限制最大行数,防止内存溢出
|
||||||
|
*
|
||||||
|
* 智能滚动:
|
||||||
|
* - 用户在底部时自动跟随新日志
|
||||||
|
* - 用户向上滚动查看时暂停跟随
|
||||||
|
* - 显示新日志数量提示,点击可跳转到底部
|
||||||
*/
|
*/
|
||||||
import { useEffect, useRef, useCallback } from 'react';
|
import { useEffect, useRef, useCallback, useState } 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';
|
||||||
|
import { ChevronDown } from 'lucide-react';
|
||||||
|
|
||||||
// 默认最大行数限制
|
// 默认最大行数限制
|
||||||
const DEFAULT_MAX_LINES = 10000;
|
const DEFAULT_MAX_LINES = 10000;
|
||||||
// 批量更新间隔(ms)
|
// 批量更新间隔(ms)
|
||||||
const BATCH_INTERVAL = 50;
|
const BATCH_INTERVAL = 50;
|
||||||
|
// 底部判定阈值(px)
|
||||||
|
const BOTTOM_THRESHOLD = 150;
|
||||||
|
|
||||||
export const MonacoLogViewer: React.FC<MonacoLogViewerProps> = ({
|
export const MonacoLogViewer: React.FC<MonacoLogViewerProps> = ({
|
||||||
autoScroll = true,
|
autoScroll = true,
|
||||||
@ -36,13 +44,27 @@ export const MonacoLogViewer: React.FC<MonacoLogViewerProps> = ({
|
|||||||
// 批量更新缓冲区
|
// 批量更新缓冲区
|
||||||
const pendingLogsRef = useRef<string[]>([]);
|
const pendingLogsRef = useRef<string[]>([]);
|
||||||
const batchTimerRef = useRef<NodeJS.Timeout | null>(null);
|
const batchTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
const isReadyRef = useRef(false);
|
|
||||||
|
// 智能滚动状态
|
||||||
|
const [isFollowing, setIsFollowing] = useState(true);
|
||||||
|
const [newLogsCount, setNewLogsCount] = useState(0);
|
||||||
|
const isFollowingRef = useRef(true);
|
||||||
|
const newLogsCountRef = useRef(0);
|
||||||
|
|
||||||
// 更新自动滚动状态
|
// 更新自动滚动状态
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
autoScrollRef.current = autoScroll;
|
autoScrollRef.current = autoScroll;
|
||||||
}, [autoScroll]);
|
}, [autoScroll]);
|
||||||
|
|
||||||
|
// 同步 ref
|
||||||
|
useEffect(() => {
|
||||||
|
isFollowingRef.current = isFollowing;
|
||||||
|
}, [isFollowing]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
newLogsCountRef.current = newLogsCount;
|
||||||
|
}, [newLogsCount]);
|
||||||
|
|
||||||
// 更新wordWrap设置
|
// 更新wordWrap设置
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (editorRef.current) {
|
if (editorRef.current) {
|
||||||
@ -67,7 +89,7 @@ export const MonacoLogViewer: React.FC<MonacoLogViewerProps> = ({
|
|||||||
const scrollTop = editorRef.current.getScrollTop();
|
const scrollTop = editorRef.current.getScrollTop();
|
||||||
const scrollHeight = editorRef.current.getScrollHeight();
|
const scrollHeight = editorRef.current.getScrollHeight();
|
||||||
const clientHeight = editorRef.current.getLayoutInfo().height;
|
const clientHeight = editorRef.current.getLayoutInfo().height;
|
||||||
return scrollHeight - scrollTop - clientHeight < 50;
|
return scrollHeight - scrollTop - clientHeight < BOTTOM_THRESHOLD;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 滚动到底部
|
// 滚动到底部
|
||||||
@ -80,18 +102,25 @@ export const MonacoLogViewer: React.FC<MonacoLogViewerProps> = ({
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// 跳转到底部并恢复跟随
|
||||||
|
const jumpToBottom = useCallback(() => {
|
||||||
|
setIsFollowing(true);
|
||||||
|
setNewLogsCount(0);
|
||||||
|
scrollToBottom();
|
||||||
|
}, [scrollToBottom]);
|
||||||
|
|
||||||
// 直接追加文本到Monaco Model(高性能)
|
// 直接追加文本到Monaco Model(高性能)
|
||||||
const appendToModel = useCallback((text: string) => {
|
const appendToModel = useCallback((text: string, logCount: number) => {
|
||||||
if (!editorRef.current) return;
|
if (!editorRef.current) return;
|
||||||
|
|
||||||
const model = editorRef.current.getModel();
|
const model = editorRef.current.getModel();
|
||||||
if (!model) return;
|
if (!model) return;
|
||||||
|
|
||||||
const shouldScroll = autoScrollRef.current && isNearBottom();
|
const shouldScroll = autoScrollRef.current && isFollowingRef.current && isNearBottom();
|
||||||
const lineCount = model.getLineCount();
|
const lineCount = model.getLineCount();
|
||||||
const lastLineLength = model.getLineLength(lineCount);
|
const lastLineLength = model.getLineLength(lineCount);
|
||||||
|
|
||||||
// 使用 applyEdits 直接追加文本,避免重新解析整个文档
|
// 使用 applyEdits 直接追加文本
|
||||||
const prefix = lineCount === 1 && lastLineLength === 0 ? '' : '\n';
|
const prefix = lineCount === 1 && lastLineLength === 0 ? '' : '\n';
|
||||||
model.applyEdits([{
|
model.applyEdits([{
|
||||||
range: {
|
range: {
|
||||||
@ -106,7 +135,7 @@ export const MonacoLogViewer: React.FC<MonacoLogViewerProps> = ({
|
|||||||
// 更新行数
|
// 更新行数
|
||||||
lineCountRef.current = model.getLineCount();
|
lineCountRef.current = model.getLineCount();
|
||||||
|
|
||||||
// 检查是否超过最大行数,删除旧日志
|
// 检查是否超过最大行数
|
||||||
if (maxLines && lineCountRef.current > maxLines) {
|
if (maxLines && lineCountRef.current > maxLines) {
|
||||||
const linesToRemove = lineCountRef.current - maxLines;
|
const linesToRemove = lineCountRef.current - maxLines;
|
||||||
model.applyEdits([{
|
model.applyEdits([{
|
||||||
@ -121,9 +150,12 @@ export const MonacoLogViewer: React.FC<MonacoLogViewerProps> = ({
|
|||||||
lineCountRef.current = model.getLineCount();
|
lineCountRef.current = model.getLineCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 自动滚动到底部
|
// 自动滚动或累计新日志数
|
||||||
if (shouldScroll) {
|
if (shouldScroll) {
|
||||||
requestAnimationFrame(scrollToBottom);
|
requestAnimationFrame(scrollToBottom);
|
||||||
|
} else if (!isFollowingRef.current || !isNearBottom()) {
|
||||||
|
// 不在底部,累计新日志数量
|
||||||
|
setNewLogsCount(prev => prev + logCount);
|
||||||
}
|
}
|
||||||
}, [isNearBottom, scrollToBottom, maxLines]);
|
}, [isNearBottom, scrollToBottom, maxLines]);
|
||||||
|
|
||||||
@ -132,19 +164,18 @@ export const MonacoLogViewer: React.FC<MonacoLogViewerProps> = ({
|
|||||||
if (pendingLogsRef.current.length === 0) return;
|
if (pendingLogsRef.current.length === 0) return;
|
||||||
|
|
||||||
const logs = pendingLogsRef.current;
|
const logs = pendingLogsRef.current;
|
||||||
|
const count = logs.length;
|
||||||
pendingLogsRef.current = [];
|
pendingLogsRef.current = [];
|
||||||
batchTimerRef.current = null;
|
batchTimerRef.current = null;
|
||||||
|
|
||||||
// 合并所有待处理日志
|
|
||||||
const text = logs.join('\n');
|
const text = logs.join('\n');
|
||||||
appendToModel(text);
|
appendToModel(text, count);
|
||||||
}, [appendToModel]);
|
}, [appendToModel]);
|
||||||
|
|
||||||
// 添加日志到缓冲区(批量处理)
|
// 添加日志到缓冲区
|
||||||
const queueLog = useCallback((log: string) => {
|
const queueLog = useCallback((log: string) => {
|
||||||
pendingLogsRef.current.push(log);
|
pendingLogsRef.current.push(log);
|
||||||
|
|
||||||
// 如果没有定时器,启动批量更新
|
|
||||||
if (!batchTimerRef.current) {
|
if (!batchTimerRef.current) {
|
||||||
batchTimerRef.current = setTimeout(flushPendingLogs, BATCH_INTERVAL);
|
batchTimerRef.current = setTimeout(flushPendingLogs, BATCH_INTERVAL);
|
||||||
}
|
}
|
||||||
@ -160,19 +191,20 @@ export const MonacoLogViewer: React.FC<MonacoLogViewerProps> = ({
|
|||||||
lineCountRef.current = 0;
|
lineCountRef.current = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清空缓冲区
|
|
||||||
pendingLogsRef.current = [];
|
pendingLogsRef.current = [];
|
||||||
if (batchTimerRef.current) {
|
if (batchTimerRef.current) {
|
||||||
clearTimeout(batchTimerRef.current);
|
clearTimeout(batchTimerRef.current);
|
||||||
batchTimerRef.current = null;
|
batchTimerRef.current = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setNewLogsCount(0);
|
||||||
|
setIsFollowing(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Monaco Editor挂载完成
|
// Monaco Editor挂载完成
|
||||||
const handleEditorDidMount = useCallback((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({
|
||||||
@ -187,7 +219,6 @@ export const MonacoLogViewer: React.FC<MonacoLogViewerProps> = ({
|
|||||||
lineNumbersMinChars: 6,
|
lineNumbersMinChars: 6,
|
||||||
renderLineHighlight: 'none',
|
renderLineHighlight: 'none',
|
||||||
automaticLayout: true,
|
automaticLayout: true,
|
||||||
// 性能优化选项
|
|
||||||
renderWhitespace: 'none',
|
renderWhitespace: 'none',
|
||||||
renderControlCharacters: false,
|
renderControlCharacters: false,
|
||||||
guides: { indentation: false },
|
guides: { indentation: false },
|
||||||
@ -202,37 +233,41 @@ export const MonacoLogViewer: React.FC<MonacoLogViewerProps> = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 监听滚动事件,智能切换跟随状态
|
||||||
|
editor.onDidScrollChange(() => {
|
||||||
|
const nearBottom = isNearBottom();
|
||||||
|
|
||||||
|
if (nearBottom && !isFollowingRef.current) {
|
||||||
|
// 滚动到底部,恢复跟随
|
||||||
|
setIsFollowing(true);
|
||||||
|
setNewLogsCount(0);
|
||||||
|
} else if (!nearBottom && isFollowingRef.current) {
|
||||||
|
// 离开底部,暂停跟随
|
||||||
|
setIsFollowing(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// 暴露API
|
// 暴露API
|
||||||
if (onReady) {
|
if (onReady) {
|
||||||
const api: LogViewerAPI = {
|
const api: LogViewerAPI = {
|
||||||
appendLog: (log: string) => {
|
appendLog: queueLog,
|
||||||
queueLog(log);
|
appendLogs: (logs: string[]) => logs.forEach(queueLog),
|
||||||
},
|
|
||||||
appendLogs: (logs: string[]) => {
|
|
||||||
logs.forEach(log => queueLog(log));
|
|
||||||
},
|
|
||||||
clear: clearLogs,
|
clear: clearLogs,
|
||||||
scrollToBottom,
|
scrollToBottom: jumpToBottom,
|
||||||
search: (text: string) => {
|
search: (text: string) => {
|
||||||
if (editorRef.current) {
|
editor.trigger('', 'actions.find', { searchString: text });
|
||||||
editorRef.current.trigger('', 'actions.find', { searchString: text });
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
findNext: () => {
|
findNext: () => {
|
||||||
if (editorRef.current) {
|
editor.trigger('', 'editor.action.nextMatchFindAction', null);
|
||||||
editorRef.current.trigger('', 'editor.action.nextMatchFindAction', null);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
findPrevious: () => {
|
findPrevious: () => {
|
||||||
if (editorRef.current) {
|
editor.trigger('', 'editor.action.previousMatchFindAction', null);
|
||||||
editorRef.current.trigger('', 'editor.action.previousMatchFindAction', null);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
getLineCount: () => lineCountRef.current,
|
getLineCount: () => lineCountRef.current,
|
||||||
};
|
};
|
||||||
onReady(api);
|
onReady(api);
|
||||||
}
|
}
|
||||||
}, [wordWrap, onReady, queueLog, clearLogs, scrollToBottom]);
|
}, [wordWrap, onReady, queueLog, clearLogs, jumpToBottom, isNearBottom]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`w-full h-full relative ${className}`} style={{ height, ...style }}>
|
<div className={`w-full h-full relative ${className}`} style={{ height, ...style }}>
|
||||||
@ -270,6 +305,17 @@ export const MonacoLogViewer: React.FC<MonacoLogViewerProps> = ({
|
|||||||
}}
|
}}
|
||||||
onMount={handleEditorDidMount}
|
onMount={handleEditorDidMount}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* 新日志提示按钮 */}
|
||||||
|
{newLogsCount > 0 && (
|
||||||
|
<button
|
||||||
|
onClick={jumpToBottom}
|
||||||
|
className="absolute bottom-4 right-6 flex items-center gap-1.5 px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white text-xs font-medium rounded-full shadow-lg transition-all hover:scale-105 z-10"
|
||||||
|
>
|
||||||
|
<span>{newLogsCount > 99 ? '99+' : newLogsCount} 条新日志</span>
|
||||||
|
<ChevronDown className="h-3.5 w-3.5" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user