pages_smoke_water.tsx 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. import React, { useState, useEffect, useRef } from 'react';
  2. import { getDeviceStatus, getDeviceHistory } from './api/monitor.ts';
  3. import dayjs from 'dayjs';
  4. import { Card, Space, Table, message, Select, Tag, Tabs, Button } from 'antd';
  5. import ReactECharts from 'echarts-for-react';
  6. import type { EChartsType } from 'echarts';
  7. interface DeviceStatusResponse {
  8. status: 0 | 1;
  9. timestamp: string;
  10. }
  11. interface DeviceData {
  12. id: string;
  13. name: string;
  14. type: 'smoke' | 'water';
  15. status: 0 | 1; // 1:正常, 0:异常
  16. timestamp: string;
  17. value?: number;
  18. }
  19. interface HistoryData {
  20. status: 0 | 1;
  21. timestamp: string;
  22. }
  23. interface WaterIconProps {
  24. timestamp?: string;
  25. }
  26. const WaterIcon = () => (
  27. <div style={{position: 'relative', width: 80, height: 80}}>
  28. <img
  29. src="/client/shuijintubiao.png"
  30. alt="水浸图标"
  31. style={{width: '100%', height: '100%'}}
  32. />
  33. </div>
  34. );
  35. interface SmokeIconProps {
  36. status?: 0 | 1;
  37. timestamp?: string;
  38. }
  39. const SmokeIcon = ({status = 1}: SmokeIconProps) => (
  40. <div style={{position: 'relative', width: 80, height: 80}}>
  41. <img
  42. src="/client/admin/api/yangantubiao.png"
  43. alt="烟感图标"
  44. style={{
  45. width: '100%',
  46. height: '100%',
  47. border: status === 0 ? '2px solid #ff4d4f' : 'none',
  48. backgroundColor: status === 0 ? 'rgba(255, 77, 79, 0.1)' : 'transparent',
  49. borderRadius: 4
  50. }}
  51. />
  52. </div>
  53. );
  54. const StatusDisplay = ({type, status}: {type: 'smoke' | 'water', status: 0 | 1}) => (
  55. <div style={{
  56. display: 'flex',
  57. flexDirection: 'column',
  58. justifyContent: 'center',
  59. alignItems: 'center',
  60. backgroundColor: status === 1 ? '#f6ffed' : '#fff2f0',
  61. border: `1px solid ${status === 1 ? '#b7eb8f' : '#ffccc7'}`,
  62. borderRadius: 4,
  63. fontSize: 14,
  64. fontWeight: 'bold',
  65. padding: 8
  66. }}>
  67. <div>{type === 'smoke' ? '烟感' : '水浸'}</div>
  68. <div style={{color: status === 1 ? '#52c41a' : '#f5222d'}}>
  69. {status === 1 ? '正常' : '异常'}
  70. </div>
  71. </div>
  72. );
  73. const SmokeWaterPage: React.FC = () => {
  74. const [devices, setDevices] = useState<DeviceData[]>([
  75. {id: 'smoke1', name: '烟感1', type: 'smoke', status: 0, timestamp: new Date().toISOString()},
  76. {id: 'smoke2', name: '烟感2', type: 'smoke', status: 0, timestamp: new Date().toISOString()},
  77. {id: 'water1', name: '水浸1', type: 'water', status: 0, timestamp: new Date().toISOString()},
  78. {id: 'water2', name: '水浸2', type: 'water', status: 0, timestamp: new Date().toISOString()}
  79. ]);
  80. const [loading, setLoading] = useState(false);
  81. const [selectedDevice, setSelectedDevice] = useState<string>('smoke1');
  82. const [activeTab, setActiveTab] = useState<'table' | 'chart'>('table');
  83. const [isPolling, setIsPolling] = useState(true);
  84. const [pollInterval, setPollInterval] = useState(30000); // 默认30秒
  85. const chartRef = useRef<EChartsType>(null);
  86. const fetchDeviceStatus = async () => {
  87. try {
  88. setLoading(true);
  89. const res = await getDeviceStatus({
  90. device_id: Number(selectedDevice.replace(/\D/g, '')),
  91. device_type: selectedDevice.startsWith('smoke') ? 'smoke' : 'water'
  92. });
  93. setDevices(prev => prev.map(d =>
  94. d.id === selectedDevice
  95. ? {...d, status: res.status, timestamp: res.timestamp}
  96. : d
  97. ));
  98. } catch (error) {
  99. message.error('获取设备状态失败');
  100. } finally {
  101. setLoading(false);
  102. }
  103. };
  104. const fetchDeviceHistory = async (deviceId: string) => {
  105. try {
  106. const res = await getDeviceHistory({
  107. device_id: Number(deviceId.replace(/\D/g, '')),
  108. device_type: selectedDevice.startsWith('smoke') ? 'smoke' : 'water',
  109. start_time: new Date(Date.now() - 86400000).toISOString(),
  110. end_time: new Date().toISOString()
  111. });
  112. return res.data;
  113. } catch (error) {
  114. message.error('获取历史数据失败');
  115. return [];
  116. }
  117. };
  118. useEffect(() => {
  119. fetchDeviceStatus();
  120. if (!isPolling) return;
  121. const timer = setInterval(fetchDeviceStatus, pollInterval);
  122. return () => clearInterval(timer);
  123. }, [isPolling, pollInterval]);
  124. useEffect(() => {
  125. if (activeTab === 'chart' && selectedDevice && chartRef.current) {
  126. fetchDeviceHistory(selectedDevice).then(data => {
  127. const option = {
  128. xAxis: {
  129. type: 'category',
  130. data: data.map((d: HistoryData) => new Date(d.timestamp).toLocaleTimeString())
  131. },
  132. yAxis: {
  133. type: 'value',
  134. min: 0,
  135. max: 1,
  136. interval: 1
  137. },
  138. series: [{
  139. data: data.map((d: HistoryData) => d.status),
  140. type: 'line',
  141. smooth: true
  142. }]
  143. };
  144. chartRef.current?.setOption(option);
  145. });
  146. }
  147. }, [activeTab, selectedDevice]);
  148. const columns = [
  149. {
  150. title: '设备名称',
  151. dataIndex: 'name',
  152. key: 'name',
  153. },
  154. {
  155. title: '设备类型',
  156. dataIndex: 'type',
  157. key: 'type',
  158. render: (type: string) => type === 'smoke' ? '烟感' : '水浸',
  159. },
  160. {
  161. title: '状态',
  162. dataIndex: 'status',
  163. key: 'status',
  164. render: (status: number) => (
  165. <Tag color={status === 1 ? 'green' : 'red'}>
  166. {status === 1 ? '正常' : '异常'}
  167. </Tag>
  168. ),
  169. },
  170. {
  171. title: '更新时间',
  172. dataIndex: 'timestamp',
  173. key: 'timestamp',
  174. render: (text?: string) => text ? new Date(text).toLocaleString() : '-',
  175. }
  176. ];
  177. const currentDevice = devices.find(d => d.id === selectedDevice);
  178. return (
  179. <div style={{ padding: 24 }}>
  180. <Space direction="vertical" size="large" style={{ width: '100%' }}>
  181. <Card
  182. title={
  183. <Space>
  184. <Select
  185. value={selectedDevice}
  186. style={{ width: 200 }}
  187. onChange={setSelectedDevice}
  188. options={devices.map(d => ({
  189. value: d.id,
  190. label: d.name
  191. }))}
  192. />
  193. <Button
  194. type={isPolling ? 'default' : 'primary'}
  195. onClick={() => setIsPolling(!isPolling)}
  196. >
  197. {isPolling ? '停止轮询' : '开始轮询'}
  198. </Button>
  199. <Select
  200. value={pollInterval}
  201. style={{ width: 120 }}
  202. onChange={(value) => setPollInterval(value)}
  203. options={[
  204. { value: 10000, label: '10秒' },
  205. { value: 30000, label: '30秒' },
  206. { value: 60000, label: '1分钟' }
  207. ]}
  208. />
  209. </Space>
  210. }
  211. >
  212. <div style={{
  213. display: 'flex',
  214. justifyContent: 'center',
  215. alignItems: 'center',
  216. height: 120
  217. }}>
  218. {currentDevice && (
  219. <div style={{display: 'flex', gap: 16, position: 'relative', justifyContent: 'center', alignItems: 'center'}}>
  220. {currentDevice.type === 'water' ? (
  221. <WaterIcon />
  222. ) : (
  223. <SmokeIcon status={currentDevice.status} />
  224. )}
  225. <StatusDisplay type={currentDevice.type} status={currentDevice.status} />
  226. <div style={{marginLeft: '100px'}}>
  227. <div style={{
  228. color: 'black',
  229. fontSize: 12,
  230. padding: '2px 8px',
  231. borderRadius: 4
  232. }}>
  233. 时间
  234. </div>
  235. <div style={{
  236. color: 'black',
  237. fontSize: 12,
  238. padding: '2px 8px',
  239. borderRadius: 4,
  240. marginTop: 4
  241. }}>
  242. {dayjs(currentDevice.timestamp).format('YYYY/M/D HH:mm:ss')}
  243. </div>
  244. </div>
  245. </div>
  246. )}
  247. </div>
  248. </Card>
  249. <Tabs
  250. activeKey={activeTab}
  251. onChange={(key: string) => setActiveTab(key as 'table' | 'chart')}
  252. items={[
  253. {
  254. key: 'table',
  255. label: '数据表格',
  256. children: (
  257. <Table
  258. columns={columns}
  259. dataSource={devices}
  260. rowKey="id"
  261. loading={loading}
  262. />
  263. )
  264. },
  265. {
  266. key: 'chart',
  267. label: '趋势图表',
  268. children: (
  269. <ReactECharts
  270. // @ts-ignore - ReactECharts ref类型问题
  271. ref={chartRef}
  272. style={{ height: 400 }}
  273. option={{
  274. xAxis: { type: 'category' },
  275. yAxis: { type: 'value' },
  276. series: [{ type: 'line' }]
  277. }}
  278. />
  279. )
  280. }
  281. ]}
  282. />
  283. </Space>
  284. </div>
  285. );
  286. };
  287. export default SmokeWaterPage;