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 ? ( - ) : (