修复 fallback fetch 依赖:rows.length 改为 useRef 标记,避免 terminalData 刷新时无意义 re-run
This commit is contained in:
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user