1.45
This commit is contained in:
parent
8131d4994c
commit
ee18a6220b
@ -6,16 +6,24 @@
|
||||
* - 直接操作Monaco Model,避免React状态更新
|
||||
* - 批量追加日志,减少更新频率
|
||||
* - 限制最大行数,防止内存溢出
|
||||
*
|
||||
* 智能滚动:
|
||||
* - 用户在底部时自动跟随新日志
|
||||
* - 用户向上滚动查看时暂停跟随
|
||||
* - 显示新日志数量提示,点击可跳转到底部
|
||||
*/
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user