修复终端首屏图表加载动画

This commit is contained in:
2569718930@qq.com
2026-06-16 04:29:24 +08:00
parent 14e356c3c9
commit 1afd3c6976
3 changed files with 82 additions and 3 deletions
@@ -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 (
<div
onClick={onSelectSlot}
className={clsx(
"relative flex h-full min-h-0 flex-col overflow-hidden rounded-[4px] border bg-white cursor-default",
isActive ? "border-blue-500 ring-2 ring-blue-500/20 shadow-md z-10" : "border-[#d2d9e2]",
)}
>
<div className="flex h-9 shrink-0 items-center justify-between border-b border-slate-200 px-3">
<div className="flex min-w-0 items-center gap-2 text-[11px] font-bold text-slate-600">
<span className="h-2 w-2 animate-pulse rounded-full bg-blue-500" />
<span className="truncate">{cityLabel}</span>
<span className="text-slate-300">·</span>
<span className="text-slate-400">{isEn ? "Loading chart" : "加载图表"}</span>
</div>
<span className="h-3 w-3 animate-spin rounded-full border-2 border-slate-200 border-t-blue-500" />
</div>
<div className="relative min-h-0 flex-1 overflow-hidden bg-white">
<div className="absolute inset-x-3 bottom-7 top-5 rounded-sm border border-slate-100">
{Array.from({ length: 6 }).map((_, index) => (
<span
key={`h-${index}`}
className="absolute left-0 right-0 border-t border-dashed border-sky-100"
style={{ top: `${(index / 5) * 100}%` }}
/>
))}
{Array.from({ length: 7 }).map((_, index) => (
<span
key={`v-${index}`}
className="absolute bottom-0 top-0 border-l border-dashed border-sky-100"
style={{ left: `${(index / 6) * 100}%` }}
/>
))}
<div className="absolute left-8 right-8 top-1/3 h-10 animate-pulse rounded bg-gradient-to-r from-slate-100 via-blue-100 to-slate-100" />
<div className="absolute inset-y-0 -left-1/3 w-1/3 animate-[pulse_1.2s_ease-in-out_infinite] bg-gradient-to-r from-transparent via-white/80 to-transparent" />
</div>
</div>
</div>
);
}
const TerminalSidebar = memo(function TerminalSidebar({ const TerminalSidebar = memo(function TerminalSidebar({
activeNavKey, activeNavKey,
isEn, isEn,
@@ -1186,6 +1244,19 @@ function PolyWeatherTerminal({
(r) => String(r.city || "").toLowerCase() === cityInSlot (r) => String(r.city || "").toLowerCase() === cityInSlot
) || null ) || null
: null; : null;
const isSavedSlotLoading = Boolean(cityInSlot && !rowForSlot && refreshing);
if (isSavedSlotLoading && cityInSlot) {
return (
<LoadingSlotCard
key={slotIndex}
city={cityInSlot}
isActive={isSlotActive}
isEn={isEn}
onSelectSlot={() => setActiveSlotIndex(slotIndex)}
/>
);
}
if (!cityInSlot || !rowForSlot) { if (!cityInSlot || !rowForSlot) {
return ( return (
@@ -89,7 +89,8 @@ function TemperatureChartSkeleton({ compact }: { compact: boolean }) {
style={{ left: `${(index / Math.max(1, verticalLines - 1)) * 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 className="absolute inset-x-10 top-1/3 h-10 animate-pulse rounded bg-gradient-to-r from-slate-100 via-blue-100 to-slate-100" />
<div className="absolute inset-y-0 -left-1/3 w-1/3 animate-pulse bg-gradient-to-r from-transparent via-white/80 to-transparent" />
</div> </div>
</div> </div>
); );
@@ -101,9 +101,10 @@ export function runTests() {
"terminal guide must describe the new 3x2 maximum layout instead of 3x3", "terminal guide must describe the new 3x2 maximum layout instead of 3x3",
); );
assert( assert(
dashboardSource.includes("if (!cityInSlot || !rowForSlot)") && dashboardSource.includes("LoadingSlotCard") &&
dashboardSource.includes("cityInSlot && !rowForSlot && refreshing") &&
dashboardSource.includes("handleSelectCityForSlot(slotIndex, null);"), 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( assert(
dashboardSource.includes("absolute left-1/2 top-12 z-50") && 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", "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( assert(
!__shouldKeepTemperatureChartLoadingForTest({ !__shouldKeepTemperatureChartLoadingForTest({
row: { city: "Moscow" } as any, row: { city: "Moscow" } as any,