key-indicators-monitoring.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438
  1. "use client"
  2. import {useEffect, useState} from "react"
  3. import {Alert, Card, Col, DatePicker, Row, Select, Space, Statistic, Table, Tag} from "antd"
  4. import EChart from "@/components/echarts"
  5. import {ArrowDownOutlined, ArrowUpOutlined, WarningOutlined} from "@ant-design/icons"
  6. const { RangePicker } = DatePicker
  7. const { Option } = Select
  8. // 模拟实时监测数据
  9. const mockRealTimeData = {
  10. pressure: {
  11. current: 0.42,
  12. normal: [0.35, 0.45],
  13. status: "normal",
  14. trend: "up",
  15. change: 0.02,
  16. },
  17. flow: {
  18. current: 2850,
  19. normal: [2000, 3500],
  20. status: "normal",
  21. trend: "down",
  22. change: -150,
  23. },
  24. temperature: {
  25. current: 18.5,
  26. normal: [15, 25],
  27. status: "normal",
  28. trend: "stable",
  29. change: 0.1,
  30. },
  31. leakage: {
  32. current: 0.02,
  33. normal: [0, 0.05],
  34. status: "warning",
  35. trend: "up",
  36. change: 0.01,
  37. },
  38. }
  39. // 模拟历史趋势数据
  40. const mockTrendData = [
  41. { time: "00:00", pressure: 0.38, flow: 2200, temperature: 16.2, leakage: 0.01 },
  42. { time: "02:00", pressure: 0.39, flow: 2100, temperature: 16.8, leakage: 0.01 },
  43. { time: "04:00", pressure: 0.41, flow: 2300, temperature: 17.2, leakage: 0.015 },
  44. { time: "06:00", pressure: 0.43, flow: 2800, temperature: 18.1, leakage: 0.018 },
  45. { time: "08:00", pressure: 0.42, flow: 3200, temperature: 18.5, leakage: 0.02 },
  46. { time: "10:00", pressure: 0.41, flow: 3100, temperature: 18.8, leakage: 0.022 },
  47. { time: "12:00", pressure: 0.4, flow: 2900, temperature: 19.2, leakage: 0.019 },
  48. { time: "14:00", pressure: 0.42, flow: 2850, temperature: 18.5, leakage: 0.02 },
  49. ]
  50. // 模拟设备监测数据
  51. const mockDeviceData = [
  52. {
  53. id: "D001",
  54. name: "解放路压力监测点",
  55. type: "压力监测",
  56. value: 0.42,
  57. unit: "MPa",
  58. status: "正常",
  59. lastUpdate: "2024-01-15 14:30:25",
  60. location: "解放路调压站",
  61. },
  62. {
  63. id: "D002",
  64. name: "人民路流量监测点",
  65. type: "流量监测",
  66. value: 2850,
  67. unit: "m³/h",
  68. status: "正常",
  69. lastUpdate: "2024-01-15 14:30:20",
  70. location: "人民路段",
  71. },
  72. {
  73. id: "D003",
  74. name: "建设路温度监测点",
  75. type: "温度监测",
  76. value: 18.5,
  77. unit: "°C",
  78. status: "正常",
  79. lastUpdate: "2024-01-15 14:30:15",
  80. location: "建设路段",
  81. },
  82. {
  83. id: "D004",
  84. name: "解放路泄漏检测点",
  85. type: "泄漏检测",
  86. value: 0.02,
  87. unit: "ppm",
  88. status: "预警",
  89. lastUpdate: "2024-01-15 14:30:30",
  90. location: "解放路北段",
  91. },
  92. ]
  93. export default function KeyIndicatorsMonitoring() {
  94. const [selectedIndicator, setSelectedIndicator] = useState("pressure")
  95. const [realTimeData, setRealTimeData] = useState(mockRealTimeData)
  96. // 模拟实时数据更新
  97. useEffect(() => {
  98. const interval = setInterval(() => {
  99. setRealTimeData((prev) => ({
  100. ...prev,
  101. pressure: {
  102. ...prev.pressure,
  103. current: +(prev.pressure.current + (Math.random() - 0.5) * 0.02).toFixed(3),
  104. },
  105. flow: {
  106. ...prev.flow,
  107. current: Math.round(prev.flow.current + (Math.random() - 0.5) * 100),
  108. },
  109. temperature: {
  110. ...prev.temperature,
  111. current: +(prev.temperature.current + (Math.random() - 0.5) * 0.5).toFixed(1),
  112. },
  113. leakage: {
  114. ...prev.leakage,
  115. current: +(prev.leakage.current + (Math.random() - 0.5) * 0.005).toFixed(3),
  116. },
  117. }))
  118. }, 5000)
  119. return () => clearInterval(interval)
  120. }, [])
  121. const getStatusColor = (status: string) => {
  122. switch (status) {
  123. case "normal":
  124. return "#52c41a"
  125. case "warning":
  126. return "#faad14"
  127. case "danger":
  128. return "#f5222d"
  129. default:
  130. return "#1890ff"
  131. }
  132. }
  133. const getTrendIcon = (trend: string, change: number) => {
  134. if (trend === "up") {
  135. return <ArrowUpOutlined style={{ color: change > 0 ? "#f5222d" : "#52c41a" }} />
  136. } else if (trend === "down") {
  137. return <ArrowDownOutlined style={{ color: change < 0 ? "#f5222d" : "#52c41a" }} />
  138. }
  139. return null
  140. }
  141. const gaugeOption = {
  142. series: [
  143. {
  144. type: "gauge",
  145. startAngle: 180,
  146. endAngle: 0,
  147. center: ["50%", "75%"],
  148. radius: "90%",
  149. min: 0,
  150. max: 1,
  151. splitNumber: 8,
  152. axisLine: {
  153. lineStyle: {
  154. width: 6,
  155. color: [
  156. [0.25, "#FF6E76"],
  157. [0.5, "#FDDD60"],
  158. [0.75, "#58D9F9"],
  159. [1, "#7CFFB2"],
  160. ],
  161. },
  162. },
  163. pointer: {
  164. icon: "path://M12.8,0.7l12,40.1H0.7L12.8,0.7z",
  165. length: "12%",
  166. width: 20,
  167. offsetCenter: [0, "-60%"],
  168. itemStyle: {
  169. color: "auto",
  170. },
  171. },
  172. axisTick: {
  173. length: 12,
  174. lineStyle: {
  175. color: "auto",
  176. width: 2,
  177. },
  178. },
  179. splitLine: {
  180. length: 20,
  181. lineStyle: {
  182. color: "auto",
  183. width: 5,
  184. },
  185. },
  186. axisLabel: {
  187. color: "#464646",
  188. fontSize: 20,
  189. distance: -60,
  190. rotate: "tangential",
  191. formatter: (value: number) => {
  192. if (value === 0.875) {
  193. return "A+"
  194. } else if (value === 0.625) {
  195. return "A"
  196. } else if (value === 0.375) {
  197. return "B"
  198. } else if (value === 0.125) {
  199. return "C"
  200. }
  201. return ""
  202. },
  203. },
  204. title: {
  205. offsetCenter: [0, "-10%"],
  206. fontSize: 20,
  207. },
  208. detail: {
  209. fontSize: 30,
  210. offsetCenter: [0, "-35%"],
  211. valueAnimation: true,
  212. formatter: (value: number) => Math.round(value * 100) / 100,
  213. color: "inherit",
  214. },
  215. data: [
  216. {
  217. value: realTimeData.pressure.current,
  218. name: "压力 (MPa)",
  219. },
  220. ],
  221. },
  222. ],
  223. }
  224. const getIndicatorName = (indicator: string) => {
  225. const names = {
  226. pressure: "压力",
  227. flow: "流量",
  228. temperature: "温度",
  229. leakage: "泄漏",
  230. }
  231. return names[indicator as keyof typeof names] || indicator
  232. }
  233. const areaOption = {
  234. title: { text: `${getIndicatorName(selectedIndicator)}趋势`, left: "center" },
  235. tooltip: { trigger: "axis" },
  236. grid: { left: "3%", right: "4%", bottom: "3%", containLabel: true },
  237. xAxis: {
  238. type: "category",
  239. data: mockTrendData.map((item) => item.time),
  240. },
  241. yAxis: { type: "value" },
  242. series: [
  243. {
  244. type: "line",
  245. data: mockTrendData.map((item) => item[selectedIndicator as keyof typeof item]),
  246. smooth: true,
  247. areaStyle: {
  248. color: {
  249. type: "linear",
  250. x: 0,
  251. y: 0,
  252. x2: 0,
  253. y2: 1,
  254. colorStops: [
  255. {
  256. offset: 0,
  257. color: "rgba(24, 144, 255, 0.8)",
  258. },
  259. {
  260. offset: 1,
  261. color: "rgba(24, 144, 255, 0.1)",
  262. },
  263. ],
  264. },
  265. },
  266. itemStyle: { color: "#1890ff" },
  267. },
  268. ],
  269. }
  270. const deviceColumns = [
  271. { title: "设备编号", dataIndex: "id", key: "id", width: 100 },
  272. { title: "设备名称", dataIndex: "name", key: "name", width: 200 },
  273. { title: "监测类型", dataIndex: "type", key: "type", width: 120 },
  274. {
  275. title: "当前值",
  276. dataIndex: "value",
  277. key: "value",
  278. width: 100,
  279. render: (value: number, record: any) => `${value} ${record.unit}`,
  280. },
  281. {
  282. title: "状态",
  283. dataIndex: "status",
  284. key: "status",
  285. width: 100,
  286. render: (status: string) => {
  287. const colorMap = { 正常: "green", 预警: "orange", 异常: "red" }
  288. return <Tag color={colorMap[status as keyof typeof colorMap]}>{status}</Tag>
  289. },
  290. },
  291. { title: "位置", dataIndex: "location", key: "location", width: 150 },
  292. { title: "更新时间", dataIndex: "lastUpdate", key: "lastUpdate", width: 180 },
  293. ]
  294. return (
  295. <div className="p-6">
  296. <Card title="关键指标实时监测">
  297. {/* 实时指标概览 */}
  298. <Row gutter={16} className="mb-6">
  299. <Col span={6}>
  300. <Card size="small" className="text-center">
  301. <Statistic
  302. title="管网压力"
  303. value={realTimeData.pressure.current}
  304. suffix="MPa"
  305. valueStyle={{ color: getStatusColor(realTimeData.pressure.status) }}
  306. prefix={getTrendIcon(realTimeData.pressure.trend, realTimeData.pressure.change)}
  307. />
  308. <div className="text-xs text-gray-500 mt-2">
  309. 正常范围: {realTimeData.pressure.normal[0]} - {realTimeData.pressure.normal[1]} MPa
  310. </div>
  311. </Card>
  312. </Col>
  313. <Col span={6}>
  314. <Card size="small" className="text-center">
  315. <Statistic
  316. title="流量监测"
  317. value={realTimeData.flow.current}
  318. suffix="m³/h"
  319. valueStyle={{ color: getStatusColor(realTimeData.flow.status) }}
  320. prefix={getTrendIcon(realTimeData.flow.trend, realTimeData.flow.change)}
  321. />
  322. <div className="text-xs text-gray-500 mt-2">
  323. 正常范围: {realTimeData.flow.normal[0]} - {realTimeData.flow.normal[1]} m³/h
  324. </div>
  325. </Card>
  326. </Col>
  327. <Col span={6}>
  328. <Card size="small" className="text-center">
  329. <Statistic
  330. title="温度监测"
  331. value={realTimeData.temperature.current}
  332. suffix="°C"
  333. valueStyle={{ color: getStatusColor(realTimeData.temperature.status) }}
  334. prefix={getTrendIcon(realTimeData.temperature.trend, realTimeData.temperature.change)}
  335. />
  336. <div className="text-xs text-gray-500 mt-2">
  337. 正常范围: {realTimeData.temperature.normal[0]} - {realTimeData.temperature.normal[1]} °C
  338. </div>
  339. </Card>
  340. </Col>
  341. <Col span={6}>
  342. <Card size="small" className="text-center">
  343. <Statistic
  344. title="泄漏检测"
  345. value={realTimeData.leakage.current}
  346. suffix="ppm"
  347. valueStyle={{ color: getStatusColor(realTimeData.leakage.status) }}
  348. prefix={getTrendIcon(realTimeData.leakage.trend, realTimeData.leakage.change)}
  349. />
  350. <div className="text-xs text-gray-500 mt-2">
  351. 安全范围: {realTimeData.leakage.normal[0]} - {realTimeData.leakage.normal[1]} ppm
  352. </div>
  353. </Card>
  354. </Col>
  355. </Row>
  356. {/* 预警信息 */}
  357. {realTimeData.leakage.status === "warning" && (
  358. <Alert
  359. message="泄漏预警"
  360. description="解放路北段检测到燃气泄漏浓度超标,请及时处理"
  361. type="warning"
  362. icon={<WarningOutlined />}
  363. showIcon
  364. className="mb-6"
  365. />
  366. )}
  367. {/* 图表展示 */}
  368. <Row gutter={16} className="mb-6">
  369. <Col span={12}>
  370. <Card title="压力监测仪表" size="small">
  371. <EChart option={gaugeOption} style={{ height: 300 }} />
  372. </Card>
  373. </Col>
  374. <Col span={12}>
  375. <Card
  376. title="指标趋势分析"
  377. size="small"
  378. extra={
  379. <Select value={selectedIndicator} onChange={setSelectedIndicator} style={{ width: 120 }}>
  380. <Option value="pressure">压力</Option>
  381. <Option value="flow">流量</Option>
  382. <Option value="temperature">温度</Option>
  383. <Option value="leakage">泄漏</Option>
  384. </Select>
  385. }
  386. >
  387. <EChart option={areaOption} style={{ height: 300 }} />
  388. </Card>
  389. </Col>
  390. </Row>
  391. {/* 设备监测状态 */}
  392. <Card title="设备监测状态" size="small">
  393. <div className="mb-4">
  394. <Space>
  395. <Select placeholder="设备类型" style={{ width: 120 }}>
  396. <Option value="pressure">压力监测</Option>
  397. <Option value="flow">流量监测</Option>
  398. <Option value="temperature">温度监测</Option>
  399. <Option value="leak">泄漏检测</Option>
  400. </Select>
  401. <Select placeholder="设备状态" style={{ width: 120 }}>
  402. <Option value="normal">正常</Option>
  403. <Option value="warning">预警</Option>
  404. <Option value="error">异常</Option>
  405. </Select>
  406. <RangePicker showTime />
  407. </Space>
  408. </div>
  409. <Table
  410. columns={deviceColumns}
  411. dataSource={mockDeviceData}
  412. rowKey="id"
  413. pagination={{ pageSize: 10 }}
  414. size="small"
  415. />
  416. </Card>
  417. </Card>
  418. </div>
  419. )
  420. }