risk-visualization.tsx 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468
  1. "use client"
  2. import {useState} from "react"
  3. import {Button, Card, Col, Row, Select, Slider, Space, Switch, Tag} from "antd"
  4. import {FullscreenOutlined, ReloadOutlined} from "@ant-design/icons"
  5. import EChart from "@/components/echarts"
  6. import dynamic from "next/dynamic"
  7. const GasNetworkMap = dynamic(() => import("./gas-network-map"), {
  8. ssr: false,
  9. loading: () => <div className="flex items-center justify-center h-96">加载地图中...</div>,
  10. })
  11. const { Option } = Select
  12. // 模拟风险点位数据
  13. const mockRiskPoints = [
  14. {
  15. id: "R001",
  16. name: "解放路段高压管线风险",
  17. position: [39.9042, 116.4074],
  18. riskLevel: "高",
  19. riskScore: 85,
  20. riskType: "管线老化",
  21. color: "#f5222d",
  22. affectedPopulation: 2000,
  23. },
  24. {
  25. id: "R002",
  26. name: "人民路段泄漏风险",
  27. position: [39.9052, 116.4084],
  28. riskLevel: "中",
  29. riskScore: 65,
  30. riskType: "泄漏隐患",
  31. color: "#faad14",
  32. affectedPopulation: 800,
  33. },
  34. {
  35. id: "R003",
  36. name: "建设路段压力风险",
  37. position: [39.9032, 116.4064],
  38. riskLevel: "低",
  39. riskScore: 35,
  40. riskType: "压力异常",
  41. color: "#52c41a",
  42. affectedPopulation: 300,
  43. },
  44. {
  45. id: "R004",
  46. name: "商业街管网风险",
  47. position: [39.9062, 116.4094],
  48. riskLevel: "中",
  49. riskScore: 70,
  50. riskType: "管线老化",
  51. color: "#faad14",
  52. affectedPopulation: 1500,
  53. },
  54. ]
  55. // 模拟风险区域数据(热力图)
  56. const mockRiskHeatmapData = [
  57. { x: "解放路", y: "北段", value: 85 },
  58. { x: "解放路", y: "中段", value: 65 },
  59. { x: "解放路", y: "南段", value: 45 },
  60. { x: "人民路", y: "北段", value: 70 },
  61. { x: "人民路", y: "中段", value: 55 },
  62. { x: "人民路", y: "南段", value: 40 },
  63. { x: "建设路", y: "北段", value: 35 },
  64. { x: "建设路", y: "中段", value: 50 },
  65. { x: "建设路", y: "南段", value: 60 },
  66. { x: "商业街", y: "东段", value: 75 },
  67. { x: "商业街", y: "西段", value: 80 },
  68. ]
  69. // 模拟风险统计数据
  70. const mockRiskTypeStats = [
  71. { type: "管线老化", count: 12, percentage: 48 },
  72. { type: "泄漏隐患", count: 8, percentage: 32 },
  73. { type: "压力异常", count: 3, percentage: 12 },
  74. { type: "其他", count: 2, percentage: 8 },
  75. ]
  76. const mockRiskLevelStats = [
  77. { level: "高风险", count: 5, color: "#f5222d" },
  78. { level: "中风险", count: 12, color: "#faad14" },
  79. { level: "低风险", count: 8, color: "#52c41a" },
  80. ]
  81. const mockRiskTrendData = [
  82. { month: "2023-07", 高风险: 3, 中风险: 8, 低风险: 12 },
  83. { month: "2023-08", 高风险: 4, 中风险: 10, 低风险: 11 },
  84. { month: "2023-09", 高风险: 6, 中风险: 12, 低风险: 9 },
  85. { month: "2023-10", 高风险: 5, 中风险: 11, 低风险: 10 },
  86. { month: "2023-11", 高风险: 4, 中风险: 9, 低风险: 12 },
  87. { month: "2023-12", 高风险: 3, 中风险: 8, 低风险: 14 },
  88. { month: "2024-01", 高风险: 5, 中风险: 12, 低风险: 8 },
  89. ]
  90. export default function RiskVisualization() {
  91. const [layerVisibility, setLayerVisibility] = useState({
  92. pipeline: true,
  93. highRisk: true,
  94. mediumRisk: true,
  95. lowRisk: true,
  96. riskHeatmap: true,
  97. riskBuffer: true,
  98. })
  99. const [riskThreshold, setRiskThreshold] = useState([40, 70])
  100. const [selectedRiskType, setSelectedRiskType] = useState("all")
  101. const [isFullscreen, setIsFullscreen] = useState(false)
  102. const handleLayerToggle = (layer: string, visible: boolean) => {
  103. setLayerVisibility((prev) => ({
  104. ...prev,
  105. [layer]: visible,
  106. }))
  107. }
  108. const handleRefresh = () => {
  109. console.log("刷新风险数据")
  110. }
  111. const getRiskLevelColor = (level: string) => {
  112. switch (level) {
  113. case "高":
  114. return "#f5222d"
  115. case "中":
  116. return "#faad14"
  117. case "低":
  118. return "#52c41a"
  119. default:
  120. return "#1890ff"
  121. }
  122. }
  123. const pieOption = {
  124. title: { text: "风险类型分布", left: "center" },
  125. tooltip: { trigger: "item" },
  126. legend: { orient: "vertical", left: "left" },
  127. series: [
  128. {
  129. type: "pie",
  130. radius: "50%",
  131. data: mockRiskTypeStats.map((item) => ({
  132. value: item.count,
  133. name: item.type,
  134. })),
  135. emphasis: {
  136. itemStyle: {
  137. shadowBlur: 10,
  138. shadowOffsetX: 0,
  139. shadowColor: "rgba(0, 0, 0, 0.5)",
  140. },
  141. },
  142. },
  143. ],
  144. }
  145. const columnOption = {
  146. title: { text: "风险趋势分析", left: "center" },
  147. tooltip: { trigger: "axis" },
  148. legend: { data: ["高风险", "中风险", "低风险"] },
  149. grid: { left: "3%", right: "4%", bottom: "3%", containLabel: true },
  150. xAxis: {
  151. type: "category",
  152. data: mockRiskTrendData.map((item) => item.month),
  153. },
  154. yAxis: { type: "value", stack: "total" },
  155. series: [
  156. {
  157. name: "高风险",
  158. type: "bar",
  159. stack: "total",
  160. data: mockRiskTrendData.map((item) => item.高风险),
  161. itemStyle: { color: "#f5222d" },
  162. },
  163. {
  164. name: "中风险",
  165. type: "bar",
  166. stack: "total",
  167. data: mockRiskTrendData.map((item) => item.中风险),
  168. itemStyle: { color: "#faad14" },
  169. },
  170. {
  171. name: "低风险",
  172. type: "bar",
  173. stack: "total",
  174. data: mockRiskTrendData.map((item) => item.低风险),
  175. itemStyle: { color: "#52c41a" },
  176. },
  177. ],
  178. }
  179. const heatmapOption = {
  180. title: { text: "区域风险热力图", left: "center" },
  181. tooltip: { position: "top" },
  182. grid: { height: "50%", top: "10%" },
  183. xAxis: {
  184. type: "category",
  185. data: [...new Set(mockRiskHeatmapData.map((item) => item.x))],
  186. splitArea: { show: true },
  187. },
  188. yAxis: {
  189. type: "category",
  190. data: [...new Set(mockRiskHeatmapData.map((item) => item.y))],
  191. splitArea: { show: true },
  192. },
  193. visualMap: {
  194. min: 0,
  195. max: 100,
  196. calculable: true,
  197. orient: "horizontal",
  198. left: "center",
  199. bottom: "15%",
  200. inRange: {
  201. color: ["#52c41a", "#faad14", "#f5222d"],
  202. },
  203. },
  204. series: [
  205. {
  206. type: "heatmap",
  207. data: mockRiskHeatmapData.map((item) => [item.x, item.y, item.value]),
  208. label: {
  209. show: true,
  210. color: "#fff",
  211. fontSize: 12,
  212. },
  213. emphasis: {
  214. itemStyle: {
  215. shadowBlur: 10,
  216. shadowColor: "rgba(0, 0, 0, 0.5)",
  217. },
  218. },
  219. },
  220. ],
  221. }
  222. const highRiskCount = mockRiskPoints.filter((point) => point.riskLevel === "高").length
  223. const mediumRiskCount = mockRiskPoints.filter((point) => point.riskLevel === "中").length
  224. const lowRiskCount = mockRiskPoints.filter((point) => point.riskLevel === "低").length
  225. return (
  226. <div className="p-6">
  227. <Card title="风险评估可视化">
  228. {/* 风险概览统计 */}
  229. <Row gutter={16} className="mb-6">
  230. <Col span={6}>
  231. <Card size="small">
  232. <div className="text-center">
  233. <div className="text-2xl font-bold text-red-600">{highRiskCount}</div>
  234. <div className="text-gray-600">高风险区域</div>
  235. </div>
  236. </Card>
  237. </Col>
  238. <Col span={6}>
  239. <Card size="small">
  240. <div className="text-center">
  241. <div className="text-2xl font-bold text-orange-600">{mediumRiskCount}</div>
  242. <div className="text-gray-600">中风险区域</div>
  243. </div>
  244. </Card>
  245. </Col>
  246. <Col span={6}>
  247. <Card size="small">
  248. <div className="text-center">
  249. <div className="text-2xl font-bold text-blue-600">{lowRiskCount}</div>
  250. <div className="text-gray-600">低风险区域</div>
  251. </div>
  252. </Card>
  253. </Col>
  254. <Col span={6}>
  255. <Card size="small">
  256. <div className="text-center">
  257. <div className="text-2xl font-bold text-gray-600">{mockRiskPoints.length}</div>
  258. <div className="text-gray-600">风险点总数</div>
  259. </div>
  260. </Card>
  261. </Col>
  262. </Row>
  263. {/* 图层控制 */}
  264. <Row gutter={16} className="mb-4">
  265. <Col span={24}>
  266. <Card size="small" title="风险图层控制">
  267. <Row gutter={16}>
  268. <Col span={4}>
  269. <div className="mb-2">
  270. <Switch
  271. checked={layerVisibility.pipeline}
  272. onChange={(checked) => handleLayerToggle("pipeline", checked)}
  273. size="small"
  274. />
  275. <span className="ml-2">管网管线</span>
  276. </div>
  277. </Col>
  278. <Col span={4}>
  279. <div className="mb-2">
  280. <Switch
  281. checked={layerVisibility.highRisk}
  282. onChange={(checked) => handleLayerToggle("highRisk", checked)}
  283. size="small"
  284. />
  285. <span className="ml-2">高风险区域</span>
  286. </div>
  287. </Col>
  288. <Col span={4}>
  289. <div className="mb-2">
  290. <Switch
  291. checked={layerVisibility.mediumRisk}
  292. onChange={(checked) => handleLayerToggle("mediumRisk", checked)}
  293. size="small"
  294. />
  295. <span className="ml-2">中风险区域</span>
  296. </div>
  297. </Col>
  298. <Col span={4}>
  299. <div className="mb-2">
  300. <Switch
  301. checked={layerVisibility.lowRisk}
  302. onChange={(checked) => handleLayerToggle("lowRisk", checked)}
  303. size="small"
  304. />
  305. <span className="ml-2">低风险区域</span>
  306. </div>
  307. </Col>
  308. <Col span={4}>
  309. <div className="mb-2">
  310. <Switch
  311. checked={layerVisibility.riskHeatmap}
  312. onChange={(checked) => handleLayerToggle("riskHeatmap", checked)}
  313. size="small"
  314. />
  315. <span className="ml-2">风险热力图</span>
  316. </div>
  317. </Col>
  318. <Col span={4}>
  319. <div className="mb-2">
  320. <Switch
  321. checked={layerVisibility.riskBuffer}
  322. onChange={(checked) => handleLayerToggle("riskBuffer", checked)}
  323. size="small"
  324. />
  325. <span className="ml-2">影响范围</span>
  326. </div>
  327. </Col>
  328. </Row>
  329. </Card>
  330. </Col>
  331. </Row>
  332. {/* 风险控制参数 */}
  333. <Row gutter={16} className="mb-4">
  334. <Col span={8}>
  335. <Card size="small" title="风险阈值设置">
  336. <div className="mb-2">
  337. <span className="text-sm text-gray-600">风险评分范围: </span>
  338. </div>
  339. <Slider
  340. range
  341. min={0}
  342. max={100}
  343. value={riskThreshold}
  344. onChange={setRiskThreshold}
  345. marks={{ 0: "0", 40: "40", 70: "70", 100: "100" }}
  346. />
  347. <div className="text-center text-sm text-gray-500 mt-2">
  348. 当前范围: {riskThreshold[0]} - {riskThreshold[1]}
  349. </div>
  350. </Card>
  351. </Col>
  352. <Col span={8}>
  353. <Card size="small" title="风险类型筛选">
  354. <Select value={selectedRiskType} onChange={setSelectedRiskType} style={{ width: "100%" }}>
  355. <Option value="all">全部类型</Option>
  356. <Option value="aging">管线老化</Option>
  357. <Option value="leak">泄漏隐患</Option>
  358. <Option value="pressure">压力异常</Option>
  359. <Option value="other">其他</Option>
  360. </Select>
  361. </Card>
  362. </Col>
  363. <Col span={8}>
  364. <Card size="small" title="操作控制">
  365. <Space>
  366. <Button icon={<ReloadOutlined />} onClick={handleRefresh} size="small">
  367. 刷新数据
  368. </Button>
  369. <Button icon={<FullscreenOutlined />} onClick={() => setIsFullscreen(!isFullscreen)} size="small">
  370. 全屏显示
  371. </Button>
  372. </Space>
  373. </Card>
  374. </Col>
  375. </Row>
  376. {/* 风险四色图例 */}
  377. <Row gutter={16} className="mb-4">
  378. <Col span={24}>
  379. <Card size="small" title="风险四色图例">
  380. <Space>
  381. <Tag color="#f5222d">红色 - 高风险 (80-100分)</Tag>
  382. <Tag color="#faad14">橙色 - 中高风险 (60-79分)</Tag>
  383. <Tag color="#52c41a">黄色 - 中低风险 (40-59分)</Tag>
  384. <Tag color="#1890ff">蓝色 - 低风险 (0-39分)</Tag>
  385. </Space>
  386. </Card>
  387. </Col>
  388. </Row>
  389. {/* 地图和统计图表 */}
  390. <Row gutter={16}>
  391. <Col span={16}>
  392. <Card title="风险四色图" size="small" className={isFullscreen ? "fixed inset-0 z-50" : ""}>
  393. <GasNetworkMap height={isFullscreen ? "calc(100vh - 120px)" : "600px"} />
  394. </Card>
  395. </Col>
  396. <Col span={8}>
  397. <Row gutter={16}>
  398. <Col span={24}>
  399. <Card title="风险类型分布" size="small" className="mb-4">
  400. <EChart option={pieOption} style={{ height: 200 }} />
  401. </Card>
  402. </Col>
  403. <Col span={24}>
  404. <Card title="风险趋势分析" size="small" className="mb-4">
  405. <EChart option={columnOption} style={{ height: 200 }} />
  406. </Card>
  407. </Col>
  408. <Col span={24}>
  409. <Card title="区域风险热力图" size="small">
  410. <EChart option={heatmapOption} style={{ height: 200 }} />
  411. </Card>
  412. </Col>
  413. </Row>
  414. </Col>
  415. </Row>
  416. {/* 风险点位详情 */}
  417. <Row gutter={16} className="mt-4">
  418. <Col span={24}>
  419. <Card title="高风险区域详情" size="small">
  420. <div className="space-y-3 max-h-64 overflow-y-auto">
  421. {mockRiskPoints
  422. .filter((point) => point.riskLevel === "高")
  423. .map((point) => (
  424. <div key={point.id} className="p-3 border rounded-lg">
  425. <div className="flex justify-between items-center mb-2">
  426. <span className="font-medium">{point.name}</span>
  427. <div className="space-x-2">
  428. <Tag color={getRiskLevelColor(point.riskLevel)}>{point.riskLevel}风险</Tag>
  429. <Tag color="blue">评分: {point.riskScore}</Tag>
  430. </div>
  431. </div>
  432. <div className="text-sm text-gray-600 space-y-1">
  433. <div>风险类型: {point.riskType}</div>
  434. <div>影响人口: {point.affectedPopulation}人</div>
  435. <div>风险等级: {point.riskLevel}</div>
  436. </div>
  437. </div>
  438. ))}
  439. </div>
  440. </Card>
  441. </Col>
  442. </Row>
  443. </Card>
  444. </div>
  445. )
  446. }