diff --git a/src/app/(main)/monitoring-place-optimization/page.tsx b/src/app/(main)/monitoring-place-optimization/page.tsx
new file mode 100644
index 0000000..917acf3
--- /dev/null
+++ b/src/app/(main)/monitoring-place-optimization/page.tsx
@@ -0,0 +1,15 @@
+"use client";
+
+import MapComponent from "@app/OlMap/MapComponent";
+import MapToolbar from "@app/OlMap/Controls/Toolbar";
+import MonitoringPlaceOptimizationPanel from "@components/olmap/MonitoringPlaceOptimizationPanel";
+export default function Home() {
+ return (
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/olmap/MonitoringPlaceOptimization/OptimizationParameters.tsx b/src/components/olmap/MonitoringPlaceOptimization/OptimizationParameters.tsx
new file mode 100644
index 0000000..9475758
--- /dev/null
+++ b/src/components/olmap/MonitoringPlaceOptimization/OptimizationParameters.tsx
@@ -0,0 +1,292 @@
+"use client";
+
+import React, { useState } from "react";
+import {
+ Box,
+ TextField,
+ Button,
+ Typography,
+ MenuItem,
+ Stack,
+} from "@mui/material";
+import { PlayArrow as PlayArrowIcon } from "@mui/icons-material";
+import { useNotification } from "@refinedev/core";
+import axios from "axios";
+import { config, NETWORK_NAME } from "@/config/config";
+
+const OptimizationParameters: React.FC = () => {
+ const { open } = useNotification();
+
+ // 表单状态
+ const [sensorType, setSensorType] = useState("压力");
+ const [method, setMethod] = useState("聚类分析");
+ const [sensorCount, setSensorCount] = useState(5);
+ const [minDiameter, setMinDiameter] = useState(5);
+ const [schemeName, setSchemeName] = useState(
+ "Fangan" + new Date().getTime()
+ );
+ const [network] = useState(NETWORK_NAME);
+ const [analyzing, setAnalyzing] = useState(false);
+
+ // 传感器类型选项
+ const sensorTypeOptions = [
+ { value: "压力", label: "压力" },
+ { value: "流量", label: "流量" },
+ ];
+
+ // 方法选项
+ const methodOptions = [
+ { value: "聚类分析", label: "聚类分析" },
+ { value: "灵敏度分析", label: "灵敏度分析" },
+ ];
+
+ // 创建方案
+ const handleCreateScheme = async () => {
+ // 验证输入
+ if (!schemeName.trim()) {
+ open?.({
+ type: "error",
+ message: "请输入方案名称",
+ });
+ return;
+ }
+
+ if (sensorCount <= 0) {
+ open?.({
+ type: "error",
+ message: "监测点数目必须大于0",
+ });
+ return;
+ }
+
+ if (minDiameter < 0) {
+ open?.({
+ type: "error",
+ message: "最小管径不能为负数",
+ });
+ return;
+ }
+
+ setAnalyzing(true);
+
+ try {
+ // 构建请求参数
+ const requestData = {
+ network: network,
+ scheme_name: schemeName,
+ sensor_type: sensorType,
+ method: method,
+ sensor_count: sensorCount,
+ min_diameter: minDiameter,
+ };
+
+ // 发送优化请求
+ const response = await axios.post(
+ `${config.backendUrl}/monitoring-optimization/create`,
+ requestData
+ );
+
+ if (response.data && response.data.success) {
+ open?.({
+ type: "success",
+ message: "方案创建成功",
+ description: `方案 "${schemeName}" 已提交优化分析`,
+ });
+
+ // 重置方案名称
+ setSchemeName("Fangan" + new Date().getTime());
+ } else {
+ throw new Error(response.data?.message || "创建失败");
+ }
+ } catch (error: any) {
+ console.error("创建方案失败:", error);
+ open?.({
+ type: "error",
+ message: "创建方案失败",
+ description:
+ error.response?.data?.message || error.message || "未知错误",
+ });
+ } finally {
+ setAnalyzing(false);
+ }
+ };
+
+ return (
+
+ {/* 类型选择 */}
+
+
+ 类型
+
+ setSensorType(e.target.value)}
+ sx={{
+ "& .MuiOutlinedInput-root": {
+ "&:hover fieldset": {
+ borderColor: "#257DD4",
+ },
+ "&.Mui-focused fieldset": {
+ borderColor: "#257DD4",
+ },
+ },
+ }}
+ >
+ {sensorTypeOptions.map((option) => (
+
+ ))}
+
+
+
+ {/* 方法选择 */}
+
+
+ 方法
+
+ setMethod(e.target.value)}
+ sx={{
+ "& .MuiOutlinedInput-root": {
+ "&:hover fieldset": {
+ borderColor: "#257DD4",
+ },
+ "&.Mui-focused fieldset": {
+ borderColor: "#257DD4",
+ },
+ },
+ }}
+ >
+ {methodOptions.map((option) => (
+
+ ))}
+
+
+
+ {/* 监测点数目 */}
+
+
+ 监测点数目
+
+ setSensorCount(parseInt(e.target.value) || 0)}
+ inputProps={{ min: 1 }}
+ sx={{
+ "& .MuiOutlinedInput-root": {
+ "&:hover fieldset": {
+ borderColor: "#257DD4",
+ },
+ "&.Mui-focused fieldset": {
+ borderColor: "#257DD4",
+ },
+ },
+ }}
+ />
+
+
+ {/* 压力监测点安装最小管径(可选) */}
+
+
+ {sensorType}监测点安装最小管径(可选)
+
+ setMinDiameter(parseInt(e.target.value) || 0)}
+ inputProps={{ min: 0 }}
+ sx={{
+ "& .MuiOutlinedInput-root": {
+ "&:hover fieldset": {
+ borderColor: "#257DD4",
+ },
+ "&.Mui-focused fieldset": {
+ borderColor: "#257DD4",
+ },
+ },
+ }}
+ />
+
+
+ {/* 方案名称 */}
+
+
+ 方案名称
+
+ setSchemeName(e.target.value)}
+ placeholder="请输入方案名称"
+ sx={{
+ "& .MuiOutlinedInput-root": {
+ "&:hover fieldset": {
+ borderColor: "#257DD4",
+ },
+ "&.Mui-focused fieldset": {
+ borderColor: "#257DD4",
+ },
+ },
+ }}
+ />
+
+
+ {/* 创建方案按钮 */}
+
+ }
+ onClick={handleCreateScheme}
+ disabled={analyzing}
+ sx={{
+ backgroundColor: "#257DD4",
+ textTransform: "none",
+ px: 4,
+ py: 1,
+ "&:hover": {
+ backgroundColor: "#1e6bb8",
+ },
+ "&:disabled": {
+ backgroundColor: "#ccc",
+ },
+ }}
+ >
+ {analyzing ? "分析中..." : "创建方案"}
+
+
+
+ );
+};
+
+export default OptimizationParameters;
diff --git a/src/components/olmap/MonitoringPlaceOptimization/SchemeQuery.tsx b/src/components/olmap/MonitoringPlaceOptimization/SchemeQuery.tsx
new file mode 100644
index 0000000..f694115
--- /dev/null
+++ b/src/components/olmap/MonitoringPlaceOptimization/SchemeQuery.tsx
@@ -0,0 +1,511 @@
+"use client";
+
+import React, { useState } from "react";
+import {
+ Box,
+ Button,
+ Typography,
+ Checkbox,
+ FormControlLabel,
+ IconButton,
+ Card,
+ CardContent,
+ Chip,
+ Tooltip,
+ Collapse,
+ Link,
+} from "@mui/material";
+import {
+ Info as InfoIcon,
+ LocationOn as LocationIcon,
+} from "@mui/icons-material";
+import { DatePicker } from "@mui/x-date-pickers/DatePicker";
+import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
+import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
+import dayjs, { Dayjs } from "dayjs";
+import "dayjs/locale/zh-cn";
+import axios from "axios";
+import moment from "moment";
+import { config, NETWORK_NAME } from "@config/config";
+import { useNotification } from "@refinedev/core";
+import { useMap } from "@app/OlMap/MapComponent";
+import { queryFeaturesByIds } from "@/utils/mapQueryService";
+import * as turf from "@turf/turf";
+
+interface SchemeRecord {
+ id: number;
+ schemeName: string;
+ sensorNumber: number;
+ minDiameter: number;
+ user: string;
+ create_time: string;
+ sensorLocation?: string[];
+}
+
+interface SchemaItem {
+ id: number;
+ scheme_name: string;
+ sensor_number: number;
+ min_diameter: number;
+ username: string;
+ create_time: string;
+ sensor_location?: string[];
+}
+
+interface SchemeQueryProps {
+ schemes?: SchemeRecord[];
+ onSchemesChange?: (schemes: SchemeRecord[]) => void;
+ onLocate?: (id: number) => void;
+ network?: string;
+}
+
+const SchemeQuery: React.FC = ({
+ schemes: externalSchemes,
+ onSchemesChange,
+ onLocate,
+ network = NETWORK_NAME,
+}) => {
+ const [queryAll, setQueryAll] = useState(true);
+ const [queryDate, setQueryDate] = useState(dayjs(new Date()));
+ const [internalSchemes, setInternalSchemes] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [expandedId, setExpandedId] = useState(null);
+
+ const { open } = useNotification();
+ const map = useMap();
+
+ // 使用外部提供的 schemes 或内部状态
+ const schemes =
+ externalSchemes !== undefined ? externalSchemes : internalSchemes;
+ const setSchemes = onSchemesChange || setInternalSchemes;
+
+ // 格式化日期
+ const formatTime = (timeStr: string) => {
+ const time = moment(timeStr);
+ return time.format("YYYY-MM-DD HH:mm:ss");
+ };
+
+ // 格式化简短日期
+ const formatShortDate = (timeStr: string) => {
+ const time = moment(timeStr);
+ return time.format("MM-DD");
+ };
+
+ // 查询方案
+ const handleQuery = async () => {
+ if (!queryAll && !queryDate) return;
+
+ setLoading(true);
+ try {
+ const response = await axios.get(
+ `${config.backendUrl}/getallsensorplacements/?network=${network}`
+ );
+
+ let filteredResults = response.data;
+
+ // 按日期过滤
+ if (!queryAll && queryDate) {
+ const formattedDate = queryDate.format("YYYY-MM-DD");
+ filteredResults = filteredResults.filter((item: SchemaItem) => {
+ const itemDate = moment(item.create_time).format("YYYY-MM-DD");
+ return itemDate === formattedDate;
+ });
+ }
+
+ setSchemes(
+ filteredResults.map((item: SchemaItem) => ({
+ id: item.id,
+ schemeName: item.scheme_name,
+ sensorNumber: item.sensor_number,
+ minDiameter: item.min_diameter,
+ user: item.username,
+ create_time: item.create_time,
+ sensorLocation: item.sensor_location,
+ }))
+ );
+
+ if (filteredResults.length === 0) {
+ open?.({
+ type: "error",
+ message: "查询结果",
+ description: queryAll
+ ? "没有找到任何方案"
+ : `${queryDate?.format("YYYY-MM-DD")} 没有找到相关方案`,
+ });
+ }
+ } catch (error) {
+ console.error("查询请求失败:", error);
+ open?.({
+ type: "error",
+ message: "查询失败",
+ description: "获取方案列表失败,请稍后重试",
+ });
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ // 定位传感器
+ const handleLocateSensors = (sensorIds: string[]) => {
+ if (!map) {
+ open?.({
+ type: "error",
+ message: "地图未加载",
+ });
+ return;
+ }
+
+ if (sensorIds.length > 0) {
+ queryFeaturesByIds(sensorIds).then((features) => {
+ if (features.length > 0) {
+ // 计算范围并缩放
+ const geojsonFormat = new (window as any).ol.format.GeoJSON();
+ const geojsonFeatures = features.map((feature) =>
+ geojsonFormat.writeFeatureObject(feature)
+ );
+
+ const extent = turf.bbox(
+ turf.featureCollection(geojsonFeatures as any)
+ );
+
+ if (extent) {
+ map.getView().fit(extent, { maxZoom: 18, duration: 1000 });
+ }
+
+ open?.({
+ type: "success",
+ message: `已定位 ${features.length} 个传感器位置`,
+ });
+ } else {
+ open?.({
+ type: "error",
+ message: "未找到传感器位置",
+ });
+ }
+ });
+ }
+ };
+
+ // 查看详情(展开/收起)
+ const handleViewDetails = (id: number) => {
+ setExpandedId(expandedId === id ? null : id);
+ };
+
+ // 保存方案(示例功能)
+ const handleSaveScheme = (scheme: SchemeRecord) => {
+ open?.({
+ type: "success",
+ message: "保存成功",
+ description: `方案 "${scheme.schemeName}" 已保存`,
+ });
+ };
+
+ return (
+
+ {/* 查询条件 - 单行布局 */}
+
+
+
+ setQueryAll(e.target.checked)}
+ size="small"
+ />
+ }
+ label={查询全部日期}
+ className="m-0"
+ />
+
+
+ value && dayjs.isDayjs(value) && setQueryDate(value)
+ }
+ format="YYYY-MM-DD"
+ disabled={queryAll}
+ slotProps={{
+ textField: {
+ size: "small",
+ sx: { width: 200 },
+ },
+ }}
+ />
+
+
+
+
+
+
+ {/* 结果列表 */}
+
+ {schemes.length === 0 ? (
+
+
+
+
+ 总共 0 条
+
+ No data
+
+
+ ) : (
+
+
+ 共 {schemes.length} 条记录
+
+ {schemes.map((scheme) => (
+
+
+ {/* 主要信息行 */}
+
+
+
+
+ {scheme.schemeName}
+
+
+
+
+ 最小半径: {scheme.minDiameter} · 用户: {scheme.user} · 日期: {formatShortDate(scheme.create_time)}
+
+
+ {/* 操作按钮 */}
+
+
+
+ setExpandedId(
+ expandedId === scheme.id ? null : scheme.id
+ )
+ }
+ color="primary"
+ className="p-1"
+ >
+
+
+
+
+ onLocate?.(scheme.id)}
+ color="primary"
+ className="p-1"
+ >
+
+
+
+
+
+
+ {/* 可折叠的详细信息 */}
+
+
+ {/* 信息网格布局 */}
+
+ {/* 传感器信息列 */}
+
+
+
+
+ 传感器数量:
+
+
+ {scheme.sensorNumber}
+
+
+
+
+ 最小半径:
+
+
+ {scheme.minDiameter}
+
+
+
+
+
+ {/* 方案信息列 */}
+
+
+
+
+ 用户:
+
+
+ {scheme.user}
+
+
+
+
+ 创建时间:
+
+
+ {moment(scheme.create_time).format(
+ "YYYY-MM-DD HH:mm"
+ )}
+
+
+
+
+
+
+ {/* 传感器位置列表 */}
+ {scheme.sensorLocation && scheme.sensorLocation.length > 0 && (
+
+
+ 传感器位置 ({scheme.sensorLocation.length}个):
+
+
+
+ {scheme.sensorLocation.map((sensorId, index) => (
+ {
+ e.preventDefault();
+ handleLocateSensors([sensorId]);
+ }}
+ >
+ {sensorId}
+
+ ))}
+
+
+
+ )}
+
+ {/* 操作按钮区域 */}
+
+ {scheme.sensorLocation && scheme.sensorLocation.length > 0 && (
+
+ )}
+
+
+
+
+
+
+ ))}
+
+ )}
+
+
+ );
+};
+
+export default SchemeQuery;
diff --git a/src/components/olmap/MonitoringPlaceOptimizationPanel.tsx b/src/components/olmap/MonitoringPlaceOptimizationPanel.tsx
new file mode 100644
index 0000000..5bda86c
--- /dev/null
+++ b/src/components/olmap/MonitoringPlaceOptimizationPanel.tsx
@@ -0,0 +1,203 @@
+"use client";
+
+import React, { useState } from "react";
+import { Box, Drawer, Tabs, Tab, Typography, IconButton } from "@mui/material";
+import {
+ ChevronRight as ChevronRightIcon,
+ ChevronLeft as ChevronLeftIcon,
+ Sensors as SensorsIcon,
+ Analytics as AnalyticsIcon,
+ Search as SearchIcon,
+} from "@mui/icons-material";
+import OptimizationParameters from "./MonitoringPlaceOptimization/OptimizationParameters";
+import SchemeQuery from "./MonitoringPlaceOptimization/SchemeQuery";
+
+interface SchemeRecord {
+ id: number;
+ schemeName: string;
+ sensorNumber: number;
+ minDiameter: number;
+ user: string;
+ create_time: string;
+ sensorLocation?: string[];
+}
+
+interface TabPanelProps {
+ children?: React.ReactNode;
+ index: number;
+ value: number;
+}
+
+const TabPanel: React.FC = ({ children, value, index }) => {
+ return (
+
+ {value === index && (
+ {children}
+ )}
+
+ );
+};
+
+interface MonitoringPlaceOptimizationPanelProps {
+ open?: boolean;
+ onToggle?: () => void;
+}
+
+const MonitoringPlaceOptimizationPanel: React.FC<
+ MonitoringPlaceOptimizationPanelProps
+> = ({ open: controlledOpen, onToggle }) => {
+ const [internalOpen, setInternalOpen] = useState(true);
+ const [currentTab, setCurrentTab] = useState(0);
+
+ // 持久化方案查询结果
+ const [schemes, setSchemes] = useState([]);
+
+ // 使用受控或非受控状态
+ const isOpen = controlledOpen !== undefined ? controlledOpen : internalOpen;
+ const handleToggle = () => {
+ if (onToggle) {
+ onToggle();
+ } else {
+ setInternalOpen(!internalOpen);
+ }
+ };
+
+ const handleTabChange = (_event: React.SyntheticEvent, newValue: number) => {
+ setCurrentTab(newValue);
+ };
+
+ const drawerWidth = 520;
+
+ return (
+ <>
+ {/* 收起时的触发按钮 */}
+ {!isOpen && (
+
+
+
+
+ 监测点优化
+
+
+
+
+ )}
+
+ {/* 主面板 */}
+
+
+ {/* 头部 */}
+
+
+
+
+ 监测点优化
+
+
+
+
+
+
+
+ {/* Tabs 导航 */}
+
+
+ }
+ iconPosition="start"
+ label="优化要件"
+ />
+ }
+ iconPosition="start"
+ label="方案查询"
+ />
+
+
+
+ {/* Tab 内容 */}
+
+
+
+
+
+ {
+ console.log("定位方案:", id);
+ // TODO: 在地图上定位
+ }}
+ />
+
+
+
+ >
+ );
+};
+
+export default MonitoringPlaceOptimizationPanel;