feat(logs): optimize log loading with auto-prepend functionality

This commit is contained in:
hkfires
2026-02-06 12:09:25 +08:00
parent e568e4a2b5
commit 26fa1ea98e

View File

@@ -1,4 +1,4 @@
import { useDeferredValue, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { useCallback, useDeferredValue, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import type { PointerEvent as ReactPointerEvent } from 'react'; import type { PointerEvent as ReactPointerEvent } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Card } from '@/components/ui/Card'; import { Card } from '@/components/ui/Card';
@@ -643,6 +643,29 @@ export function LogsPage() {
const canLoadMore = !isSearching && logState.visibleFrom > 0; const canLoadMore = !isSearching && logState.visibleFrom > 0;
const prependVisibleLines = useCallback(() => {
const node = logViewerRef.current;
if (!node) return;
if (pendingPrependScrollRef.current) return;
if (isSearching) return;
setLogState((prev) => {
if (prev.visibleFrom <= 0) {
return prev;
}
pendingPrependScrollRef.current = {
scrollHeight: node.scrollHeight,
scrollTop: node.scrollTop,
};
return {
...prev,
visibleFrom: Math.max(prev.visibleFrom - LOAD_MORE_LINES, 0),
};
});
}, [isSearching]);
const handleLogScroll = () => { const handleLogScroll = () => {
const node = logViewerRef.current; const node = logViewerRef.current;
if (!node) return; if (!node) return;
@@ -651,14 +674,7 @@ export function LogsPage() {
if (pendingPrependScrollRef.current) return; if (pendingPrependScrollRef.current) return;
if (node.scrollTop > LOAD_MORE_THRESHOLD_PX) return; if (node.scrollTop > LOAD_MORE_THRESHOLD_PX) return;
pendingPrependScrollRef.current = { prependVisibleLines();
scrollHeight: node.scrollHeight,
scrollTop: node.scrollTop,
};
setLogState((prev) => ({
...prev,
visibleFrom: Math.max(prev.visibleFrom - LOAD_MORE_LINES, 0),
}));
}; };
useLayoutEffect(() => { useLayoutEffect(() => {
@@ -671,6 +687,53 @@ export function LogsPage() {
pendingPrependScrollRef.current = null; pendingPrependScrollRef.current = null;
}, [logState.visibleFrom]); }, [logState.visibleFrom]);
const tryAutoLoadMoreUntilScrollable = useCallback(() => {
const node = logViewerRef.current;
if (!node) return;
if (!canLoadMore) return;
if (isSearching) return;
if (pendingPrependScrollRef.current) return;
const hasVerticalOverflow = node.scrollHeight > node.clientHeight + 1;
if (hasVerticalOverflow) return;
prependVisibleLines();
}, [canLoadMore, isSearching, prependVisibleLines]);
useEffect(() => {
if (loading) return;
if (activeTab !== 'logs') return;
const raf = window.requestAnimationFrame(() => {
tryAutoLoadMoreUntilScrollable();
});
return () => {
window.cancelAnimationFrame(raf);
};
}, [
activeTab,
loading,
tryAutoLoadMoreUntilScrollable,
filteredLines.length,
showRawLogs,
logState.visibleFrom,
]);
useEffect(() => {
if (activeTab !== 'logs') return;
const onResize = () => {
window.requestAnimationFrame(() => {
tryAutoLoadMoreUntilScrollable();
});
};
window.addEventListener('resize', onResize);
return () => {
window.removeEventListener('resize', onResize);
};
}, [activeTab, tryAutoLoadMoreUntilScrollable]);
const copyLogLine = async (raw: string) => { const copyLogLine = async (raw: string) => {
const ok = await copyToClipboard(raw); const ok = await copyToClipboard(raw);
if (ok) { if (ok) {