From ca654754aede59e7a67ede2515726d822228b67e Mon Sep 17 00:00:00 2001 From: feie9454 Date: Tue, 9 Jun 2026 17:32:35 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=89=8D=E7=AB=AF?= =?UTF-8?q?=E5=BF=AB=E9=80=9F=E6=BB=91=E5=8A=A8=E6=97=B6=E5=A4=9A=E4=B8=AA?= =?UTF-8?q?=E7=BD=91=E7=BB=9C=E8=AF=B7=E6=B1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../[hostname]/components/ScreenshotsTab.tsx | 90 +++++++++++++++---- 1 file changed, 72 insertions(+), 18 deletions(-) diff --git a/app/hosts/[hostname]/components/ScreenshotsTab.tsx b/app/hosts/[hostname]/components/ScreenshotsTab.tsx index 14c9cce..34bd984 100644 --- a/app/hosts/[hostname]/components/ScreenshotsTab.tsx +++ b/app/hosts/[hostname]/components/ScreenshotsTab.tsx @@ -14,20 +14,22 @@ import { useStarToggle } from '../hooks/useStarToggle'; const inFlightJsonRequests = new Map>(); +const fetchJson = async (url: string, signal?: AbortSignal): Promise => { + const response = await fetch(url, { cache: 'no-store', signal }); + if (!response.ok) { + throw new Error(`Request failed with status ${response.status}`); + } + + return response.json() as Promise; +}; + const fetchJsonOnce = async (url: string): Promise => { const existingRequest = inFlightJsonRequests.get(url); if (existingRequest) { return existingRequest as Promise; } - const request = fetch(url, { cache: 'no-store' }) - .then(async (response) => { - if (!response.ok) { - throw new Error(`Request failed with status ${response.status}`); - } - - return response.json() as Promise; - }) + const request = fetchJson(url) .finally(() => { inFlightJsonRequests.delete(url); }); @@ -36,6 +38,12 @@ const fetchJsonOnce = async (url: string): Promise => { return request as Promise; }; +const isAbortError = (error: unknown) => ( + error instanceof DOMException && error.name === 'AbortError' +); + +const HOURLY_RECORDS_REQUEST_DEBOUNCE_MS = 120; + interface ScreenshotsTabProps { hostname: string; selectedDate: string | null; @@ -53,6 +61,8 @@ export default function ScreenshotsTab({ }: ScreenshotsTabProps) { const timeDistributionRequestIdRef = useRef(0); const hourlyRecordsRequestIdRef = useRef(0); + const hourlyRecordsAbortControllerRef = useRef(null); + const hourlyRecordsDebounceTimerRef = useRef | null>(null); // 状态管理 const [timeDistribution, setTimeDistribution] = useState([]); @@ -83,6 +93,25 @@ export default function ScreenshotsTab({ const { updatingStars, toggleStar } = useStarToggle(); + const clearPendingHourlyRecordsRequest = () => { + if (hourlyRecordsDebounceTimerRef.current) { + clearTimeout(hourlyRecordsDebounceTimerRef.current); + hourlyRecordsDebounceTimerRef.current = null; + } + }; + + const scheduleHourlyRecordsFetch = ( + startTime: number, + endTime: number, + options?: { targetRecordId?: string, keepSelection?: boolean } + ) => { + clearPendingHourlyRecordsRequest(); + hourlyRecordsDebounceTimerRef.current = setTimeout(() => { + hourlyRecordsDebounceTimerRef.current = null; + fetchHourlyRecords(startTime, endTime, options); + }, HOURLY_RECORDS_REQUEST_DEBOUNCE_MS); + }; + // 获取时间分布数据 const fetchTimeDistribution = async () => { const requestId = ++timeDistributionRequestIdRef.current; @@ -112,19 +141,23 @@ export default function ScreenshotsTab({ // 获取小时记录 const fetchHourlyRecords = async (startTime: number, endTime: number, options?: { targetRecordId?: string, keepSelection?: boolean }) => { + clearPendingHourlyRecordsRequest(); const requestId = ++hourlyRecordsRequestIdRef.current; const requestUrl = `/hosts/${hostname}/screenshots?startTime=${startTime}&endTime=${endTime}`; + hourlyRecordsAbortControllerRef.current?.abort(); + const abortController = new AbortController(); + hourlyRecordsAbortControllerRef.current = abortController; try { setLoadingRecords(true); setShowDetailTimeline(true); - const data = await fetchJsonOnce<{ + const data = await fetchJson<{ lastUpdate: string | null; records: ScreenRecord[]; - }>(requestUrl); + }>(requestUrl, abortController.signal); - if (requestId !== hourlyRecordsRequestIdRef.current) { + if (requestId !== hourlyRecordsRequestIdRef.current || abortController.signal.aborted) { return; } @@ -134,6 +167,10 @@ export default function ScreenshotsTab({ setTimeRange({ min: startTime * 1000, max: endTime * 1000 }); requestAnimationFrame(() => { + if (requestId !== hourlyRecordsRequestIdRef.current || abortController.signal.aborted) { + return; + } + if (options?.targetRecordId) { const found = newRecords.find((r: ScreenRecord) => r.id === options.targetRecordId); if (found) { @@ -151,11 +188,19 @@ export default function ScreenshotsTab({ } }); } catch (error) { + if (isAbortError(error)) { + return; + } + if (requestId === hourlyRecordsRequestIdRef.current) { console.error('获取记录数据失败:', error); } } finally { - if (requestId === hourlyRecordsRequestIdRef.current) { + if (hourlyRecordsAbortControllerRef.current === abortController) { + hourlyRecordsAbortControllerRef.current = null; + } + + if (requestId === hourlyRecordsRequestIdRef.current && !abortController.signal.aborted) { setLoadingRecords(false); } } @@ -273,12 +318,8 @@ export default function ScreenshotsTab({ // 事件处理函数 const onHourlySliderChange = (newValue: number) => { const selectedSec = Math.floor(newValue / 3600000) * 3600; - - // 使用 setTimeout 避免在渲染过程中更新状态 - setTimeout(() => { - setHourlySliderValue(newValue); - fetchHourlyRecords(selectedSec, selectedSec + 3600); - }, 0); + setHourlySliderValue(newValue); + scheduleHourlyRecordsFetch(selectedSec, selectedSec + 3600); }; const onDetailedSliderChange = (newValue: number) => { @@ -443,6 +484,19 @@ export default function ScreenshotsTab({ }, [prevFrame, nextFrame]); // Effects + useEffect(() => { + return () => { + if (hourlyRecordsDebounceTimerRef.current) { + clearTimeout(hourlyRecordsDebounceTimerRef.current); + hourlyRecordsDebounceTimerRef.current = null; + } + + hourlyRecordsRequestIdRef.current += 1; + hourlyRecordsAbortControllerRef.current?.abort(); + hourlyRecordsAbortControllerRef.current = null; + }; + }, []); + useEffect(() => { fetchTimeDistribution(); }, [hostname]);