From 56d4b5cb46e80c4295af92ee05affecab792a1d7 Mon Sep 17 00:00:00 2001 From: JIANG Date: Fri, 24 Oct 2025 17:24:47 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=9B=E5=BB=BA=E7=9B=91=E6=B5=8B=E7=82=B9?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=B8=83=E7=BD=AE=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../monitoring-place-optimization/page.tsx | 15 + .../OptimizationParameters.tsx | 292 ++++++++++ .../SchemeQuery.tsx | 511 ++++++++++++++++++ .../MonitoringPlaceOptimizationPanel.tsx | 203 +++++++ 4 files changed, 1021 insertions(+) create mode 100644 src/app/(main)/monitoring-place-optimization/page.tsx create mode 100644 src/components/olmap/MonitoringPlaceOptimization/OptimizationParameters.tsx create mode 100644 src/components/olmap/MonitoringPlaceOptimization/SchemeQuery.tsx create mode 100644 src/components/olmap/MonitoringPlaceOptimizationPanel.tsx 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) => ( + + {option.label} + + ))} + + + + {/* 方法选择 */} + + + 方法 + + setMethod(e.target.value)} + sx={{ + "& .MuiOutlinedInput-root": { + "&:hover fieldset": { + borderColor: "#257DD4", + }, + "&.Mui-focused fieldset": { + borderColor: "#257DD4", + }, + }, + }} + > + {methodOptions.map((option) => ( + + {option.label} + + ))} + + + + {/* 监测点数目 */} + + + 监测点数目 + + 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", + }, + }, + }} + /> + + + {/* 创建方案按钮 */} + + + + + ); +}; + +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 ( + + ); +}; + +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;