前端展示层

# 前端展示层

# 1. 章节概述

前端展示层是物联网系统的用户界面层,负责为用户提供直观、友好的交互体验。本章将详细介绍前端架构设计、技术选型、组件开发、数据可视化、实时通信等关键技术。

# 2. 学习目标

  • 掌握现代前端架构设计原则
  • 理解物联网前端的特殊需求
  • 学会使用React/Vue等框架构建IoT应用
  • 掌握数据可视化技术
  • 实现实时数据展示和交互
  • 了解移动端适配和响应式设计

# 3. 前端架构设计

# 3.1 整体架构

graph TB
    subgraph "前端展示层架构"
        A["用户界面层"] --> B["组件层"]
        B --> C["状态管理层"]
        C --> D["数据服务层"]
        D --> E["通信层"]
        
        subgraph "用户界面层"
            A1["设备监控界面"]
            A2["数据分析界面"]
            A3["告警管理界面"]
            A4["系统配置界面"]
        end
        
        subgraph "组件层"
            B1["通用组件"]
            B2["业务组件"]
            B3["图表组件"]
            B4["表单组件"]
        end
        
        subgraph "状态管理层"
            C1["全局状态"]
            C2["设备状态"]
            C3["用户状态"]
            C4["缓存管理"]
        end
        
        subgraph "数据服务层"
            D1["API服务"]
            D2["WebSocket服务"]
            D3["数据转换"]
            D4["缓存服务"]
        end
        
        subgraph "通信层"
            E1["HTTP客户端"]
            E2["WebSocket客户端"]
            E3["SSE客户端"]
            E4["错误处理"]
        end
    end
    
    F["后端API"] --> E
    G["WebSocket服务"] --> E
    H["消息推送"] --> E

# 3.2 技术栈选择

// 示例:技术栈配置
interface TechStack {
  framework: 'React' | 'Vue' | 'Angular';
  stateManagement: 'Redux' | 'Vuex' | 'Zustand' | 'Pinia';
  uiLibrary: 'Ant Design' | 'Element UI' | 'Material-UI';
  chartsLibrary: 'ECharts' | 'Chart.js' | 'D3.js';
  buildTool: 'Vite' | 'Webpack' | 'Rollup';
  cssFramework: 'Tailwind CSS' | 'Styled Components' | 'SCSS';
}

const iotFrontendStack: TechStack = {
  framework: 'React',
  stateManagement: 'Redux',
  uiLibrary: 'Ant Design',
  chartsLibrary: 'ECharts',
  buildTool: 'Vite',
  cssFramework: 'Tailwind CSS'
};

// 项目结构
const projectStructure = {
  src: {
    components: {
      common: ['Button', 'Modal', 'Table', 'Form'],
      business: ['DeviceCard', 'SensorChart', 'AlertPanel'],
      charts: ['LineChart', 'BarChart', 'PieChart', 'GaugeChart']
    },
    pages: {
      dashboard: 'Dashboard.tsx',
      devices: 'DeviceManagement.tsx',
      analytics: 'DataAnalytics.tsx',
      alerts: 'AlertManagement.tsx'
    },
    services: {
      api: 'apiService.ts',
      websocket: 'websocketService.ts',
      auth: 'authService.ts'
    },
    store: {
      slices: ['deviceSlice.ts', 'userSlice.ts', 'alertSlice.ts'],
      middleware: ['apiMiddleware.ts', 'websocketMiddleware.ts']
    },
    utils: {
      helpers: 'helpers.ts',
      constants: 'constants.ts',
      types: 'types.ts'
    }
  }
};

# 4. 核心组件开发

# 4.1 设备监控组件

// 示例:设备监控组件
import React, { useState, useEffect } from 'react';
import { Card, Row, Col, Statistic, Badge, Button, Table, Space } from 'antd';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
import { useSelector, useDispatch } from 'react-redux';
import { fetchDevices, updateDeviceStatus } from '../store/slices/deviceSlice';
import { DeviceStatus, SensorData } from '../types/device';

interface DeviceMonitorProps {
  tenantId: string;
  refreshInterval?: number;
}

