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;