| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363 |
- import React, { useState, useEffect, useRef } from 'react';
- import ReactECharts from 'echarts-for-react';
- import type { EChartsOption } from 'echarts';
- import { Card, Statistic, Space, Table, message, Select, Button, DatePicker, Radio, Tabs, Tag } from 'antd';
- import { MonitorOutlined, EnvironmentOutlined, CloudOutlined, LineChartOutlined } from '@ant-design/icons';
- import dayjs, { Dayjs } from 'dayjs';
- import { MonitorAPI, getLatestTemperatureHumidity } from './api/monitor.ts';
- import { DeviceMonitorData } from '../share/monitorTypes.ts';
- interface SensorData {
- timestamp: string;
- temperature?: number;
- humidity?: number;
- metric_type: string;
- }
- const TemperatureHumidityPage: React.FC = () => {
- const [currentData, setCurrentData] = useState<SensorData | null>(null);
- const [tableData, setTableData] = useState<SensorData[]>([]);
- const [loading, setLoading] = useState(false);
- const [selectedDevice, setSelectedDevice] = useState('device1');
- const [timeRange, setTimeRange] = useState<'4h' | '12h' | '1d' | '7d' | 'custom'>('4h');
- const [customDateRange, setCustomDateRange] = useState<[dayjs.Dayjs, dayjs.Dayjs]>();
- const [activeTab, setActiveTab] = useState('table');
- const [isPolling, setIsPolling] = useState(true);
- const [pollInterval, setPollInterval] = useState(30000); // 默认30秒
- const chartRef = useRef<typeof ReactECharts | null>(null);
- const fetchData = async () => {
- try {
- setLoading(true);
-
- // 获取最新温湿度数据
- const latestData = await getLatestTemperatureHumidity();
- setCurrentData({
- timestamp: latestData.timestamp,
- temperature: latestData.temperature,
- humidity: latestData.humidity,
- metric_type: 'combined'
- });
- let startTime: Date, endTime = new Date();
-
- if (timeRange === '4h') {
- startTime = new Date(Date.now() - 4 * 60 * 60 * 1000);
- } else if (timeRange === '12h') {
- startTime = new Date(Date.now() - 12 * 60 * 60 * 1000);
- } else if (timeRange === '1d') {
- startTime = new Date(Date.now() - 24 * 60 * 60 * 1000);
- } else if (timeRange === '7d') {
- startTime = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
- } else if (customDateRange) {
- startTime = customDateRange[0].toDate();
- endTime = customDateRange[1].toDate();
- } else {
- // Default to 4h if no range is selected
- startTime = new Date(Date.now() - 4 * 60 * 60 * 1000);
- }
- const response = await MonitorAPI.getMonitorData({
- device_type: 'temperature_humidity',
- start_time: startTime.toISOString(),
- end_time: endTime.toISOString()
- });
- if (response.data.length > 0) {
- // 分别获取温度和湿度数据
- const tempData = response.data
- .filter(item => item.metric_type === 'temperature')
- .map(item => ({
- timestamp: item.created_at.toISOString(),
- temperature: item.metric_value,
- metric_type: item.metric_type
- }));
- const humidityData = response.data
- .filter(item => item.metric_type === 'humidity')
- .map(item => ({
- timestamp: item.created_at.toISOString(),
- humidity: item.metric_value,
- metric_type: item.metric_type
- }));
- // 合并数据
- const sensorData = tempData.map(temp => {
- const humidity = humidityData.find(h => h.timestamp === temp.timestamp);
- return {
- timestamp: temp.timestamp,
- temperature: temp.temperature,
- humidity: humidity?.humidity,
- metric_type: 'combined'
- };
- });
- setCurrentData(sensorData[0]);
- setTableData(sensorData);
- }
- } catch (error) {
- message.error('获取温湿度数据失败');
- } finally {
- setLoading(false);
- }
- };
- useEffect(() => {
- fetchData();
- if (!isPolling) return;
-
- const timer = setInterval(fetchData, pollInterval);
- return () => clearInterval(timer);
- }, [isPolling, pollInterval]);
- const getChartOption = (): EChartsOption => {
- const xAxisData = tableData.map(item =>
- new Date(item.timestamp).toLocaleTimeString()
- ).reverse();
-
- const temperatureData = tableData.map(item => item.temperature).reverse();
- const humidityData = tableData.map(item => item.humidity).reverse();
- return {
- title: {
- text: '温湿度趋势图',
- left: 'center'
- },
- tooltip: {
- trigger: 'axis' as const,
- axisPointer: {
- type: 'cross' as const
- }
- },
- legend: {
- data: ['温度(℃)', '湿度(%)'],
- top: 30
- },
- grid: {
- left: '3%',
- right: '4%',
- bottom: '3%',
- containLabel: true
- },
- xAxis: {
- type: 'category' as const,
- boundaryGap: false,
- data: xAxisData
- },
- yAxis: [
- {
- type: 'value' as const,
- name: '温度(℃)',
- min: 10,
- max: 35,
- axisLabel: {
- formatter: '{value} °C'
- }
- },
- {
- type: 'value' as const,
- name: '湿度(%)',
- min: 20,
- max: 90,
- axisLabel: {
- formatter: '{value} %'
- }
- }
- ],
- series: [
- {
- name: '温度(℃)',
- type: 'line' as const,
- data: temperatureData,
- smooth: true,
- lineStyle: {
- color: '#ff4d4f'
- },
- itemStyle: {
- color: '#ff4d4f'
- }
- },
- {
- name: '湿度(%)',
- type: 'line' as const,
- yAxisIndex: 1,
- data: humidityData,
- smooth: true,
- lineStyle: {
- color: '#1890ff'
- },
- itemStyle: {
- color: '#1890ff'
- }
- }
- ]
- };
- };
- const getStatus = (temp?: number, humidity?: number) => {
- // 定义正常范围:温度10-30°C,湿度30-70%
- const isTempNormal = temp && temp >= 10 && temp <= 30;
- const isHumidityNormal = humidity && humidity >= 30 && humidity <= 70;
- return isTempNormal && isHumidityNormal ? '正常' : '异常';
- };
- const columns = [
- {
- title: '序号',
- key: 'index',
- render: (text: string, record: any, index: number) => index + 1,
- width: 80
- },
- {
- title: '设备名称',
- key: 'device',
- render: () => selectedDevice === 'device1' ? '温湿度1' : '温湿度2',
- width: 120
- },
- {
- title: '温度 (°C)',
- dataIndex: 'temperature',
- key: 'temperature',
- render: (text: number) => text?.toFixed(1),
- width: 100
- },
- {
- title: '湿度 (%)',
- dataIndex: 'humidity',
- key: 'humidity',
- render: (text: number) => text?.toFixed(1),
- width: 100
- },
- {
- title: '状态',
- key: 'status',
- render: (text: string, record: SensorData) => (
- <span style={{ color: getStatus(record.temperature, record.humidity) === '正常' ? 'green' : 'red' }}>
- {getStatus(record.temperature, record.humidity)}
- </span>
- ),
- width: 100
- },
- {
- title: '时间',
- dataIndex: 'timestamp',
- key: 'timestamp',
- render: (text: string) => new Date(text).toLocaleString(),
- width: 180
- }
- ];
- return (
- <div style={{ padding: 24 }}>
- <Space direction="vertical" size="large" style={{ width: '100%' }}>
- <Card
- title={
- <Space>
- <Select
- defaultValue="device1"
- style={{ width: 200 }}
- onChange={setSelectedDevice}
- options={[
- { value: 'device1', label: '温湿度1' },
- { value: 'device2', label: '温湿度2' }
- ]}
- />
- <Button
- type={isPolling ? 'default' : 'primary'}
- onClick={() => setIsPolling(!isPolling)}
- >
- {isPolling ? '停止轮询' : '开始轮询'}
- </Button>
- <Select
- value={pollInterval}
- style={{ width: 120 }}
- onChange={(value) => setPollInterval(value)}
- options={[
- { value: 10000, label: '10秒' },
- { value: 30000, label: '30秒' },
- { value: 60000, label: '1分钟' }
- ]}
- />
- </Space>
- }
- >
- <Space size={40} align="center" style={{
- display: 'flex',
- justifyContent: 'center',
- width: '100%'
- }}>
- <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
- <Statistic
- title="温度"
- value={currentData?.temperature}
- precision={1}
- suffix="°C"
- prefix={<EnvironmentOutlined style={{ fontSize: '80px' }} />}
- style={{ textAlign: 'center' }}
- valueStyle={{ fontSize: '32px' }}
- />
- </div>
- <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
- <Statistic
- title="湿度"
- value={currentData?.humidity}
- precision={1}
- suffix="%"
- prefix={<CloudOutlined style={{ fontSize: '80px' }} />}
- style={{ textAlign: 'center' }}
- valueStyle={{ fontSize: '32px' }}
- />
- </div>
- </Space>
- </Card>
- <Tabs
- activeKey={activeTab}
- onChange={setActiveTab}
- tabBarExtraContent={
- <Space>
- <Radio.Group
- value={timeRange}
- onChange={(e) => setTimeRange(e.target.value)}
- buttonStyle="solid"
- >
- <Radio.Button value="4h">4小时</Radio.Button>
- <Radio.Button value="12h">12小时</Radio.Button>
- <Radio.Button value="1d">1天</Radio.Button>
- <Radio.Button value="7d">7天</Radio.Button>
- <Radio.Button value="custom">自定义</Radio.Button>
- </Radio.Group>
- {timeRange === 'custom' && (
- <DatePicker.RangePicker
- showTime
- value={customDateRange}
- onChange={(dates) => {
- if (dates && dates[0] && dates[1]) {
- setCustomDateRange([dates[0], dates[1]]);
- }
- }}
- />
- )}
- </Space>
- }
- >
- <Tabs.TabPane tab="数据表格" key="table">
- <Table
- columns={columns}
- dataSource={tableData}
- rowKey="timestamp"
- loading={loading}
- pagination={{ pageSize: 10 }}
- />
- </Tabs.TabPane>
- <Tabs.TabPane tab="趋势图表" key="chart">
- <ReactECharts
- option={getChartOption()}
- style={{ height: 500 }}
- theme="light"
- />
- </Tabs.TabPane>
- </Tabs>
- </Space>
- </div>
- );
- };
- export default TemperatureHumidityPage;
|