const DeviceMonitor: React.FC<DeviceMonitorProps> = ({ 
  tenantId, 
  refreshInterval = 5000 
}) => {
  const dispatch = useDispatch();
  const { devices, loading, error } = useSelector((state: RootState) => state.device);
  const [selectedDevice, setSelectedDevice] = useState<string | null>(null);
  const [sensorData, setSensorData] = useState<SensorData[]>([]);

  useEffect(() => {
    // 初始加载设备列表
    dispatch(fetchDevices({ tenantId }));

    // 设置定时刷新
    const interval = setInterval(() => {
      dispatch(fetchDevices({ tenantId }));
    }, refreshInterval);

    return () => clearInterval(interval);
  }, [dispatch, tenantId, refreshInterval]);

  // 计算设备统计信息
  const deviceStats = React.useMemo(() => {
    const total = devices.length;
    const online = devices.filter(d => d.status === DeviceStatus.ONLINE).length;
    const offline = devices.filter(d => d.status === DeviceStatus.OFFLINE).length;
    const warning = devices.filter(d => d.status === DeviceStatus.WARNING).length;
    const error = devices.filter(d => d.status === DeviceStatus.ERROR).length;

    return { total, online, offline, warning, error };
  }, [devices]);

  // 设备状态颜色映射
  const getStatusColor = (status: DeviceStatus): string => {
    switch (status) {
      case DeviceStatus.ONLINE: return 'success';
      case DeviceStatus.OFFLINE: return 'default';
      case DeviceStatus.WARNING: return 'warning';
      case DeviceStatus.ERROR: return 'error';
      default: return 'default';
    }
  };

  // 设备表格列定义
  const deviceColumns = [
    {
      title: '设备ID',
      dataIndex: 'deviceId',
      key: 'deviceId',
      width: 150,
    },
    {
      title: '设备名称',
      dataIndex: 'name',
      key: 'name',
      width: 200,
    },
    {
      title: '设备类型',
      dataIndex: 'type',
      key: 'type',
      width: 120,
    },
    {
      title: '状态',
      dataIndex: 'status',
      key: 'status',
      width: 100,
      render: (status: DeviceStatus) => (
        <Badge 
          status={getStatusColor(status) as any} 
          text={status.toUpperCase()} 
        />
      ),
    },
    {
      title: '最后上线时间',
      dataIndex: 'lastOnlineTime',
      key: 'lastOnlineTime',
      width: 180,
      render: (time: string) => new Date(time).toLocaleString(),
    },
    {
      title: '操作',
      key: 'action',
      width: 150,
      render: (_, record: Device) => (
        <Space size="middle">
          <Button 
            type="link" 
            size="small"
            onClick={() => handleViewDevice(record.deviceId)}
          >
            查看
          </Button>
          <Button 
            type="link" 
            size="small"
            onClick={() => handleControlDevice(record.deviceId)}
          >
            控制
          </Button>
        </Space>
      ),
    },
  ];

  const handleViewDevice = (deviceId: string) => {
    setSelectedDevice(deviceId);
    // 加载设备详细数据
    loadDeviceSensorData(deviceId);
  };

  const handleControlDevice = (deviceId: string) => {
    // 打开设备控制面板
    console.log('Control device:', deviceId);
  };

  const loadDeviceSensorData = async (deviceId: string) => {
    try {
      // 这里应该调用API获取传感器数据
      const response = await fetch(`/api/devices/${deviceId}/sensor-data?limit=50`);
      const data = await response.json();
      setSensorData(data);
    } catch (error) {
      console.error('Failed to load sensor data:', error);
    }
  };

  return (
    <div className="device-monitor">
      {/* 设备统计卡片 */}
      <Row gutter={16} className="mb-6">
        <Col span={6}>
          <Card>
            <Statistic
              title="设备总数"
              value={deviceStats.total}
              valueStyle={{ color: '#1890ff' }}
            />
          </Card>
        </Col>
        <Col span={6}>
          <Card>
            <Statistic
              title="在线设备"
              value={deviceStats.online}
              valueStyle={{ color: '#52c41a' }}
            />
          </Card>
        </Col>
        <Col span={6}>
          <Card>
            <Statistic
              title="离线设备"
              value={deviceStats.offline}
              valueStyle={{ color: '#8c8c8c' }}
            />
          </Card>
        </Col>
        <Col span={6}>
          <Card>
            <Statistic
              title="异常设备"
              value={deviceStats.warning + deviceStats.error}
              valueStyle={{ color: '#ff4d4f' }}
            />
          </Card>
        </Col>
      </Row>

      {/* 设备列表 */}
      <Card title="设备列表" className="mb-6">
        <Table
          columns={deviceColumns}
          dataSource={devices}
          rowKey="deviceId"
          loading={loading}
          pagination={{
            pageSize: 10,
            showSizeChanger: true,
            showQuickJumper: true,
            showTotal: (total) => `${total} 条记录`,
          }}
          scroll={{ x: 1000 }}
        />
      </Card>

      {/* 设备详情图表 */}
      {selectedDevice && sensorData.length > 0 && (
        <Card title={`设备 ${selectedDevice} 传感器数据`}>
          <ResponsiveContainer width="100%" height={300}>
            <LineChart data={sensorData}>
              <CartesianGrid strokeDasharray="3 3" />
              <XAxis 
                dataKey="timestamp" 
                tickFormatter={(value) => new Date(value).toLocaleTimeString()}
              />
              <YAxis />
              <Tooltip 
                labelFormatter={(value) => new Date(value).toLocaleString()}
              />
              <Line 
                type="monotone" 
                dataKey="temperature" 
                stroke="#ff7300" 
                name="温度(°C)"
              />
              <Line 
                type="monotone" 
                dataKey="humidity" 
                stroke="#387908" 
                name="湿度(%)"
              />
            </LineChart>
          </ResponsiveContainer>
        </Card>
      )}
    </div>
  );
};

