refactor(frontend): align TJWater Next integrations

This commit is contained in:
2026-08-26 12:26:29 +08:00
parent 6623f4f7fb
commit fee4fc6ce1
51 changed files with 3074 additions and 22280 deletions
@@ -26,11 +26,11 @@ jest.mock("./SchemeQuery", () => ({
onEdit,
active,
}: {
onEdit: (schemeId: number) => void;
onEdit: (schemeId: string) => void;
active?: boolean;
}) => {
mockSchemeQueryRender(active);
return <button onClick={() => onEdit(7)}></button>;
return <button onClick={() => onEdit("run-7")}></button>;
},
createMonitoringSchemeQueryState: () => ({}),
}));
@@ -61,7 +61,7 @@ jest.mock("@components/olmap/common/PanelEmptyState", () => ({
const mockGetSensorPlacementScheme = jest.mocked(getSensorPlacementScheme);
const scheme: SensorPlacementScheme = {
id: 7,
id: "run-7",
scheme_name: "测试方案",
sensor_number: 1,
min_diameter: 100,
@@ -103,7 +103,7 @@ const MonitoringPlaceOptimizationPanel: React.FC<
const [activeScheme, setActiveScheme] =
useState<SensorPlacementScheme | null>(null);
const [loadingScheme, setLoadingScheme] = useState(false);
const [pendingOpenSchemeId, setPendingOpenSchemeId] = useState<number | null>(
const [pendingOpenSchemeId, setPendingOpenSchemeId] = useState<string | null>(
null,
);
const { open: notify } = useNotification();
@@ -138,7 +138,7 @@ const MonitoringPlaceOptimizationPanel: React.FC<
setPendingOpenSchemeId(null);
}, []);
const handleOpenScheme = async (schemeId: number) => {
const handleOpenScheme = async (schemeId: string) => {
setLoadingScheme(true);
try {
const scheme = await getSensorPlacementScheme(schemeId);
@@ -98,7 +98,7 @@ const OptimizationParameters: React.FC<OptimizationParametersProps> = ({
try {
const created = await optimizeSensorPlacement({
scheme_name: normalizeSchemeName(schemeName),
run_name: normalizeSchemeName(schemeName),
sensor_type: "pressure",
method: method as "sensitivity" | "kmeans",
sensor_count: sensorCount,
@@ -146,7 +146,7 @@ jest.mock("./schemeApi", () => ({
}));
const scheme: SensorPlacementScheme = {
id: 1,
id: "run-1",
scheme_name: "测试方案",
sensor_number: 1,
min_diameter: 300,
@@ -24,9 +24,8 @@ import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import "dayjs/locale/zh-cn"; // 引入中文包
import dayjs, { Dayjs } from "dayjs";
import { api } from "@/lib/api";
import moment from "moment";
import { config, NETWORK_NAME } from "@config/config";
import { NETWORK_NAME } from "@config/config";
import { useNotification } from "@refinedev/core";
import { useMap } from "@components/olmap/core/MapComponent";
import { queryFeaturesByIds } from "@/utils/mapQueryService";
@@ -40,22 +39,13 @@ import { bbox, featureCollection } from "@turf/turf";
import type { SchemeRecord } from "./types";
import { useSchemeCreatorName } from "@components/olmap/core/useSchemeCreatorName";
import { SchemeQueryEmptyState } from "@components/olmap/common/PanelEmptyState";
interface SchemaItem {
id: number;
scheme_name: string;
sensor_number: number;
min_diameter: number;
username: string;
create_time: string;
sensor_location?: string[];
}
import { listSensorPlacementSchemes } from "./schemeApi";
interface SchemeQueryProps {
schemes?: SchemeRecord[];
active?: boolean;
onSchemesChange?: (schemes: SchemeRecord[]) => void;
onEdit?: (id: number) => void;
onEdit?: (id: string) => void;
network?: string;
state?: MonitoringSchemeQueryState;
onStateChange?: (state: MonitoringSchemeQueryState) => void;
@@ -64,7 +54,7 @@ interface SchemeQueryProps {
export interface MonitoringSchemeQueryState {
queryAll: boolean;
queryDate: Dayjs | null;
expandedId: number | null;
expandedId: string | null;
hasQueried: boolean;
}
@@ -185,22 +175,18 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
setLoading(true);
try {
const response = await api.get(
`${config.BACKEND_URL}/api/v1/sensor-placement-schemes`,
);
let filteredResults = response.data;
let filteredResults = await listSensorPlacementSchemes();
// 按日期过滤
if (!queryAll && queryDate) {
const formattedDate = queryDate.format("YYYY-MM-DD");
filteredResults = filteredResults.filter((item: SchemaItem) => {
filteredResults = filteredResults.filter((item) => {
const itemDate = moment(item.create_time).format("YYYY-MM-DD");
return itemDate === formattedDate;
});
}
const nextSchemes = filteredResults.map((item: SchemaItem) => ({
const nextSchemes = filteredResults.map((item) => ({
id: item.id,
schemeName: item.scheme_name,
sensorNumber: item.sensor_number,
@@ -250,7 +236,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
}
if (sensorIds.length > 0) {
queryFeaturesByIds(sensorIds, "geo_junctions_mat").then((features) => {
queryFeaturesByIds(sensorIds, "junctions").then((features) => {
if (features.length > 0) {
// 设置高亮要素
setHighlightFeatures(features);
@@ -271,7 +257,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
};
// 查看详情(展开/收起)
const handleViewDetails = (id: number) => {
const handleViewDetails = (id: string) => {
setQueryField("expandedId", expandedId === id ? null : id);
};
@@ -122,7 +122,7 @@ const point = (
const rows = [point("A", 1, 400, 200), point("B", 2, 500, 300)];
const scheme: SensorPlacementScheme = {
id: 1,
id: "run-1",
scheme_name: "测试方案",
sensor_number: rows.length,
min_diameter: 300,
@@ -0,0 +1,98 @@
import { api } from "@/lib/api";
import {
exportSensorPlacementExcel,
getSensorPlacementScheme,
listSensorPlacementSchemes,
optimizeSensorPlacement,
overwriteSensorPlacementScheme,
} from "./schemeApi";
jest.mock("@/lib/api", () => ({
api: {
get: jest.fn(),
post: jest.fn(),
put: jest.fn(),
},
}));
const run = {
run_id: "9fb546cb-5f72-4ddd-8a46-d13e9aee2c6b",
name: "sensor-plan-a",
sensor_count: 2,
min_diameter: 300,
created_by: "operator",
created_at: "2026-08-25T08:00:00Z",
sensor_locations: ["J-1", "J-2"],
sensor_points: [],
can_edit: true,
status: "completed",
};
describe("sensor placement runs API adapter", () => {
beforeEach(() => jest.clearAllMocks());
it("lists paged runs and maps run fields to the existing screen model", async () => {
jest.mocked(api.get).mockResolvedValue({
data: { items: [run], total: 1, limit: 1000, offset: 0 },
});
await expect(listSensorPlacementSchemes()).resolves.toEqual([
expect.objectContaining({
id: run.run_id,
scheme_name: run.name,
sensor_number: 2,
sensor_location: ["J-1", "J-2"],
}),
]);
expect(api.get).toHaveBeenCalledWith(
expect.stringContaining("/api/v1/sensor-placement-runs"),
{ params: { limit: 1000, offset: 0 } },
);
});
it("uses run endpoints for creation and detail lookup", async () => {
jest.mocked(api.post).mockResolvedValue({ data: run });
jest.mocked(api.get).mockResolvedValue({ data: run });
await optimizeSensorPlacement({
run_name: run.name,
sensor_type: "pressure",
method: "sensitivity",
sensor_count: 2,
min_diameter: 300,
});
await getSensorPlacementScheme(run.run_id);
expect(api.post).toHaveBeenCalledWith(
expect.stringMatching(/\/api\/v1\/sensor-placement-runs$/),
expect.objectContaining({ run_name: run.name }),
);
expect(api.get).toHaveBeenLastCalledWith(
expect.stringContaining(`/api/v1/sensor-placement-runs/${run.run_id}`),
);
});
it("uses the plural result fields required by update and export", async () => {
jest.mocked(api.put).mockResolvedValue({ data: run });
jest.mocked(api.post).mockResolvedValue({ data: new Blob() });
await overwriteSensorPlacementScheme(run.run_id, ["J-1"], ["J-2"]);
await exportSensorPlacementExcel(run.run_id, ["J-2"], { "J-2": "added" });
expect(api.put).toHaveBeenCalledWith(
expect.stringContaining(`/sensor-placement-runs/${run.run_id}`),
{
expected_sensor_locations: ["J-1"],
sensor_locations: ["J-2"],
},
);
expect(api.post).toHaveBeenCalledWith(
expect.stringContaining(`/sensor-placement-runs/${run.run_id}/exports/excel`),
{
sensor_locations: ["J-2"],
adjustment_status: { "J-2": "added" },
},
{ responseType: "blob" },
);
});
});
@@ -7,30 +7,67 @@ import type {
} from "./types";
export interface OptimizeSchemeInput {
scheme_name: string;
run_name: string;
sensor_type: "pressure";
method: "sensitivity" | "kmeans";
sensor_count: number;
min_diameter: number;
}
type SensorPlacementRunResponse = {
run_id: string;
name: string;
sensor_count: number;
min_diameter: number;
created_by: string;
created_at: string;
sensor_locations: string[];
sensor_points: SensorPoint[];
can_edit: boolean;
};
const toSensorPlacementScheme = (
run: SensorPlacementRunResponse,
): SensorPlacementScheme => ({
id: run.run_id,
scheme_name: run.name,
sensor_number: run.sensor_count,
min_diameter: run.min_diameter,
username: run.created_by,
create_time: run.created_at,
sensor_location: run.sensor_locations,
sensor_points: run.sensor_points,
can_edit: run.can_edit,
});
export const optimizeSensorPlacement = async (
input: OptimizeSchemeInput,
): Promise<SensorPlacementScheme> => {
const response = await api.post<SensorPlacementScheme>(
`${config.BACKEND_URL}/api/v1/sensor-placement-optimization-runs`,
const response = await api.post<SensorPlacementRunResponse>(
`${config.BACKEND_URL}/api/v1/sensor-placement-runs`,
input,
);
return response.data;
return toSensorPlacementScheme(response.data);
};
export const listSensorPlacementSchemes = async (): Promise<
SensorPlacementScheme[]
> => {
const response = await api.get<{
items: SensorPlacementRunResponse[];
}>(`${config.BACKEND_URL}/api/v1/sensor-placement-runs`, {
params: { limit: 1000, offset: 0 },
});
return response.data.items.map(toSensorPlacementScheme);
};
export const getSensorPlacementScheme = async (
schemeId: number,
schemeId: string,
): Promise<SensorPlacementScheme> => {
const response = await api.get<SensorPlacementScheme>(
`${config.BACKEND_URL}/api/v1/sensor-placement-schemes/${schemeId}`,
const response = await api.get<SensorPlacementRunResponse>(
`${config.BACKEND_URL}/api/v1/sensor-placement-runs/${encodeURIComponent(schemeId)}`,
);
return response.data;
return toSensorPlacementScheme(response.data);
};
export const getSensorPlacementCandidate = async (
@@ -43,29 +80,29 @@ export const getSensorPlacementCandidate = async (
};
export const overwriteSensorPlacementScheme = async (
schemeId: number,
schemeId: string,
expectedSensorLocation: string[],
sensorLocation: string[],
): Promise<SensorPlacementScheme> => {
const response = await api.put<SensorPlacementScheme>(
`${config.BACKEND_URL}/api/v1/sensor-placement-schemes/${schemeId}`,
const response = await api.put<SensorPlacementRunResponse>(
`${config.BACKEND_URL}/api/v1/sensor-placement-runs/${encodeURIComponent(schemeId)}`,
{
expected_sensor_location: expectedSensorLocation,
sensor_location: sensorLocation,
expected_sensor_locations: expectedSensorLocation,
sensor_locations: sensorLocation,
},
);
return response.data;
return toSensorPlacementScheme(response.data);
};
export const exportSensorPlacementExcel = async (
schemeId: number,
schemeId: string,
sensorLocation: string[],
adjustmentStatus: Record<string, AdjustmentStatus>,
) => {
const response = await api.post<Blob>(
`${config.BACKEND_URL}/api/v1/sensor-placement-schemes/${schemeId}/exports/excel`,
`${config.BACKEND_URL}/api/v1/sensor-placement-runs/${encodeURIComponent(schemeId)}/exports/excel`,
{
sensor_location: sensorLocation,
sensor_locations: sensorLocation,
adjustment_status: adjustmentStatus,
},
{
@@ -24,7 +24,7 @@ const point = (node_id: string): SensorPoint => ({
});
const scheme: SensorPlacementScheme = {
id: 1,
id: "run-1",
scheme_name: "测试方案",
sensor_number: 2,
min_diameter: 300,
@@ -13,7 +13,7 @@ export interface SensorPoint {
}
export interface SensorPlacementScheme {
id: number;
id: string;
scheme_name: string;
sensor_number: number;
min_diameter: number;
@@ -25,7 +25,7 @@ export interface SensorPlacementScheme {
}
export interface SchemeRecord {
id: number;
id: string;
schemeName: string;
sensorNumber: number;
minDiameter: number;