alarm-visualization.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408
  1. "use client"
  2. import {useState} from "react"
  3. import {Button, Card, Col, Row, Select, Space, Statistic, Switch, Tag} from "antd"
  4. import {BellOutlined, 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 mockAlarmPoints = [
  14. {
  15. id: "A001",
  16. name: "解放路压力报警",
  17. position: [39.9042, 116.4074],
  18. type: "pressure",
  19. level: "高",
  20. status: "未处理",
  21. createTime: "2024-01-15 14:30:25",
  22. value: 0.52,
  23. threshold: 0.45,
  24. unit: "MPa",
  25. },
  26. {
  27. id: "A002",
  28. name: "人民路流量报警",
  29. position: [39.9052, 116.4084],
  30. type: "flow",
  31. level: "中",
  32. status: "处理中",
  33. createTime: "2024-01-15 13:45:10",
  34. value: 4200,
  35. threshold: 3500,
  36. unit: "m³/h",
  37. },
  38. {
  39. id: "A003",
  40. name: "建设路泄漏报警",
  41. position: [39.9032, 116.4064],
  42. type: "leakage",
  43. level: "高",
  44. status: "已处理",
  45. createTime: "2024-01-15 12:20:15",
  46. value: 0.08,
  47. threshold: 0.05,
  48. unit: "ppm",
  49. },
  50. ]
  51. // 模拟报警统计数据
  52. const mockAlarmTypeStats = [
  53. { type: "压力异常", count: 15, percentage: 42.9 },
  54. { type: "流量异常", count: 12, percentage: 34.3 },
  55. { type: "泄漏检测", count: 6, percentage: 17.1 },
  56. { type: "温度异常", count: 2, percentage: 5.7 },
  57. ]
  58. const mockAlarmLevelStats = [
  59. { level: "高", count: 8, color: "#f5222d" },
  60. { level: "中", count: 18, color: "#faad14" },
  61. { level: "低", count: 9, color: "#52c41a" },
  62. ]
  63. const mockHourlyStats = [
  64. { hour: "00", count: 2 },
  65. { hour: "01", count: 1 },
  66. { hour: "02", count: 0 },
  67. { hour: "03", count: 1 },
  68. { hour: "04", count: 3 },
  69. { hour: "05", count: 2 },
  70. { hour: "06", count: 4 },
  71. { hour: "07", count: 6 },
  72. { hour: "08", count: 8 },
  73. { hour: "09", count: 5 },
  74. { hour: "10", count: 7 },
  75. { hour: "11", count: 9 },
  76. { hour: "12", count: 12 },
  77. { hour: "13", count: 8 },
  78. { hour: "14", count: 6 },
  79. { hour: "15", count: 4 },
  80. { hour: "16", count: 3 },
  81. { hour: "17", count: 5 },
  82. { hour: "18", count: 7 },
  83. { hour: "19", count: 4 },
  84. { hour: "20", count: 2 },
  85. { hour: "21", count: 1 },
  86. { hour: "22", count: 1 },
  87. { hour: "23", count: 0 },
  88. ]
  89. export default function AlarmVisualization() {
  90. const [layerVisibility, setLayerVisibility] = useState({
  91. pipeline: true,
  92. pressureAlarm: true,
  93. flowAlarm: true,
  94. temperatureAlarm: true,
  95. leakageAlarm: true,
  96. highLevel: true,
  97. mediumLevel: true,
  98. lowLevel: true,
  99. })
  100. const [selectedTimeRange, setSelectedTimeRange] = useState("24hours")
  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 getStatusColor = (status: string) => {
  112. switch (status) {
  113. case "未处理":
  114. return "#f5222d"
  115. case "处理中":
  116. return "#faad14"
  117. case "已处理":
  118. return "#52c41a"
  119. default:
  120. return "#1890ff"
  121. }
  122. }
  123. const getLevelColor = (level: string) => {
  124. switch (level) {
  125. case "高":
  126. return "#f5222d"
  127. case "中":
  128. return "#faad14"
  129. case "低":
  130. return "#52c41a"
  131. default:
  132. return "#1890ff"
  133. }
  134. }
  135. const getTypeLabel = (type: string) => {
  136. const typeMap = {
  137. pressure: "压力",
  138. flow: "流量",
  139. temperature: "温度",
  140. leakage: "泄漏",
  141. }
  142. return typeMap[type as keyof typeof typeMap] || type
  143. }
  144. const pieOption = {
  145. title: { text: "报警类型分布", left: "center" },
  146. tooltip: { trigger: "item" },
  147. legend: { orient: "vertical", left: "left" },
  148. series: [
  149. {
  150. type: "pie",
  151. radius: "50%",
  152. data: mockAlarmTypeStats.map((item) => ({
  153. value: item.count,
  154. name: item.type,
  155. })),
  156. emphasis: {
  157. itemStyle: {
  158. shadowBlur: 10,
  159. shadowOffsetX: 0,
  160. shadowColor: "rgba(0, 0, 0, 0.5)",
  161. },
  162. },
  163. },
  164. ],
  165. }
  166. const columnOption = {
  167. title: { text: "24小时报警分布", left: "center" },
  168. tooltip: { trigger: "axis" },
  169. grid: { left: "3%", right: "4%", bottom: "3%", containLabel: true },
  170. xAxis: {
  171. type: "category",
  172. data: mockHourlyStats.map((item) => item.hour + ":00"),
  173. },
  174. yAxis: { type: "value", name: "报警数量" },
  175. series: [
  176. {
  177. type: "bar",
  178. data: mockHourlyStats.map((item) => item.count),
  179. itemStyle: { color: "#f5222d" },
  180. },
  181. ],
  182. }
  183. const unhandledCount = mockAlarmPoints.filter((point) => point.status === "未处理").length
  184. const processingCount = mockAlarmPoints.filter((point) => point.status === "处理中").length
  185. const handledCount = mockAlarmPoints.filter((point) => point.status === "已处理").length
  186. return (
  187. <div className="p-6">
  188. <Card title="监测报警可视化">
  189. {/* 报警概览统计 */}
  190. <Row gutter={16} className="mb-6">
  191. <Col span={6}>
  192. <Card size="small">
  193. <Statistic
  194. title="未处理报警"
  195. value={unhandledCount}
  196. valueStyle={{ color: "#f5222d" }}
  197. prefix={<BellOutlined />}
  198. />
  199. </Card>
  200. </Col>
  201. <Col span={6}>
  202. <Card size="small">
  203. <Statistic title="处理中" value={processingCount} valueStyle={{ color: "#faad14" }} />
  204. </Card>
  205. </Col>
  206. <Col span={6}>
  207. <Card size="small">
  208. <Statistic title="已处理" value={handledCount} valueStyle={{ color: "#52c41a" }} />
  209. </Card>
  210. </Col>
  211. <Col span={6}>
  212. <Card size="small">
  213. <Statistic title="报警总数" value={mockAlarmPoints.length} valueStyle={{ color: "#1890ff" }} />
  214. </Card>
  215. </Col>
  216. </Row>
  217. {/* 图层控制 */}
  218. <Row gutter={16} className="mb-4">
  219. <Col span={24}>
  220. <Card size="small" title="报警图层控制">
  221. <Row gutter={16}>
  222. <Col span={3}>
  223. <div className="mb-2">
  224. <Switch
  225. checked={layerVisibility.pipeline}
  226. onChange={(checked) => handleLayerToggle("pipeline", checked)}
  227. size="small"
  228. />
  229. <span className="ml-2">管网管线</span>
  230. </div>
  231. </Col>
  232. <Col span={3}>
  233. <div className="mb-2">
  234. <Switch
  235. checked={layerVisibility.pressureAlarm}
  236. onChange={(checked) => handleLayerToggle("pressureAlarm", checked)}
  237. size="small"
  238. />
  239. <span className="ml-2">压力报警</span>
  240. </div>
  241. </Col>
  242. <Col span={3}>
  243. <div className="mb-2">
  244. <Switch
  245. checked={layerVisibility.flowAlarm}
  246. onChange={(checked) => handleLayerToggle("flowAlarm", checked)}
  247. size="small"
  248. />
  249. <span className="ml-2">流量报警</span>
  250. </div>
  251. </Col>
  252. <Col span={3}>
  253. <div className="mb-2">
  254. <Switch
  255. checked={layerVisibility.leakageAlarm}
  256. onChange={(checked) => handleLayerToggle("leakageAlarm", checked)}
  257. size="small"
  258. />
  259. <span className="ml-2">泄漏报警</span>
  260. </div>
  261. </Col>
  262. <Col span={3}>
  263. <div className="mb-2">
  264. <Switch
  265. checked={layerVisibility.highLevel}
  266. onChange={(checked) => handleLayerToggle("highLevel", checked)}
  267. size="small"
  268. />
  269. <span className="ml-2">高级报警</span>
  270. </div>
  271. </Col>
  272. <Col span={3}>
  273. <div className="mb-2">
  274. <Switch
  275. checked={layerVisibility.mediumLevel}
  276. onChange={(checked) => handleLayerToggle("mediumLevel", checked)}
  277. size="small"
  278. />
  279. <span className="ml-2">中级报警</span>
  280. </div>
  281. </Col>
  282. <Col span={3}>
  283. <div className="mb-2">
  284. <Switch
  285. checked={layerVisibility.lowLevel}
  286. onChange={(checked) => handleLayerToggle("lowLevel", checked)}
  287. size="small"
  288. />
  289. <span className="ml-2">低级报警</span>
  290. </div>
  291. </Col>
  292. <Col span={3}>
  293. <Space>
  294. <Button icon={<ReloadOutlined />} onClick={handleRefresh} size="small">
  295. 刷新
  296. </Button>
  297. <Button icon={<FullscreenOutlined />} onClick={() => setIsFullscreen(!isFullscreen)} size="small">
  298. 全屏
  299. </Button>
  300. </Space>
  301. </Col>
  302. </Row>
  303. </Card>
  304. </Col>
  305. </Row>
  306. {/* 时间范围选择 */}
  307. <Row gutter={16} className="mb-4">
  308. <Col span={6}>
  309. <Card size="small" title="时间范围">
  310. <Select value={selectedTimeRange} onChange={setSelectedTimeRange} style={{ width: "100%" }}>
  311. <Option value="1hour">近1小时</Option>
  312. <Option value="6hours">近6小时</Option>
  313. <Option value="24hours">近24小时</Option>
  314. <Option value="7days">近7天</Option>
  315. <Option value="30days">近30天</Option>
  316. </Select>
  317. </Card>
  318. </Col>
  319. <Col span={18}>
  320. <Card size="small" title="报警态势图例">
  321. <Space>
  322. <Tag color="#f5222d">高级报警</Tag>
  323. <Tag color="#faad14">中级报警</Tag>
  324. <Tag color="#52c41a">低级报警</Tag>
  325. <Tag color="#1890ff">压力异常</Tag>
  326. <Tag color="#722ed1">流量异常</Tag>
  327. <Tag color="#13c2c2">泄漏检测</Tag>
  328. <Tag color="#eb2f96">温度异常</Tag>
  329. </Space>
  330. </Card>
  331. </Col>
  332. </Row>
  333. {/* 地图和统计图表 */}
  334. <Row gutter={16}>
  335. <Col span={16}>
  336. <Card title="报警态势一张图" size="small" className={isFullscreen ? "fixed inset-0 z-50" : ""}>
  337. <GasNetworkMap height={isFullscreen ? "calc(100vh - 120px)" : "600px"} />
  338. </Card>
  339. </Col>
  340. <Col span={8}>
  341. <Row gutter={16}>
  342. <Col span={24}>
  343. <Card title="报警类型分布" size="small" className="mb-4">
  344. <EChart option={pieOption} style={{ height: 250 }} />
  345. </Card>
  346. </Col>
  347. <Col span={24}>
  348. <Card title="24小时报警分布" size="small">
  349. <EChart option={columnOption} style={{ height: 250 }} />
  350. </Card>
  351. </Col>
  352. </Row>
  353. </Col>
  354. </Row>
  355. {/* 报警点位详情 */}
  356. <Row gutter={16} className="mt-4">
  357. <Col span={24}>
  358. <Card title="当前活跃报警" size="small">
  359. <div className="space-y-3 max-h-64 overflow-y-auto">
  360. {mockAlarmPoints
  361. .filter((point) => point.status !== "已处理")
  362. .map((point) => (
  363. <div key={point.id} className="p-3 border rounded-lg">
  364. <div className="flex justify-between items-center mb-2">
  365. <span className="font-medium">{point.name}</span>
  366. <div className="space-x-2">
  367. <Tag color={getLevelColor(point.level)}>{point.level}</Tag>
  368. <Tag color={getStatusColor(point.status)}>{point.status}</Tag>
  369. </div>
  370. </div>
  371. <div className="text-sm text-gray-600 space-y-1">
  372. <div>类型: {getTypeLabel(point.type)}</div>
  373. <div>
  374. 异常值: {point.value} {point.unit} (阈值: {point.threshold} {point.unit})
  375. </div>
  376. <div>报警时间: {point.createTime}</div>
  377. </div>
  378. </div>
  379. ))}
  380. </div>
  381. </Card>
  382. </Col>
  383. </Row>
  384. </Card>
  385. </div>
  386. )
  387. }