export default DeviceMonitor;

# 4.2 数据可视化组件

// 示例:数据可视化组件
import React, { useState, useEffect } from 'react';
import { Card, Select, DatePicker, Row, Col, Spin } from 'antd';
import * as echarts from 'echarts';
import { EChartsOption } from 'echarts';

const { RangePicker } = DatePicker;
const { Option } = Select;

interface DataVisualizationProps {
  tenantId: string;
}

const DataVisualization: React.FC<DataVisualizationProps> = ({ tenantId }) => {
  const [loading, setLoading] = useState(false);
  const [selectedDevices, setSelectedDevices] = useState<string[]>([]);
  const [dateRange, setDateRange] = useState<[moment.Moment, moment.Moment] | null>(null);
  const [chartData, setChartData] = useState<any>(null);

  // 初始化图表
  useEffect(() => {
    initializeCharts();
  }, []);

  const initializeCharts = () => {
    // 温度趋势图
    const temperatureChart = echarts.init(document.getElementById('temperature-chart'));
    const temperatureOption: EChartsOption = {
      title: {
        text: '温度趋势',
        left: 'center'
      },
      tooltip: {
        trigger: 'axis',
        axisPointer: {
          type: 'cross'
        }
      },
      legend: {
        data: ['设备1', '设备2', '设备3'],
        top: 30
      },
      grid: {
        left: '3%',
        right: '4%',
        bottom: '3%',
        containLabel: true
      },
      xAxis: {
        type: 'time',
        boundaryGap: false
      },
      yAxis: {
        type: 'value',
        name: '温度(°C)',
        axisLabel: {
          formatter: '{value} °C'
        }
      },
      series: [
        {
          name: '设备1',
          type: 'line',
          smooth: true,
          data: generateMockData('temperature', 24)
        },
        {
          name: '设备2',
          type: 'line',
          smooth: true,
          data: generateMockData('temperature', 24)
        },
        {
          name: '设备3',
          type: 'line',
          smooth: true,
          data: generateMockData('temperature', 24)
        }
      ]
    };
    temperatureChart.setOption(temperatureOption);

    // 湿度分布饼图
    const humidityChart = echarts.init(document.getElementById('humidity-chart'));
    const humidityOption: EChartsOption = {
      title: {
        text: '湿度分布',
        left: 'center'
      },
      tooltip: {
        trigger: 'item',
        formatter: '{a} <br/>{b}: {c} ({d}%)'
      },
      legend: {
        orient: 'vertical',
        left: 'left',
        top: 'middle'
      },
      series: [
        {
          name: '湿度范围',
          type: 'pie',
          radius: ['40%', '70%'],
          avoidLabelOverlap: false,
          label: {
            show: false,
            position: 'center'
          },
          emphasis: {
            label: {
              show: true,
              fontSize: '18',
              fontWeight: 'bold'
            }
          },
          labelLine: {
            show: false
          },
          data: [
            { value: 35, name: '30-40%' },
            { value: 25, name: '40-50%' },
            { value: 20, name: '50-60%' },
            { value: 15, name: '60-70%' },
            { value: 5, name: '70%+' }
          ]
        }
      ]
    };
    humidityChart.setOption(humidityOption);

    // 设备状态仪表盘
    const statusChart = echarts.init(document.getElementById('status-chart'));
    const statusOption: EChartsOption = {
      title: {
        text: '设备在线率',
        left: 'center'
      },
      series: [
        {
          type: 'gauge',
          startAngle: 180,
          endAngle: 0,
          center: ['50%', '75%'],
          radius: '90%',
          min: 0,
          max: 100,
          splitNumber: 8,
          axisLine: {
            lineStyle: {
              width: 6,
              color: [
                [0.25, '#FF6E76'],
                [0.5, '#FDDD60'],
                [0.75, '#58D9F9'],
                [1, '#7CFFB2']
              ]
            }
          },
          pointer: {
            icon: 'path://M12.8,0.7l12,40.1H0.7L12.8,0.7z',
            length: '12%',
            width: 20,
            offsetCenter: [0, '-60%'],
            itemStyle: {
              color: 'auto'
            }
          },
          axisTick: {
            length: 12,
            lineStyle: {
              color: 'auto',
              width: 2
            }
          },
          splitLine: {
            length: 20,
            lineStyle: {
              color: 'auto',
              width: 5
            }
          },
          axisLabel: {
            color: '#464646',
            fontSize: 20,
            distance: -60,
            formatter: function (value: number) {
              return value + '%';
            }
          },
          title: {
            offsetCenter: [0, '-20%'],
            fontSize: 20
          },
          detail: {
            fontSize: 30,
            offsetCenter: [0, '-35%'],
            valueAnimation: true,
            formatter: function (value: number) {
              return Math.round(value) + '%';
            },
            color: 'auto'
          },
          data: [
            {
              value: 85.6,
              name: '在线率'
            }
          ]
        }
      ]
    };
    statusChart.setOption(statusOption);

    // 响应式处理
    const handleResize = () => {
      temperatureChart.resize();
      humidityChart.resize();
      statusChart.resize();
    };

    window.addEventListener('resize', handleResize);

    return () => {
      window.removeEventListener('resize', handleResize);
      temperatureChart.dispose();
      humidityChart.dispose();
      statusChart.dispose();
    };
  };

  // 生成模拟数据
  const generateMockData = (type: string, hours: number) => {
    const data = [];
    const now = new Date();
    
    for (let i = hours; i >= 0; i--) {
      const time = new Date(now.getTime() - i * 60 * 60 * 1000);
      let value;
      
      switch (type) {
        case 'temperature':
          value = 20 + Math.random() * 15; // 20-35°C
          break;
        case 'humidity':
          value = 40 + Math.random() * 40; // 40-80%
          break;
        default:
          value = Math.random() * 100;
      }
      
      data.push([time, Math.round(value * 100) / 100]);
    }
    
    return data;
  };

  const handleDeviceChange = (devices: string[]) => {
    setSelectedDevices(devices);
    // 重新加载数据
    loadChartData(devices, dateRange);
  };

  const handleDateRangeChange = (dates: [moment.Moment, moment.Moment] | null) => {
    setDateRange(dates);
    // 重新加载数据
    loadChartData(selectedDevices, dates);
  };

  const loadChartData = async (devices: string[], dateRange: [moment.Moment, moment.Moment] | null) => {
    if (!devices.length || !dateRange) return;

    setLoading(true);
    try {
      // 这里应该调用API获取图表数据
      const response = await fetch('/api/analytics/chart-data', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          deviceIds: devices,
          startTime: dateRange[0].toISOString(),
          endTime: dateRange[1].toISOString(),
          tenantId
        })
      });
      
      const data = await response.json();
      setChartData(data);
      
      // 更新图表数据
      updateCharts(data);
    } catch (error) {
      console.error('Failed to load chart data:', error);
    } finally {
      setLoading(false);
    }
  };

  const updateCharts = (data: any) => {
    // 更新各个图表的数据
    // 这里应该根据实际数据更新图表
  };

  return (
    <div className="data-visualization">
      {/* 控制面板 */}
      <Card className="mb-6">
        <Row gutter={16}>
          <Col span={8}>
            <Select
              mode="multiple"
              placeholder="选择设备"
              style={{ width: '100%' }}
              onChange={handleDeviceChange}
              value={selectedDevices}
            >
              <Option value="device1">设备1</Option>
              <Option value="device2">设备2</Option>
              <Option value="device3">设备3</Option>
            </Select>
          </Col>
          <Col span={8}>
            <RangePicker
              style={{ width: '100%' }}
              onChange={handleDateRangeChange}
              value={dateRange}
            />
          </Col>
        </Row>
      </Card>

      {/* 图表区域 */}
      <Spin spinning={loading}>
        <Row gutter={16}>
          <Col span={16}>
            <Card title="温度趋势" className="mb-6">
              <div id="temperature-chart" style={{ width: '100%', height: '400px' }}></div>
            </Card>
          </Col>
          <Col span={8}>
            <Card title="湿度分布" className="mb-6">
              <div id="humidity-chart" style={{ width: '100%', height: '400px' }}></div>
            </Card>
          </Col>
        </Row>
        
        <Row gutter={16}>
          <Col span={12}>
            <Card title="设备状态">
              <div id="status-chart" style={{ width: '100%', height: '400px' }}></div>
            </Card>
          </Col>
          <Col span={12}>
            {/* 其他图表 */}
          </Col>
        </Row>
      </Spin>
    </div>
  );
};

