gas-network-map.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  1. "use client"
  2. import {useState} from "react"
  3. import {Circle, LayersControl, MapContainer, Marker, Polyline, Popup, TileLayer} from "react-leaflet"
  4. import {Badge, Card, Descriptions, Divider, Switch} from "antd"
  5. import {Icon} from "leaflet"
  6. import "leaflet/dist/leaflet.css"
  7. // 修复 Leaflet 默认图标问题
  8. if (typeof window !== "undefined") {
  9. delete (Icon.Default.prototype as any)._getIconUrl
  10. Icon.Default.mergeOptions({
  11. iconRetinaUrl: "https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon-2x.png",
  12. iconUrl: "https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon.png",
  13. shadowUrl: "https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png",
  14. })
  15. }
  16. const mockPipelineData = [
  17. {
  18. id: "pipeline_001",
  19. name: "五一大道主干管",
  20. coordinates: [
  21. [28.2282, 112.9388],
  22. [28.2292, 112.9398],
  23. [28.2302, 112.9408],
  24. [28.2312, 112.9418],
  25. ],
  26. pressure: "中压",
  27. material: "钢管",
  28. diameter: "DN400",
  29. status: "正常",
  30. },
  31. {
  32. id: "pipeline_002",
  33. name: "芙蓉路支管",
  34. coordinates: [
  35. [28.2312, 112.9418],
  36. [28.2322, 112.9428],
  37. [28.2332, 112.9438],
  38. ],
  39. pressure: "低压",
  40. material: "PE管",
  41. diameter: "DN200",
  42. status: "维护中",
  43. },
  44. {
  45. id: "pipeline_003",
  46. name: "韶山路干管",
  47. coordinates: [
  48. [28.2202, 112.9308],
  49. [28.2212, 112.9318],
  50. [28.2222, 112.9328],
  51. [28.2232, 112.9338],
  52. ],
  53. pressure: "高压",
  54. material: "钢管",
  55. diameter: "DN600",
  56. status: "正常",
  57. },
  58. ]
  59. const mockMonitoringDevices = [
  60. {
  61. id: "device_CS001",
  62. name: "压力监测点CS001",
  63. position: [28.2282, 112.9388],
  64. type: "压力监测",
  65. status: "在线",
  66. value: "0.28MPa",
  67. lastUpdate: "2024-01-15 14:30:00",
  68. },
  69. {
  70. id: "device_CS002",
  71. name: "流量监测点CS002",
  72. position: [28.2302, 112.9408],
  73. type: "流量监测",
  74. status: "在线",
  75. value: "156m³/h",
  76. lastUpdate: "2024-01-15 14:29:45",
  77. },
  78. {
  79. id: "device_CS003",
  80. name: "甲烷浓度监测CS003",
  81. position: [28.2322, 112.9428],
  82. type: "气体浓度",
  83. status: "报警",
  84. value: "0.6%LEL",
  85. lastUpdate: "2024-01-15 14:31:12",
  86. },
  87. {
  88. id: "device_CS004",
  89. name: "温度监测点CS004",
  90. position: [28.2222, 112.9328],
  91. type: "温度监测",
  92. status: "在线",
  93. value: "15.2°C",
  94. lastUpdate: "2024-01-15 14:30:30",
  95. },
  96. ]
  97. const mockHazardPoints = [
  98. {
  99. id: "hazard_CS001",
  100. name: "管道老化隐患",
  101. position: [28.2292, 112.9398],
  102. level: "较大",
  103. type: "设备老化",
  104. description: "五一大道段管道使用年限超过20年,存在老化风险",
  105. status: "待处理",
  106. },
  107. {
  108. id: "hazard_CS002",
  109. name: "地铁施工风险",
  110. position: [28.2332, 112.9438],
  111. level: "重大",
  112. type: "外部威胁",
  113. description: "芙蓉路地铁3号线施工,可能影响管道安全",
  114. status: "处理中",
  115. },
  116. {
  117. id: "hazard_CS003",
  118. name: "道路改造影响",
  119. position: [28.2212, 112.9318],
  120. level: "一般",
  121. type: "外部威胁",
  122. description: "韶山路改造工程可能对管道造成影响",
  123. status: "已处理",
  124. },
  125. ]
  126. const mockRiskAreas = [
  127. {
  128. id: "risk_CS001",
  129. center: [28.2292, 112.9398],
  130. radius: 200,
  131. level: "较大风险",
  132. color: "#faad14",
  133. },
  134. {
  135. id: "risk_CS002",
  136. center: [28.2332, 112.9438],
  137. radius: 300,
  138. level: "重大风险",
  139. color: "#f5222d",
  140. },
  141. {
  142. id: "risk_CS003",
  143. center: [28.2212, 112.9318],
  144. radius: 150,
  145. level: "低风险",
  146. color: "#52c41a",
  147. },
  148. ]
  149. // 自定义图标
  150. const createCustomIcon = (color: string, symbol: string) => {
  151. return new Icon({
  152. iconUrl: `data:image/svg+xml;base64,${btoa(`
  153. <svg width="25" height="41" viewBox="0 0 25 41" xmlns="http://www.w3.org/2000/svg">
  154. <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"/>
  155. <text x="12.5" y="17" textAnchor="middle" fill="white" fontSize="12" fontWeight="bold">${symbol}</text>
  156. </svg>
  157. `)}`,
  158. iconSize: [25, 41],
  159. iconAnchor: [12, 41],
  160. popupAnchor: [1, -34],
  161. })
  162. }
  163. const deviceIcon = createCustomIcon("#1890ff", "D")
  164. const hazardIcon = createCustomIcon("#faad14", "!")
  165. const criticalHazardIcon = createCustomIcon("#f5222d", "!!")
  166. interface GasNetworkMapProps {
  167. height?: string
  168. }
  169. export default function GasNetworkMap({ height = "600px" }: GasNetworkMapProps) {
  170. const [showPipelines, setShowPipelines] = useState(true)
  171. const [showDevices, setShowDevices] = useState(true)
  172. const [showHazards, setShowHazards] = useState(true)
  173. const [showRiskAreas, setShowRiskAreas] = useState(true)
  174. return (
  175. <div className="w-full">
  176. <Card className="mb-4">
  177. <div className="flex flex-wrap gap-4 items-center">
  178. <div className="flex items-center gap-2">
  179. <Switch checked={showPipelines} onChange={setShowPipelines} size="small" />
  180. <span>管网管线</span>
  181. </div>
  182. <div className="flex items-center gap-2">
  183. <Switch checked={showDevices} onChange={setShowDevices} size="small" />
  184. <span>监测设备</span>
  185. </div>
  186. <div className="flex items-center gap-2">
  187. <Switch checked={showHazards} onChange={setShowHazards} size="small" />
  188. <span>隐患点位</span>
  189. </div>
  190. <div className="flex items-center gap-2">
  191. <Switch checked={showRiskAreas} onChange={setShowRiskAreas} size="small" />
  192. <span>风险区域</span>
  193. </div>
  194. </div>
  195. </Card>
  196. <Card>
  197. <div style={{ height }}>
  198. <MapContainer center={[28.2282, 112.9388]} zoom={14} style={{ height: "100%", width: "100%" }}>
  199. <LayersControl position="topright">
  200. <LayersControl.BaseLayer checked name="标准地图">
  201. <TileLayer
  202. attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
  203. url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
  204. />
  205. </LayersControl.BaseLayer>
  206. <LayersControl.BaseLayer name="卫星地图">
  207. <TileLayer
  208. attribution='&copy; <a href="https://www.esri.com/">Esri</a>'
  209. url="https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}"
  210. />
  211. </LayersControl.BaseLayer>
  212. </LayersControl>
  213. {/* 管网管线 */}
  214. {showPipelines &&
  215. mockPipelineData.map((pipeline) => (
  216. <Polyline
  217. key={pipeline.id}
  218. positions={pipeline.coordinates as [number, number][]}
  219. color={pipeline.status === "正常" ? "#52c41a" : "#faad14"}
  220. weight={6}
  221. opacity={0.8}
  222. >
  223. <Popup>
  224. <div className="min-w-64">
  225. <h4 className="font-semibold mb-2">{pipeline.name}</h4>
  226. <Descriptions size="small" column={1}>
  227. <Descriptions.Item label="管道编号">{pipeline.id}</Descriptions.Item>
  228. <Descriptions.Item label="压力等级">{pipeline.pressure}</Descriptions.Item>
  229. <Descriptions.Item label="管材">{pipeline.material}</Descriptions.Item>
  230. <Descriptions.Item label="管径">{pipeline.diameter}</Descriptions.Item>
  231. <Descriptions.Item label="状态">
  232. <Badge status={pipeline.status === "正常" ? "success" : "warning"} text={pipeline.status} />
  233. </Descriptions.Item>
  234. </Descriptions>
  235. </div>
  236. </Popup>
  237. </Polyline>
  238. ))}
  239. {/* 监测设备 */}
  240. {showDevices &&
  241. mockMonitoringDevices.map((device) => (
  242. <Marker key={device.id} position={device.position as [number, number]} icon={deviceIcon}>
  243. <Popup>
  244. <div className="min-w-64">
  245. <h4 className="font-semibold mb-2">{device.name}</h4>
  246. <Descriptions size="small" column={1}>
  247. <Descriptions.Item label="设备编号">{device.id}</Descriptions.Item>
  248. <Descriptions.Item label="监测类型">{device.type}</Descriptions.Item>
  249. <Descriptions.Item label="当前值">{device.value}</Descriptions.Item>
  250. <Descriptions.Item label="状态">
  251. <Badge
  252. status={
  253. device.status === "在线" ? "success" : device.status === "报警" ? "error" : "default"
  254. }
  255. text={device.status}
  256. />
  257. </Descriptions.Item>
  258. <Descriptions.Item label="更新时间">{device.lastUpdate}</Descriptions.Item>
  259. </Descriptions>
  260. </div>
  261. </Popup>
  262. </Marker>
  263. ))}
  264. {/* 隐患点位 */}
  265. {showHazards &&
  266. mockHazardPoints.map((hazard) => (
  267. <Marker
  268. key={hazard.id}
  269. position={hazard.position as [number, number]}
  270. icon={hazard.level === "重大" ? criticalHazardIcon : hazardIcon}
  271. >
  272. <Popup>
  273. <div className="min-w-64">
  274. <h4 className="font-semibold mb-2">{hazard.name}</h4>
  275. <Descriptions size="small" column={1}>
  276. <Descriptions.Item label="隐患编号">{hazard.id}</Descriptions.Item>
  277. <Descriptions.Item label="风险等级">
  278. <Badge color={hazard.level === "重大" ? "red" : "orange"} text={hazard.level} />
  279. </Descriptions.Item>
  280. <Descriptions.Item label="隐患类型">{hazard.type}</Descriptions.Item>
  281. <Descriptions.Item label="处理状态">
  282. <Badge status={hazard.status === "待处理" ? "warning" : "processing"} text={hazard.status} />
  283. </Descriptions.Item>
  284. </Descriptions>
  285. <Divider style={{ margin: "8px 0" }} />
  286. <p className="text-sm text-gray-600">{hazard.description}</p>
  287. </div>
  288. </Popup>
  289. </Marker>
  290. ))}
  291. {/* 风险区域 */}
  292. {showRiskAreas &&
  293. mockRiskAreas.map((area) => (
  294. <Circle
  295. key={area.id}
  296. center={area.center as [number, number]}
  297. radius={area.radius}
  298. fillColor={area.color}
  299. fillOpacity={0.2}
  300. color={area.color}
  301. weight={2}
  302. >
  303. <Popup>
  304. <div>
  305. <h4 className="font-semibold mb-2">风险评估区域</h4>
  306. <p>
  307. 风险等级: <Badge color={area.color} text={area.level} />
  308. </p>
  309. <p>影响半径: {area.radius}米</p>
  310. </div>
  311. </Popup>
  312. </Circle>
  313. ))}
  314. </MapContainer>
  315. </div>
  316. </Card>
  317. </div>
  318. )
  319. }