From ffdc53c6299ea5034bb1ea3fa58296efe17662be Mon Sep 17 00:00:00 2001 From: lzt404 <2596933790@qq.com> Date: Tue, 6 Oct 2026 00:36:10 +0800 Subject: [PATCH] perf(usage): keep request pages responsive while paging and resizing Keep the current event rows visible while the next page loads, and refresh filter options only when the time range changes. Resize columns directly in the DOM and commit the width when the drag ends. --- src/pages/UsageEventsView.tsx | 87 +++++++++++++++++++++------------- src/pages/UsageRecordsPage.tsx | 41 +++++++++++----- src/styles/usage-subpages.css | 7 ++- 3 files changed, 89 insertions(+), 46 deletions(-) diff --git a/src/pages/UsageEventsView.tsx b/src/pages/UsageEventsView.tsx index 60a7dbc..a03a4e1 100644 --- a/src/pages/UsageEventsView.tsx +++ b/src/pages/UsageEventsView.tsx @@ -487,6 +487,7 @@ export function EventsView({ }) { const { t } = useI18n(); const [widths, setWidths] = useState>(getInitialColumnWidths); + const widthsRef = useRef(widths); const [visibleColumnKeys, setVisibleColumnKeys] = useState(getInitialVisibleColumns); const [columnSettingsOpen, setColumnSettingsOpen] = useState(false); const [draftVisibleColumnKeys, setDraftVisibleColumnKeys] = useState(visibleColumnKeys); @@ -508,12 +509,17 @@ export function EventsView({ const isCustomized = EVENT_COLUMNS.some((col) => widths[col.key] !== col.defaultWidth); const noRemarkLabel = t('usage.key.noRemark'); + const commitWidths = (next: Record) => { + widthsRef.current = next; + setWidths(next); + }; + const resetAllWidths = () => { const defaults: Record = {} as any; for (const col of EVENT_COLUMNS) { defaults[col.key] = col.defaultWidth; } - setWidths(defaults); + commitWidths(defaults); try { localStorage.setItem(EVENT_COL_WIDTHS_STORAGE_KEY, JSON.stringify(defaults)); } catch {} @@ -554,23 +560,19 @@ export function EventsView({ e.stopPropagation(); const colDef = EVENT_COLUMNS.find((c) => c.key === key); if (!colDef) return; - setWidths((prev) => { - const next = { ...prev, [key]: colDef.defaultWidth }; - try { - localStorage.setItem(EVENT_COL_WIDTHS_STORAGE_KEY, JSON.stringify(next)); - } catch {} - return next; - }); + const next = { ...widthsRef.current, [key]: colDef.defaultWidth }; + commitWidths(next); + try { + localStorage.setItem(EVENT_COL_WIDTHS_STORAGE_KEY, JSON.stringify(next)); + } catch {} }; const persistColumnWidth = (key: EventColumnKey, width: number) => { - setWidths((current) => { - const next = { ...current, [key]: width }; - try { - localStorage.setItem(EVENT_COL_WIDTHS_STORAGE_KEY, JSON.stringify(next)); - } catch {} - return next; - }); + const next = { ...widthsRef.current, [key]: width }; + commitWidths(next); + try { + localStorage.setItem(EVENT_COL_WIDTHS_STORAGE_KEY, JSON.stringify(next)); + } catch {} }; const handleResizeKeyDown = (key: EventColumnKey, event: KeyboardEvent) => { @@ -597,41 +599,60 @@ export function EventsView({ resizeCleanupRef.current?.(); const startX = e.clientX; const startWidth = - widths[key] ?? EVENT_COLUMNS.find((c) => c.key === key)?.defaultWidth ?? 100; + widthsRef.current[key] ?? EVENT_COLUMNS.find((c) => c.key === key)?.defaultWidth ?? 100; const colDef = EVENT_COLUMNS.find((c) => c.key === key); const minWidth = colDef?.minWidth ?? 50; + const table = e.currentTarget.closest('table'); + const column = table?.querySelector(`col[data-column="${key}"]`) ?? null; + const header = e.currentTarget.closest('th'); setResizingCol(key); document.body.classList.add('table-col-resizing'); let currentWidth = startWidth; + let frame = 0; + + const paintWidth = (nextWidth: number) => { + widthsRef.current = { ...widthsRef.current, [key]: nextWidth }; + if (column) column.style.width = `${nextWidth}px`; + if (header) header.style.width = `${nextWidth}px`; + if (table) { + const total = visibleColumns.reduce( + (sum, item) => sum + (widthsRef.current[item.key] ?? item.defaultWidth), + 0, + ); + table.style.width = `${total}px`; + } + }; const onPointerMove = (moveEvent: PointerEvent) => { const delta = moveEvent.clientX - startX; - const nextWidth = Math.min(800, Math.max(minWidth, Math.round(startWidth + delta))); - currentWidth = nextWidth; - setWidths((prev) => ({ ...prev, [key]: nextWidth })); + currentWidth = Math.min(800, Math.max(minWidth, Math.round(startWidth + delta))); + if (frame) return; + frame = window.requestAnimationFrame(() => { + frame = 0; + paintWidth(currentWidth); + }); }; const cleanup = () => { window.removeEventListener('pointermove', onPointerMove); window.removeEventListener('pointerup', onPointerUp); window.removeEventListener('pointercancel', onPointerUp); + if (frame) window.cancelAnimationFrame(frame); document.body.classList.remove('table-col-resizing'); resizeCleanupRef.current = null; }; const onPointerUp = () => { + paintWidth(currentWidth); cleanup(); setResizingCol(null); - - setWidths((prev) => { - const next = { ...prev, [key]: currentWidth }; - try { - localStorage.setItem(EVENT_COL_WIDTHS_STORAGE_KEY, JSON.stringify(next)); - } catch {} - return next; - }); + const next = widthsRef.current; + setWidths(next); + try { + localStorage.setItem(EVENT_COL_WIDTHS_STORAGE_KEY, JSON.stringify(next)); + } catch {} }; window.addEventListener('pointermove', onPointerMove); @@ -641,7 +662,7 @@ export function EventsView({ }; const totalTableWidth = visibleColumns.reduce( - (sum, col) => sum + (widths[col.key] ?? col.defaultWidth), + (sum, col) => sum + (widthsRef.current[col.key] ?? widths[col.key] ?? col.defaultWidth), 0 ); @@ -678,7 +699,7 @@ export function EventsView({ return (
- {loading ?
: events.items.length ? ( + {loading && events.items.length === 0 ?
: events.items.length ? (
{visibleColumns.map((col) => ( - + ))} @@ -697,7 +718,7 @@ export function EventsView({
{label} @@ -710,7 +731,7 @@ export function EventsView({ aria-orientation="vertical" aria-valuemin={col.minWidth} aria-valuemax={800} - aria-valuenow={widths[col.key]} + aria-valuenow={widthsRef.current[col.key] ?? widths[col.key]} onPointerDown={(e) => handleResizeStart(col.key, e)} onDoubleClick={(e) => resetSingleColumn(col.key, e)} onKeyDown={(event) => handleResizeKeyDown(col.key, event)} @@ -749,7 +770,7 @@ export function EventsView({ )} - {!loading && events.items.length > 0 ? : null} + {events.items.length > 0 ? : null}
{t('usage.events.rangeSummary', { start: startRecordNum, end: endRecordNum, total: compactNumber(events.total) })} diff --git a/src/pages/UsageRecordsPage.tsx b/src/pages/UsageRecordsPage.tsx index d341ec0..32c3cef 100644 --- a/src/pages/UsageRecordsPage.tsx +++ b/src/pages/UsageRecordsPage.tsx @@ -270,6 +270,7 @@ export function UsageRecordsPage() { const [loading, setLoading] = useState(true); const [error, setError] = useState(''); const [loadedScopeKey, setLoadedScopeKey] = useState(''); + const loadedFilterScopeKeyRef = useRef(''); const requestIdRef = useRef(0); const schedulerRef = useRef | null>(null); if (!schedulerRef.current) schedulerRef.current = createRefreshScheduler(250); @@ -311,6 +312,20 @@ export function UsageRecordsPage() { }; }, [apiKeyHash, customEnd, customStart, model, provider, range, result, source]); + const filterScopeKey = useMemo(() => usageViewScopeKey({ + tab: 'overview', + range, + customStart, + customEnd, + model: '', + provider: '', + source: '', + apiKeyHash: '', + result: 'all', + page: 1, + pageSize: 1, + }), [customEnd, customStart, range]); + const scopeKey = useMemo(() => usageViewScopeKey({ tab: activeTab, range, @@ -344,7 +359,10 @@ export function UsageRecordsPage() { if (!quiet) setLoading(true); try { const statusRequest = invoke('get_usage_collector_status'); - const optionsRequest = invoke('get_usage_analysis', { query: timeQuery }); + const filtersChanged = loadedFilterScopeKeyRef.current !== filterScopeKey; + const optionsRequest = filtersChanged + ? invoke('get_usage_analysis', { query: timeQuery }) + : Promise.resolve(null); if (activeTab === 'overview') { const [nextStatus, nextOptions, nextOverview] = await Promise.all([ statusRequest, @@ -353,7 +371,7 @@ export function UsageRecordsPage() { ]); if (requestId !== requestIdRef.current) return; setStatus(nextStatus); - setOptionsAnalysis(nextOptions); + if (nextOptions) setOptionsAnalysis(nextOptions); setOverview(nextOverview); setOverviewRange(timeQuery); } else if (activeTab === 'analysis') { @@ -361,13 +379,13 @@ export function UsageRecordsPage() { statusRequest, optionsRequest, invoke('get_usage_overview', { query }), - model || provider || source || apiKeyHash || result !== 'all' + model || provider || source || apiKeyHash || result !== 'all' || !filtersChanged ? invoke('get_usage_analysis', { query }) - : optionsRequest, + : optionsRequest.then((analysis) => analysis ?? emptyAnalysis), ]); if (requestId !== requestIdRef.current) return; setStatus(nextStatus); - setOptionsAnalysis(nextOptions); + if (nextOptions) setOptionsAnalysis(nextOptions); setOverview(nextOverview); setOverviewRange(timeQuery); setAnalysis(nextAnalysis); @@ -381,7 +399,7 @@ export function UsageRecordsPage() { ]); if (requestId !== requestIdRef.current) return; setStatus(nextStatus); - setOptionsAnalysis(nextOptions); + if (nextOptions) setOptionsAnalysis(nextOptions); setEvents(nextEvents); } else if (activeTab === 'pricing') { const [nextStatus, nextOptions, nextPricing] = await Promise.all([ @@ -391,15 +409,16 @@ export function UsageRecordsPage() { ]); if (requestId !== requestIdRef.current) return; setStatus(nextStatus); - setOptionsAnalysis(nextOptions); + if (nextOptions) setOptionsAnalysis(nextOptions); setPricing(nextPricing); } else { const [nextStatus, nextOptions] = await Promise.all([statusRequest, optionsRequest]); if (requestId !== requestIdRef.current) return; setStatus(nextStatus); - setOptionsAnalysis(nextOptions); + if (nextOptions) setOptionsAnalysis(nextOptions); } setLoadedScopeKey(scopeKey); + if (filtersChanged) loadedFilterScopeKeyRef.current = filterScopeKey; setError(''); } catch (requestError) { if (requestId === requestIdRef.current) setError(String(requestError)); @@ -407,7 +426,7 @@ export function UsageRecordsPage() { if (requestId === requestIdRef.current) setLoading(false); } }, - [activeTab, buildQueries, page, pageSize, model, provider, source, apiKeyHash, result, scopeKey] + [activeTab, buildQueries, filterScopeKey, page, pageSize, model, provider, source, apiKeyHash, result, scopeKey] ); const loadData = useCallback( @@ -720,8 +739,8 @@ export function UsageRecordsPage() { {hasCurrentSnapshot && activeTab === 'analysis' ? : null} {activeTab === 'events' ? (