diff --git a/frontend/components/dashboard/ScanTerminalDashboard.tsx b/frontend/components/dashboard/ScanTerminalDashboard.tsx
index 70762753..45ea84c3 100644
--- a/frontend/components/dashboard/ScanTerminalDashboard.tsx
+++ b/frontend/components/dashboard/ScanTerminalDashboard.tsx
@@ -499,6 +499,64 @@ function EmptySlotCard({
);
}
+function LoadingSlotCard({
+ city,
+ isActive,
+ isEn,
+ onSelectSlot,
+}: {
+ city: string;
+ isActive: boolean;
+ isEn: boolean;
+ onSelectSlot: () => void;
+}) {
+ const cityLabel = city
+ .split(/[-_\s]+/)
+ .filter(Boolean)
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
+ .join(" ") || city;
+
+ return (
+
+
+
+
+ {cityLabel}
+ ·
+ {isEn ? "Loading chart" : "加载图表"}
+
+
+
+
+
+ {Array.from({ length: 6 }).map((_, index) => (
+
+ ))}
+ {Array.from({ length: 7 }).map((_, index) => (
+
+ ))}
+
+
+
+
+
+ );
+}
+
const TerminalSidebar = memo(function TerminalSidebar({
activeNavKey,
isEn,
@@ -1186,6 +1244,19 @@ function PolyWeatherTerminal({
(r) => String(r.city || "").toLowerCase() === cityInSlot
) || null
: null;
+ const isSavedSlotLoading = Boolean(cityInSlot && !rowForSlot && refreshing);
+
+ if (isSavedSlotLoading && cityInSlot) {
+ return (
+ setActiveSlotIndex(slotIndex)}
+ />
+ );
+ }
if (!cityInSlot || !rowForSlot) {
return (
diff --git a/frontend/components/dashboard/scan-terminal/TemperatureChartCanvas.tsx b/frontend/components/dashboard/scan-terminal/TemperatureChartCanvas.tsx
index f3f51093..10e89d76 100644
--- a/frontend/components/dashboard/scan-terminal/TemperatureChartCanvas.tsx
+++ b/frontend/components/dashboard/scan-terminal/TemperatureChartCanvas.tsx
@@ -89,7 +89,8 @@ function TemperatureChartSkeleton({ compact }: { compact: boolean }) {
style={{ left: `${(index / Math.max(1, verticalLines - 1)) * 100}%` }}
/>
))}
-
+
+
);
diff --git a/frontend/components/dashboard/scan-terminal/__tests__/terminalGridPolicy.test.ts b/frontend/components/dashboard/scan-terminal/__tests__/terminalGridPolicy.test.ts
index 25fcc4eb..87b35d13 100644
--- a/frontend/components/dashboard/scan-terminal/__tests__/terminalGridPolicy.test.ts
+++ b/frontend/components/dashboard/scan-terminal/__tests__/terminalGridPolicy.test.ts
@@ -101,9 +101,10 @@ export function runTests() {
"terminal guide must describe the new 3x2 maximum layout instead of 3x3",
);
assert(
- dashboardSource.includes("if (!cityInSlot || !rowForSlot)") &&
+ dashboardSource.includes("LoadingSlotCard") &&
+ dashboardSource.includes("cityInSlot && !rowForSlot && refreshing") &&
dashboardSource.includes("handleSelectCityForSlot(slotIndex, null);"),
- "stale saved chart slots must render the empty city picker instead of a row=null Temperature Chart",
+ "terminal must show a loading chart card for saved cities while the first scan rows are still loading, then fall back to the empty picker for stale slots",
);
assert(
dashboardSource.includes("absolute left-1/2 top-12 z-50") &&
@@ -223,6 +224,12 @@ export function runTests() {
}),
"temperature chart should show the loading skeleton while the first detail fetch is in flight and no drawable data exists",
);
+ assert(
+ chartCanvasSource.includes("animate-pulse") &&
+ chartCanvasSource.includes("Loading chart") &&
+ chartCanvasSource.includes("bg-gradient-to-r"),
+ "temperature chart loading skeleton must include visible animation on cold first-open charts",
+ );
assert(
!__shouldKeepTemperatureChartLoadingForTest({
row: { city: "Moscow" } as any,