export default DataVisualization;

# 5. 实时通信实现

# 5.1 WebSocket服务

// 示例:WebSocket服务实现
class WebSocketService {
  private ws: WebSocket | null = null;
  private reconnectAttempts = 0;
  private maxReconnectAttempts = 5;
  private reconnectInterval = 5000;
  private heartbeatInterval: NodeJS.Timeout | null = null;
  private messageHandlers: Map<string, Function[]> = new Map();
  private isConnected = false;

  constructor(private url: string, private token: string) {}

  // 连接WebSocket
  connect(): Promise<void> {
    return new Promise((resolve, reject) => {
      try {
        this.ws = new WebSocket(`${this.url}?token=${this.token}`);

        this.ws.onopen = (event) => {
          console.log('WebSocket连接已建立');
          this.isConnected = true;
          this.reconnectAttempts = 0;
          this.startHeartbeat();
          resolve();
        };

        this.ws.onmessage = (event) => {
          this.handleMessage(event.data);
        };

        this.ws.onclose = (event) => {
          console.log('WebSocket连接已关闭', event.code, event.reason);
          this.isConnected = false;
          this.stopHeartbeat();
          
          if (!event.wasClean && this.reconnectAttempts < this.maxReconnectAttempts) {
            this.reconnect();
          }
        };

        this.ws.onerror = (error) => {
          console.error('WebSocket错误:', error);
          reject(error);
        };
      } catch (error) {
        reject(error);
      }
    });
  }

