修复 fallback fetch 依赖:rows.length 改为 useRef 标记,避免 terminalData 刷新时无意义 re-run

This commit is contained in:
2569718930@qq.com
2026-05-26 07:59:55 +08:00
parent 598cd17c0b
commit c331c89289
@@ -43,7 +43,7 @@ import { scanRootClass } from "@/components/dashboard/scan-root-styles";
import { useRelativeTime } from "@/hooks/useRelativeTime"; import { useRelativeTime } from "@/hooks/useRelativeTime";
import { Panel } from "@/components/dashboard/scan-terminal/Panel"; import { Panel } from "@/components/dashboard/scan-terminal/Panel";
import { TrainingDashboard } from "@/components/dashboard/scan-terminal/TrainingDashboard"; import { TrainingDashboard } from "@/components/dashboard/scan-terminal/TrainingDashboard";
import { LiveTemperatureThresholdChart } from "@/components/dashboard/scan-terminal/LiveTemperatureThresholdChart"; import { LiveTemperatureThresholdChart, clearCityDetailCache } from "@/components/dashboard/scan-terminal/LiveTemperatureThresholdChart";
import { KoyfinRowsTable } from "@/components/dashboard/scan-terminal/KoyfinRowsTable"; import { KoyfinRowsTable } from "@/components/dashboard/scan-terminal/KoyfinRowsTable";
import { rowName, pct, money, temp, edgeClass } from "@/components/dashboard/scan-terminal/utils"; import { rowName, pct, money, temp, edgeClass } from "@/components/dashboard/scan-terminal/utils";
import { CitySelectorDropdown } from "@/components/dashboard/scan-terminal/CitySelectorDropdown"; import { CitySelectorDropdown } from "@/components/dashboard/scan-terminal/CitySelectorDropdown";
@@ -400,26 +400,23 @@ function PolyWeatherTerminal({
const totalSlots = getSlotCount(gridCols, gridRows); const totalSlots = getSlotCount(gridCols, gridRows);
const [slots, setSlots] = useState<Array<string | null>>(() => { const [slots, setSlots] = useState<Array<string | null>>(() => {
const storedCols = getStoredGridSide("polyweather_terminal_grid_cols");
const storedRows = getStoredGridSide("polyweather_terminal_grid_rows");
const initialSlotCount = getSlotCount(storedCols, storedRows);
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
try { try {
const stored = localStorage.getItem("polyweather_terminal_slots"); const stored = localStorage.getItem("polyweather_terminal_slots");
if (stored) { if (stored) {
const parsed = JSON.parse(stored); const parsed = JSON.parse(stored);
if (Array.isArray(parsed)) { if (Array.isArray(parsed)) {
return normalizeSlotList(parsed, initialSlotCount); return normalizeSlotList(parsed, MAX_TERMINAL_CHARTS);
} }
} }
} catch {} } catch {}
} }
return Array(initialSlotCount).fill(null); return Array(MAX_TERMINAL_CHARTS).fill(null);
}); });
const [activeSlotIndex, setActiveSlotIndex] = useState<number>(0); const [activeSlotIndex, setActiveSlotIndex] = useState<number>(0);
const [maximizedSlotIndex, setMaximizedSlotIndex] = useState<number | null>(null); const [maximizedSlotIndex, setMaximizedSlotIndex] = useState<number | null>(null);
const [activeSearchSlotIndex, setActiveSearchSlotIndex] = useState<number | null>(null); const [activeSearchSlotIndex, setActiveSearchSlotIndex] = useState<number | null>(null);
const visibleSlots = useMemo(() => normalizeSlotList(slots, totalSlots), [slots, totalSlots]); const visibleSlots = useMemo(() => slots.slice(0, totalSlots), [slots, totalSlots]);
const handleSetGridSize = (cols: number, rows: number) => { const handleSetGridSize = (cols: number, rows: number) => {
const safeCols = clampGridSide(cols); const safeCols = clampGridSide(cols);
@@ -434,13 +431,6 @@ function PolyWeatherTerminal({
localStorage.setItem("polyweather_terminal_grid_rows", String(safeRows)); localStorage.setItem("polyweather_terminal_grid_rows", String(safeRows));
} catch {} } catch {}
const nextSlots = normalizeSlotList(visibleSlots, nextTotalSlots);
setSlots(nextSlots);
try {
localStorage.setItem("polyweather_terminal_slots", JSON.stringify(nextSlots));
} catch {}
if (activeSlotIndex >= nextTotalSlots) { if (activeSlotIndex >= nextTotalSlots) {
setActiveSlotIndex(0); setActiveSlotIndex(0);
} }
@@ -469,8 +459,8 @@ function PolyWeatherTerminal({
}, [rows, selectedRegionKey]); }, [rows, selectedRegionKey]);
useEffect(() => { useEffect(() => {
if (filteredRegionRows.length && visibleSlots.every((s) => s === null)) { if (filteredRegionRows.length && slots.every((s) => s === null)) {
const next = Array(totalSlots) const next = Array(MAX_TERMINAL_CHARTS)
.fill(null) .fill(null)
.map((_, idx) => filteredRegionRows[idx]?.city || null); .map((_, idx) => filteredRegionRows[idx]?.city || null);
setSlots(next); setSlots(next);
@@ -478,11 +468,11 @@ function PolyWeatherTerminal({
localStorage.setItem("polyweather_terminal_slots", JSON.stringify(next)); localStorage.setItem("polyweather_terminal_slots", JSON.stringify(next));
} catch {} } catch {}
} }
}, [filteredRegionRows, visibleSlots, totalSlots]); }, [filteredRegionRows, slots]);
const handleSelectCityForSlot = (index: number, city: string | null) => { const handleSelectCityForSlot = (index: number, city: string | null) => {
if (index < 0 || index >= totalSlots) return; if (index < 0 || index >= MAX_TERMINAL_CHARTS) return;
const next = [...visibleSlots]; const next = [...slots];
next[index] = city; next[index] = city;
setSlots(next); setSlots(next);
try { try {
@@ -860,6 +850,8 @@ function PolyWeatherTerminal({
row={rowForSlot} row={rowForSlot}
allRows={filteredRegionRows} allRows={filteredRegionRows}
compact={true} compact={true}
isActive={isSlotActive}
slotIndex={slotIndex}
onSearchClick={() => setActiveSearchSlotIndex(slotIndex)} onSearchClick={() => setActiveSearchSlotIndex(slotIndex)}
onMaximize={() => { onMaximize={() => {
setMaximizedSlotIndex(slotIndex); setMaximizedSlotIndex(slotIndex);
@@ -1025,9 +1017,24 @@ function ScanTerminalScreen() {
timezoneOffsetSeconds: useLocalTimezoneDefault ? localTimezoneOffsetSeconds : null, timezoneOffsetSeconds: useLocalTimezoneDefault ? localTimezoneOffsetSeconds : null,
tradingRegion: selectedRegionKey, tradingRegion: selectedRegionKey,
}); });
const handleRefresh = useCallback(() => {
clearCityDetailCache();
refreshScanTerminalManually();
}, [refreshScanTerminalManually]);
const [cityFallbackRows, setCityFallbackRows] = useState<ScanOpportunityRow[]>([]); const [cityFallbackRows, setCityFallbackRows] = useState<ScanOpportunityRow[]>([]);
const rows = useMemo(
() => {
const scanRows = terminalData?.rows || [];
return sortRowsByUserTime(scanRows.length ? scanRows : cityFallbackRows);
},
[cityFallbackRows, terminalData?.rows],
);
const fallbackFetchedRef = useRef(false);
useEffect(() => { useEffect(() => {
if (!isPro || typeof fetch !== "function") return; if (!isPro || typeof fetch !== "function") return;
if (fallbackFetchedRef.current) return;
const controller = new AbortController(); const controller = new AbortController();
fetch("/api/cities", { fetch("/api/cities", {
cache: "no-store", cache: "no-store",
@@ -1040,18 +1047,12 @@ function ScanTerminalScreen() {
}) })
.then((payload) => { .then((payload) => {
if (!payload || !Array.isArray(payload.cities)) return; if (!payload || !Array.isArray(payload.cities)) return;
fallbackFetchedRef.current = true;
setCityFallbackRows(cityListItemsToScanRows(payload.cities)); setCityFallbackRows(cityListItemsToScanRows(payload.cities));
}) })
.catch(() => {}); .catch(() => {});
return () => controller.abort(); return () => controller.abort();
}, [isPro]); }, [isPro]);
const rows = useMemo(
() => {
const scanRows = terminalData?.rows || [];
return sortRowsByUserTime(scanRows.length ? scanRows : cityFallbackRows);
},
[cityFallbackRows, terminalData?.rows],
);
const [searchQuery, setSearchQuery] = useState(""); const [searchQuery, setSearchQuery] = useState("");
const searchInputRef = useRef<HTMLInputElement>(null); const searchInputRef = useRef<HTMLInputElement>(null);
@@ -1115,7 +1116,7 @@ function ScanTerminalScreen() {
generatedText={generatedText || ""} generatedText={generatedText || ""}
isEn={isEn} isEn={isEn}
locale={locale} locale={locale}
onRefresh={refreshScanTerminalManually} onRefresh={handleRefresh}
refreshing={scanLoading} refreshing={scanLoading}
rows={filteredRows} rows={filteredRows}
selectedRow={selectedRow} selectedRow={selectedRow}