前端展示层
# 前端展示层
# 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中的应用