  // 断开连接
  disconnect(): void {
    if (this.ws) {
      this.ws.close(1000, 'Client disconnect');
      this.ws = null;
    }
    this.stopHeartbeat();
    this.isConnected = false;
  }

  // 发送消息
  send(type: string, data: any): void {
    if (this.ws && this.isConnected) {
      const message = {
        type,
        data,
        timestamp: Date.now()
      };
      this.ws.send(JSON.stringify(message));
    } else {
      console.warn('WebSocket未连接,无法发送消息');
    }
  }

  // 订阅消息
  subscribe(type: string, handler: Function): () => void {
    if (!this.messageHandlers.has(type)) {
      this.messageHandlers.set(type, []);
    }
    this.messageHandlers.get(type)!.push(handler);

    // 返回取消订阅函数
    return () => {
      const handlers = this.messageHandlers.get(type);
      if (handlers) {
        const index = handlers.indexOf(handler);
        if (index > -1) {
          handlers.splice(index, 1);
        }
      }
    };
  }

  // 处理接收到的消息
  private handleMessage(data: string): void {
    try {
      const message = JSON.parse(data);
      const { type, data: messageData } = message;

      // 处理心跳响应
      if (type === 'pong') {
        return;
      }

      // 分发消息给订阅者
      const handlers = this.messageHandlers.get(type);
      if (handlers) {
        handlers.forEach(handler => {
          try {
            handler(messageData);
          } catch (error) {
            console.error('消息处理器执行错误:', error);
          }
        });
      }
    } catch (error) {
      console.error('消息解析错误:', error);
    }
  }

  // 重连
  private reconnect(): void {
    this.reconnectAttempts++;
    console.log(`尝试重连 (${this.reconnectAttempts}/${this.maxReconnectAttempts})`);

    setTimeout(() => {
      this.connect().catch(error => {
        console.error('重连失败:', error);
        if (this.reconnectAttempts < this.maxReconnectAttempts) {
          this.reconnect();
        }
      });
    }, this.reconnectInterval);
  }

  // 开始心跳
  private startHeartbeat(): void {
    this.heartbeatInterval = setInterval(() => {
      this.send('ping', {});
    }, 30000); // 30秒心跳
  }

  // 停止心跳
  private stopHeartbeat(): void {
    if (this.heartbeatInterval) {
      clearInterval(this.heartbeatInterval);
      this.heartbeatInterval = null;
    }
  }

  // 获取连接状态
  getConnectionState(): boolean {
    return this.isConnected;
  }
}

// WebSocket Hook
import { useEffect, useRef, useState } from 'react';

interface UseWebSocketOptions {
  url: string;
  token: string;
  autoConnect?: boolean;
}

export const useWebSocket = ({ url, token, autoConnect = true }: UseWebSocketOptions) => {
  const [isConnected, setIsConnected] = useState(false);
  const [error, setError] = useState<Error | null>(null);
  const wsRef = useRef<WebSocketService | null>(null);

  useEffect(() => {
    if (autoConnect) {
      connect();
    }

    return () => {
      disconnect();
    };
  }, [url, token, autoConnect]);

  const connect = async () => {
    try {
      wsRef.current = new WebSocketService(url, token);
      await wsRef.current.connect();
      setIsConnected(true);
      setError(null);
    } catch (err) {
      setError(err as Error);
      setIsConnected(false);
    }
  };

  const disconnect = () => {
    if (wsRef.current) {
      wsRef.current.disconnect();
      wsRef.current = null;
    }
    setIsConnected(false);
  };

  const send = (type: string, data: any) => {
    if (wsRef.current) {
      wsRef.current.send(type, data);
    }
  };

  const subscribe = (type: string, handler: Function) => {
    if (wsRef.current) {
      return wsRef.current.subscribe(type, handler);
    }
    return () => {};
  };

  return {
    isConnected,
    error,
    connect,
    disconnect,
    send,
    subscribe
  };
};

