|
|
@@ -0,0 +1,336 @@
|
|
|
+"use client"
|
|
|
+
|
|
|
+import {useState} from "react"
|
|
|
+import {Circle, LayersControl, MapContainer, Marker, Polyline, Popup, TileLayer} from "react-leaflet"
|
|
|
+import {Badge, Card, Descriptions, Divider, Switch} from "antd"
|
|
|
+import {Icon} from "leaflet"
|
|
|
+import "leaflet/dist/leaflet.css"
|
|
|
+
|
|
|
+// 修复 Leaflet 默认图标问题
|
|
|
+if (typeof window !== "undefined") {
|
|
|
+ delete (Icon.Default.prototype as any)._getIconUrl
|
|
|
+ Icon.Default.mergeOptions({
|
|
|
+ iconRetinaUrl: "https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon-2x.png",
|
|
|
+ iconUrl: "https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon.png",
|
|
|
+ shadowUrl: "https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png",
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+const mockPipelineData = [
|
|
|
+ {
|
|
|
+ id: "pipeline_001",
|
|
|
+ name: "五一大道主干管",
|
|
|
+ coordinates: [
|
|
|
+ [28.2282, 112.9388],
|
|
|
+ [28.2292, 112.9398],
|
|
|
+ [28.2302, 112.9408],
|
|
|
+ [28.2312, 112.9418],
|
|
|
+ ],
|
|
|
+ pressure: "中压",
|
|
|
+ material: "钢管",
|
|
|
+ diameter: "DN400",
|
|
|
+ status: "正常",
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: "pipeline_002",
|
|
|
+ name: "芙蓉路支管",
|
|
|
+ coordinates: [
|
|
|
+ [28.2312, 112.9418],
|
|
|
+ [28.2322, 112.9428],
|
|
|
+ [28.2332, 112.9438],
|
|
|
+ ],
|
|
|
+ pressure: "低压",
|
|
|
+ material: "PE管",
|
|
|
+ diameter: "DN200",
|
|
|
+ status: "维护中",
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: "pipeline_003",
|
|
|
+ name: "韶山路干管",
|
|
|
+ coordinates: [
|
|
|
+ [28.2202, 112.9308],
|
|
|
+ [28.2212, 112.9318],
|
|
|
+ [28.2222, 112.9328],
|
|
|
+ [28.2232, 112.9338],
|
|
|
+ ],
|
|
|
+ pressure: "高压",
|
|
|
+ material: "钢管",
|
|
|
+ diameter: "DN600",
|
|
|
+ status: "正常",
|
|
|
+ },
|
|
|
+]
|
|
|
+
|
|
|
+const mockMonitoringDevices = [
|
|
|
+ {
|
|
|
+ id: "device_CS001",
|
|
|
+ name: "压力监测点CS001",
|
|
|
+ position: [28.2282, 112.9388],
|
|
|
+ type: "压力监测",
|
|
|
+ status: "在线",
|
|
|
+ value: "0.28MPa",
|
|
|
+ lastUpdate: "2024-01-15 14:30:00",
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: "device_CS002",
|
|
|
+ name: "流量监测点CS002",
|
|
|
+ position: [28.2302, 112.9408],
|
|
|
+ type: "流量监测",
|
|
|
+ status: "在线",
|
|
|
+ value: "156m³/h",
|
|
|
+ lastUpdate: "2024-01-15 14:29:45",
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: "device_CS003",
|
|
|
+ name: "甲烷浓度监测CS003",
|
|
|
+ position: [28.2322, 112.9428],
|
|
|
+ type: "气体浓度",
|
|
|
+ status: "报警",
|
|
|
+ value: "0.6%LEL",
|
|
|
+ lastUpdate: "2024-01-15 14:31:12",
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: "device_CS004",
|
|
|
+ name: "温度监测点CS004",
|
|
|
+ position: [28.2222, 112.9328],
|
|
|
+ type: "温度监测",
|
|
|
+ status: "在线",
|
|
|
+ value: "15.2°C",
|
|
|
+ lastUpdate: "2024-01-15 14:30:30",
|
|
|
+ },
|
|
|
+]
|
|
|
+
|
|
|
+const mockHazardPoints = [
|
|
|
+ {
|
|
|
+ id: "hazard_CS001",
|
|
|
+ name: "管道老化隐患",
|
|
|
+ position: [28.2292, 112.9398],
|
|
|
+ level: "较大",
|
|
|
+ type: "设备老化",
|
|
|
+ description: "五一大道段管道使用年限超过20年,存在老化风险",
|
|
|
+ status: "待处理",
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: "hazard_CS002",
|
|
|
+ name: "地铁施工风险",
|
|
|
+ position: [28.2332, 112.9438],
|
|
|
+ level: "重大",
|
|
|
+ type: "外部威胁",
|
|
|
+ description: "芙蓉路地铁3号线施工,可能影响管道安全",
|
|
|
+ status: "处理中",
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: "hazard_CS003",
|
|
|
+ name: "道路改造影响",
|
|
|
+ position: [28.2212, 112.9318],
|
|
|
+ level: "一般",
|
|
|
+ type: "外部威胁",
|
|
|
+ description: "韶山路改造工程可能对管道造成影响",
|
|
|
+ status: "已处理",
|
|
|
+ },
|
|
|
+]
|
|
|
+
|
|
|
+const mockRiskAreas = [
|
|
|
+ {
|
|
|
+ id: "risk_CS001",
|
|
|
+ center: [28.2292, 112.9398],
|
|
|
+ radius: 200,
|
|
|
+ level: "较大风险",
|
|
|
+ color: "#faad14",
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: "risk_CS002",
|
|
|
+ center: [28.2332, 112.9438],
|
|
|
+ radius: 300,
|
|
|
+ level: "重大风险",
|
|
|
+ color: "#f5222d",
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: "risk_CS003",
|
|
|
+ center: [28.2212, 112.9318],
|
|
|
+ radius: 150,
|
|
|
+ level: "低风险",
|
|
|
+ color: "#52c41a",
|
|
|
+ },
|
|
|
+]
|
|
|
+
|
|
|
+// 自定义图标
|
|
|
+const createCustomIcon = (color: string, symbol: string) => {
|
|
|
+ return new Icon({
|
|
|
+ iconUrl: `data:image/svg+xml;base64,${btoa(`
|
|
|
+ <svg width="25" height="41" viewBox="0 0 25 41" xmlns="http://www.w3.org/2000/svg">
|
|
|
+ <path fill="${color}" stroke="#fff" strokeWidth="2" d="M12.5 0C5.6 0 0 5.6 0 12.5c0 12.5 12.5 28.5 12.5 28.5s12.5-16 12.5-28.5C25 5.6 19.4 0 12.5 0z"/>
|
|
|
+ <text x="12.5" y="17" textAnchor="middle" fill="white" fontSize="12" fontWeight="bold">${symbol}</text>
|
|
|
+ </svg>
|
|
|
+ `)}`,
|
|
|
+ iconSize: [25, 41],
|
|
|
+ iconAnchor: [12, 41],
|
|
|
+ popupAnchor: [1, -34],
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+const deviceIcon = createCustomIcon("#1890ff", "D")
|
|
|
+const hazardIcon = createCustomIcon("#faad14", "!")
|
|
|
+const criticalHazardIcon = createCustomIcon("#f5222d", "!!")
|
|
|
+
|
|
|
+interface GasNetworkMapProps {
|
|
|
+ height?: string
|
|
|
+}
|
|
|
+
|
|
|
+export default function GasNetworkMap({ height = "600px" }: GasNetworkMapProps) {
|
|
|
+ const [showPipelines, setShowPipelines] = useState(true)
|
|
|
+ const [showDevices, setShowDevices] = useState(true)
|
|
|
+ const [showHazards, setShowHazards] = useState(true)
|
|
|
+ const [showRiskAreas, setShowRiskAreas] = useState(true)
|
|
|
+
|
|
|
+ return (
|
|
|
+ <div className="w-full">
|
|
|
+ <Card className="mb-4">
|
|
|
+ <div className="flex flex-wrap gap-4 items-center">
|
|
|
+ <div className="flex items-center gap-2">
|
|
|
+ <Switch checked={showPipelines} onChange={setShowPipelines} size="small" />
|
|
|
+ <span>管网管线</span>
|
|
|
+ </div>
|
|
|
+ <div className="flex items-center gap-2">
|
|
|
+ <Switch checked={showDevices} onChange={setShowDevices} size="small" />
|
|
|
+ <span>监测设备</span>
|
|
|
+ </div>
|
|
|
+ <div className="flex items-center gap-2">
|
|
|
+ <Switch checked={showHazards} onChange={setShowHazards} size="small" />
|
|
|
+ <span>隐患点位</span>
|
|
|
+ </div>
|
|
|
+ <div className="flex items-center gap-2">
|
|
|
+ <Switch checked={showRiskAreas} onChange={setShowRiskAreas} size="small" />
|
|
|
+ <span>风险区域</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </Card>
|
|
|
+
|
|
|
+ <Card>
|
|
|
+ <div style={{ height }}>
|
|
|
+ <MapContainer center={[28.2282, 112.9388]} zoom={14} style={{ height: "100%", width: "100%" }}>
|
|
|
+ <LayersControl position="topright">
|
|
|
+ <LayersControl.BaseLayer checked name="标准地图">
|
|
|
+ <TileLayer
|
|
|
+ attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
|
|
+ url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
|
|
+ />
|
|
|
+ </LayersControl.BaseLayer>
|
|
|
+
|
|
|
+ <LayersControl.BaseLayer name="卫星地图">
|
|
|
+ <TileLayer
|
|
|
+ attribution='© <a href="https://www.esri.com/">Esri</a>'
|
|
|
+ url="https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}"
|
|
|
+ />
|
|
|
+ </LayersControl.BaseLayer>
|
|
|
+ </LayersControl>
|
|
|
+
|
|
|
+ {/* 管网管线 */}
|
|
|
+ {showPipelines &&
|
|
|
+ mockPipelineData.map((pipeline) => (
|
|
|
+ <Polyline
|
|
|
+ key={pipeline.id}
|
|
|
+ positions={pipeline.coordinates as [number, number][]}
|
|
|
+ color={pipeline.status === "正常" ? "#52c41a" : "#faad14"}
|
|
|
+ weight={6}
|
|
|
+ opacity={0.8}
|
|
|
+ >
|
|
|
+ <Popup>
|
|
|
+ <div className="min-w-64">
|
|
|
+ <h4 className="font-semibold mb-2">{pipeline.name}</h4>
|
|
|
+ <Descriptions size="small" column={1}>
|
|
|
+ <Descriptions.Item label="管道编号">{pipeline.id}</Descriptions.Item>
|
|
|
+ <Descriptions.Item label="压力等级">{pipeline.pressure}</Descriptions.Item>
|
|
|
+ <Descriptions.Item label="管材">{pipeline.material}</Descriptions.Item>
|
|
|
+ <Descriptions.Item label="管径">{pipeline.diameter}</Descriptions.Item>
|
|
|
+ <Descriptions.Item label="状态">
|
|
|
+ <Badge status={pipeline.status === "正常" ? "success" : "warning"} text={pipeline.status} />
|
|
|
+ </Descriptions.Item>
|
|
|
+ </Descriptions>
|
|
|
+ </div>
|
|
|
+ </Popup>
|
|
|
+ </Polyline>
|
|
|
+ ))}
|
|
|
+
|
|
|
+ {/* 监测设备 */}
|
|
|
+ {showDevices &&
|
|
|
+ mockMonitoringDevices.map((device) => (
|
|
|
+ <Marker key={device.id} position={device.position as [number, number]} icon={deviceIcon}>
|
|
|
+ <Popup>
|
|
|
+ <div className="min-w-64">
|
|
|
+ <h4 className="font-semibold mb-2">{device.name}</h4>
|
|
|
+ <Descriptions size="small" column={1}>
|
|
|
+ <Descriptions.Item label="设备编号">{device.id}</Descriptions.Item>
|
|
|
+ <Descriptions.Item label="监测类型">{device.type}</Descriptions.Item>
|
|
|
+ <Descriptions.Item label="当前值">{device.value}</Descriptions.Item>
|
|
|
+ <Descriptions.Item label="状态">
|
|
|
+ <Badge
|
|
|
+ status={
|
|
|
+ device.status === "在线" ? "success" : device.status === "报警" ? "error" : "default"
|
|
|
+ }
|
|
|
+ text={device.status}
|
|
|
+ />
|
|
|
+ </Descriptions.Item>
|
|
|
+ <Descriptions.Item label="更新时间">{device.lastUpdate}</Descriptions.Item>
|
|
|
+ </Descriptions>
|
|
|
+ </div>
|
|
|
+ </Popup>
|
|
|
+ </Marker>
|
|
|
+ ))}
|
|
|
+
|
|
|
+ {/* 隐患点位 */}
|
|
|
+ {showHazards &&
|
|
|
+ mockHazardPoints.map((hazard) => (
|
|
|
+ <Marker
|
|
|
+ key={hazard.id}
|
|
|
+ position={hazard.position as [number, number]}
|
|
|
+ icon={hazard.level === "重大" ? criticalHazardIcon : hazardIcon}
|
|
|
+ >
|
|
|
+ <Popup>
|
|
|
+ <div className="min-w-64">
|
|
|
+ <h4 className="font-semibold mb-2">{hazard.name}</h4>
|
|
|
+ <Descriptions size="small" column={1}>
|
|
|
+ <Descriptions.Item label="隐患编号">{hazard.id}</Descriptions.Item>
|
|
|
+ <Descriptions.Item label="风险等级">
|
|
|
+ <Badge color={hazard.level === "重大" ? "red" : "orange"} text={hazard.level} />
|
|
|
+ </Descriptions.Item>
|
|
|
+ <Descriptions.Item label="隐患类型">{hazard.type}</Descriptions.Item>
|
|
|
+ <Descriptions.Item label="处理状态">
|
|
|
+ <Badge status={hazard.status === "待处理" ? "warning" : "processing"} text={hazard.status} />
|
|
|
+ </Descriptions.Item>
|
|
|
+ </Descriptions>
|
|
|
+ <Divider style={{ margin: "8px 0" }} />
|
|
|
+ <p className="text-sm text-gray-600">{hazard.description}</p>
|
|
|
+ </div>
|
|
|
+ </Popup>
|
|
|
+ </Marker>
|
|
|
+ ))}
|
|
|
+
|
|
|
+ {/* 风险区域 */}
|
|
|
+ {showRiskAreas &&
|
|
|
+ mockRiskAreas.map((area) => (
|
|
|
+ <Circle
|
|
|
+ key={area.id}
|
|
|
+ center={area.center as [number, number]}
|
|
|
+ radius={area.radius}
|
|
|
+ fillColor={area.color}
|
|
|
+ fillOpacity={0.2}
|
|
|
+ color={area.color}
|
|
|
+ weight={2}
|
|
|
+ >
|
|
|
+ <Popup>
|
|
|
+ <div>
|
|
|
+ <h4 className="font-semibold mb-2">风险评估区域</h4>
|
|
|
+ <p>
|
|
|
+ 风险等级: <Badge color={area.color} text={area.level} />
|
|
|
+ </p>
|
|
|
+ <p>影响半径: {area.radius}米</p>
|
|
|
+ </div>
|
|
|
+ </Popup>
|
|
|
+ </Circle>
|
|
|
+ ))}
|
|
|
+ </MapContainer>
|
|
|
+ </div>
|
|
|
+ </Card>
|
|
|
+ </div>
|
|
|
+ )
|
|
|
+}
|