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状态更新 * - Monaco ModelReact状态更新
* - * -
* - * -
*
*
* -
* -
* -
*/ */
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>
); );
}; };