# 5.2 实时数据更新

// 示例:实时数据更新组件
import React, { useEffect, useState } from 'react';
import { Card, Alert, Badge } from 'antd';
import { useWebSocket } from '../hooks/useWebSocket';
import { useSelector } from 'react-redux';

interface RealTimeDataProps {
  deviceId: string;
}

const RealTimeData: React.FC<RealTimeDataProps> = ({ deviceId }) => {
  const { user } = useSelector((state: RootState) => state.auth);
  const [sensorData, setSensorData] = useState<any>(null);
  const [alerts, setAlerts] = useState<any[]>([]);
  const [deviceStatus, setDeviceStatus] = useState<string>('unknown');

  const { isConnected, error, subscribe } = useWebSocket({
    url: process.env.REACT_APP_WS_URL || 'ws://localhost:8080/ws',
    token: user?.token || '',
    autoConnect: true
  });

  useEffect(() => {
    if (!isConnected) return;

    // 订阅设备传感器数据
    const unsubscribeSensorData = subscribe('sensor_data', (data: any) => {
      if (data.deviceId === deviceId) {
        setSensorData(data);
      }
    });

    // 订阅设备状态变化
    const unsubscribeDeviceStatus = subscribe('device_status', (data: any) => {
      if (data.deviceId === deviceId) {
        setDeviceStatus(data.status);
      }
    });

    // 订阅告警信息
    const unsubscribeAlerts = subscribe('alert', (data: any) => {
      if (data.deviceId === deviceId) {
        setAlerts(prev => [data, ...prev.slice(0, 9)]); // 保留最新10条
      }
    });

    return () => {
      unsubscribeSensorData();
      unsubscribeDeviceStatus();
      unsubscribeAlerts();
    };
  }, [isConnected, deviceId, subscribe]);

  const getStatusColor = (status: string) => {
    switch (status) {
      case 'online': return 'success';
      case 'offline': return 'default';
      case 'warning': return 'warning';
      case 'error': return 'error';
      default: return 'default';
    }
  };

  const formatSensorValue = (value: number, unit: string) => {
    return `${value.toFixed(2)} ${unit}`;
  };

  if (error) {
    return (
      <Alert
        message="连接错误"
        description={error.message}
        type="error"
        showIcon
      />
    );
  }

  return (
    <div className="real-time-data">
      {/* 连接状态 */}
      <Card size="small" className="mb-4">
        <div className="flex items-center justify-between">
          <span>连接状态:</span>
          <Badge 
            status={isConnected ? 'success' : 'error'} 
            text={isConnected ? '已连接' : '未连接'} 
          />
        </div>
        <div className="flex items-center justify-between mt-2">
          <span>设备状态:</span>
          <Badge 
            status={getStatusColor(deviceStatus) as any} 
            text={deviceStatus.toUpperCase()} 
          />
        </div>
      </Card>

      {/* 实时传感器数据 */}
      {sensorData && (
        <Card title="实时传感器数据" size="small" className="mb-4">
          <div className="grid grid-cols-2 gap-4">
            {sensorData.temperature && (
              <div className="text-center">
                <div className="text-2xl font-bold text-red-500">
                  {formatSensorValue(sensorData.temperature, '°C')}
                </div>
                <div className="text-gray-500">温度</div>
              </div>
            )}
            {sensorData.humidity && (
              <div className="text-center">
                <div className="text-2xl font-bold text-blue-500">
                  {formatSensorValue(sensorData.humidity, '%')}
                </div>
                <div className="text-gray-500">湿度</div>
              </div>
            )}
            {sensorData.pressure && (
              <div className="text-center">
                <div className="text-2xl font-bold text-green-500">
                  {formatSensorValue(sensorData.pressure, 'Pa')}
                </div>
                <div className="text-gray-500">气压</div>
              </div>
            )}
            {sensorData.light && (
              <div className="text-center">
                <div className="text-2xl font-bold text-yellow-500">
                  {formatSensorValue(sensorData.light, 'lux')}
                </div>
                <div className="text-gray-500">光照</div>
              </div>
            )}
          </div>
          <div className="text-xs text-gray-400 mt-2 text-center">
            更新时间: {new Date(sensorData.timestamp).toLocaleString()}
          </div>
        </Card>
      )}

      {/* 实时告警 */}
      {alerts.length > 0 && (
        <Card title="实时告警" size="small">
          <div className="space-y-2">
            {alerts.map((alert, index) => (
              <Alert
                key={index}
                message={alert.message}
                type={alert.severity === 'high' ? 'error' : alert.severity === 'medium' ? 'warning' : 'info'}
                size="small"
                showIcon
                description={
                  <div className="text-xs">
                    {new Date(alert.timestamp).toLocaleString()}
                  </div>
                }
              />
            ))}
          </div>
        </Card>
      )}
    </div>
  );
};

