diff --git a/src/components/olmap/MonitoringPlaceOptimization/MonitoringPlaceOptimizationPanel.test.tsx b/src/components/olmap/MonitoringPlaceOptimization/MonitoringPlaceOptimizationPanel.test.tsx
new file mode 100644
index 0000000..5b0206a
--- /dev/null
+++ b/src/components/olmap/MonitoringPlaceOptimization/MonitoringPlaceOptimizationPanel.test.tsx
@@ -0,0 +1,123 @@
+import { act, fireEvent, render, screen } from "@testing-library/react";
+import MonitoringPlaceOptimizationPanel, {
+ getTabIndicatorSx,
+ getTabIndicatorTransform,
+} from "./MonitoringPlaceOptimizationPanel";
+import { getSensorPlacementScheme } from "./schemeApi";
+import type { SensorPlacementScheme } from "./types";
+
+const mockSchemeEditorRender = jest.fn();
+
+jest.mock("@refinedev/core", () => ({
+ useNotification: () => ({ open: jest.fn() }),
+}));
+
+jest.mock("./OptimizationParameters", () => ({
+ __esModule: true,
+ default: () =>
optimization parameters
,
+ createOptimizationParametersState: () => ({}),
+}));
+
+jest.mock("./SchemeQuery", () => ({
+ __esModule: true,
+ default: ({ onEdit }: { onEdit: (schemeId: number) => void }) => (
+
+ ),
+ createMonitoringSchemeQueryState: () => ({}),
+}));
+
+jest.mock("./SchemeEditor", () => ({
+ __esModule: true,
+ default: ({
+ scheme,
+ active,
+ }: {
+ scheme: SensorPlacementScheme;
+ active: boolean;
+ }) => {
+ mockSchemeEditorRender(active);
+ return {scheme.scheme_name}
;
+ },
+}));
+
+jest.mock("./schemeApi", () => ({
+ getSensorPlacementScheme: jest.fn(),
+}));
+
+jest.mock("@components/olmap/common/PanelEmptyState", () => ({
+ __esModule: true,
+ default: () => empty state
,
+}));
+
+const mockGetSensorPlacementScheme = jest.mocked(getSensorPlacementScheme);
+
+const scheme: SensorPlacementScheme = {
+ id: 7,
+ scheme_name: "测试方案",
+ sensor_number: 1,
+ min_diameter: 100,
+ username: "operator",
+ create_time: "2026-07-30T08:00:00+08:00",
+ sensor_location: ["J-1"],
+ sensor_points: [],
+ can_edit: true,
+};
+
+describe("MonitoringPlaceOptimizationPanel", () => {
+ beforeEach(() => {
+ mockSchemeEditorRender.mockClear();
+ });
+
+ it("applies equal-width positioning to the rendered tab indicator", () => {
+ expect(getTabIndicatorTransform(0)).toBe("translateX(0%)");
+ expect(getTabIndicatorTransform(1)).toBe("translateX(100%)");
+ expect(getTabIndicatorTransform(2)).toBe("translateX(200%)");
+ expect(getTabIndicatorSx(1)).toMatchObject({
+ left: "0 !important",
+ width: "33.333333% !important",
+ transform: "translateX(100%)",
+ });
+
+ const { container } = render();
+ const indicator = container.querySelector(
+ ".MuiTabs-indicator",
+ );
+
+ expect(indicator).not.toBeNull();
+ expect(indicator).toHaveStyle({ transform: "translateX(0%)" });
+
+ fireEvent.click(screen.getByRole("tab", { name: /方案查询/ }));
+ expect(indicator).toHaveStyle({ transform: "translateX(200%)" });
+ });
+
+ it("prepares a queried scheme before switching to the result editor", async () => {
+ let resolveScheme: (value: SensorPlacementScheme) => void = () => {};
+ mockGetSensorPlacementScheme.mockReturnValue(
+ new Promise((resolve) => {
+ resolveScheme = resolve;
+ }),
+ );
+
+ render();
+
+ const queryTab = screen.getByRole("tab", { name: /方案查询/ });
+ const editorTab = screen.getByRole("tab", { name: /结果编辑/ });
+ fireEvent.click(queryTab);
+ fireEvent.click(screen.getByRole("button", { name: "打开测试方案" }));
+
+ expect(queryTab).toHaveAttribute("aria-selected", "true");
+ expect(editorTab).toHaveAttribute("aria-selected", "false");
+
+ await act(async () => {
+ resolveScheme(scheme);
+ await Promise.resolve();
+ });
+
+ expect(editorTab).toHaveAttribute("aria-selected", "true");
+ expect(screen.getByTestId("scheme-editor")).toBeVisible();
+ expect(mockSchemeEditorRender.mock.calls.map(([active]) => active)).toEqual([
+ false,
+ true,
+ ]);
+ });
+});
diff --git a/src/components/olmap/MonitoringPlaceOptimization/MonitoringPlaceOptimizationPanel.tsx b/src/components/olmap/MonitoringPlaceOptimization/MonitoringPlaceOptimizationPanel.tsx
index 772a720..c77de01 100644
--- a/src/components/olmap/MonitoringPlaceOptimization/MonitoringPlaceOptimizationPanel.tsx
+++ b/src/components/olmap/MonitoringPlaceOptimization/MonitoringPlaceOptimizationPanel.tsx
@@ -1,6 +1,6 @@
"use client";
-import React, { useState } from "react";
+import React, { useCallback, useEffect, useState } from "react";
import {
Box,
Drawer,
@@ -52,6 +52,39 @@ const TabPanel: React.FC = ({ children, value, index }) => {
);
};
+const PANEL_MOTION_DURATION_MS = 280;
+const PANEL_MOTION_EASING = "cubic-bezier(0.4, 0, 0.2, 1)";
+
+export const getTabIndicatorTransform = (tabIndex: number) =>
+ `translateX(${tabIndex * 100}%)`;
+
+export const getTabIndicatorSx = (tabIndex: number) => ({
+ backgroundColor: "#257DD4",
+ left: "0 !important",
+ width: "33.333333% !important",
+ transform: getTabIndicatorTransform(tabIndex),
+ transition: `transform ${PANEL_MOTION_DURATION_MS}ms ${PANEL_MOTION_EASING}`,
+ "@media (prefers-reduced-motion: reduce)": {
+ transition: "none",
+ },
+});
+
+interface PreparedSchemeEditorProps
+ extends React.ComponentProps {
+ onReady?: () => void;
+}
+
+const PreparedSchemeEditor: React.FC = ({
+ onReady,
+ ...props
+}) => {
+ useEffect(() => {
+ onReady?.();
+ }, [onReady]);
+
+ return ;
+};
+
interface MonitoringPlaceOptimizationPanelProps {
open?: boolean;
onToggle?: () => void;
@@ -65,6 +98,9 @@ const MonitoringPlaceOptimizationPanel: React.FC<
const [activeScheme, setActiveScheme] =
useState(null);
const [loadingScheme, setLoadingScheme] = useState(false);
+ const [pendingOpenSchemeId, setPendingOpenSchemeId] = useState(
+ null,
+ );
const { open: notify } = useNotification();
// 持久化方案查询结果
@@ -91,11 +127,18 @@ const MonitoringPlaceOptimizationPanel: React.FC<
const drawerWidth = currentTab === 1 ? 820 : 520;
+ const handleSchemeEditorReady = useCallback(() => {
+ setCurrentTab(1);
+ setPendingOpenSchemeId(null);
+ }, []);
+
const handleOpenScheme = async (schemeId: number) => {
setLoadingScheme(true);
- setCurrentTab(1);
try {
- setActiveScheme(await getSensorPlacementScheme(NETWORK_NAME, schemeId));
+ const scheme = await getSensorPlacementScheme(NETWORK_NAME, schemeId);
+ setActiveScheme(scheme);
+ setLoadingScheme(false);
+ setPendingOpenSchemeId(scheme.id);
} catch (error) {
const detail =
(error as { response?: { data?: { detail?: string } } }).response?.data
@@ -105,8 +148,6 @@ const MonitoringPlaceOptimizationPanel: React.FC<
message: "方案加载失败",
description: detail,
});
- setCurrentTab(2);
- } finally {
setLoadingScheme(false);
}
};
@@ -173,11 +214,17 @@ const MonitoringPlaceOptimizationPanel: React.FC<
"0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)",
backdropFilter: "blur(8px)",
opacity: 0.95,
- transition: "transform 0.3s ease-in-out, opacity 0.3s ease-in-out",
+ transition:
+ `width ${PANEL_MOTION_DURATION_MS}ms ${PANEL_MOTION_EASING}, ` +
+ "transform 300ms ease-in-out, opacity 300ms ease-in-out",
+ willChange: "width, transform, opacity",
border: "none",
"&:hover": {
opacity: 1,
},
+ "@media (prefers-reduced-motion: reduce)": {
+ transition: "none",
+ },
},
}}
>
@@ -219,9 +266,7 @@ const MonitoringPlaceOptimizationPanel: React.FC<
"& .Mui-selected": {
color: "#257DD4",
},
- "& .MuiTabs-indicator": {
- backgroundColor: "#257DD4",
- },
+ "& .MuiTabs-indicator": getTabIndicatorSx(currentTab),
}}
>
) : activeScheme ? (
-
) : (