This commit is contained in:
dengqichen 2025-12-30 15:37:08 +08:00
parent 8131d4994c
commit ee18a6220b

View File

@ -6,16 +6,24 @@
* - Monaco ModelReact状态更新
* -
* -
*
*
* -
* -
* -
*/
import { useEffect, useRef, useCallback } from 'react';
import { useEffect, useRef, useCallback, useState } from 'react';
import Editor, { Monaco } from '@monaco-editor/react';
import type { editor } from 'monaco-editor';
import type { MonacoLogViewerProps, LogViewerAPI } from './types';
import { ChevronDown } from 'lucide-react';
// 默认最大行数限制
const DEFAULT_MAX_LINES = 10000;
// 批量更新间隔(ms)
const BATCH_INTERVAL = 50;
// 底部判定阈值(px)
const BOTTOM_THRESHOLD = 150;
export const MonacoLogViewer: React.FC<MonacoLogViewerProps> = ({
autoScroll = true,
@ -36,13 +44,27 @@ export const MonacoLogViewer: React.FC<MonacoLogViewerProps> = ({
// 批量更新缓冲区
const pendingLogsRef = useRef<string[]>([]);
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(() => {
autoScrollRef.current = autoScroll;
}, [autoScroll]);
// 同步 ref
useEffect(() => {
isFollowingRef.current = isFollowing;
}, [isFollowing]);
useEffect(() => {
newLogsCountRef.current = newLogsCount;
}, [newLogsCount]);
// 更新wordWrap设置
useEffect(() => {
if (editorRef.current) {
@ -67,7 +89,7 @@ export const MonacoLogViewer: React.FC<MonacoLogViewerProps> = ({
const scrollTop = editorRef.current.getScrollTop();
const scrollHeight = editorRef.current.getScrollHeight();
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高性能
const appendToModel = useCallback((text: string) => {
const appendToModel = useCallback((text: string, logCount: number) => {
if (!editorRef.current) return;
const model = editorRef.current.getModel();
if (!model) return;
const shouldScroll = autoScrollRef.current && isNearBottom();
const shouldScroll = autoScrollRef.current && isFollowingRef.current && isNearBottom();
const lineCount = model.getLineCount();
const lastLineLength = model.getLineLength(lineCount);
// 使用 applyEdits 直接追加文本,避免重新解析整个文档
// 使用 applyEdits 直接追加文本
const prefix = lineCount === 1 && lastLineLength === 0 ? '' : '\n';
model.applyEdits([{
range: {
@ -106,7 +135,7 @@ export const MonacoLogViewer: React.FC<MonacoLogViewerProps> = ({
// 更新行数
lineCountRef.current = model.getLineCount();
// 检查是否超过最大行数,删除旧日志
// 检查是否超过最大行数
if (maxLines && lineCountRef.current > maxLines) {
const linesToRemove = lineCountRef.current - maxLines;
model.applyEdits([{
@ -121,9 +150,12 @@ export const MonacoLogViewer: React.FC<MonacoLogViewerProps> = ({
lineCountRef.current = model.getLineCount();
}
// 自动滚动到底部
// 自动滚动或累计新日志数
if (shouldScroll) {
requestAnimationFrame(scrollToBottom);
} else if (!isFollowingRef.current || !isNearBottom()) {
// 不在底部,累计新日志数量
setNewLogsCount(prev => prev + logCount);
}
}, [isNearBottom, scrollToBottom, maxLines]);
@ -132,19 +164,18 @@ export const MonacoLogViewer: React.FC<MonacoLogViewerProps> = ({
if (pendingLogsRef.current.length === 0) return;
const logs = pendingLogsRef.current;
const count = logs.length;
pendingLogsRef.current = [];
batchTimerRef.current = null;
// 合并所有待处理日志
const text = logs.join('\n');
appendToModel(text);
appendToModel(text, count);
}, [appendToModel]);
// 添加日志到缓冲区(批量处理)
// 添加日志到缓冲区
const queueLog = useCallback((log: string) => {
pendingLogsRef.current.push(log);
// 如果没有定时器,启动批量更新
if (!batchTimerRef.current) {
batchTimerRef.current = setTimeout(flushPendingLogs, BATCH_INTERVAL);
}
@ -160,19 +191,20 @@ export const MonacoLogViewer: React.FC<MonacoLogViewerProps> = ({
lineCountRef.current = 0;
}
// 清空缓冲区
pendingLogsRef.current = [];
if (batchTimerRef.current) {
clearTimeout(batchTimerRef.current);
batchTimerRef.current = null;
}
setNewLogsCount(0);
setIsFollowing(true);
}, []);
// Monaco Editor挂载完成
const handleEditorDidMount = useCallback((editor: editor.IStandaloneCodeEditor, monaco: Monaco) => {
editorRef.current = editor;
monacoRef.current = monaco;
isReadyRef.current = true;
// 配置编辑器选项
editor.updateOptions({
@ -187,7 +219,6 @@ export const MonacoLogViewer: React.FC<MonacoLogViewerProps> = ({
lineNumbersMinChars: 6,
renderLineHighlight: 'none',
automaticLayout: true,
// 性能优化选项
renderWhitespace: 'none',
renderControlCharacters: 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
if (onReady) {
const api: LogViewerAPI = {
appendLog: (log: string) => {
queueLog(log);
},
appendLogs: (logs: string[]) => {
logs.forEach(log => queueLog(log));
},
appendLog: queueLog,
appendLogs: (logs: string[]) => logs.forEach(queueLog),
clear: clearLogs,
scrollToBottom,
scrollToBottom: jumpToBottom,
search: (text: string) => {
if (editorRef.current) {
editorRef.current.trigger('', 'actions.find', { searchString: text });
}
editor.trigger('', 'actions.find', { searchString: text });
},
findNext: () => {
if (editorRef.current) {
editorRef.current.trigger('', 'editor.action.nextMatchFindAction', null);
}
editor.trigger('', 'editor.action.nextMatchFindAction', null);
},
findPrevious: () => {
if (editorRef.current) {
editorRef.current.trigger('', 'editor.action.previousMatchFindAction', null);
}
editor.trigger('', 'editor.action.previousMatchFindAction', null);
},
getLineCount: () => lineCountRef.current,
};
onReady(api);
}
}, [wordWrap, onReady, queueLog, clearLogs, scrollToBottom]);
}, [wordWrap, onReady, queueLog, clearLogs, jumpToBottom, isNearBottom]);
return (
<div className={`w-full h-full relative ${className}`} style={{ height, ...style }}>
@ -270,6 +305,17 @@ export const MonacoLogViewer: React.FC<MonacoLogViewerProps> = ({
}}
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>
);
};