Keep model summary data during refresh gaps

This commit is contained in:
2569718930@qq.com
2026-06-29 21:17:04 +08:00
parent 1df13bb9e6
commit 168b813754
3 changed files with 84 additions and 2 deletions
@@ -2,7 +2,7 @@
import clsx from "clsx";
import { Search, Table2 } from "lucide-react";
import { useEffect, useMemo, useState } from "react";
import { useEffect, useMemo, useRef, useState } from "react";
import type { ScanOpportunityRow } from "@/lib/dashboard-types";
import {
MODEL_SUMMARY_MODEL_COLUMNS,
@@ -10,6 +10,7 @@ import {
filterModelSummaryRows,
formatModelSummaryLocalTime,
formatModelSummaryTemp,
hasModelSummaryForecastData,
type ModelSummaryRow,
} from "@/lib/model-summary";
@@ -132,6 +133,43 @@ function ModelSummaryRowView({
);
}
function mergeWithLastGoodSummaryRows(
incomingRows: ModelSummaryRow[],
lastGoodRows: ModelSummaryRow[],
) {
if (!lastGoodRows.length) return incomingRows;
if (!incomingRows.length) return lastGoodRows;
const incomingCityKeys = new Set<string>();
const lastGoodByCity = new Map(lastGoodRows.map((row) => [row.cityKey, row]));
const mergedRows = incomingRows.map((incomingRow) => {
incomingCityKeys.add(incomingRow.cityKey);
const lastGoodRow = lastGoodByCity.get(incomingRow.cityKey);
if (!lastGoodRow) return incomingRow;
return {
...lastGoodRow,
cityName: incomingRow.cityName,
regionLabel: incomingRow.regionLabel,
regionLabelZh: incomingRow.regionLabelZh,
regionSort: incomingRow.regionSort,
tempSymbol: incomingRow.tempSymbol || lastGoodRow.tempSymbol,
localTime: incomingRow.localTime || lastGoodRow.localTime,
timezoneOffsetSeconds:
incomingRow.timezoneOffsetSeconds ?? lastGoodRow.timezoneOffsetSeconds,
searchText: incomingRow.searchText || lastGoodRow.searchText,
};
});
return [
...mergedRows,
...lastGoodRows.filter((row) => !incomingCityKeys.has(row.cityKey)),
].sort((a, b) => {
if (a.regionSort !== b.regionSort) return a.regionSort - b.regionSort;
return a.cityName.localeCompare(b.cityName, "en", { sensitivity: "base" });
});
}
export function ModelSummaryDashboard({
rows,
isEn,
@@ -141,6 +179,7 @@ export function ModelSummaryDashboard({
const [debOnly, setDebOnly] = useState(false);
const [wideSpreadOnly, setWideSpreadOnly] = useState(false);
const [nowMs, setNowMs] = useState<number | null>(null);
const lastGoodSummaryRowsRef = useRef<ModelSummaryRow[]>([]);
useEffect(() => {
const syncClock = () => setNowMs(Date.now());
@@ -149,7 +188,22 @@ export function ModelSummaryDashboard({
return () => window.clearInterval(timer);
}, []);
const summaryRows = useMemo(() => buildModelSummaryRows(rows, isEn), [rows, isEn]);
const incomingSummaryRows = useMemo(() => buildModelSummaryRows(rows, isEn), [rows, isEn]);
const incomingHasForecastData = hasModelSummaryForecastData(incomingSummaryRows);
const summaryRows = useMemo(
() =>
incomingHasForecastData
? incomingSummaryRows
: mergeWithLastGoodSummaryRows(incomingSummaryRows, lastGoodSummaryRowsRef.current),
[incomingSummaryRows, incomingHasForecastData],
);
useEffect(() => {
if (incomingHasForecastData) {
lastGoodSummaryRowsRef.current = incomingSummaryRows;
}
}, [incomingSummaryRows, incomingHasForecastData]);
const visibleRows = useMemo(
() =>
filterModelSummaryRows(summaryRows, {
@@ -6,6 +6,7 @@ import {
filterModelSummaryRows,
formatModelSummaryLocalTime,
formatModelSummaryTemp,
hasModelSummaryForecastData,
} from "@/lib/model-summary";
function assert(condition: unknown, message: string) {
@@ -113,6 +114,24 @@ export function runTests() {
assert(parisRow.modelSpread === 2.5, "model spread should use available model min/max only");
assert(formatModelSummaryTemp(null, "°C") === "—", "missing model temperatures should render as an em dash");
assert(formatModelSummaryTemp(32.16, "°C") === "32.2°C", "model temperatures should render to one decimal");
assert(hasModelSummaryForecastData(summaryRows), "model summary should recognize populated forecast rows");
assert(
!hasModelSummaryForecastData(
buildModelSummaryRows([
{
id: "fallback:beijing",
city: "beijing",
city_display_name: "Beijing",
trading_region_label: "East Asia",
trading_region_label_zh: "东亚",
trading_region_sort: 1,
local_time: "21:11",
tz_offset_seconds: 28800,
},
] as any, false),
),
"model summary should recognize fallback-only rows without DEB or model forecasts",
);
const searched = filterModelSummaryRows(summaryRows, {
debOnly: true,
@@ -155,6 +174,8 @@ export function runTests() {
);
assert(
modelSummarySource.includes("MODEL_SUMMARY_MODEL_COLUMNS") &&
modelSummarySource.includes("lastGoodSummaryRowsRef") &&
modelSummarySource.includes("hasModelSummaryForecastData") &&
modelSummarySource.includes("Local Time") &&
modelSummarySource.includes("当地时间") &&
!modelSummarySource.includes("Current High") &&
+7
View File
@@ -196,3 +196,10 @@ export function filterModelSummaryRows(
return true;
});
}
export function hasModelSummaryForecastData(rows: ModelSummaryRow[]) {
return rows.some((row) => {
if (row.debPrediction != null) return true;
return MODEL_SUMMARY_MODEL_COLUMNS.some((column) => row.models[column.key] != null);
});
}