export default RealTimeData;

# 6. 状态管理

# 6.1 Redux Store配置

// 示例:Redux Store配置
import { configureStore } from '@reduxjs/toolkit';
import { persistStore, persistReducer } from 'redux-persist';
import storage from 'redux-persist/lib/storage';
import { combineReducers } from '@reduxjs/toolkit';

// Slices
import authSlice from './slices/authSlice';
import deviceSlice from './slices/deviceSlice';
import alertSlice from './slices/alertSlice';
import analyticsSlice from './slices/analyticsSlice';
import uiSlice from './slices/uiSlice';

// 持久化配置
const persistConfig = {
  key: 'root',
  storage,
  whitelist: ['auth', 'ui'], // 只持久化认证和UI状态
};

const rootReducer = combineReducers({
  auth: authSlice,
  device: deviceSlice,
  alert: alertSlice,
  analytics: analyticsSlice,
  ui: uiSlice,
});

const persistedReducer = persistReducer(persistConfig, rootReducer);

export const store = configureStore({
  reducer: persistedReducer,
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware({
      serializableCheck: {
        ignoredActions: ['persist/PERSIST', 'persist/REHYDRATE'],
      },
    }),
  devTools: process.env.NODE_ENV !== 'production',
});

export const persistor = persistStore(store);

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

# 6.2 设备状态管理

// 示例:设备状态管理
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
import { Device, DeviceStatus, SensorData } from '../../types/device';
import { apiService } from '../../services/apiService';

interface DeviceState {
  devices: Device[];
  selectedDevice: Device | null;
  sensorData: Record<string, SensorData[]>;
  loading: boolean;
  error: string | null;
  filters: {
    status?: DeviceStatus;
    type?: string;
    search?: string;
  };
}

const initialState: DeviceState = {
  devices: [],
  selectedDevice: null,
  sensorData: {},
  loading: false,
  error: null,
  filters: {},
};

// 异步操作
export const fetchDevices = createAsyncThunk(
  'device/fetchDevices',
  async (params: { tenantId: string; filters?: any }) => {
    const response = await apiService.get('/devices', {
      params: {
        tenantId: params.tenantId,
        ...params.filters,
      },
    });
    return response.data;
  }
);

export const fetchDeviceById = createAsyncThunk(
  'device/fetchDeviceById',
  async (params: { deviceId: string; tenantId: string }) => {
    const response = await apiService.get(`/devices/${params.deviceId}`, {
      params: { tenantId: params.tenantId },
    });
    return response.data;
  }
);

export const fetchSensorData = createAsyncThunk(
  'device/fetchSensorData',
  async (params: {
    deviceId: string;
    tenantId: string;
    startTime?: string;
    endTime?: string;
    sensorType?: string;
  }) => {
    const response = await apiService.get(`/devices/${params.deviceId}/sensor-data`, {
      params: {
        tenantId: params.tenantId,
        startTime: params.startTime,
        endTime: params.endTime,
        sensorType: params.sensorType,
      },
    });
    return { deviceId: params.deviceId, data: response.data };
  }
);

export const sendDeviceCommand = createAsyncThunk(
  'device/sendCommand',
  async (params: {
    deviceId: string;
    tenantId: string;
    command: string;
    parameters?: Record<string, any>;
  }) => {
    const response = await apiService.post(`/devices/${params.deviceId}/commands`, {
      command: params.command,
      parameters: params.parameters,
      tenantId: params.tenantId,
    });
    return response.data;
  }
);

export const updateDeviceConfig = createAsyncThunk(
  'device/updateConfig',
  async (params: {
    deviceId: string;
    tenantId: string;
    config: Record<string, any>;
  }) => {
    const response = await apiService.put(`/devices/${params.deviceId}/config`, {
      config: params.config,
      tenantId: params.tenantId,
    });
    return response.data;
  }
);

