Files
TJWaterFrontend_Refine/src/components/loading/MapComponentSkeletonLayouts.tsx
T

525 lines
12 KiB
TypeScript

import { Box, Skeleton } from "@mui/material";
import type { MapSkeletonVariant } from "./mapComponentSkeletonConfig";
const skeletonSx = {
transform: "none",
bgcolor: "rgba(37, 125, 212, 0.10)",
"&::after": {
background:
"linear-gradient(90deg, transparent, rgba(37, 125, 212, 0.12), transparent)",
},
"@media (prefers-reduced-motion: reduce)": {
animation: "none",
"&::after": {
animation: "none",
},
},
} as const;
const Line = ({ width = "44%", height = 18 }: { width?: string; height?: number }) => (
<Skeleton
variant="text"
animation="wave"
width={width}
height={height}
sx={skeletonSx}
/>
);
const Field = () => (
<Box sx={{ width: "100%" }}>
<Line width="34%" height={18} />
<Skeleton
variant="rounded"
animation="wave"
height={40}
sx={{ ...skeletonSx, mt: 0.5, borderRadius: 1 }}
/>
</Box>
);
const TwoColumns = ({ children }: { children: React.ReactNode }) => (
<Box
sx={{
display: "grid",
gridTemplateColumns: "repeat(2, minmax(0, 1fr))",
gap: 1.5,
}}
>
{children}
</Box>
);
const Action = ({
width = "100%",
height = 40,
testId,
}: {
width?: string;
height?: number;
testId?: string;
}) => (
<Skeleton
data-testid={testId}
variant="rounded"
animation="wave"
width={width}
height={height}
sx={{ ...skeletonSx, borderRadius: 1 }}
/>
);
const Notice = ({ testId }: { testId?: string }) => (
<Box
data-testid={testId}
sx={{
display: "grid",
gap: 0.5,
p: 1.5,
bgcolor: "rgba(37, 125, 212, 0.07)",
borderRadius: 1,
}}
>
<Line width="58%" height={18} />
<Line width="88%" height={16} />
</Box>
);
const SelectionHeader = ({ actionWidth = "34%" }: { actionWidth?: string }) => (
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
<Box sx={{ flex: 1 }}>
<Line width="44%" height={20} />
</Box>
<Action width={actionWidth} height={32} />
</Box>
);
const EmptySelection = ({ height = 44 }: { height?: number }) => (
<Skeleton
variant="rounded"
animation="wave"
height={height}
sx={{ ...skeletonSx, borderRadius: 1 }}
/>
);
const Divider = () => (
<Box sx={{ height: 1, flex: "0 0 auto", bgcolor: "rgba(15, 23, 42, 0.08)" }} />
);
const AdvancedRow = ({ testId }: { testId?: string }) => (
<Box
data-testid={testId}
sx={{
minHeight: 40,
display: "flex",
alignItems: "center",
gap: 1,
px: 1.25,
border: "1px solid",
borderColor: "rgba(15, 23, 42, 0.10)",
borderRadius: 1,
}}
>
<Box sx={{ flex: 1 }}>
<Line width="34%" height={18} />
</Box>
<Skeleton
variant="circular"
animation="wave"
width={24}
height={24}
sx={skeletonSx}
/>
</Box>
);
const SelectionList = ({ rows = 3 }: { rows?: number }) => (
<Box
sx={{
display: "grid",
gap: 1,
p: 1.25,
bgcolor: "rgba(15, 23, 42, 0.025)",
borderRadius: 1,
}}
>
{Array.from({ length: rows }, (_, index) => (
<Box
key={index}
sx={{ display: "flex", alignItems: "center", gap: 1 }}
>
<Skeleton
variant="circular"
animation="wave"
width={26}
height={26}
sx={skeletonSx}
/>
<Box sx={{ flex: 1 }}>
<Line width={`${72 - index * 8}%`} height={16} />
</Box>
<Skeleton
variant="rounded"
animation="wave"
width={48}
height={26}
sx={{ ...skeletonSx, borderRadius: 1 }}
/>
</Box>
))}
</Box>
);
const AnalysisFormSkeleton = ({ variant }: { variant: MapSkeletonVariant }) => {
switch (variant) {
case "burst-detection":
return (
<>
<Box
data-testid="burst-detection-primary-fields"
data-layout="vertical"
sx={{ display: "grid", gap: 1.5 }}
>
<Field />
<Field />
<Field />
</Box>
<Notice />
<Line width="88%" height={16} />
<Box sx={{ display: "flex", gap: 1.5, mt: "auto", pt: 1.5 }}>
<Action />
<Action />
</Box>
</>
);
case "burst-location":
return (
<>
<Box
data-testid="burst-location-primary-fields"
data-layout="vertical"
sx={{ display: "grid", gap: 1.5 }}
>
<Field />
<Field />
</Box>
<Notice />
<TwoColumns>
<Field />
<Field />
</TwoColumns>
<Field />
<AdvancedRow />
<Box sx={{ mt: "auto", pt: 1.5 }}>
<Action />
</Box>
</>
);
case "burst-simulation":
return (
<>
<Box
data-testid="burst-simulation-selection"
sx={{ display: "grid", gap: 1 }}
>
<SelectionHeader actionWidth="32%" />
</Box>
<Box
data-testid="burst-simulation-primary-fields"
data-layout="vertical"
sx={{ display: "grid", gap: 1.5 }}
>
<Field />
<Field />
<Field />
</Box>
<Box sx={{ mt: "auto", pt: 1.5 }}>
<Action />
</Box>
</>
);
case "contaminant-simulation":
return (
<>
<Box
data-testid="contaminant-simulation-selection"
sx={{ display: "grid", gap: 1 }}
>
<SelectionHeader actionWidth="32%" />
<EmptySelection />
</Box>
<Box
data-testid="contaminant-simulation-primary-fields"
data-layout="vertical"
sx={{ display: "grid", gap: 1.5 }}
>
{Array.from({ length: 4 }, (_, index) => (
<Field key={index} />
))}
</Box>
<Box sx={{ mt: "auto", pt: 1.5 }}>
<Action />
</Box>
</>
);
case "dma-leak-detection":
return (
<>
<Notice testId="dma-notice-skeleton" />
<Box
data-testid="dma-primary-fields"
data-layout="vertical"
sx={{ display: "grid", gap: 1.5 }}
>
{Array.from({ length: 5 }, (_, index) => (
<Field key={index} />
))}
</Box>
<AdvancedRow testId="dma-advanced-skeleton" />
<Box sx={{ mt: "auto", pt: 1.5 }}>
<Action testId="dma-primary-action-skeleton" />
</Box>
</>
);
case "flushing-analysis":
return (
<>
<Box
data-testid="flushing-valve-selection"
sx={{ display: "grid", gap: 1 }}
>
<SelectionHeader actionWidth="32%" />
<EmptySelection height={192} />
</Box>
<Divider />
<Box
data-testid="flushing-node-selection"
sx={{ display: "grid", gap: 1 }}
>
<SelectionHeader actionWidth="32%" />
<EmptySelection height={48} />
</Box>
<Divider />
<Box
data-testid="flushing-primary-fields"
data-layout="vertical-vertical-two-columns"
sx={{ display: "grid", gap: 1.5 }}
>
<Field />
<Field />
<TwoColumns>
<Field />
<Field />
</TwoColumns>
</Box>
<Box sx={{ mt: "auto", pt: 1.5 }}>
<Action />
</Box>
</>
);
case "monitoring-place-optimization":
return (
<>
<Box
data-testid="monitoring-primary-fields"
data-layout="vertical"
sx={{ display: "grid", gap: 2 }}
>
{Array.from({ length: 5 }, (_, index) => (
<Field key={index} />
))}
</Box>
<Box sx={{ display: "flex", justifyContent: "flex-end", mt: 1 }}>
<Action width="132px" />
</Box>
</>
);
default:
return null;
}
};
const DeviceListSkeleton = ({ cleaning }: { cleaning: boolean }) => (
<Box
data-testid={cleaning ? "cleaning-device-content" : "network-device-content"}
sx={{ display: "grid", gap: 2 }}
>
<Skeleton
variant="rounded"
animation="wave"
height={40}
sx={{ ...skeletonSx, borderRadius: 1 }}
/>
<Box
sx={{
display: "grid",
gridTemplateColumns: "repeat(3, minmax(0, 1fr))",
gap: 1,
}}
>
{Array.from({ length: 3 }, (_, index) => (
<Skeleton
key={index}
variant="rounded"
animation="wave"
height={38}
sx={{ ...skeletonSx, borderRadius: 1 }}
/>
))}
</Box>
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
<Box sx={{ flex: 1 }}>
<Line width="58%" height={16} />
</Box>
{cleaning && (
<Skeleton
data-testid="cleaning-action-skeleton"
variant="circular"
animation="wave"
width={32}
height={32}
sx={skeletonSx}
/>
)}
<Skeleton
variant="circular"
animation="wave"
width={32}
height={32}
sx={skeletonSx}
/>
</Box>
<Box sx={{ height: 1, bgcolor: "rgba(15, 23, 42, 0.08)" }} />
<SelectionList rows={6} />
</Box>
);
const RiskChartSkeleton = () => (
<Box
data-testid="health-risk-chart-content"
sx={{ display: "grid", height: "100%", minHeight: 360, gap: 2 }}
>
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
<Line width="32%" height={18} />
<Box sx={{ flex: 1 }} />
<Action width="22%" />
</Box>
<Box
sx={{
position: "relative",
display: "flex",
alignItems: "flex-end",
justifyContent: "space-around",
gap: 2,
minHeight: 300,
px: 3,
pt: 3,
pb: 2,
bgcolor: "rgba(15, 23, 42, 0.025)",
borderRadius: 1,
boxShadow: "inset 0 -1px 0 rgba(15, 23, 42, 0.10)",
}}
>
{[36, 58, 44, 76, 62, 84].map((height, index) => (
<Skeleton
key={index}
variant="rounded"
animation="wave"
width="9%"
height={`${height}%`}
sx={{ ...skeletonSx, borderRadius: "6px 6px 2px 2px" }}
/>
))}
</Box>
</Box>
);
export function MapPageSkeletonContent({
variant,
}: {
variant: MapSkeletonVariant;
}) {
if (variant === "network-simulation") {
return <DeviceListSkeleton cleaning={false} />;
}
if (variant === "scada-data-cleaning") {
return <DeviceListSkeleton cleaning />;
}
if (variant === "health-risk-analysis") {
return <RiskChartSkeleton />;
}
return (
<Box
data-testid={`${variant}-content`}
sx={{
display: "flex",
flexDirection: "column",
gap: 2,
height: "100%",
minHeight: 0,
}}
>
<AnalysisFormSkeleton variant={variant} />
</Box>
);
}
export function MapTimelineSkeleton() {
return (
<Box
role="status"
aria-live="polite"
aria-label="正在加载时间轴"
data-component-skeleton="map-timeline"
data-testid="map-timeline-skeleton"
sx={{
position: "absolute",
left: "50%",
bottom: 16,
width: "min(950px, calc(100% - 32px))",
transform: "translateX(-50%)",
display: "grid",
gap: 1.5,
p: 2,
bgcolor: "rgba(255, 255, 255, 0.96)",
borderRadius: 1.5,
boxShadow:
"0 14px 30px rgba(15, 23, 42, 0.16), 0 2px 6px rgba(15, 23, 42, 0.08)",
pointerEvents: "none",
}}
>
<Box sx={{ display: "flex", alignItems: "center", gap: 1.5 }}>
{Array.from({ length: 5 }, (_, index) => (
<Skeleton
key={index}
variant="circular"
animation="wave"
width={32}
height={32}
sx={skeletonSx}
/>
))}
<Box sx={{ flex: 1 }} />
<Skeleton
variant="rounded"
animation="wave"
width={132}
height={32}
sx={{ ...skeletonSx, borderRadius: 1 }}
/>
</Box>
<Skeleton
variant="rounded"
animation="wave"
height={8}
sx={{ ...skeletonSx, borderRadius: 999 }}
/>
</Box>
);
}