Improve terminal chart loading states

This commit is contained in:
2569718930@qq.com
2026-05-30 22:37:08 +08:00
parent a48a7bcca5
commit 6222f60573
4 changed files with 93 additions and 3 deletions
@@ -64,10 +64,37 @@ function shouldKeepTemperatureChartLoading({
zoomedData: Array<Record<string, any>>;
}) {
if (!row?.city) return false;
if (isHourlyLoading) return true;
if (!isHourlyLoading) return false;
return !hasDrawableTemperatureChartContent({ activeSeries, probabilityOverlay, zoomedData });
}
function TemperatureChartSkeleton({ compact }: { compact: boolean }) {
const horizontalLines = compact ? 5 : 7;
const verticalLines = compact ? 5 : 8;
return (
<div className="absolute inset-0 overflow-hidden bg-white">
<div className="absolute inset-x-3 bottom-7 top-4 rounded-sm border border-slate-100">
{Array.from({ length: horizontalLines }).map((_, index) => (
<span
key={`h-${index}`}
className="absolute left-0 right-0 border-t border-dashed border-sky-100"
style={{ top: `${(index / Math.max(1, horizontalLines - 1)) * 100}%` }}
/>
))}
{Array.from({ length: verticalLines }).map((_, index) => (
<span
key={`v-${index}`}
className="absolute bottom-0 top-0 border-l border-dashed border-sky-100"
style={{ left: `${(index / Math.max(1, verticalLines - 1)) * 100}%` }}
/>
))}
<div className="absolute inset-x-10 top-1/3 h-10 rounded bg-slate-100/60" />
</div>
</div>
);
}
function TemperatureChartCanvasComponent({
isEn,
compact,
@@ -173,6 +200,11 @@ function TemperatureChartCanvasComponent({
hasRunwayData &&
individualRunwaySeriesCount > 1 &&
collapsedRunwaySeries.length < chartSeries.length;
const hasDrawableChartContent = hasDrawableTemperatureChartContent({
activeSeries,
probabilityOverlay,
zoomedData,
});
const shouldShowChartLoading = shouldKeepTemperatureChartLoading({
row,
isHourlyLoading,
@@ -180,6 +212,9 @@ function TemperatureChartCanvasComponent({
probabilityOverlay,
zoomedData,
});
const shouldRenderChart = canRenderChart && hasDrawableChartContent;
const shouldShowEmptyState = Boolean(row?.city) && !isHourlyLoading && !hasDrawableChartContent;
const shouldShowBackgroundRefresh = isHourlyLoading && hasDrawableChartContent;
return (
<div className={clsx("relative flex flex-1 flex-col p-2", compact ? "min-h-[120px]" : "min-h-[240px]")}>
@@ -242,7 +277,8 @@ function TemperatureChartCanvasComponent({
)}
</div>
<div ref={chartHostRef} className={clsx("relative flex-1", compact ? "min-h-[120px]" : "min-h-[220px]")}>
{canRenderChart && !shouldShowChartLoading && (
{!shouldRenderChart && <TemperatureChartSkeleton compact={compact} />}
{shouldRenderChart && (
<ReComposedChart
width={chartWidth}
height={chartHeight}
@@ -384,7 +420,20 @@ function TemperatureChartCanvasComponent({
))}
</ReComposedChart>
)}
{shouldShowEmptyState && (
<div className="pointer-events-none absolute inset-0 grid place-items-center px-4 text-center">
<div className="rounded border border-slate-200 bg-white/90 px-3 py-2 text-[11px] font-semibold text-slate-500 shadow-sm">
{isEn ? "No drawable chart data yet" : "暂无可绘制图表数据"}
</div>
</div>
)}
</div>
{shouldShowBackgroundRefresh && (
<div className="pointer-events-none absolute right-3 top-12 z-10 inline-flex items-center gap-1.5 rounded border border-slate-200 bg-white/80 px-2 py-1 text-[10px] font-semibold text-slate-500 shadow-sm backdrop-blur-[1px]">
<span className="h-2.5 w-2.5 animate-spin rounded-full border-2 border-slate-200 border-t-blue-500" />
<span>{isEn ? "Updating" : "更新中"}</span>
</div>
)}
{shouldShowChartLoading && (
<div className="pointer-events-none absolute inset-2 z-10 grid place-items-center bg-white/65 backdrop-blur-[1px]">
<div className="flex items-center gap-2 rounded border border-slate-200 bg-white px-3 py-2 text-[11px] font-semibold text-slate-600 shadow-sm">
@@ -1,4 +1,5 @@
import { __buildTemperatureStatsLabelsForTest } from "@/components/dashboard/scan-terminal/TemperatureStatsBars";
import { temp } from "@/components/dashboard/scan-terminal/utils";
function assert(condition: unknown, message: string) {
if (!condition) throw new Error(message);
@@ -46,4 +47,8 @@ export function runTests() {
assert(zh.primary === "天文台实测 (10分钟)", "Chinese primary label should remain unchanged");
assert(zh.compactSecondary === "当日最高", "Chinese Shenzhen compact secondary label should remain 当日最高");
assert(temp(null, "°C") === "--", "empty temperature values should not render as 0.0°C while city detail is loading");
assert(temp(undefined, "°C") === "--", "undefined temperature values should not render as 0.0°C while city detail is loading");
assert(temp("", "°C") === "--", "blank temperature values should not render as 0.0°C while city detail is loading");
}
@@ -121,6 +121,19 @@ export function runTests() {
);
assert(
__shouldKeepTemperatureChartLoadingForTest({
row: { city: "Moscow" } as any,
isHourlyLoading: true,
activeSeries: [],
probabilityOverlay: null,
zoomedData: [
{ label: "00:00", ts: 1 },
{ label: "05:00", ts: 2 },
],
}),
"temperature chart should show the loading skeleton while the first detail fetch is in flight and no drawable data exists",
);
assert(
!__shouldKeepTemperatureChartLoadingForTest({
row: { city: "Moscow" } as any,
isHourlyLoading: false,
activeSeries: [],
@@ -130,7 +143,7 @@ export function runTests() {
{ label: "05:00", ts: 2 },
],
}),
"temperature chart must keep loading instead of rendering an empty axis grid when no drawable series is available",
"temperature chart must stop showing an indefinite loading overlay after the detail fetch finishes without drawable data",
);
assert(
!__shouldKeepTemperatureChartLoadingForTest({
@@ -153,4 +166,25 @@ export function runTests() {
}),
"temperature chart should render once a visible series has drawable values",
);
assert(
!__shouldKeepTemperatureChartLoadingForTest({
row: { city: "Moscow" } as any,
isHourlyLoading: true,
activeSeries: [
{
key: "current",
label: "Current reference",
source: "Live",
color: "#009688",
values: [13, 13],
},
] as any,
probabilityOverlay: null,
zoomedData: [
{ label: "00:00", ts: 1, current: 13 },
{ label: "05:00", ts: 2, current: 13 },
],
}),
"temperature chart must render seeded or cached data immediately while full detail continues loading in the background",
);
}
@@ -17,6 +17,8 @@ export function money(value: unknown) {
}
export function temp(value: unknown, unit?: string | null) {
if (value == null) return "--";
if (typeof value === "string" && value.trim() === "") return "--";
const n = Number(value);
if (!Number.isFinite(n)) return "--";
return `${n.toFixed(1)}${unit || "°"}`;