feat(frontend): cache 3D assets and restore loading feedback
Persist versioned models and validated network metadata in IndexedDB. Show scene download progress and recoverable errors with the existing frosted material. Connect existing skeletons to pending navigation and route boundaries while preserving the map. Previously only dynamic imports showed fallbacks, and rejected model promises prevented retries. Verified: 32 focused unit tests; 4 navigation and scene browser checks; 7 cache browser checks including invalid-metadata recovery; isolated production build and type checks.
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
"use client";
|
||||
|
||||
import { Box, CircularProgress, Skeleton, Typography } from "@mui/material";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
import { MapPanelSkeleton, MapTimelineSkeleton } from "./MapComponentSkeletons";
|
||||
import {
|
||||
MAP_SKELETON_CONFIGS,
|
||||
type MapSkeletonVariant,
|
||||
} from "./mapComponentSkeletonConfig";
|
||||
|
||||
const sceneLoadingSurface = {
|
||||
width: "min(100%, 360px)",
|
||||
p: 3,
|
||||
boxSizing: "border-box",
|
||||
borderRadius: "16px",
|
||||
background: "linear-gradient(135deg,rgba(255,255,255,.50),rgba(224,239,250,.28))",
|
||||
backdropFilter: "blur(24px) saturate(170%) contrast(94%)",
|
||||
WebkitBackdropFilter: "blur(24px) saturate(170%) contrast(94%)",
|
||||
boxShadow: "inset 0 1px 0 rgba(255,255,255,.88),inset 0 -1px 0 rgba(112,145,168,.18),0 18px 50px rgba(15,43,69,.20),0 0 0 1px rgba(255,255,255,.55)",
|
||||
};
|
||||
|
||||
const mapPaths: Record<string, MapSkeletonVariant> = {
|
||||
"/network-simulation": "network-simulation",
|
||||
"/scada-data-cleaning": "scada-data-cleaning",
|
||||
"/health-risk-analysis": "health-risk-analysis",
|
||||
"/monitoring-place-optimization": "monitoring-place-optimization",
|
||||
"/hydraulic-simulation/burst-detection": "burst-detection",
|
||||
"/hydraulic-simulation/burst-location": "burst-location",
|
||||
"/hydraulic-simulation/burst-simulation": "burst-simulation",
|
||||
"/hydraulic-simulation/contaminant-simulation": "contaminant-simulation",
|
||||
"/hydraulic-simulation/dma-leak-detection": "dma-leak-detection",
|
||||
"/hydraulic-simulation/flushing-analysis": "flushing-analysis",
|
||||
};
|
||||
|
||||
export const mapSkeletonForPath = (
|
||||
pathname: string,
|
||||
): MapSkeletonVariant | undefined => mapPaths[pathname];
|
||||
|
||||
export const pageTitleForPath = (pathname: string) => {
|
||||
const variant = mapSkeletonForPath(pathname);
|
||||
if (variant) return MAP_SKELETON_CONFIGS[variant].title;
|
||||
return ({
|
||||
"/system-admin": "系统管理",
|
||||
"/audit-logs": "审计日志",
|
||||
"/three-dimensional-scene": "三维场景",
|
||||
} as Record<string, string>)[pathname] ?? "页面";
|
||||
};
|
||||
|
||||
export function MapRouteSkeleton({ pathname }: { pathname: string }) {
|
||||
const variant = mapSkeletonForPath(pathname);
|
||||
if (!variant) return null;
|
||||
return (
|
||||
<>
|
||||
<MapPanelSkeleton variant={variant} />
|
||||
{(variant === "network-simulation" || variant === "health-risk-analysis") && (
|
||||
<MapTimelineSkeleton />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function PageLoadingSkeleton({ pathname }: { pathname?: string }) {
|
||||
const currentPath = usePathname();
|
||||
const path = pathname ?? currentPath;
|
||||
if (path === "/three-dimensional-scene") {
|
||||
return (
|
||||
<Box
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
aria-label="正在加载三维场景"
|
||||
sx={{ height: "100%", minHeight: 0, display: "grid", placeItems: "center", p: 2, boxSizing: "border-box", bgcolor: "#e2e8e9" }}
|
||||
>
|
||||
<Box sx={sceneLoadingSurface}>
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1.25 }}>
|
||||
<CircularProgress size={20} />
|
||||
<Typography sx={{ fontSize: 14, fontWeight: 600, color: "#0f172a" }}>
|
||||
正在准备三维场景
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography sx={{ mt: 1.5, fontSize: 12, lineHeight: 1.75, color: "#334155" }}>
|
||||
正在载入页面,随后将检查本地模型缓存。
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
if (mapSkeletonForPath(path)) {
|
||||
return (
|
||||
<Box sx={{ position: "relative", height: "100%", minHeight: 320, overflow: "hidden", bgcolor: "#f1f5f9" }}>
|
||||
<MapRouteSkeleton pathname={path} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
const title = pageTitleForPath(path);
|
||||
return (
|
||||
<Box
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
aria-label={`正在加载${title}`}
|
||||
sx={{ p: { xs: 2, md: 3 }, height: "100%", boxSizing: "border-box", bgcolor: "background.default" }}
|
||||
>
|
||||
<Typography sx={{ fontSize: 16, fontWeight: 600, mb: 3 }}>正在加载{title}</Typography>
|
||||
<Skeleton variant="rounded" height={40} sx={{ maxWidth: 480, mb: 3 }} />
|
||||
{[0, 1, 2, 3].map((row) => (
|
||||
<Skeleton key={row} variant="rounded" height={56} sx={{ mb: 1.5 }} />
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user