const deviceSlice = createSlice({
  name: 'device',
  initialState,
  reducers: {
    setSelectedDevice: (state, action: PayloadAction<Device | null>) => {
      state.selectedDevice = action.payload;
    },
    updateDeviceStatus: (state, action: PayloadAction<{ deviceId: string; status: DeviceStatus }>) => {
      const device = state.devices.find(d => d.deviceId === action.payload.deviceId);
      if (device) {
        device.status = action.payload.status;
        device.lastOnlineTime = new Date().toISOString();
      }
      
      if (state.selectedDevice?.deviceId === action.payload.deviceId) {
        state.selectedDevice.status = action.payload.status;
        state.selectedDevice.lastOnlineTime = new Date().toISOString();
      }
    },
    addSensorData: (state, action: PayloadAction<{ deviceId: string; data: SensorData }>) => {
      const { deviceId, data } = action.payload;
      if (!state.sensorData[deviceId]) {
        state.sensorData[deviceId] = [];
      }
      
      // 添加新数据并保持最新100条
      state.sensorData[deviceId].unshift(data);
      if (state.sensorData[deviceId].length > 100) {
        state.sensorData[deviceId] = state.sensorData[deviceId].slice(0, 100);
      }
    },
    setFilters: (state, action: PayloadAction<Partial<DeviceState['filters']>>) => {
      state.filters = { ...state.filters, ...action.payload };
    },
    clearError: (state) => {
      state.error = null;
    },
    clearSensorData: (state, action: PayloadAction<string>) => {
      delete state.sensorData[action.payload];
    },
  },
  extraReducers: (builder) => {
    builder
      // fetchDevices
      .addCase(fetchDevices.pending, (state) => {
        state.loading = true;
        state.error = null;
      })
      .addCase(fetchDevices.fulfilled, (state, action) => {
        state.loading = false;
        state.devices = action.payload;
      })
      .addCase(fetchDevices.rejected, (state, action) => {
        state.loading = false;
        state.error = action.error.message || 'Failed to fetch devices';
      })
      
      // fetchDeviceById
      .addCase(fetchDeviceById.pending, (state) => {
        state.loading = true;
        state.error = null;
      })
      .addCase(fetchDeviceById.fulfilled, (state, action) => {
        state.loading = false;
        state.selectedDevice = action.payload;
      })
      .addCase(fetchDeviceById.rejected, (state, action) => {
        state.loading = false;
        state.error = action.error.message || 'Failed to fetch device';
      })
      
      // fetchSensorData
      .addCase(fetchSensorData.fulfilled, (state, action) => {
        const { deviceId, data } = action.payload;
        state.sensorData[deviceId] = data;
      })
      
      // sendDeviceCommand
      .addCase(sendDeviceCommand.pending, (state) => {
        state.loading = true;
      })
      .addCase(sendDeviceCommand.fulfilled, (state) => {
        state.loading = false;
      })
      .addCase(sendDeviceCommand.rejected, (state, action) => {
        state.loading = false;
        state.error = action.error.message || 'Failed to send command';
      });
  },
});

export const {
  setSelectedDevice,
  updateDeviceStatus,
  addSensorData,
  setFilters,
  clearError,
  clearSensorData,
} = deviceSlice.actions;

export default deviceSlice.reducer;

// Selectors
export const selectDevices = (state: { device: DeviceState }) => state.device.devices;
export const selectSelectedDevice = (state: { device: DeviceState }) => state.device.selectedDevice;
export const selectDeviceLoading = (state: { device: DeviceState }) => state.device.loading;
export const selectDeviceError = (state: { device: DeviceState }) => state.device.error;
export const selectSensorData = (deviceId: string) => (state: { device: DeviceState }) => 
  state.device.sensorData[deviceId] || [];
export const selectDeviceFilters = (state: { device: DeviceState }) => state.device.filters;

// 过滤后的设备列表
export const selectFilteredDevices = (state: { device: DeviceState }) => {
  const { devices, filters } = state.device;
  
  return devices.filter(device => {
    if (filters.status && device.status !== filters.status) {
      return false;
    }
    
    if (filters.type && device.type !== filters.type) {
      return false;
    }
    
    if (filters.search) {
      const searchLower = filters.search.toLowerCase();
      return (
        device.deviceId.toLowerCase().includes(searchLower) ||
        device.name.toLowerCase().includes(searchLower)
      );
    }
    
    return true;
  });
};

# 7. 最佳实践总结

# 7.1 设计原则

  • 组件化: 将UI拆分为可复用的组件
  • 响应式: 适配不同屏幕尺寸和设备
  • 性能优化: 使用虚拟化、懒加载等技术
  • 用户体验: 提供流畅的交互和及时的反馈
  • 可访问性: 遵循无障碍设计原则

# 7.2 开发建议

  • 代码规范: 使用TypeScript和ESLint保证代码质量
  • 测试覆盖: 编写单元测试和集成测试
  • 错误处理: 实现全局错误处理和用户友好的错误提示
  • 性能监控: 使用性能监控工具跟踪应用性能
  • 安全防护: 实现XSS防护和CSRF保护

# 8. 下一步学习

  • 学习移动端开发和混合应用
  • 深入了解PWA和离线功能
  • 掌握微前端架构
  • 研究WebAssembly在IoT中的应用