diff --git a/src/app/(main)/(map)/health-risk-analysis/loading.tsx b/src/app/(main)/(map)/health-risk-analysis/loading.tsx
deleted file mode 100644
index 2c57921..0000000
--- a/src/app/(main)/(map)/health-risk-analysis/loading.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-import { MapSkeleton } from "@components/loading/MapSkeleton";
-
-export default function Loading() {
- return ;
-}
diff --git a/src/app/(main)/(map)/health-risk-analysis/page.tsx b/src/app/(main)/(map)/health-risk-analysis/page.tsx
index 779e035..12bb1e2 100644
--- a/src/app/(main)/(map)/health-risk-analysis/page.tsx
+++ b/src/app/(main)/(map)/health-risk-analysis/page.tsx
@@ -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: () => ,
+ },
+);
+const HealthRiskStatistics = dynamic(
+ () =>
+ import("@components/olmap/HealthRiskAnalysis/HealthRiskStatistics"),
+ {
+ loading: () => ,
+ },
+);
+const PredictDataPanel = dynamic(
+ () => import("@components/olmap/HealthRiskAnalysis/PredictDataPanel"),
+ {
+ loading: () => null,
+ },
+);
+
export default function Home() {
return (
diff --git a/src/app/(main)/(map)/hydraulic-simulation/burst-detection/loading.tsx b/src/app/(main)/(map)/hydraulic-simulation/burst-detection/loading.tsx
deleted file mode 100644
index 2c57921..0000000
--- a/src/app/(main)/(map)/hydraulic-simulation/burst-detection/loading.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-import { MapSkeleton } from "@components/loading/MapSkeleton";
-
-export default function Loading() {
- return ;
-}
diff --git a/src/app/(main)/(map)/hydraulic-simulation/burst-detection/page.tsx b/src/app/(main)/(map)/hydraulic-simulation/burst-detection/page.tsx
index ca5408c..91a87fa 100644
--- a/src/app/(main)/(map)/hydraulic-simulation/burst-detection/page.tsx
+++ b/src/app/(main)/(map)/hydraulic-simulation/burst-detection/page.tsx
@@ -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: () => ,
+ },
+);
export default function Home() {
return (
diff --git a/src/app/(main)/(map)/hydraulic-simulation/burst-location/loading.tsx b/src/app/(main)/(map)/hydraulic-simulation/burst-location/loading.tsx
deleted file mode 100644
index 2c57921..0000000
--- a/src/app/(main)/(map)/hydraulic-simulation/burst-location/loading.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-import { MapSkeleton } from "@components/loading/MapSkeleton";
-
-export default function Loading() {
- return ;
-}
diff --git a/src/app/(main)/(map)/hydraulic-simulation/burst-location/page.tsx b/src/app/(main)/(map)/hydraulic-simulation/burst-location/page.tsx
index d5770b1..751a827 100644
--- a/src/app/(main)/(map)/hydraulic-simulation/burst-location/page.tsx
+++ b/src/app/(main)/(map)/hydraulic-simulation/burst-location/page.tsx
@@ -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: () => ,
+ },
+);
export default function Home() {
return (
diff --git a/src/app/(main)/(map)/hydraulic-simulation/burst-simulation/loading.tsx b/src/app/(main)/(map)/hydraulic-simulation/burst-simulation/loading.tsx
deleted file mode 100644
index 2c57921..0000000
--- a/src/app/(main)/(map)/hydraulic-simulation/burst-simulation/loading.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-import { MapSkeleton } from "@components/loading/MapSkeleton";
-
-export default function Loading() {
- return ;
-}
diff --git a/src/app/(main)/(map)/hydraulic-simulation/burst-simulation/page.tsx b/src/app/(main)/(map)/hydraulic-simulation/burst-simulation/page.tsx
index 699997f..1391ee3 100644
--- a/src/app/(main)/(map)/hydraulic-simulation/burst-simulation/page.tsx
+++ b/src/app/(main)/(map)/hydraulic-simulation/burst-simulation/page.tsx
@@ -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: () => ,
+ },
+);
export default function Home() {
return (
diff --git a/src/app/(main)/(map)/hydraulic-simulation/contaminant-simulation/loading.tsx b/src/app/(main)/(map)/hydraulic-simulation/contaminant-simulation/loading.tsx
deleted file mode 100644
index 2c57921..0000000
--- a/src/app/(main)/(map)/hydraulic-simulation/contaminant-simulation/loading.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-import { MapSkeleton } from "@components/loading/MapSkeleton";
-
-export default function Loading() {
- return ;
-}
diff --git a/src/app/(main)/(map)/hydraulic-simulation/contaminant-simulation/page.tsx b/src/app/(main)/(map)/hydraulic-simulation/contaminant-simulation/page.tsx
index 6addcb6..c23ab33 100644
--- a/src/app/(main)/(map)/hydraulic-simulation/contaminant-simulation/page.tsx
+++ b/src/app/(main)/(map)/hydraulic-simulation/contaminant-simulation/page.tsx
@@ -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: () => ,
+ },
+);
export default function Home() {
return (
diff --git a/src/app/(main)/(map)/hydraulic-simulation/dma-leak-detection/loading.tsx b/src/app/(main)/(map)/hydraulic-simulation/dma-leak-detection/loading.tsx
deleted file mode 100644
index 2c57921..0000000
--- a/src/app/(main)/(map)/hydraulic-simulation/dma-leak-detection/loading.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-import { MapSkeleton } from "@components/loading/MapSkeleton";
-
-export default function Loading() {
- return ;
-}
diff --git a/src/app/(main)/(map)/hydraulic-simulation/dma-leak-detection/page.tsx b/src/app/(main)/(map)/hydraulic-simulation/dma-leak-detection/page.tsx
index 39058bb..a11e9b1 100644
--- a/src/app/(main)/(map)/hydraulic-simulation/dma-leak-detection/page.tsx
+++ b/src/app/(main)/(map)/hydraulic-simulation/dma-leak-detection/page.tsx
@@ -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: () => ,
+ },
+);
export default function Home() {
return (
diff --git a/src/app/(main)/(map)/hydraulic-simulation/flushing-analysis/loading.tsx b/src/app/(main)/(map)/hydraulic-simulation/flushing-analysis/loading.tsx
deleted file mode 100644
index 2c57921..0000000
--- a/src/app/(main)/(map)/hydraulic-simulation/flushing-analysis/loading.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-import { MapSkeleton } from "@components/loading/MapSkeleton";
-
-export default function Loading() {
- return ;
-}
diff --git a/src/app/(main)/(map)/hydraulic-simulation/flushing-analysis/page.tsx b/src/app/(main)/(map)/hydraulic-simulation/flushing-analysis/page.tsx
index 19df843..067ea3e 100644
--- a/src/app/(main)/(map)/hydraulic-simulation/flushing-analysis/page.tsx
+++ b/src/app/(main)/(map)/hydraulic-simulation/flushing-analysis/page.tsx
@@ -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: () => ,
+ },
+);
export default function Home() {
return (
diff --git a/src/app/(main)/(map)/monitoring-place-optimization/loading.tsx b/src/app/(main)/(map)/monitoring-place-optimization/loading.tsx
deleted file mode 100644
index 2c57921..0000000
--- a/src/app/(main)/(map)/monitoring-place-optimization/loading.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-import { MapSkeleton } from "@components/loading/MapSkeleton";
-
-export default function Loading() {
- return ;
-}
diff --git a/src/app/(main)/(map)/monitoring-place-optimization/page.tsx b/src/app/(main)/(map)/monitoring-place-optimization/page.tsx
index 0272841..9339ef0 100644
--- a/src/app/(main)/(map)/monitoring-place-optimization/page.tsx
+++ b/src/app/(main)/(map)/monitoring-place-optimization/page.tsx
@@ -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: () => (
+
+ ),
+ },
+);
export default function Home() {
return (
diff --git a/src/app/(main)/(map)/network-simulation/loading.tsx b/src/app/(main)/(map)/network-simulation/loading.tsx
deleted file mode 100644
index 2c57921..0000000
--- a/src/app/(main)/(map)/network-simulation/loading.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-import { MapSkeleton } from "@components/loading/MapSkeleton";
-
-export default function Loading() {
- return ;
-}
diff --git a/src/app/(main)/(map)/network-simulation/page.tsx b/src/app/(main)/(map)/network-simulation/page.tsx
index 705afe1..b85a1d8 100644
--- a/src/app/(main)/(map)/network-simulation/page.tsx
+++ b/src/app/(main)/(map)/network-simulation/page.tsx
@@ -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: () => ,
+ },
+);
+const SCADADeviceList = dynamic(
+ () => import("@components/olmap/SCADA/SCADADeviceList"),
+ {
+ loading: () => ,
+ },
+);
+const SCADADataPanel = dynamic(
+ () => import("@components/olmap/SCADA/SCADADataPanel"),
+ {
+ loading: () => null,
+ },
+);
export default function Home() {
const [selectedDeviceIds, setSelectedDeviceIds] = useState([]);
diff --git a/src/app/(main)/(map)/scada-data-cleaning/loading.tsx b/src/app/(main)/(map)/scada-data-cleaning/loading.tsx
deleted file mode 100644
index 2c57921..0000000
--- a/src/app/(main)/(map)/scada-data-cleaning/loading.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-import { MapSkeleton } from "@components/loading/MapSkeleton";
-
-export default function Loading() {
- return ;
-}
diff --git a/src/app/(main)/(map)/scada-data-cleaning/page.tsx b/src/app/(main)/(map)/scada-data-cleaning/page.tsx
index 592a3b5..155b56e 100644
--- a/src/app/(main)/(map)/scada-data-cleaning/page.tsx
+++ b/src/app/(main)/(map)/scada-data-cleaning/page.tsx
@@ -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: () => ,
+ },
+);
+const SCADADataPanel = dynamic(
+ () => import("@components/olmap/SCADA/SCADADataPanel"),
+ {
+ loading: () => null,
+ },
+);
export default function Home() {
const [selectedDeviceIds, setSelectedDeviceIds] = useState([]);
diff --git a/src/app/(main)/layout.tsx b/src/app/(main)/layout.tsx
index ed82d39..b78c33a 100644
--- a/src/app/(main)/layout.tsx
+++ b/src/app/(main)/layout.tsx
@@ -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" },
}}
>
- }>
- {children}
-
+ {children}
);
}
diff --git a/src/components/loading/MapComponentSkeletonLayouts.tsx b/src/components/loading/MapComponentSkeletonLayouts.tsx
new file mode 100644
index 0000000..42f75d5
--- /dev/null
+++ b/src/components/loading/MapComponentSkeletonLayouts.tsx
@@ -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 }) => (
+
+);
+
+const Field = () => (
+
+
+
+
+);
+
+const TwoColumns = ({ children }: { children: React.ReactNode }) => (
+
+ {children}
+
+);
+
+const Action = ({
+ width = "100%",
+ height = 40,
+ testId,
+}: {
+ width?: string;
+ height?: number;
+ testId?: string;
+}) => (
+
+);
+
+const Notice = ({ testId }: { testId?: string }) => (
+
+
+
+
+);
+
+const SelectionHeader = ({ actionWidth = "34%" }: { actionWidth?: string }) => (
+
+
+
+
+
+
+);
+
+const EmptySelection = ({ height = 44 }: { height?: number }) => (
+
+);
+
+const Divider = () => (
+
+);
+
+const AdvancedRow = ({ testId }: { testId?: string }) => (
+
+
+
+
+
+
+);
+
+const SelectionList = ({ rows = 3 }: { rows?: number }) => (
+
+ {Array.from({ length: rows }, (_, index) => (
+
+
+
+
+
+
+
+ ))}
+
+);
+
+const AnalysisFormSkeleton = ({ variant }: { variant: MapSkeletonVariant }) => {
+ switch (variant) {
+ case "burst-detection":
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+ case "burst-location":
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+ case "burst-simulation":
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+ case "contaminant-simulation":
+ return (
+ <>
+
+
+
+
+
+ {Array.from({ length: 4 }, (_, index) => (
+
+ ))}
+
+
+
+
+ >
+ );
+ case "dma-leak-detection":
+ return (
+ <>
+
+
+ {Array.from({ length: 5 }, (_, index) => (
+
+ ))}
+
+
+
+
+
+ >
+ );
+ case "flushing-analysis":
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+ case "monitoring-place-optimization":
+ return (
+ <>
+
+ {Array.from({ length: 5 }, (_, index) => (
+
+ ))}
+
+
+
+
+ >
+ );
+ default:
+ return null;
+ }
+};
+
+const DeviceListSkeleton = ({ cleaning }: { cleaning: boolean }) => (
+
+
+
+ {Array.from({ length: 3 }, (_, index) => (
+
+ ))}
+
+
+
+
+
+ {cleaning && (
+
+ )}
+
+
+
+
+
+);
+
+const RiskChartSkeleton = () => (
+
+
+
+
+
+
+
+ {[36, 58, 44, 76, 62, 84].map((height, index) => (
+
+ ))}
+
+
+);
+
+export function MapPageSkeletonContent({
+ variant,
+}: {
+ variant: MapSkeletonVariant;
+}) {
+ if (variant === "network-simulation") {
+ return ;
+ }
+
+ if (variant === "scada-data-cleaning") {
+ return ;
+ }
+
+ if (variant === "health-risk-analysis") {
+ return ;
+ }
+
+ return (
+
+
+
+ );
+}
+
+export function MapTimelineSkeleton() {
+ return (
+
+
+ {Array.from({ length: 5 }, (_, index) => (
+
+ ))}
+
+
+
+
+
+ );
+}
diff --git a/src/components/loading/MapComponentSkeletons.test.tsx b/src/components/loading/MapComponentSkeletons.test.tsx
new file mode 100644
index 0000000..ab7535a
--- /dev/null
+++ b/src/components/loading/MapComponentSkeletons.test.tsx
@@ -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();
+
+ 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();
+
+ 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();
+
+ 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(
+ ,
+ );
+
+ expect(screen.getByTestId("network-device-content")).toBeInTheDocument();
+
+ rerender();
+
+ 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();
+
+ 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();
+
+ 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();
+
+ expect(screen.getByTestId("flushing-valve-selection")).toBeInTheDocument();
+ expect(screen.getByTestId("flushing-node-selection")).toBeInTheDocument();
+ });
+
+ it("renders the timeline as an independent loading boundary", () => {
+ render();
+
+ expect(
+ screen.getByRole("status", { name: "正在加载时间轴" }),
+ ).toHaveAttribute("data-component-skeleton", "map-timeline");
+ });
+});
diff --git a/src/components/loading/MapComponentSkeletons.tsx b/src/components/loading/MapComponentSkeletons.tsx
new file mode 100644
index 0000000..9b29f16
--- /dev/null
+++ b/src/components/loading/MapComponentSkeletons.tsx
@@ -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 (
+
+
+
+
+ {config.panelTitle}
+
+
+
+
+
+ {config.tabLabels.length > 0 && (
+
+ {config.tabLabels.map((label, index) => (
+
+
+ {label}
+
+ {index === 0 && (
+
+ )}
+
+ ))}
+
+ )}
+
+
+
+
+
+ );
+}
diff --git a/src/components/loading/MapSkeleton.tsx b/src/components/loading/MapSkeleton.tsx
deleted file mode 100644
index 755d1ca..0000000
--- a/src/components/loading/MapSkeleton.tsx
+++ /dev/null
@@ -1,191 +0,0 @@
-import { Box, Skeleton, CircularProgress } from "@mui/material";
-
-/**
- * 地图页面骨架屏组件
- * 提供即时视觉反馈,模拟地图界面布局
- */
-export function MapSkeleton() {
- return (
-
- {/* 主地图区域骨架 */}
-
-
- {/* 中央加载指示器 */}
-
-
-
-
- {/* 左侧工具栏骨架 (垂直) */}
-
- {[1, 2, 3, 4].map((i) => (
-
- ))}
-
-
- {/* 右侧控制面板骨架 (抽屉式) */}
-
-
-
- {/* 面板内容区块 */}
-
-
-
-
-
-
- {[1, 2, 3].map((i) => (
-
-
-
-
-
-
-
- ))}
-
-
-
-
- {/* 底部时间轴/控制条骨架 */}
-
-
-
-
-
-
- {/* 缩放控制骨架 (右下) */}
-
-
-
-
-
- );
-}
-
-/**
- * 简化版骨架屏 - 用于非地图页面
- */
-export function SimpleSkeleton() {
- return (
-
-
-
-
-
-
-
-
-
-
- );
-}
diff --git a/src/components/loading/mapComponentSkeletonConfig.ts b/src/components/loading/mapComponentSkeletonConfig.ts
new file mode 100644
index 0000000..9a1a041
--- /dev/null
+++ b/src/components/loading/mapComponentSkeletonConfig.ts
@@ -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: ["分析参数", "方案查询"],
+ },
+};
diff --git a/src/components/olmap/core/Controls/Toolbar.tsx b/src/components/olmap/core/Controls/Toolbar.tsx
index b35f50c..25d500f 100644
--- a/src/components/olmap/core/Controls/Toolbar.tsx
+++ b/src/components/olmap/core/Controls/Toolbar.tsx
@@ -35,7 +35,7 @@ interface ToolbarProps {
hiddenButtons?: string[]; // 可选的隐藏按钮列表,例如 ['info', 'draw', 'style']
queryType?: string; // 可选的查询类型参数
schemeType?: string; // 可选的方案类型参数
- HistoryPanel?: React.FC; // 可选的自定义历史数据面板
+ HistoryPanel?: React.ComponentType; // 可选的自定义历史数据面板
enableCompare?: boolean;
}
diff --git a/src/components/olmap/core/Controls/ToolbarHistoryPanel.tsx b/src/components/olmap/core/Controls/ToolbarHistoryPanel.tsx
index 552aad4..cfed455 100644
--- a/src/components/olmap/core/Controls/ToolbarHistoryPanel.tsx
+++ b/src/components/olmap/core/Controls/ToolbarHistoryPanel.tsx
@@ -16,7 +16,7 @@ type ToolbarHistoryPanelProps = {
endTime?: string;
} | null;
highlightFeatures: Feature[];
- HistoryPanel?: React.FC;
+ HistoryPanel?: React.ComponentType;
schemeName?: string;
queryType?: string;
onClose: () => void;