pages_temperature_humidity.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363
  1. import React, { useState, useEffect, useRef } from 'react';
  2. import ReactECharts from 'echarts-for-react';
  3. import type { EChartsOption } from 'echarts';
  4. import { Card, Statistic, Space, Table, message, Select, Button, DatePicker, Radio, Tabs, Tag } from 'antd';
  5. import { MonitorOutlined, EnvironmentOutlined, CloudOutlined, LineChartOutlined } from '@ant-design/icons';
  6. import dayjs, { Dayjs } from 'dayjs';
  7. import { MonitorAPI, getLatestTemperatureHumidity } from './api/monitor.ts';
  8. import { DeviceMonitorData } from '../share/monitorTypes.ts';
  9. interface SensorData {
  10. timestamp: string;
  11. temperature?: number;
  12. humidity?: number;
  13. metric_type: string;
  14. }
  15. const TemperatureHumidityPage: React.FC = () => {
  16. const [currentData, setCurrentData] = useState<SensorData | null>(null);
  17. const [tableData, setTableData] = useState<SensorData[]>([]);
  18. const [loading, setLoading] = useState(false);
  19. const [selectedDevice, setSelectedDevice] = useState('device1');
  20. const [timeRange, setTimeRange] = useState<'4h' | '12h' | '1d' | '7d' | 'custom'>('4h');
  21. const [customDateRange, setCustomDateRange] = useState<[dayjs.Dayjs, dayjs.Dayjs]>();
  22. const [activeTab, setActiveTab] = useState('table');
  23. const [isPolling, setIsPolling] = useState(true);
  24. const [pollInterval, setPollInterval] = useState(30000); // 默认30秒
  25. const chartRef = useRef<typeof ReactECharts | null>(null);
  26. const fetchData = async () => {
  27. try {
  28. setLoading(true);
  29. // 获取最新温湿度数据
  30. const latestData = await getLatestTemperatureHumidity();
  31. setCurrentData({
  32. timestamp: latestData.timestamp,
  33. temperature: latestData.temperature,
  34. humidity: latestData.humidity,
  35. metric_type: 'combined'
  36. });
  37. let startTime: Date, endTime = new Date();
  38. if (timeRange === '4h') {
  39. startTime = new Date(Date.now() - 4 * 60 * 60 * 1000);
  40. } else if (timeRange === '12h') {
  41. startTime = new Date(Date.now() - 12 * 60 * 60 * 1000);
  42. } else if (timeRange === '1d') {
  43. startTime = new Date(Date.now() - 24 * 60 * 60 * 1000);
  44. } else if (timeRange === '7d') {
  45. startTime = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
  46. } else if (customDateRange) {
  47. startTime = customDateRange[0].toDate();
  48. endTime = customDateRange[1].toDate();
  49. } else {
  50. // Default to 4h if no range is selected
  51. startTime = new Date(Date.now() - 4 * 60 * 60 * 1000);
  52. }
  53. const response = await MonitorAPI.getMonitorData({
  54. device_type: 'temperature_humidity',
  55. start_time: startTime.toISOString(),
  56. end_time: endTime.toISOString()
  57. });
  58. if (response.data.length > 0) {
  59. // 分别获取温度和湿度数据
  60. const tempData = response.data
  61. .filter(item => item.metric_type === 'temperature')
  62. .map(item => ({
  63. timestamp: item.created_at.toISOString(),
  64. temperature: item.metric_value,
  65. metric_type: item.metric_type
  66. }));
  67. const humidityData = response.data
  68. .filter(item => item.metric_type === 'humidity')
  69. .map(item => ({
  70. timestamp: item.created_at.toISOString(),
  71. humidity: item.metric_value,
  72. metric_type: item.metric_type
  73. }));
  74. // 合并数据
  75. const sensorData = tempData.map(temp => {
  76. const humidity = humidityData.find(h => h.timestamp === temp.timestamp);
  77. return {
  78. timestamp: temp.timestamp,
  79. temperature: temp.temperature,
  80. humidity: humidity?.humidity,
  81. metric_type: 'combined'
  82. };
  83. });
  84. setCurrentData(sensorData[0]);
  85. setTableData(sensorData);
  86. }
  87. } catch (error) {
  88. message.error('获取温湿度数据失败');
  89. } finally {
  90. setLoading(false);
  91. }
  92. };
  93. useEffect(() => {
  94. fetchData();
  95. if (!isPolling) return;
  96. const timer = setInterval(fetchData, pollInterval);
  97. return () => clearInterval(timer);
  98. }, [isPolling, pollInterval]);
  99. const getChartOption = (): EChartsOption => {
  100. const xAxisData = tableData.map(item =>
  101. new Date(item.timestamp).toLocaleTimeString()
  102. ).reverse();
  103. const temperatureData = tableData.map(item => item.temperature).reverse();
  104. const humidityData = tableData.map(item => item.humidity).reverse();
  105. return {
  106. title: {
  107. text: '温湿度趋势图',
  108. left: 'center'
  109. },
  110. tooltip: {
  111. trigger: 'axis' as const,
  112. axisPointer: {
  113. type: 'cross' as const
  114. }
  115. },
  116. legend: {
  117. data: ['温度(℃)', '湿度(%)'],
  118. top: 30
  119. },
  120. grid: {
  121. left: '3%',
  122. right: '4%',
  123. bottom: '3%',
  124. containLabel: true
  125. },
  126. xAxis: {
  127. type: 'category' as const,
  128. boundaryGap: false,
  129. data: xAxisData
  130. },
  131. yAxis: [
  132. {
  133. type: 'value' as const,
  134. name: '温度(℃)',
  135. min: 10,
  136. max: 35,
  137. axisLabel: {
  138. formatter: '{value} °C'
  139. }
  140. },
  141. {
  142. type: 'value' as const,
  143. name: '湿度(%)',
  144. min: 20,
  145. max: 90,
  146. axisLabel: {
  147. formatter: '{value} %'
  148. }
  149. }
  150. ],
  151. series: [
  152. {
  153. name: '温度(℃)',
  154. type: 'line' as const,
  155. data: temperatureData,
  156. smooth: true,
  157. lineStyle: {
  158. color: '#ff4d4f'
  159. },
  160. itemStyle: {
  161. color: '#ff4d4f'
  162. }
  163. },
  164. {
  165. name: '湿度(%)',
  166. type: 'line' as const,
  167. yAxisIndex: 1,
  168. data: humidityData,
  169. smooth: true,
  170. lineStyle: {
  171. color: '#1890ff'
  172. },
  173. itemStyle: {
  174. color: '#1890ff'
  175. }
  176. }
  177. ]
  178. };
  179. };
  180. const getStatus = (temp?: number, humidity?: number) => {
  181. // 定义正常范围:温度10-30°C,湿度30-70%
  182. const isTempNormal = temp && temp >= 10 && temp <= 30;
  183. const isHumidityNormal = humidity && humidity >= 30 && humidity <= 70;
  184. return isTempNormal && isHumidityNormal ? '正常' : '异常';
  185. };
  186. const columns = [
  187. {
  188. title: '序号',
  189. key: 'index',
  190. render: (text: string, record: any, index: number) => index + 1,
  191. width: 80
  192. },
  193. {
  194. title: '设备名称',
  195. key: 'device',
  196. render: () => selectedDevice === 'device1' ? '温湿度1' : '温湿度2',
  197. width: 120
  198. },
  199. {
  200. title: '温度 (°C)',
  201. dataIndex: 'temperature',
  202. key: 'temperature',
  203. render: (text: number) => text?.toFixed(1),
  204. width: 100
  205. },
  206. {
  207. title: '湿度 (%)',
  208. dataIndex: 'humidity',
  209. key: 'humidity',
  210. render: (text: number) => text?.toFixed(1),
  211. width: 100
  212. },
  213. {
  214. title: '状态',
  215. key: 'status',
  216. render: (text: string, record: SensorData) => (
  217. <span style={{ color: getStatus(record.temperature, record.humidity) === '正常' ? 'green' : 'red' }}>
  218. {getStatus(record.temperature, record.humidity)}
  219. </span>
  220. ),
  221. width: 100
  222. },
  223. {
  224. title: '时间',
  225. dataIndex: 'timestamp',
  226. key: 'timestamp',
  227. render: (text: string) => new Date(text).toLocaleString(),
  228. width: 180
  229. }
  230. ];
  231. return (
  232. <div style={{ padding: 24 }}>
  233. <Space direction="vertical" size="large" style={{ width: '100%' }}>
  234. <Card
  235. title={
  236. <Space>
  237. <Select
  238. defaultValue="device1"
  239. style={{ width: 200 }}
  240. onChange={setSelectedDevice}
  241. options={[
  242. { value: 'device1', label: '温湿度1' },
  243. { value: 'device2', label: '温湿度2' }
  244. ]}
  245. />
  246. <Button
  247. type={isPolling ? 'default' : 'primary'}
  248. onClick={() => setIsPolling(!isPolling)}
  249. >
  250. {isPolling ? '停止轮询' : '开始轮询'}
  251. </Button>
  252. <Select
  253. value={pollInterval}
  254. style={{ width: 120 }}
  255. onChange={(value) => setPollInterval(value)}
  256. options={[
  257. { value: 10000, label: '10秒' },
  258. { value: 30000, label: '30秒' },
  259. { value: 60000, label: '1分钟' }
  260. ]}
  261. />
  262. </Space>
  263. }
  264. >
  265. <Space size={40} align="center" style={{
  266. display: 'flex',
  267. justifyContent: 'center',
  268. width: '100%'
  269. }}>
  270. <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
  271. <Statistic
  272. title="温度"
  273. value={currentData?.temperature}
  274. precision={1}
  275. suffix="°C"
  276. prefix={<EnvironmentOutlined style={{ fontSize: '80px' }} />}
  277. style={{ textAlign: 'center' }}
  278. valueStyle={{ fontSize: '32px' }}
  279. />
  280. </div>
  281. <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
  282. <Statistic
  283. title="湿度"
  284. value={currentData?.humidity}
  285. precision={1}
  286. suffix="%"
  287. prefix={<CloudOutlined style={{ fontSize: '80px' }} />}
  288. style={{ textAlign: 'center' }}
  289. valueStyle={{ fontSize: '32px' }}
  290. />
  291. </div>
  292. </Space>
  293. </Card>
  294. <Tabs
  295. activeKey={activeTab}
  296. onChange={setActiveTab}
  297. tabBarExtraContent={
  298. <Space>
  299. <Radio.Group
  300. value={timeRange}
  301. onChange={(e) => setTimeRange(e.target.value)}
  302. buttonStyle="solid"
  303. >
  304. <Radio.Button value="4h">4小时</Radio.Button>
  305. <Radio.Button value="12h">12小时</Radio.Button>
  306. <Radio.Button value="1d">1天</Radio.Button>
  307. <Radio.Button value="7d">7天</Radio.Button>
  308. <Radio.Button value="custom">自定义</Radio.Button>
  309. </Radio.Group>
  310. {timeRange === 'custom' && (
  311. <DatePicker.RangePicker
  312. showTime
  313. value={customDateRange}
  314. onChange={(dates) => {
  315. if (dates && dates[0] && dates[1]) {
  316. setCustomDateRange([dates[0], dates[1]]);
  317. }
  318. }}
  319. />
  320. )}
  321. </Space>
  322. }
  323. >
  324. <Tabs.TabPane tab="数据表格" key="table">
  325. <Table
  326. columns={columns}
  327. dataSource={tableData}
  328. rowKey="timestamp"
  329. loading={loading}
  330. pagination={{ pageSize: 10 }}
  331. />
  332. </Tabs.TabPane>
  333. <Tabs.TabPane tab="趋势图表" key="chart">
  334. <ReactECharts
  335. option={getChartOption()}
  336. style={{ height: 500 }}
  337. theme="light"
  338. />
  339. </Tabs.TabPane>
  340. </Tabs>
  341. </Space>
  342. </div>
  343. );
  344. };
  345. export default TemperatureHumidityPage;