fix(map): use component-level loading skeletons
This commit is contained in:
@@ -1,5 +0,0 @@
|
||||
import { MapSkeleton } from "@components/loading/MapSkeleton";
|
||||
|
||||
export default function Loading() {
|
||||
return <MapSkeleton />;
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import Timeline from "@components/olmap/HealthRiskAnalysis/Timeline";
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
import { MapPanelSkeleton, MapTimelineSkeleton } from "@components/loading/MapComponentSkeletons";
|
||||
import MapToolbar from "@components/olmap/core/Controls/Toolbar";
|
||||
import { HealthRiskProvider } from "@components/olmap/HealthRiskAnalysis/HealthRiskContext";
|
||||
import HealthRiskStatistics from "@components/olmap/HealthRiskAnalysis/HealthRiskStatistics";
|
||||
import PredictDataPanel from "@components/olmap/HealthRiskAnalysis/PredictDataPanel";
|
||||
import StyleLegend from "@components/olmap/core/Controls/StyleLegend";
|
||||
import {
|
||||
RAINBOW_COLORS,
|
||||
@@ -12,6 +12,26 @@ import {
|
||||
} from "@components/olmap/HealthRiskAnalysis/types";
|
||||
import { Box } from "@mui/material";
|
||||
|
||||
const Timeline = dynamic(
|
||||
() => import("@components/olmap/HealthRiskAnalysis/Timeline"),
|
||||
{
|
||||
loading: () => <MapTimelineSkeleton />,
|
||||
},
|
||||
);
|
||||
const HealthRiskStatistics = dynamic(
|
||||
() =>
|
||||
import("@components/olmap/HealthRiskAnalysis/HealthRiskStatistics"),
|
||||
{
|
||||
loading: () => <MapPanelSkeleton variant="health-risk-analysis" />,
|
||||
},
|
||||
);
|
||||
const PredictDataPanel = dynamic(
|
||||
() => import("@components/olmap/HealthRiskAnalysis/PredictDataPanel"),
|
||||
{
|
||||
loading: () => null,
|
||||
},
|
||||
);
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<HealthRiskProvider>
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import { MapSkeleton } from "@components/loading/MapSkeleton";
|
||||
|
||||
export default function Loading() {
|
||||
return <MapSkeleton />;
|
||||
}
|
||||
@@ -1,7 +1,16 @@
|
||||
"use client";
|
||||
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
import { MapPanelSkeleton } from "@components/loading/MapComponentSkeletons";
|
||||
import MapToolbar from "@components/olmap/core/Controls/Toolbar";
|
||||
import BurstDetectionPanel from "@/components/olmap/BurstDetection/BurstDetectionPanel";
|
||||
|
||||
const BurstDetectionPanel = dynamic(
|
||||
() => import("@/components/olmap/BurstDetection/BurstDetectionPanel"),
|
||||
{
|
||||
loading: () => <MapPanelSkeleton variant="burst-detection" />,
|
||||
},
|
||||
);
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import { MapSkeleton } from "@components/loading/MapSkeleton";
|
||||
|
||||
export default function Loading() {
|
||||
return <MapSkeleton />;
|
||||
}
|
||||
@@ -1,7 +1,16 @@
|
||||
"use client";
|
||||
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
import { MapPanelSkeleton } from "@components/loading/MapComponentSkeletons";
|
||||
import MapToolbar from "@components/olmap/core/Controls/Toolbar";
|
||||
import BurstLocationPanel from "@/components/olmap/BurstLocation/BurstLocationPanel";
|
||||
|
||||
const BurstLocationPanel = dynamic(
|
||||
() => import("@/components/olmap/BurstLocation/BurstLocationPanel"),
|
||||
{
|
||||
loading: () => <MapPanelSkeleton variant="burst-location" />,
|
||||
},
|
||||
);
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import { MapSkeleton } from "@components/loading/MapSkeleton";
|
||||
|
||||
export default function Loading() {
|
||||
return <MapSkeleton />;
|
||||
}
|
||||
@@ -1,7 +1,16 @@
|
||||
"use client";
|
||||
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
import { MapPanelSkeleton } from "@components/loading/MapComponentSkeletons";
|
||||
import MapToolbar from "@components/olmap/core/Controls/Toolbar";
|
||||
import BurstPipeAnalysisPanel from "@/components/olmap/BurstSimulation/BurstPipeAnalysisPanel";
|
||||
|
||||
const BurstPipeAnalysisPanel = dynamic(
|
||||
() => import("@/components/olmap/BurstSimulation/BurstPipeAnalysisPanel"),
|
||||
{
|
||||
loading: () => <MapPanelSkeleton variant="burst-simulation" />,
|
||||
},
|
||||
);
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import { MapSkeleton } from "@components/loading/MapSkeleton";
|
||||
|
||||
export default function Loading() {
|
||||
return <MapSkeleton />;
|
||||
}
|
||||
@@ -1,7 +1,16 @@
|
||||
"use client";
|
||||
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
import { MapPanelSkeleton } from "@components/loading/MapComponentSkeletons";
|
||||
import MapToolbar from "@components/olmap/core/Controls/Toolbar";
|
||||
import WaterQualityPanel from "@/components/olmap/ContaminantSimulation/WaterQualityPanel";
|
||||
|
||||
const WaterQualityPanel = dynamic(
|
||||
() => import("@/components/olmap/ContaminantSimulation/WaterQualityPanel"),
|
||||
{
|
||||
loading: () => <MapPanelSkeleton variant="contaminant-simulation" />,
|
||||
},
|
||||
);
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import { MapSkeleton } from "@components/loading/MapSkeleton";
|
||||
|
||||
export default function Loading() {
|
||||
return <MapSkeleton />;
|
||||
}
|
||||
@@ -1,7 +1,16 @@
|
||||
"use client";
|
||||
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
import { MapPanelSkeleton } from "@components/loading/MapComponentSkeletons";
|
||||
import MapToolbar from "@components/olmap/core/Controls/Toolbar";
|
||||
import DMALeakDetectionPanel from "@/components/olmap/DMALeakDetection/DMALeakDetectionPanel";
|
||||
|
||||
const DMALeakDetectionPanel = dynamic(
|
||||
() => import("@/components/olmap/DMALeakDetection/DMALeakDetectionPanel"),
|
||||
{
|
||||
loading: () => <MapPanelSkeleton variant="dma-leak-detection" />,
|
||||
},
|
||||
);
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import { MapSkeleton } from "@components/loading/MapSkeleton";
|
||||
|
||||
export default function Loading() {
|
||||
return <MapSkeleton />;
|
||||
}
|
||||
@@ -1,7 +1,16 @@
|
||||
"use client";
|
||||
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
import { MapPanelSkeleton } from "@components/loading/MapComponentSkeletons";
|
||||
import MapToolbar from "@components/olmap/core/Controls/Toolbar";
|
||||
import FlushingAnalysisPanel from "@/components/olmap/FlushingAnalysis/FlushingAnalysisPanel";
|
||||
|
||||
const FlushingAnalysisPanel = dynamic(
|
||||
() => import("@/components/olmap/FlushingAnalysis/FlushingAnalysisPanel"),
|
||||
{
|
||||
loading: () => <MapPanelSkeleton variant="flushing-analysis" />,
|
||||
},
|
||||
);
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import { MapSkeleton } from "@components/loading/MapSkeleton";
|
||||
|
||||
export default function Loading() {
|
||||
return <MapSkeleton />;
|
||||
}
|
||||
@@ -1,7 +1,21 @@
|
||||
"use client";
|
||||
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
import { MapPanelSkeleton } from "@components/loading/MapComponentSkeletons";
|
||||
import MapToolbar from "@components/olmap/core/Controls/Toolbar";
|
||||
import MonitoringPlaceOptimizationPanel from "@components/olmap/MonitoringPlaceOptimization/MonitoringPlaceOptimizationPanel";
|
||||
|
||||
const MonitoringPlaceOptimizationPanel = dynamic(
|
||||
() =>
|
||||
import(
|
||||
"@components/olmap/MonitoringPlaceOptimization/MonitoringPlaceOptimizationPanel"
|
||||
),
|
||||
{
|
||||
loading: () => (
|
||||
<MapPanelSkeleton variant="monitoring-place-optimization" />
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import { MapSkeleton } from "@components/loading/MapSkeleton";
|
||||
|
||||
export default function Loading() {
|
||||
return <MapSkeleton />;
|
||||
}
|
||||
@@ -1,11 +1,32 @@
|
||||
"use client";
|
||||
|
||||
import dynamic from "next/dynamic";
|
||||
import { useCallback, useState } from "react";
|
||||
import Timeline from "@components/olmap/core/Controls/Timeline";
|
||||
|
||||
import {
|
||||
MapPanelSkeleton,
|
||||
MapTimelineSkeleton,
|
||||
} from "@components/loading/MapComponentSkeletons";
|
||||
import MapToolbar from "@components/olmap/core/Controls/Toolbar";
|
||||
|
||||
import SCADADeviceList from "@components/olmap/SCADA/SCADADeviceList";
|
||||
import SCADADataPanel from "@components/olmap/SCADA/SCADADataPanel";
|
||||
const Timeline = dynamic(
|
||||
() => import("@components/olmap/core/Controls/Timeline"),
|
||||
{
|
||||
loading: () => <MapTimelineSkeleton />,
|
||||
},
|
||||
);
|
||||
const SCADADeviceList = dynamic(
|
||||
() => import("@components/olmap/SCADA/SCADADeviceList"),
|
||||
{
|
||||
loading: () => <MapPanelSkeleton variant="network-simulation" />,
|
||||
},
|
||||
);
|
||||
const SCADADataPanel = dynamic(
|
||||
() => import("@components/olmap/SCADA/SCADADataPanel"),
|
||||
{
|
||||
loading: () => null,
|
||||
},
|
||||
);
|
||||
|
||||
export default function Home() {
|
||||
const [selectedDeviceIds, setSelectedDeviceIds] = useState<string[]>([]);
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import { MapSkeleton } from "@components/loading/MapSkeleton";
|
||||
|
||||
export default function Loading() {
|
||||
return <MapSkeleton />;
|
||||
}
|
||||
@@ -1,10 +1,23 @@
|
||||
"use client";
|
||||
|
||||
import dynamic from "next/dynamic";
|
||||
import { useCallback, useState } from "react";
|
||||
|
||||
import { MapPanelSkeleton } from "@components/loading/MapComponentSkeletons";
|
||||
import MapToolbar from "@components/olmap/core/Controls/Toolbar";
|
||||
|
||||
import SCADADeviceList from "@components/olmap/SCADA/SCADADeviceList";
|
||||
import SCADADataPanel from "@components/olmap/SCADA/SCADADataPanel";
|
||||
const SCADADeviceList = dynamic(
|
||||
() => import("@components/olmap/SCADA/SCADADeviceList"),
|
||||
{
|
||||
loading: () => <MapPanelSkeleton variant="scada-data-cleaning" />,
|
||||
},
|
||||
);
|
||||
const SCADADataPanel = dynamic(
|
||||
() => import("@components/olmap/SCADA/SCADADataPanel"),
|
||||
{
|
||||
loading: () => null,
|
||||
},
|
||||
);
|
||||
|
||||
export default function Home() {
|
||||
const [selectedDeviceIds, setSelectedDeviceIds] = useState<string[]>([]);
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import type { Metadata } from "next";
|
||||
import { cookies } from "next/headers";
|
||||
import React, { Suspense } from "react";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import authOptions from "@app/api/auth/[...nextauth]/options";
|
||||
import { Header } from "@components/header";
|
||||
import { Title } from "@components/title";
|
||||
import { MapSkeleton } from "@components/loading/MapSkeleton";
|
||||
import { AppSider } from "@components/sider/AppSider";
|
||||
import { ThemedLayout } from "@refinedev/mui";
|
||||
import { getServerSession } from "next-auth/next";
|
||||
@@ -20,7 +19,7 @@ export const metadata: Metadata = META_DATA;
|
||||
export default async function MainLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
children: ReactNode;
|
||||
}>) {
|
||||
const cookieStore = await cookies();
|
||||
const theme = cookieStore.get("theme");
|
||||
@@ -49,9 +48,7 @@ export default async function MainLayout({
|
||||
sx: { height: "100vh", overflow: "hidden" },
|
||||
}}
|
||||
>
|
||||
<Suspense fallback={<MapSkeleton />}>
|
||||
{children}
|
||||
</Suspense>
|
||||
</ThemedLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,524 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
import { render, screen, within } from "@testing-library/react";
|
||||
|
||||
import {
|
||||
MapPanelSkeleton,
|
||||
MapTimelineSkeleton,
|
||||
} from "./MapComponentSkeletons";
|
||||
import {
|
||||
MAP_SKELETON_CONFIGS,
|
||||
MAP_SKELETON_VARIANTS,
|
||||
type MapSkeletonVariant,
|
||||
} from "./mapComponentSkeletonConfig";
|
||||
|
||||
describe("map component skeletons", () => {
|
||||
it("keeps the loading state inside the analysis panel geometry", () => {
|
||||
render(<MapPanelSkeleton variant="burst-simulation" />);
|
||||
|
||||
const panel = screen.getByRole("status", {
|
||||
name: "正在加载爆管分析",
|
||||
});
|
||||
|
||||
expect(panel).toHaveAttribute("data-component-skeleton", "map-panel");
|
||||
expect(panel).toHaveAttribute("data-panel-side", "right");
|
||||
expect(panel).toHaveAttribute("data-panel-width", "520");
|
||||
expect(panel).toHaveStyle({
|
||||
position: "absolute",
|
||||
pointerEvents: "none",
|
||||
});
|
||||
expect(screen.getByTestId("burst-simulation-content")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("keeps an explicit panel configuration for every heavy map component", () => {
|
||||
expect(Object.keys(MAP_SKELETON_CONFIGS).sort()).toEqual(
|
||||
[...MAP_SKELETON_VARIANTS].sort(),
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps known panel chrome out of the skeleton layer", () => {
|
||||
render(<MapPanelSkeleton variant="burst-simulation" />);
|
||||
|
||||
const header = screen.getByTestId("map-panel-header");
|
||||
const tabs = screen.getByTestId("map-panel-tabs");
|
||||
|
||||
expect(header.querySelector(".MuiSkeleton-root")).not.toBeInTheDocument();
|
||||
expect(tabs.querySelector(".MuiSkeleton-root")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("正在加载组件")).not.toBeInTheDocument();
|
||||
expect(
|
||||
within(tabs).getByText("分析要件"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it.each(MAP_SKELETON_VARIANTS)(
|
||||
"renders the component-specific panel for %s",
|
||||
(variant: MapSkeletonVariant) => {
|
||||
const config = MAP_SKELETON_CONFIGS[variant];
|
||||
|
||||
render(<MapPanelSkeleton variant={variant} />);
|
||||
|
||||
const panel = screen.getByRole("status", {
|
||||
name: `正在加载${config.title}`,
|
||||
});
|
||||
expect(panel).toHaveAttribute("data-component-skeleton", "map-panel");
|
||||
expect(panel).toHaveAttribute("data-panel-side", config.side);
|
||||
expect(panel).toHaveAttribute(
|
||||
"data-panel-width",
|
||||
String(config.panelWidth),
|
||||
);
|
||||
expect(screen.getByText(config.panelTitle)).toBeInTheDocument();
|
||||
expect(screen.queryAllByTestId("map-panel-tab-label")).toHaveLength(
|
||||
config.tabLabels.length,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it("distinguishes cleaning controls from the simulation device list", () => {
|
||||
const { rerender } = render(
|
||||
<MapPanelSkeleton variant="network-simulation" />,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("network-device-content")).toBeInTheDocument();
|
||||
|
||||
rerender(<MapPanelSkeleton variant="scada-data-cleaning" />);
|
||||
|
||||
expect(screen.getByTestId("cleaning-device-content")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("cleaning-action-skeleton")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("matches the DMA parameter panel's vertical control distribution", () => {
|
||||
render(<MapPanelSkeleton variant="dma-leak-detection" />);
|
||||
|
||||
const fields = screen.getByTestId("dma-primary-fields");
|
||||
|
||||
expect(screen.getByTestId("dma-notice-skeleton")).toBeInTheDocument();
|
||||
expect(fields).toHaveAttribute("data-layout", "vertical");
|
||||
expect(fields.children).toHaveLength(5);
|
||||
expect(screen.getByTestId("dma-advanced-skeleton")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("dma-primary-action-skeleton")).toHaveStyle({
|
||||
width: "100%",
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
["burst-detection", "burst-detection-primary-fields", "vertical", 3],
|
||||
["burst-location", "burst-location-primary-fields", "vertical", 2],
|
||||
["burst-simulation", "burst-simulation-primary-fields", "vertical", 3],
|
||||
[
|
||||
"contaminant-simulation",
|
||||
"contaminant-simulation-primary-fields",
|
||||
"vertical",
|
||||
4,
|
||||
],
|
||||
[
|
||||
"flushing-analysis",
|
||||
"flushing-primary-fields",
|
||||
"vertical-vertical-two-columns",
|
||||
3,
|
||||
],
|
||||
[
|
||||
"monitoring-place-optimization",
|
||||
"monitoring-primary-fields",
|
||||
"vertical",
|
||||
5,
|
||||
],
|
||||
] as const)(
|
||||
"matches the primary control distribution for %s",
|
||||
(variant, testId, layout, childCount) => {
|
||||
render(<MapPanelSkeleton variant={variant} />);
|
||||
|
||||
const fields = screen.getByTestId(testId);
|
||||
|
||||
expect(fields).toHaveAttribute("data-layout", layout);
|
||||
expect(fields.children).toHaveLength(childCount);
|
||||
},
|
||||
);
|
||||
|
||||
it("keeps the two flushing selection regions separate", () => {
|
||||
render(<MapPanelSkeleton variant="flushing-analysis" />);
|
||||
|
||||
expect(screen.getByTestId("flushing-valve-selection")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("flushing-node-selection")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders the timeline as an independent loading boundary", () => {
|
||||
render(<MapTimelineSkeleton />);
|
||||
|
||||
expect(
|
||||
screen.getByRole("status", { name: "正在加载时间轴" }),
|
||||
).toHaveAttribute("data-component-skeleton", "map-timeline");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,160 @@
|
||||
"use client";
|
||||
|
||||
import { Box, CircularProgress, Typography } from "@mui/material";
|
||||
|
||||
import { MapPageSkeletonContent } from "./MapComponentSkeletonLayouts";
|
||||
import {
|
||||
MAP_SKELETON_CONFIGS,
|
||||
type MapSkeletonVariant,
|
||||
} from "./mapComponentSkeletonConfig";
|
||||
|
||||
export { MapTimelineSkeleton } from "./MapComponentSkeletonLayouts";
|
||||
|
||||
export interface MapPanelSkeletonProps {
|
||||
variant: MapSkeletonVariant;
|
||||
}
|
||||
|
||||
/**
|
||||
* 地图业务面板的组件级加载占位。
|
||||
* 只占据最终面板区域,不替换地图、工具栏或其他已经完成加载的组件。
|
||||
*/
|
||||
export function MapPanelSkeleton({ variant }: MapPanelSkeletonProps) {
|
||||
const config = MAP_SKELETON_CONFIGS[variant];
|
||||
const horizontalPosition =
|
||||
config.side === "left"
|
||||
? { left: { xs: 12, md: 16 }, right: "auto" }
|
||||
: { right: { xs: 12, md: 16 }, left: "auto" };
|
||||
|
||||
return (
|
||||
<Box
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
aria-label={`正在加载${config.title}`}
|
||||
data-component-skeleton="map-panel"
|
||||
data-testid="map-panel-skeleton"
|
||||
data-panel-side={config.side}
|
||||
data-panel-width={config.panelWidth}
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: { xs: 12, md: config.panelTop },
|
||||
width: {
|
||||
xs: "calc(100% - 24px)",
|
||||
sm: `min(${Math.min(config.panelWidth, 640)}px, calc(100% - 32px))`,
|
||||
md: config.panelWidth,
|
||||
},
|
||||
height: {
|
||||
xs: "calc(100% - 24px)",
|
||||
md: `min(${config.panelMaxHeight}px, calc(100% - ${config.panelTop + 16}px))`,
|
||||
},
|
||||
zIndex: 1300,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
overflow: "hidden",
|
||||
bgcolor: "rgba(255, 255, 255, 0.97)",
|
||||
borderRadius: 1.5,
|
||||
boxShadow:
|
||||
"0 20px 34px rgba(15, 23, 42, 0.18), 0 4px 10px rgba(15, 23, 42, 0.08)",
|
||||
pointerEvents: "none",
|
||||
...horizontalPosition,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
data-testid="map-panel-header"
|
||||
sx={{
|
||||
minHeight: 64,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 1.25,
|
||||
px: 2.5,
|
||||
py: 1.5,
|
||||
bgcolor: "#257DD4",
|
||||
color: "#fff",
|
||||
}}
|
||||
>
|
||||
<Box sx={{ flex: 1, minWidth: 0 }}>
|
||||
<Typography
|
||||
component="span"
|
||||
lang="zh-CN"
|
||||
sx={{
|
||||
display: "block",
|
||||
fontSize: 18,
|
||||
fontWeight: 600,
|
||||
lineHeight: 1.4,
|
||||
}}
|
||||
>
|
||||
{config.panelTitle}
|
||||
</Typography>
|
||||
</Box>
|
||||
<CircularProgress
|
||||
aria-label={`正在加载${config.title}`}
|
||||
size={20}
|
||||
thickness={4}
|
||||
sx={{ color: "rgba(255, 255, 255, 0.90)" }}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{config.tabLabels.length > 0 && (
|
||||
<Box
|
||||
data-testid="map-panel-tabs"
|
||||
sx={{
|
||||
minHeight: 48,
|
||||
display: "grid",
|
||||
gridTemplateColumns: `repeat(${config.tabLabels.length}, minmax(0, 1fr))`,
|
||||
alignItems: "center",
|
||||
gap: 1,
|
||||
px: 2,
|
||||
bgcolor: "#fff",
|
||||
boxShadow: "inset 0 -1px 0 rgba(15, 23, 42, 0.08)",
|
||||
}}
|
||||
>
|
||||
{config.tabLabels.map((label, index) => (
|
||||
<Box
|
||||
key={label}
|
||||
data-testid="map-panel-tab-label"
|
||||
sx={{ display: "grid", justifyItems: "center", gap: 0.5 }}
|
||||
>
|
||||
<Typography
|
||||
component="span"
|
||||
lang="zh-CN"
|
||||
sx={{
|
||||
fontSize: 14,
|
||||
fontWeight: 500,
|
||||
lineHeight: 1.5,
|
||||
color: index === 0 ? "#257DD4" : "text.secondary",
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</Typography>
|
||||
{index === 0 && (
|
||||
<Box
|
||||
sx={{
|
||||
width: "78%",
|
||||
height: 2,
|
||||
bgcolor: "rgba(37, 125, 212, 0.34)",
|
||||
borderRadius: 999,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
flex: 1,
|
||||
minHeight: 0,
|
||||
overflow: "hidden",
|
||||
p: variant === "health-risk-analysis" ? 2 : 2.5,
|
||||
bgcolor:
|
||||
variant === "network-simulation" ||
|
||||
variant === "scada-data-cleaning"
|
||||
? "rgba(248, 250, 252, 0.98)"
|
||||
: "#fff",
|
||||
}}
|
||||
>
|
||||
<MapPageSkeletonContent variant={variant} />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -1,191 +0,0 @@
|
||||
import { Box, Skeleton, CircularProgress } from "@mui/material";
|
||||
|
||||
/**
|
||||
* 地图页面骨架屏组件
|
||||
* 提供即时视觉反馈,模拟地图界面布局
|
||||
*/
|
||||
export function MapSkeleton() {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
position: "relative",
|
||||
bgcolor: "background.default",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
{/* 主地图区域骨架 */}
|
||||
<Skeleton
|
||||
variant="rectangular"
|
||||
animation="wave"
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
bgcolor: "action.hover",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 中央加载指示器 */}
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
zIndex: 10,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
<CircularProgress size={48} thickness={4} color="primary" />
|
||||
</Box>
|
||||
|
||||
{/* 左侧工具栏骨架 (垂直) */}
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: 20,
|
||||
left: 20,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 1.5,
|
||||
zIndex: 5,
|
||||
}}
|
||||
>
|
||||
{[1, 2, 3, 4].map((i) => (
|
||||
<Skeleton
|
||||
key={i}
|
||||
variant="circular"
|
||||
width={40}
|
||||
height={40}
|
||||
animation="wave"
|
||||
sx={{ boxShadow: 1 }}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
{/* 右侧控制面板骨架 (抽屉式) */}
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
right: 0,
|
||||
width: { xs: "100%", sm: 360 },
|
||||
height: "100%",
|
||||
bgcolor: "background.paper",
|
||||
borderLeft: 1,
|
||||
borderColor: "divider",
|
||||
p: 3,
|
||||
zIndex: 5,
|
||||
display: { xs: "none", md: "flex" },
|
||||
flexDirection: "column",
|
||||
boxShadow: -2,
|
||||
}}
|
||||
>
|
||||
<Skeleton variant="text" width="60%" height={40} sx={{ mb: 3 }} />
|
||||
|
||||
{/* 面板内容区块 */}
|
||||
<Box sx={{ flex: 1, overflow: "hidden" }}>
|
||||
<Skeleton variant="rectangular" width="100%" height={100} sx={{ mb: 2, borderRadius: 1 }} />
|
||||
<Skeleton variant="text" width="40%" height={24} sx={{ mb: 1 }} />
|
||||
<Skeleton variant="rectangular" width="100%" height={180} sx={{ mb: 2, borderRadius: 1 }} />
|
||||
|
||||
<Box sx={{ mt: 2 }}>
|
||||
{[1, 2, 3].map((i) => (
|
||||
<Box key={i} sx={{ display: "flex", gap: 2, mb: 2 }}>
|
||||
<Skeleton variant="circular" width={36} height={36} />
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Skeleton variant="text" width="80%" />
|
||||
<Skeleton variant="text" width="50%" />
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* 底部时间轴/控制条骨架 */}
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
bottom: 30,
|
||||
left: "50%",
|
||||
transform: "translateX(-50%)",
|
||||
width: { xs: "90%", md: "60%" },
|
||||
height: 64,
|
||||
bgcolor: "background.paper",
|
||||
borderRadius: 4,
|
||||
boxShadow: 3,
|
||||
p: 2,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 2,
|
||||
zIndex: 5,
|
||||
}}
|
||||
>
|
||||
<Skeleton variant="circular" width={32} height={32} />
|
||||
<Skeleton variant="rectangular" width="100%" height={8} sx={{ borderRadius: 4 }} />
|
||||
<Skeleton variant="text" width={40} />
|
||||
</Box>
|
||||
|
||||
{/* 缩放控制骨架 (右下) */}
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
bottom: 110,
|
||||
right: { xs: 20, md: 380 }, // Adjust if drawer is open
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 1,
|
||||
zIndex: 4,
|
||||
}}
|
||||
>
|
||||
<Skeleton variant="rectangular" width={36} height={36} sx={{ borderRadius: 1 }} />
|
||||
<Skeleton variant="rectangular" width={36} height={36} sx={{ borderRadius: 1 }} />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 简化版骨架屏 - 用于非地图页面
|
||||
*/
|
||||
export function SimpleSkeleton() {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
p: 3,
|
||||
bgcolor: "background.default",
|
||||
}}
|
||||
>
|
||||
<Skeleton width="40%" height={40} animation="wave" sx={{ mb: 3 }} />
|
||||
<Skeleton width="100%" height={60} animation="wave" sx={{ mb: 2 }} />
|
||||
<Skeleton width="100%" height={300} animation="wave" sx={{ mb: 2 }} />
|
||||
<Box sx={{ display: "flex", gap: 2, mb: 2 }}>
|
||||
<Skeleton
|
||||
variant="rectangular"
|
||||
width="30%"
|
||||
height={150}
|
||||
animation="wave"
|
||||
/>
|
||||
<Skeleton
|
||||
variant="rectangular"
|
||||
width="30%"
|
||||
height={150}
|
||||
animation="wave"
|
||||
/>
|
||||
<Skeleton
|
||||
variant="rectangular"
|
||||
width="30%"
|
||||
height={150}
|
||||
animation="wave"
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
export const MAP_SKELETON_VARIANTS = [
|
||||
"network-simulation",
|
||||
"scada-data-cleaning",
|
||||
"health-risk-analysis",
|
||||
"monitoring-place-optimization",
|
||||
"burst-detection",
|
||||
"burst-location",
|
||||
"burst-simulation",
|
||||
"contaminant-simulation",
|
||||
"dma-leak-detection",
|
||||
"flushing-analysis",
|
||||
] as const;
|
||||
|
||||
export type MapSkeletonVariant = (typeof MAP_SKELETON_VARIANTS)[number];
|
||||
|
||||
export interface MapSkeletonConfig {
|
||||
title: string;
|
||||
panelTitle: string;
|
||||
side: "left" | "right";
|
||||
panelWidth: number;
|
||||
panelTop: number;
|
||||
panelMaxHeight: number;
|
||||
tabLabels: readonly string[];
|
||||
}
|
||||
|
||||
export const MAP_SKELETON_CONFIGS: Record<
|
||||
MapSkeletonVariant,
|
||||
MapSkeletonConfig
|
||||
> = {
|
||||
"network-simulation": {
|
||||
title: "管网模拟",
|
||||
panelTitle: "SCADA 设备列表",
|
||||
side: "left",
|
||||
panelWidth: 360,
|
||||
panelTop: 80,
|
||||
panelMaxHeight: 860,
|
||||
tabLabels: [],
|
||||
},
|
||||
"scada-data-cleaning": {
|
||||
title: "数据清洗",
|
||||
panelTitle: "SCADA 设备列表",
|
||||
side: "left",
|
||||
panelWidth: 360,
|
||||
panelTop: 80,
|
||||
panelMaxHeight: 860,
|
||||
tabLabels: [],
|
||||
},
|
||||
"health-risk-analysis": {
|
||||
title: "健康风险分析",
|
||||
panelTitle: "管道健康风险统计",
|
||||
side: "right",
|
||||
panelWidth: 640,
|
||||
panelTop: 16,
|
||||
panelMaxHeight: 614,
|
||||
tabLabels: [],
|
||||
},
|
||||
"monitoring-place-optimization": {
|
||||
title: "监测点优化",
|
||||
panelTitle: "监测点优化",
|
||||
side: "right",
|
||||
panelWidth: 520,
|
||||
panelTop: 16,
|
||||
panelMaxHeight: 850,
|
||||
tabLabels: ["优化要件", "结果编辑", "方案查询"],
|
||||
},
|
||||
"burst-detection": {
|
||||
title: "爆管侦测",
|
||||
panelTitle: "爆管侦测",
|
||||
side: "right",
|
||||
panelWidth: 450,
|
||||
panelTop: 16,
|
||||
panelMaxHeight: 850,
|
||||
tabLabels: ["侦测参数", "方案查询", "侦测结果"],
|
||||
},
|
||||
"burst-location": {
|
||||
title: "爆管定位",
|
||||
panelTitle: "爆管定位",
|
||||
side: "right",
|
||||
panelWidth: 450,
|
||||
panelTop: 16,
|
||||
panelMaxHeight: 850,
|
||||
tabLabels: ["定位参数", "方案查询", "定位结果"],
|
||||
},
|
||||
"burst-simulation": {
|
||||
title: "爆管分析",
|
||||
panelTitle: "爆管分析",
|
||||
side: "right",
|
||||
panelWidth: 520,
|
||||
panelTop: 16,
|
||||
panelMaxHeight: 850,
|
||||
tabLabels: ["分析要件", "方案查询", "分析报告", "关阀分析"],
|
||||
},
|
||||
"contaminant-simulation": {
|
||||
title: "水质模拟",
|
||||
panelTitle: "水质模拟",
|
||||
side: "right",
|
||||
panelWidth: 520,
|
||||
panelTop: 16,
|
||||
panelMaxHeight: 850,
|
||||
tabLabels: ["分析要件", "方案查询", "模拟结果"],
|
||||
},
|
||||
"dma-leak-detection": {
|
||||
title: "DMA 漏损识别",
|
||||
panelTitle: "DMA 漏损识别",
|
||||
side: "right",
|
||||
panelWidth: 450,
|
||||
panelTop: 16,
|
||||
panelMaxHeight: 850,
|
||||
tabLabels: ["识别参数", "方案查询", "识别结果"],
|
||||
},
|
||||
"flushing-analysis": {
|
||||
title: "管道冲洗分析",
|
||||
panelTitle: "管道冲洗分析",
|
||||
side: "right",
|
||||
panelWidth: 450,
|
||||
panelTop: 16,
|
||||
panelMaxHeight: 850,
|
||||
tabLabels: ["分析参数", "方案查询"],
|
||||
},
|
||||
};
|
||||
@@ -35,7 +35,7 @@ interface ToolbarProps {
|
||||
hiddenButtons?: string[]; // 可选的隐藏按钮列表,例如 ['info', 'draw', 'style']
|
||||
queryType?: string; // 可选的查询类型参数
|
||||
schemeType?: string; // 可选的方案类型参数
|
||||
HistoryPanel?: React.FC<any>; // 可选的自定义历史数据面板
|
||||
HistoryPanel?: React.ComponentType<any>; // 可选的自定义历史数据面板
|
||||
enableCompare?: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ type ToolbarHistoryPanelProps = {
|
||||
endTime?: string;
|
||||
} | null;
|
||||
highlightFeatures: Feature[];
|
||||
HistoryPanel?: React.FC<any>;
|
||||
HistoryPanel?: React.ComponentType<any>;
|
||||
schemeName?: string;
|
||||
queryType?: string;
|
||||
onClose: () => void;
|
||||
|
||||
Reference in New Issue
Block a user