文件7: frontend/dashboard.js - 仪表板控制脚本

/**
 * @file dashboard.js
 * @brief IoT平台仪表板控制脚本
 * @author IoT Platform Team
 * @version 1.0
 * @date 2024
 * 
 * 实现前端数据展示和交互逻辑
 * 使用观察者模式和单例模式
 */
​
/**
 * @class Dashboard
 * @brief 仪表板主类
 * @details 管理所有仪表板组件和状态
 * 
 * 设计模式:单例模式(Singleton Pattern)
 * - 处理突发事件:多个组件状态同步、资源重复初始化
 * - 工程作用:确保全局状态一致性,避免重复实例化
 */
class Dashboard {
    /**
     * @private @static
     * @brief 单例实例
     */
    static #instance = null;
    
    /**
     * @private
     * @brief WebSocket连接对象
     */
    #websocket = null;
    
    /**
     * @private
     * @brief 服务器连接状态
     */
    #connected = false;
    
    /**
     * @private
     * @brief 重连尝试次数
     */
    #reconnectAttempts = 0;
    
    /**
     * @private
     * @brief 最大重连尝试次数
     */
    #maxReconnectAttempts = 10;
    
    /**
     * @private
     * @brief 重连延迟(毫秒)
     */
    #reconnectDelay = 5000;
    
    /**
     * @private
     * @brief 设备数据缓存
     * @type {Map<string, Object>}
     */
    #deviceCache = new Map();
    
    /**
     * @private
     * @brief 图表实例映射
     * @type {Map<string, Chart>}
     */
    #charts = new Map();
    
    /**
     * @private
     * @brief 观察者回调函数数组
     * @type {Array<Function>}
     */
    #observers = [];
    
    /**
     * @private
     * @brief 实时数据缓冲区
     */
    #dataBuffer = {
        temperature: [],
        humidity: [],
        power: [],
        timestamp: []
    };
    
    /**
     * @private
     * @brief 最大缓冲区大小
     */
    #maxBufferSize = 1000;
    
    /**
     * @private
     * @brief 构造函数(私有,实现单例模式)
     */
    constructor() {
        console.log('Dashboard instance created');
        this.#initializeComponents();
    }
    
    /**
     * @static
     * @brief 获取Dashboard单例实例
     * @return {Dashboard} Dashboard实例
     */
    static getInstance() {
        if (!Dashboard.#instance) {
            Dashboard.#instance = new Dashboard();
        }
        return Dashboard.#instance;
    }
    
    /**
     * @private
     * @brief 初始化所有组件
     */
    #initializeComponents() {
        this.#initializeWebSocket();
        this.#initializeCharts();
        this.#initializeEventListeners();
        this.#startDataUpdateLoop();
        
        console.log('Dashboard components initialized');
    }
    
    /**
     * @private
     * @brief 初始化WebSocket连接
     */
    #initializeWebSocket() {
        // 获取WebSocket服务器地址
        const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
        const host = window.location.host;
        const wsUrl = `${protocol}//${host}/ws`;
        
        console.log(`Connecting to WebSocket: ${wsUrl}`);
        
        try {
            this.#websocket = new WebSocket(wsUrl);
            
            // 设置事件处理函数
            this.#websocket.onopen = (event) => this.#handleWebSocketOpen(event);
            this.#websocket.onmessage = (event) => this.#handleWebSocketMessage(event);
            this.#websocket.onclose = (event) => this.#handleWebSocketClose(event);
            this.#websocket.onerror = (event) => this.#handleWebSocketError(event);
            
        } catch (error) {
            console.error('Failed to create WebSocket connection:', error);
            this.#showConnectionError('创建WebSocket连接失败');
        }
    }
    
    /**
     * @private
     * @brief 处理WebSocket连接打开
     * @param {Event} event WebSocket打开事件
     */
    #handleWebSocketOpen(event) {
        console.log('WebSocket connection established');
        this.#connected = true;
        this.#reconnectAttempts = 0;
        
        // 更新UI状态
        this.#updateConnectionStatus(true);
        
        // 发送连接成功消息
        this.#sendWebSocketMessage({
            type: 'connection_established',
            timestamp: Date.now(),
            client: 'web_dashboard'
        });
        
        // 请求初始数据
        this.#requestInitialData();
        
        // 通知观察者
        this.#notifyObservers('connected', { connected: true });
    }
    
    /**
     * @private
     * @brief 处理WebSocket消息
     * @param {MessageEvent} event WebSocket消息事件
     */
    #handleWebSocketMessage(event) {
        try {
            const data = JSON.parse(event.data);
            this.#processIncomingData(data);
        } catch (error) {
            console.error('Failed to parse WebSocket message:', error, event.data);
        }
    }
    
    /**
     * @private
     * @brief 处理WebSocket连接关闭
     * @param {CloseEvent} event WebSocket关闭事件
     */
    #handleWebSocketClose(event) {
        console.log(`WebSocket connection closed: ${event.code} ${event.reason}`);
        this.#connected = false;
        
        // 更新UI状态
        this.#updateConnectionStatus(false);
        
        // 通知观察者
        this.#notifyObservers('disconnected', { 
            code: event.code, 
            reason: event.reason 
        });
        
        // 尝试重新连接
        if (this.#reconnectAttempts < this.#maxReconnectAttempts) {
            this.#scheduleReconnect();
        } else {
            this.#showConnectionError('无法连接到服务器,请刷新页面重试');
        }
    }
    
    /**
     * @private
     * @brief 处理WebSocket错误
     * @param {Event} event WebSocket错误事件
     */
    #handleWebSocketError(event) {
        console.error('WebSocket error:', event);
        this.#showConnectionError('WebSocket连接错误');
    }
    
    /**
     * @private
     * @brief 安排重新连接
     */
    #scheduleReconnect() {
        this.#reconnectAttempts++;
        const delay = this.#reconnectDelay * Math.min(this.#reconnectAttempts, 5);
        
        console.log(`Scheduling reconnect in ${delay}ms (attempt ${this.#reconnectAttempts})`);
        
        setTimeout(() => {
            if (!this.#connected) {
                console.log('Attempting to reconnect...');
                this.#initializeWebSocket();
            }
        }, delay);
    }
    
    /**
     * @private
     * @brief 发送WebSocket消息
     * @param {Object} message 要发送的消息对象
     */
    #sendWebSocketMessage(message) {
        if (this.#websocket && this.#websocket.readyState === WebSocket.OPEN) {
            try {
                const jsonMessage = JSON.stringify(message);
                this.#websocket.send(jsonMessage);
            } catch (error) {
                console.error('Failed to send WebSocket message:', error);
            }
        } else {
            console.warn('WebSocket is not connected, message not sent:', message);
        }
    }
    
    /**
     * @private
     * @brief 处理传入数据
     * @param {Object} data 接收到的数据
     */
    #processIncomingData(data) {
        // 根据数据类型分发处理
        switch (data.type) {
            case 'device_data':
                this.#processDeviceData(data);
                break;
                
            case 'device_status':
                this.#processDeviceStatus(data);
                break;
                
            case 'system_stats':
                this.#processSystemStats(data);
                break;
                
            case 'alert':
                this.#processAlert(data);
                break;
                
            case 'command_response':
                this.#processCommandResponse(data);
                break;
                
            default:
                console.log('Unknown message type:', data.type);
                break;
        }
        
        // 通知观察者新数据到达
        this.#notifyObservers('data_received', data);
    }
    
    /**
     * @private
     * @brief 处理设备数据
     * @param {Object} data 设备数据
     */
    #processDeviceData(data) {
        const deviceId = data.device_id;
        const sensorData = data.data;
        
        // 更新设备缓存
        if (!this.#deviceCache.has(deviceId)) {
            this.#deviceCache.set(deviceId, {
                id: deviceId,
                type: data.device_type,
                last_seen: Date.now(),
                data_history: []
            });
        }
        
        const device = this.#deviceCache.get(deviceId);
        device.last_seen = Date.now();
        
        // 添加到历史数据(限制大小)
        device.data_history.push({
            timestamp: data.timestamp,
            data: sensorData
        });
        
        if (device.data_history.length > 100) {
            device.data_history.shift(); // 移除最旧的数据
        }
        
        // 更新实时数据缓冲区
        this.#updateDataBuffer(data);
        
        // 更新设备表格
        this.#updateDeviceTable(deviceId, device);
        
        // 更新实时图表
        this.#updateRealtimeChart(data);
        
        console.log(`Device data processed: ${deviceId}`, sensorData);
    }
    
    /**
     * @private
     * @brief 处理设备状态
     * @param {Object} data 设备状态数据
     */
    #processDeviceStatus(data) {
        const deviceId = data.device_id;
        const status = data.status;
        const online = status === 'online';
        
        // 更新设备缓存
        if (this.#deviceCache.has(deviceId)) {
            const device = this.#deviceCache.get(deviceId);
            device.online = online;
            device.last_status_update = Date.now();
            
            if (!online) {
                device.last_seen = Date.now();
            }
        } else {
            // 如果设备不存在,创建新记录
            this.#deviceCache.set(deviceId, {
                id: deviceId,
                type: data.device_type || 'unknown',
                online: online,
                last_seen: Date.now(),
                last_status_update: Date.now(),
                data_history: []
            });
        }
        
        // 更新设备表格
        this.#updateDeviceTable(deviceId, this.#deviceCache.get(deviceId));
        
        // 更新连接统计
        this.#updateConnectionStats();
        
        console.log(`Device status updated: ${deviceId} -> ${status}`);
    }
    
    /**
     * @private
     * @brief 处理系统统计信息
     * @param {Object} data 系统统计数据
     */
    #processSystemStats(data) {
        // 更新服务器状态显示
        this.#updateServerStats(data);
        
        // 更新消息统计
        this.#updateMessageStats(data);
        
        // 更新图表数据
        this.#updateStatsCharts(data);
        
        // 更新最后更新时间
        this.#updateLastUpdateTime();
    }
    
    /**
     * @private
     * @brief 处理告警信息
     * @param {Object} data 告警数据
     */
    #processAlert(data) {
        // 添加告警到列表
        this.#addAlertToList(data);
        
        // 更新告警计数
        this.#updateAlertCount();
        
        // 显示通知(如果浏览器支持)
        if (data.level === 'critical' && 'Notification' in window) {
            this.#showDesktopNotification(data);
        }
        
        // 播放声音提示(可选)
        if (data.level === 'critical' || data.level === 'warning') {
            this.#playAlertSound(data.level);
        }
        
        console.log(`Alert received: ${data.level} - ${data.message}`);
    }
    
    /**
     * @private
     * @brief 处理命令响应
     * @param {Object} data 命令响应数据
     */
    #processCommandResponse(data) {
        const commandId = data.command_id;
        const success = data.success;
        const message = data.message || '';
        
        // 显示响应消息
        this.#showCommandResponse(commandId, success, message);
        
        // 如果命令失败,记录错误
        if (!success) {
            console.error(`Command ${commandId} failed: ${message}`);
        }
    }
    
    /**
     * @private
     * @brief 更新数据缓冲区
     * @param {Object} data 设备数据
     */
    #updateDataBuffer(data) {
        const timestamp = new Date(data.timestamp);
        
        // 添加到缓冲区
        this.#dataBuffer.timestamp.push(timestamp);
        
        // 根据数据类型添加数据
        if (data.data.temperature !== undefined) {
            this.#dataBuffer.temperature.push(data.data.temperature);
        }
        
        if (data.data.humidity !== undefined) {
            this.#dataBuffer.humidity.push(data.data.humidity);
        }
        
        if (data.data.power !== undefined) {
            this.#dataBuffer.power.push(data.data.power);
        }
        
        // 保持缓冲区大小
        if (this.#dataBuffer.timestamp.length > this.#maxBufferSize) {
            this.#dataBuffer.timestamp.shift();
            this.#dataBuffer.temperature.shift();
            this.#dataBuffer.humidity.shift();
            this.#dataBuffer.power.shift();
        }
    }
    
    /**
     * @private
     * @brief 初始化图表
     */
    #initializeCharts() {
        // 实时数据图表
        this.#initializeRealtimeChart();
        
        // 消息速率图表
        this.#initializeMessageRateChart();
        
        // 其他图表可以根据需要添加
        console.log('Charts initialized');
    }
    
    /**
     * @private
     * @brief 初始化实时数据图表
     */
    #initializeRealtimeChart() {
        const ctx = document.getElementById('realtime-chart').getContext('2d');
        
        // 创建图表实例
        const chart = new Chart(ctx, {
            type: 'line',
            data: {
                datasets: [
                    {
                        label: '温度 (°C)',
                        data: [],
                        borderColor: 'rgb(255, 99, 132)',
                        backgroundColor: 'rgba(255, 99, 132, 0.1)',
                        tension: 0.4,
                        fill: true,
                        yAxisID: 'y'
                    },
                    {
                        label: '湿度 (%)',
                        data: [],
                        borderColor: 'rgb(54, 162, 235)',
                        backgroundColor: 'rgba(54, 162, 235, 0.1)',
                        tension: 0.4,
                        fill: true,
                        yAxisID: 'y1'
                    }
                ]
            },
            options: {
                responsive: true,
                maintainAspectRatio: false,
                interaction: {
                    mode: 'index',
                    intersect: false
                },
                scales: {
                    x: {
                        type: 'time',
                        time: {
                            unit: 'minute',
                            displayFormats: {
                                minute: 'HH:mm'
                            }
                        },
                        title: {
                            display: true,
                            text: '时间'
                        }
                    },
                    y: {
                        type: 'linear',
                        display: true,
                        position: 'left',
                        title: {
                            display: true,
                            text: '温度 (°C)'
                        },
                        min: 0,
                        max: 50
                    },
                    y1: {
                        type: 'linear',
                        display: true,
                        position: 'right',
                        title: {
                            display: true,
                            text: '湿度 (%)'
                        },
                        min: 0,
                        max: 100,
                        grid: {
                            drawOnChartArea: false
                        }
                    }
                },
                plugins: {
                    legend: {
                        position: 'top'
                    },
                    tooltip: {
                        mode: 'index',
                        intersect: false
                    }
                }
            }
        });
        
        this.#charts.set('realtime', chart);
    }
    
    /**
     * @private
     * @brief 初始化消息速率图表
     */
    #initializeMessageRateChart() {
        const ctx = document.getElementById('message-rate-chart').getContext('2d');
        
        const chart = new Chart(ctx, {
            type: 'line',
            data: {
                labels: [],
                datasets: [{
                    label: '消息/秒',
                    data: [],
                    borderColor: 'rgb(75, 192, 192)',
                    backgroundColor: 'rgba(75, 192, 192, 0.2)',
                    tension: 0.4,
                    fill: true
                }]
            },
            options: {
                responsive: true,
                maintainAspectRatio: false,
                scales: {
                    x: {
                        display: false
                    },
                    y: {
                        beginAtZero: true,
                        title: {
                            display: true,
                            text: '消息数'
                        }
                    }
                },
                plugins: {
                    legend: {
                        display: false
                    }
                }
            }
        });
        
        this.#charts.set('messageRate', chart);
    }
    
    /**
     * @private
     * @brief 更新实时图表
     * @param {Object} data 设备数据
     */
    #updateRealtimeChart(data) {
        const chart = this.#charts.get('realtime');
        if (!chart) return;
        
        const timestamp = new Date(data.timestamp);
        
        // 添加新数据点
        if (data.data.temperature !== undefined) {
            chart.data.datasets[0].data.push({
                x: timestamp,
                y: data.data.temperature
            });
        }
        
        if (data.data.humidity !== undefined) {
            chart.data.datasets[1].data.push({
                x: timestamp,
                y: data.data.humidity
            });
        }
        
        // 限制数据点数量(保持最近100个点)
        const maxPoints = 100;
        if (chart.data.datasets[0].data.length > maxPoints) {
            chart.data.datasets[0].data.shift();
        }
        if (chart.data.datasets[1].data.length > maxPoints) {
            chart.data.datasets[1].data.shift();
        }
        
        // 更新图表
        chart.update('none');
    }
    
    /**
     * @private
     * @brief 更新消息速率图表
     * @param {Object} stats 统计信息
     */
    #updateMessageRateChart(stats) {
        const chart = this.#charts.get('messageRate');
        if (!chart) return;
        
        const now = new Date();
        const timeLabel = now.toLocaleTimeString('zh-CN', { 
            hour: '2-digit', 
            minute: '2-digit' 
        });
        
        // 添加新数据点
        chart.data.labels.push(timeLabel);
        chart.data.datasets[0].data.push(stats.messages_per_second || 0);
        
        // 限制数据点数量(保持最近20个点)
        const maxPoints = 20;
        if (chart.data.labels.length > maxPoints) {
            chart.data.labels.shift();
            chart.data.datasets[0].data.shift();
        }
        
        // 更新图表
        chart.update();
    }
    
    /**
     * @private
     * @brief 初始化事件监听器
     */
    #initializeEventListeners() {
        // 刷新设备按钮
        const refreshBtn = document.getElementById('refresh-devices');
        if (refreshBtn) {
            refreshBtn.addEventListener('click', () => this.#refreshDevices());
        }
        
        // 保存设备按钮
        const saveDeviceBtn = document.getElementById('saveDeviceBtn');
        if (saveDeviceBtn) {
            saveDeviceBtn.addEventListener('click', () => this.#saveDevice());
        }
        
        // 图表时间范围选择
        const timeRangeSelect = document.getElementById('chart-time-range');
        if (timeRangeSelect) {
            timeRangeSelect.addEventListener('change', (e) => {
                this.#changeChartTimeRange(e.target.value);
            });
        }
        
        // 深色模式切换
        const darkModeSwitch = document.getElementById('darkModeSwitch');
        if (darkModeSwitch) {
            darkModeSwitch.addEventListener('change', (e) => {
                this.#toggleDarkMode(e.target.checked);
            });
        }
        
        // 窗口大小改变时重新调整图表
        window.addEventListener('resize', () => {
            this.#resizeCharts();
        });
        
        // 页面可见性变化
        document.addEventListener('visibilitychange', () => {
            this.#handleVisibilityChange();
        });
        
        console.log('Event listeners initialized');
    }
    
    /**
     * @private
     * @brief 开始数据更新循环
     */
    #startDataUpdateLoop() {
        // 每秒更新一次UI
        setInterval(() => {
            this.#updateUI();
        }, 1000);
        
        // 每5秒请求一次系统状态
        setInterval(() => {
            if (this.#connected) {
                this.#requestSystemStats();
            }
        }, 5000);
        
        console.log('Data update loops started');
    }
    
    /**
     * @private
     * @brief 更新UI状态
     */
    #updateUI() {
        // 更新当前时间
        this.#updateCurrentTime();
        
        // 更新设备状态(检查超时)
        this.#checkDeviceTimeouts();
        
        // 更新连接状态指示器
        this.#updateConnectionIndicator();
    }
    
    /**
     * @private
     * @brief 更新当前时间显示
     */
    #updateCurrentTime() {
        const now = new Date();
        const timeString = now.toLocaleTimeString('zh-CN', {
            hour12: false,
            hour: '2-digit',
            minute: '2-digit',
            second: '2-digit'
        });
        
        const dateString = now.toLocaleDateString('zh-CN', {
            year: 'numeric',
            month: '2-digit',
            day: '2-digit'
        });
        
        const timeElement = document.getElementById('current-time');
        if (timeElement) {
            timeElement.textContent = `${dateString} ${timeString}`;
        }
    }
    
    /**
     * @private
     * @brief 更新连接状态显示
     * @param {boolean} connected 是否已连接
     */
    #updateConnectionStatus(connected) {
        const statusElement = document.getElementById('server-status');
        const indicator = document.querySelector('.connection-indicator');
        
        if (statusElement) {
            statusElement.textContent = connected ? '运行中' : '已断开';
            statusElement.className = connected ? 'stat-value text-success' : 'stat-value text-danger';
        }
        
        if (indicator) {
            indicator.className = connected ? 
                'device-status status-online' : 
                'device-status status-offline';
        }
        
        // 更新连接状态指示器
        this.#updateConnectionIndicator();
    }
    
    /**
     * @private
     * @brief 更新连接状态指示器
     */
    #updateConnectionIndicator() {
        const indicator = document.getElementById('connection-indicator');
        if (!indicator) return;
        
        if (this.#connected) {
            indicator.className = 'device-status status-online pulse';
            indicator.title = '已连接到服务器';
        } else {
            indicator.className = 'device-status status-offline';
            indicator.title = '服务器连接已断开';
        }
    }
    
    /**
     * @private
     * @brief 更新设备表格
     * @param {string} deviceId 设备ID
     * @param {Object} deviceData 设备数据
     */
    #updateDeviceTable(deviceId, deviceData) {
        const tableBody = document.getElementById('device-table-body');
        if (!tableBody) return;
        
        // 查找或创建设备行
        let row = document.getElementById(`device-row-${deviceId}`);
        
        if (!row) {
            // 创建新行
            row = this.#createDeviceTableRow(deviceId, deviceData);
            tableBody.appendChild(row);
        } else {
            // 更新现有行
            this.#updateDeviceTableRow(row, deviceData);
        }
    }
    
    /**
     * @private
     * @brief 创建设备表格行
     * @param {string} deviceId 设备ID
     * @param {Object} deviceData 设备数据
     * @return {HTMLTableRowElement} 表格行元素
     */
    #createDeviceTableRow(deviceId, deviceData) {
        const row = document.createElement('tr');
        row.id = `device-row-${deviceId}`;
        row.className = 'device-card';
        
        // 设置点击事件
        row.addEventListener('click', () => {
            this.#showDeviceDetails(deviceId);
        });
        
        // 创建表格单元格
        row.innerHTML = `
            <td>
                <span class="device-status ${deviceData.online ? 'status-online' : 'status-offline'}"></span>
                ${deviceData.online ? '在线' : '离线'}
            </td>
            <td>${deviceId}</td>
            <td>${this.#getDeviceTypeName(deviceData.type)}</td>
            <td>${this.#formatTimestamp(deviceData.last_seen)}</td>
            <td>${deviceData.signal_strength || 'N/A'}</td>
            <td>
                ${deviceData.battery_level !== undefined ? 
                    this.#createBatteryIndicator(deviceData.battery_level) : 
                    'N/A'}
            </td>
            <td>
                <button class="btn btn-sm btn-outline-primary" 
                        οnclick="event.stopPropagation(); Dashboard.getInstance().sendCommand('${deviceId}', 'ping')">
                    <i class="fas fa-sync"></i>
                </button>
                <button class="btn btn-sm btn-outline-warning ms-1" 
                        οnclick="event.stopPropagation(); Dashboard.getInstance().sendCommand('${deviceId}', 'reboot')">
                    <i class="fas fa-redo"></i>
                </button>
            </td>
        `;
        
        return row;
    }
    
    /**
     * @private
     * @brief 更新设备表格行
     * @param {HTMLTableRowElement} row 表格行
     * @param {Object} deviceData 设备数据
     */
    #updateDeviceTableRow(row, deviceData) {
        // 更新状态单元格
        const statusCell = row.cells[0];
        const statusSpan = statusCell.querySelector('.device-status');
        const statusText = statusCell.querySelector('span:last-child');
        
        if (statusSpan) {
            statusSpan.className = `device-status ${deviceData.online ? 'status-online' : 'status-offline'}`;
        }
        
        if (statusText) {
            statusText.textContent = deviceData.online ? '在线' : '离线';
        }
        
        // 更新最后活跃时间
        const lastSeenCell = row.cells[3];
        if (lastSeenCell) {
            lastSeenCell.textContent = this.#formatTimestamp(deviceData.last_seen);
        }
        
        // 更新电池电量
        const batteryCell = row.cells[5];
        if (batteryCell && deviceData.battery_level !== undefined) {
            batteryCell.innerHTML = this.#createBatteryIndicator(deviceData.battery_level);
        }
    }
    
    /**
     * @private
     * @brief 创建设备类型名称
     * @param {string} type 设备类型代码
     * @return {string} 设备类型名称
     */
    #getDeviceTypeName(type) {
        const typeMap = {
            'temperature': '温度传感器',
            'humidity': '湿度传感器',
            'smartplug': '智能插座',
            'camera': '摄像头',
            'gateway': '网关设备',
            'controller': '控制器',
            'unknown': '未知设备'
        };
        
        return typeMap[type] || type;
    }
    
    /**
     * @private
     * @brief 格式化时间戳
     * @param {number} timestamp Unix时间戳
     * @return {string} 格式化后的时间字符串
     */
    #formatTimestamp(timestamp) {
        if (!timestamp) return '从未';
        
        const now = Date.now();
        const diff = now - timestamp;
        
        if (diff < 60000) { // 1分钟内
            return '刚刚';
        } else if (diff < 3600000) { // 1小时内
            const minutes = Math.floor(diff / 60000);
            return `${minutes}分钟前`;
        } else if (diff < 86400000) { // 1天内
            const hours = Math.floor(diff / 3600000);
            return `${hours}小时前`;
        } else {
            const date = new Date(timestamp);
            return date.toLocaleDateString('zh-CN');
        }
    }
    
    /**
     * @private
     * @brief 创建电池电量指示器
     * @param {number} level 电池电量百分比 (0-100)
     * @return {string} HTML字符串
     */
    #createBatteryIndicator(level) {
        let color = 'success';
        let icon = 'fa-battery-full';
        
        if (level < 20) {
            color = 'danger';
            icon = 'fa-battery-empty';
        } else if (level < 50) {
            color = 'warning';
            icon = 'fa-battery-half';
        } else if (level < 80) {
            color = 'info';
            icon = 'fa-battery-three-quarters';
        }
        
        return `
            <div class="d-flex align-items-center">
                <i class="fas ${icon} text-${color} me-2"></i>
                <span>${level}%</span>
            </div>
        `;
    }
    
    /**
     * @private
     * @brief 更新连接统计
     */
    #updateConnectionStats() {
        // 计算在线设备数量
        let onlineCount = 0;
        let totalCount = 0;
        
        this.#deviceCache.forEach(device => {
            totalCount++;
            if (device.online) {
                onlineCount++;
            }
        });
        
        // 更新UI
        const onlineElement = document.getElementById('online-devices');
        const totalElement = document.getElementById('total-devices');
        const progressElement = document.getElementById('connection-progress');
        const rateElement = document.getElementById('online-rate');
        
        if (onlineElement) onlineElement.textContent = onlineCount;
        if (totalElement) totalElement.textContent = totalCount;
        
        if (progressElement && rateElement && totalCount > 0) {
            const rate = Math.round((onlineCount / totalCount) * 100);
            progressElement.style.width = `${rate}%`;
            rateElement.textContent = `${rate}%`;
        }
    }
    
    /**
     * @private
     * @brief 更新服务器统计
     * @param {Object} stats 服务器统计数据
     */
    #updateServerStats(stats) {
        // 更新运行时间
        const uptimeElement = document.getElementById('uptime');
        if (uptimeElement && stats.uptime) {
            const hours = Math.floor(stats.uptime / 3600);
            uptimeElement.textContent = hours;
        }
        
        // 更新CPU使用率
        const cpuElement = document.getElementById('cpu-usage');
        if (cpuElement && stats.cpu_usage !== undefined) {
            cpuElement.textContent = `${stats.cpu_usage.toFixed(1)}%`;
            
            // 根据使用率改变颜色
            if (stats.cpu_usage > 80) {
                cpuElement.className = 'stat-value text-danger';
            } else if (stats.cpu_usage > 60) {
                cpuElement.className = 'stat-value text-warning';
            } else {
                cpuElement.className = 'stat-value text-success';
            }
        }
    }
    
    /**
     * @private
     * @brief 更新消息统计
     * @param {Object} stats 消息统计数据
     */
    #updateMessageStats(stats) {
        // 更新总消息数
        const totalMsgElement = document.getElementById('total-messages');
        if (totalMsgElement && stats.total_messages !== undefined) {
            totalMsgElement.textContent = this.#formatNumber(stats.total_messages);
        }
        
        // 更新每秒消息数
        const msgPerSecElement = document.getElementById('messages-per-sec');
        if (msgPerSecElement && stats.messages_per_second !== undefined) {
            msgPerSecElement.textContent = stats.messages_per_second.toFixed(1);
        }
        
        // 更新消息速率图表
        this.#updateMessageRateChart(stats);
    }
    
    /**
     * @private
     * @brief 更新统计图表
     * @param {Object} stats 统计数据
     */
    #updateStatsCharts(stats) {
        // 这里可以添加更多统计图表的更新逻辑
        // 例如:CPU使用率历史、内存使用历史等
    }
    
    /**
     * @private
     * @brief 更新最后更新时间
     */
    #updateLastUpdateTime() {
        const now = new Date();
        const timeString = now.toLocaleTimeString('zh-CN', {
            hour12: false,
            hour: '2-digit',
            minute: '2-digit',
            second: '2-digit'
        });
        
        const updateTimeElement = document.getElementById('update-time');
        if (updateTimeElement) {
            updateTimeElement.textContent = timeString;
        }
    }
    
    /**
     * @private
     * @brief 添加告警到列表
     * @param {Object} alertData 告警数据
     */
    #addAlertToList(alertData) {
        const alertList = document.getElementById('alert-list');
        if (!alertList) return;
        
        // 创建告警元素
        const alertElement = document.createElement('div');
        alertElement.className = `alert alert-${this.#getAlertLevelClass(alertData.level)}`;
        alertElement.innerHTML = `
            <div class="d-flex justify-content-between align-items-start">
                <div>
                    <strong>
                        <i class="fas ${this.#getAlertIcon(alertData.level)} me-2"></i>
                        ${alertData.title || '系统告警'}
                    </strong>
                    <div class="mt-1">${alertData.message}</div>
                    <small class="text-muted">
                        ${new Date(alertData.timestamp).toLocaleString('zh-CN')}
                        ${alertData.device_id ? ` - 设备: ${alertData.device_id}` : ''}
                    </small>
                </div>
                <button type="button" class="btn-close" οnclick="this.parentElement.parentElement.remove(); Dashboard.getInstance().updateAlertCount();"></button>
            </div>
        `;
        
        // 添加到列表顶部
        if (alertList.firstChild) {
            alertList.insertBefore(alertElement, alertList.firstChild);
        } else {
            alertList.appendChild(alertElement);
        }
        
        // 限制告警数量(最多保留10条)
        const maxAlerts = 10;
        while (alertList.children.length > maxAlerts) {
            alertList.removeChild(alertList.lastChild);
        }
    }
    
    /**
     * @private
     * @brief 获取告警级别对应的CSS类
     * @param {string} level 告警级别
     * @return {string} CSS类名
     */
    #getAlertLevelClass(level) {
        const levelMap = {
            'info': 'info',
            'warning': 'warning',
            'critical': 'danger',
            'error': 'danger'
        };
        
        return levelMap[level] || 'info';
    }
    
    /**
     * @private
     * @brief 获取告警图标
     * @param {string} level 告警级别
     * @return {string} Font Awesome图标类名
     */
    #getAlertIcon(level) {
        const iconMap = {
            'info': 'fa-info-circle',
            'warning': 'fa-exclamation-triangle',
            'critical': 'fa-skull-crossbones',
            'error': 'fa-bug'
        };
        
        return iconMap[level] || 'fa-bell';
    }
    
    /**
     * @private
     * @brief 更新告警计数
     */
    #updateAlertCount() {
        const alertList = document.getElementById('alert-list');
        const alertCountElement = document.getElementById('alert-count');
        
        if (alertList && alertCountElement) {
            const count = alertList.children.length;
            alertCountElement.textContent = count;
            
            // 如果没有告警,显示提示信息
            if (count === 0) {
                const noAlertsElement = document.createElement('div');
                noAlertsElement.className = 'alert alert-info';
                noAlertsElement.textContent = '暂无告警信息';
                alertList.appendChild(noAlertsElement);
            }
        }
    }
    
    /**
     * @private
     * @brief 显示桌面通知
     * @param {Object} alertData 告警数据
     */
    #showDesktopNotification(alertData) {
        // 检查通知权限
        if (Notification.permission === 'granted') {
            new Notification(alertData.title || 'IoT平台告警', {
                body: alertData.message,
                icon: '/favicon.ico',
                tag: `alert-${alertData.timestamp}`
            });
        } else if (Notification.permission !== 'denied') {
            // 请求通知权限
            Notification.requestPermission().then(permission => {
                if (permission === 'granted') {
                    this.#showDesktopNotification(alertData);
                }
            });
        }
    }
    
    /**
     * @private
     * @brief 播放告警声音
     * @param {string} level 告警级别
     */
    #playAlertSound(level) {
        // 创建音频上下文(如果需要)
        if (!window.alertAudioContext) {
            window.alertAudioContext = new (window.AudioContext || window.webkitAudioContext)();
        }
        
        const audioContext = window.alertAudioContext;
        
        // 创建振荡器
        const oscillator = audioContext.createOscillator();
        const gainNode = audioContext.createGain();
        
        // 根据告警级别设置频率
        let frequency = 800;
        if (level === 'critical') {
            frequency = 1200;
        }
        
        oscillator.frequency.setValueAtTime(frequency, audioContext.currentTime);
        oscillator.type = 'sine';
        
        // 设置音量包络
        gainNode.gain.setValueAtTime(0, audioContext.currentTime);
        gainNode.gain.linearRampToValueAtTime(0.1, audioContext.currentTime + 0.1);
        gainNode.gain.exponentialRampToValueAtTime(0.001, audioContext.currentTime + 0.5);
        
        // 连接节点
        oscillator.connect(gainNode);
        gainNode.connect(audioContext.destination);
        
        // 播放声音
        oscillator.start(audioContext.currentTime);
        oscillator.stop(audioContext.currentTime + 0.5);
    }
    
    /**
     * @private
     * @brief 检查设备超时
     */
    #checkDeviceTimeouts() {
        const now = Date.now();
        const timeout = 5 * 60 * 1000; // 5分钟超时
        
        this.#deviceCache.forEach((device, deviceId) => {
            if (device.online && now - device.last_seen > timeout) {
                // 设备超时,标记为离线
                device.online = false;
                this.#updateDeviceTable(deviceId, device);
                this.#updateConnectionStats();
                
                console.log(`Device ${deviceId} timed out`);
            }
        });
    }
    
    /**
     * @private
     * @brief 调整图表大小
     */
    #resizeCharts() {
        this.#charts.forEach(chart => {
            chart.resize();
        });
    }
    
    /**
     * @private
     * @brief 处理页面可见性变化
     */
    #handleVisibilityChange() {
        if (document.hidden) {
            // 页面不可见,暂停一些更新
            console.log('Page hidden, reducing updates');
        } else {
            // 页面可见,恢复正常更新
            console.log('Page visible, resuming updates');
            
            // 立即请求最新数据
            if (this.#connected) {
                this.#requestSystemStats();
                this.#refreshDevices();
            }
        }
    }
    
    /**
     * @private
     * @brief 请求初始数据
     */
    #requestInitialData() {
        // 请求系统状态
        this.#requestSystemStats();
        
        // 请求设备列表
        this.#requestDeviceList();
        
        // 请求历史数据
        this.#requestHistoricalData();
    }
    
    /**
     * @private
     * @brief 请求系统统计信息
     */
    #requestSystemStats() {
        this.#sendWebSocketMessage({
            type: 'get_system_stats',
            timestamp: Date.now()
        });
    }
    
    /**
     * @private
     * @brief 请求设备列表
     */
    #requestDeviceList() {
        this.#sendWebSocketMessage({
            type: 'get_device_list',
            timestamp: Date.now()
        });
    }
    
    /**
     * @private
     * @brief 请求历史数据
     */
    #requestHistoricalData() {
        this.#sendWebSocketMessage({
            type: 'get_historical_data',
            timestamp: Date.now(),
            hours: 24 // 请求24小时数据
        });
    }
    
    /**
     * @private
     * @brief 刷新设备列表
     */
    #refreshDevices() {
        this.#requestDeviceList();
        
        // 显示刷新状态
        const refreshBtn = document.getElementById('refresh-devices');
        if (refreshBtn) {
            const originalHtml = refreshBtn.innerHTML;
            refreshBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 刷新中';
            refreshBtn.disabled = true;
            
            setTimeout(() => {
                refreshBtn.innerHTML = originalHtml;
                refreshBtn.disabled = false;
            }, 1000);
        }
    }
    
    /**
     * @public
     * @brief 发送设备命令
     * @param {string} deviceId 设备ID
     * @param {string} command 命令类型
     * @param {Object} parameters 命令参数
     */
    sendCommand(deviceId, command, parameters = {}) {
        const message = {
            type: 'device_command',
            command_id: `cmd_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
            device_id: deviceId,
            command: command,
            parameters: parameters,
            timestamp: Date.now()
        };
        
        this.#sendWebSocketMessage(message);
        
        // 显示命令发送状态
        this.#showToast(`命令已发送到设备 ${deviceId}`, 'info');
        
        console.log(`Command sent to ${deviceId}: ${command}`, parameters);
    }
    
    /**
     * @private
     * @brief 显示命令响应
     * @param {string} commandId 命令ID
     * @param {boolean} success 是否成功
     * @param {string} message 响应消息
     */
    #showCommandResponse(commandId, success, message) {
        const type = success ? 'success' : 'error';
        const title = success ? '命令执行成功' : '命令执行失败';
        
        this.#showToast(`${title}: ${message}`, type);
    }
    
    /**
     * @private
     * @brief 显示Toast通知
     * @param {string} message 消息内容
     * @param {string} type 消息类型 (success, error, warning, info)
     */
    #showToast(message, type = 'info') {
        // 创建Toast容器(如果不存在)
        let toastContainer = document.getElementById('toast-container');
        if (!toastContainer) {
            toastContainer = document.createElement('div');
            toastContainer.id = 'toast-container';
            toastContainer.className = 'toast-container position-fixed bottom-0 end-0 p-3';
            document.body.appendChild(toastContainer);
        }
        
        // 创建Toast元素
        const toastId = `toast-${Date.now()}`;
        const toastElement = document.createElement('div');
        toastElement.id = toastId;
        toastElement.className = `toast align-items-center text-bg-${type} border-0`;
        toastElement.setAttribute('role', 'alert');
        toastElement.setAttribute('aria-live', 'assertive');
        toastElement.setAttribute('aria-atomic', 'true');
        
        toastElement.innerHTML = `
            <div class="d-flex">
                <div class="toast-body">
                    ${message}
                </div>
                <button type="button" class="btn-close btn-close-white me-2 m-auto" 
                        data-bs-dismiss="toast"></button>
            </div>
        `;
        
        toastContainer.appendChild(toastElement);
        
        // 使用Bootstrap Toast显示
        const toast = new bootstrap.Toast(toastElement, {
            delay: 3000,
            autohide: true
        });
        toast.show();
        
        // 自动移除Toast元素
        toastElement.addEventListener('hidden.bs.toast', () => {
            toastElement.remove();
        });
    }
    
    /**
     * @private
     * @brief 显示连接错误
     * @param {string} message 错误消息
     */
    #showConnectionError(message) {
        // 在页面顶部显示错误横幅
        let errorBanner = document.getElementById('connection-error-banner');
        if (!errorBanner) {
            errorBanner = document.createElement('div');
            errorBanner.id = 'connection-error-banner';
            errorBanner.className = 'alert alert-danger alert-dismissible fade show mb-0 rounded-0';
            errorBanner.style.position = 'sticky';
            errorBanner.style.top = '0';
            errorBanner.style.zIndex = '1050';
            
            errorBanner.innerHTML = `
                <div class="container-fluid">
                    <i class="fas fa-exclamation-triangle me-2"></i>
                    <span id="connection-error-message"></span>
                    <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
                </div>
            `;
            
            document.body.insertBefore(errorBanner, document.body.firstChild);
        }
        
        const messageElement = document.getElementById('connection-error-message');
        if (messageElement) {
            messageElement.textContent = message;
        }
        
        errorBanner.classList.remove('d-none');
    }
    
    /**
     * @private
     * @brief 格式化数字(添加千位分隔符)
     * @param {number} num 要格式化的数字
     * @return {string} 格式化后的字符串
     */
    #formatNumber(num) {
        return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
    }
    
    /**
     * @private
     * @brief 添加观察者
     * @param {Function} callback 回调函数
     */
    #addObserver(callback) {
        if (typeof callback === 'function') {
            this.#observers.push(callback);
        }
    }
    
    /**
     * @private
     * @brief 移除观察者
     * @param {Function} callback 要移除的回调函数
     */
    #removeObserver(callback) {
        const index = this.#observers.indexOf(callback);
        if (index > -1) {
            this.#observers.splice(index, 1);
        }
    }
    
    /**
     * @private
     * @brief 通知所有观察者
     * @param {string} event 事件名称
     * @param {Object} data 事件数据
     */
    #notifyObservers(event, data) {
        this.#observers.forEach(callback => {
            try {
                callback(event, data);
            } catch (error) {
                console.error('Observer callback error:', error);
            }
        });
    }
    
    /**
     * @private
     * @brief 显示设备详情
     * @param {string} deviceId 设备ID
     */
    #showDeviceDetails(deviceId) {
        const device = this.#deviceCache.get(deviceId);
        if (!device) return;
        
        // 设置模态框标题
        const titleElement = document.getElementById('deviceDetailTitle');
        if (titleElement) {
            titleElement.textContent = `设备详情 - ${deviceId}`;
        }
        
        // 更新设备详情内容
        const contentElement = document.getElementById('deviceDetailContent');
        if (contentElement) {
            contentElement.innerHTML = this.#createDeviceDetailContent(device);
        }
        
        // 显示模态框
        const modal = new bootstrap.Modal(document.getElementById('deviceDetailModal'));
        modal.show();
    }
    
    /**
     * @private
     * @brief 创建设备详情内容
     * @param {Object} device 设备数据
     * @return {string} HTML字符串
     */
    #createDeviceDetailContent(device) {
        return `
            <div class="row">
                <div class="col-md-6">
                    <div class="card mb-3">
                        <div class="card-header">
                            <i class="fas fa-info-circle me-2"></i>
                            基本信息
                        </div>
                        <div class="card-body">
                            <table class="table table-sm">
                                <tr>
                                    <th>设备ID:</th>
                                    <td>${device.id}</td>
                                </tr>
                                <tr>
                                    <th>设备类型:</th>
                                    <td>${this.#getDeviceTypeName(device.type)}</td>
                                </tr>
                                <tr>
                                    <th>当前状态:</th>
                                    <td>
                                        <span class="device-status ${device.online ? 'status-online' : 'status-offline'}"></span>
                                        ${device.online ? '在线' : '离线'}
                                    </td>
                                </tr>
                                <tr>
                                    <th>最后活跃:</th>
                                    <td>${this.#formatTimestamp(device.last_seen)}</td>
                                </tr>
                                <tr>
                                    <th>数据点数:</th>
                                    <td>${device.data_history ? device.data_history.length : 0}</td>
                                </tr>
                            </table>
                        </div>
                    </div>
                </div>
                
                <div class="col-md-6">
                    <div class="card mb-3">
                        <div class="card-header">
                            <i class="fas fa-chart-line me-2"></i>
                            最近数据
                        </div>
                        <div class="card-body">
                            ${this.#createRecentDataTable(device)}
                        </div>
                    </div>
                </div>
            </div>
            
            <div class="row">
                <div class="col-12">
                    <div class="card">
                        <div class="card-header">
                            <i class="fas fa-cogs me-2"></i>
                            设备控制
                        </div>
                        <div class="card-body">
                            <div class="btn-group" role="group">
                                <button class="btn btn-primary" 
                                        οnclick="Dashboard.getInstance().sendCommand('${device.id}', 'ping')">
                                    <i class="fas fa-sync me-2"></i>
                                    发送心跳
                                </button>
                                <button class="btn btn-warning" 
                                        οnclick="Dashboard.getInstance().sendCommand('${device.id}', 'reboot')">
                                    <i class="fas fa-redo me-2"></i>
                                    重启设备
                                </button>
                                <button class="btn btn-info" 
                                        οnclick="Dashboard.getInstance().sendCommand('${device.id}', 'get_config')">
                                    <i class="fas fa-cog me-2"></i>
                                    获取配置
                                </button>
                                <button class="btn btn-danger" 
                                        οnclick="Dashboard.getInstance().sendCommand('${device.id}', 'factory_reset')">
                                    <i class="fas fa-trash-alt me-2"></i>
                                    恢复出厂
                                </button>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        `;
    }
    
    /**
     * @private
     * @brief 创建最近数据表格
     * @param {Object} device 设备数据
     * @return {string} HTML字符串
     */
    #createRecentDataTable(device) {
        if (!device.data_history || device.data_history.length === 0) {
            return '<p class="text-muted text-center">暂无数据</p>';
        }
        
        let tableHtml = `
            <div class="table-responsive">
                <table class="table table-sm">
                    <thead>
                        <tr>
                            <th>时间</th>
                            <th>数据</th>
                        </tr>
                    </thead>
                    <tbody>
        `;
        
        // 显示最近5条数据
        const recentData = device.data_history.slice(-5).reverse();
        
        recentData.forEach(dataPoint => {
            const time = new Date(dataPoint.timestamp).toLocaleString('zh-CN');
            let dataStr = '';
            
            if (typeof dataPoint.data === 'object') {
                dataStr = Object.entries(dataPoint.data)
                    .map(([key, value]) => `${key}: ${value}`)
                    .join(', ');
            } else {
                dataStr = dataPoint.data.toString();
            }
            
            tableHtml += `
                <tr>
                    <td>${time}</td>
                    <td>${dataStr}</td>
                </tr>
            `;
        });
        
        tableHtml += `
                    </tbody>
                </table>
            </div>
        `;
        
        return tableHtml;
    }
    
    /**
     * @private
     * @brief 保存新设备
     */
    #saveDevice() {
        const deviceId = document.getElementById('deviceId').value.trim();
        const deviceType = document.getElementById('deviceType').value;
        const deviceGroup = document.getElementById('deviceGroup').value;
        const description = document.getElementById('deviceDescription').value.trim();
        
        // 验证输入
        if (!deviceId) {
            this.#showToast('请输入设备ID', 'warning');
            return;
        }
        
        if (!deviceType) {
            this.#showToast('请选择设备类型', 'warning');
            return;
        }
        
        // 发送添加设备请求
        this.#sendWebSocketMessage({
            type: 'add_device',
            device_id: deviceId,
            device_type: deviceType,
            group: deviceGroup || undefined,
            description: description || undefined,
            timestamp: Date.now()
        });
        
        // 关闭模态框
        const modal = bootstrap.Modal.getInstance(document.getElementById('addDeviceModal'));
        if (modal) {
            modal.hide();
        }
        
        // 清空表单
        document.getElementById('addDeviceForm').reset();
        
        // 显示成功消息
        this.#showToast(`设备 ${deviceId} 添加请求已发送`, 'success');
    }
    
    /**
     * @private
     * @brief 切换深色模式
     * @param {boolean} enabled 是否启用深色模式
     */
    #toggleDarkMode(enabled) {
        if (enabled) {
            document.documentElement.setAttribute('data-bs-theme', 'dark');
            localStorage.setItem('darkMode', 'enabled');
        } else {
            document.documentElement.setAttribute('data-bs-theme', 'light');
            localStorage.setItem('darkMode', 'disabled');
        }
    }
    
    /**
     * @private
     * @brief 初始化深色模式
     */
    #initDarkMode() {
        const darkModePref = localStorage.getItem('darkMode');
        const darkModeSwitch = document.getElementById('darkModeSwitch');
        
        if (darkModePref === 'enabled' || 
            (darkModePref === null && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
            document.documentElement.setAttribute('data-bs-theme', 'dark');
            if (darkModeSwitch) {
                darkModeSwitch.checked = true;
            }
        }
    }
    
    /**
     * @private
     * @brief 更改图表时间范围
     * @param {string} range 时间范围(小时)
     */
    #changeChartTimeRange(range) {
        // 根据选择的时间范围请求历史数据
        this.#sendWebSocketMessage({
            type: 'get_historical_data',
            timestamp: Date.now(),
            hours: parseInt(range)
        });
        
        console.log(`Chart time range changed to ${range} hours`);
    }
}
​
// 全局辅助函数
/**
 * @brief 初始化当前时间显示(独立函数)
 */
function updateCurrentTime() {
    const now = new Date();
    const timeString = now.toLocaleTimeString('zh-CN', {
        hour12: false,
        hour: '2-digit',
        minute: '2-digit',
        second: '2-digit'
    });
    
    const dateString = now.toLocaleDateString('zh-CN', {
        year: 'numeric',
        month: '2-digit',
        day: '2-digit'
    });
    
    const timeElement = document.getElementById('current-time');
    if (timeElement) {
        timeElement.textContent = `${dateString} ${timeString}`;
    }
}
​
/**
 * @brief 初始化深色模式(独立函数)
 */
function initDarkMode() {
    const darkModePref = localStorage.getItem('darkMode');
    const darkModeSwitch = document.getElementById('darkModeSwitch');
    
    if (darkModePref === 'enabled' || 
        (darkModePref === null && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
        document.documentElement.setAttribute('data-bs-theme', 'dark');
        if (darkModeSwitch) {
            darkModeSwitch.checked = true;
        }
    }
}
​
/**
 * @brief 初始化WebSocket连接(独立函数)
 */
function initWebSocket() {
    // 创建Dashboard实例(单例模式)
    const dashboard = Dashboard.getInstance();
    console.log('WebSocket connection initialized via Dashboard');
}
​
/**
 * @brief 初始化图表(独立函数)
 */
function initCharts() {
    // 图表初始化由Dashboard类处理
    console.log('Charts initialization delegated to Dashboard');
}
​
/**
 * @brief 加载设备列表(独立函数)
 */
function loadDevices() {
    // 设备加载由Dashboard类处理
    console.log('Device loading delegated to Dashboard');
}
​
/**
 * @brief 绑定事件监听器(独立函数)
 */
function bindEventListeners() {
    // 事件监听器由Dashboard类处理
    console.log('Event listeners delegated to Dashboard');
}

设计模式分析(第六部分)

1. 单例模式(Singleton Pattern)

  • 应用位置Dashboard 类的静态实例管理

  • 处理突发事件

    • 重复初始化:防止Dashboard被多次实例化

    • 状态不一致:确保全局只有一个状态源

    • 资源浪费:避免重复创建WebSocket连接

  • 工程作用

    • 全局状态管理

    • 资源共享

    • 统一的接口访问点

  • 性能分析

    • 内存节省:只创建一个实例

    • 初始化开销:首次访问时创建,后续快速访问

    • 线程安全:JavaScript单线程环境自然线程安全

2. 观察者模式(Observer Pattern)

  • 应用位置Dashboard 中的观察者回调系统

  • 处理突发事件

    • 数据更新通知:多个组件需要响应同一数据变化

    • 组件解耦:数据源不依赖具体UI组件

    • 动态订阅:运行时添加/移除观察者

  • 工程作用

    • 松耦合的事件处理

    • 支持一对多通知

    • 易于扩展新观察者

  • 性能分析

    • 通知开销:O(n)时间,n为观察者数量

    • 内存开销:每个观察者函数引用

    • 事件冒泡:支持事件传播

3. 策略模式(Strategy Pattern)

  • 应用位置:数据处理的多种策略

  • 处理突发事件

    • 数据格式变化:不同设备类型使用不同处理策略

    • 处理逻辑变更:运行时切换处理算法

    • 条件分支复杂:简化复杂的if-else逻辑

  • 工程作用

    • 封装算法变化

    • 提高代码复用性

    • 易于测试和维护

4. 工厂方法模式

  • 应用位置:UI组件动态创建

  • 处理突发事件

    • 动态UI生成:根据数据动态创建表格行、图表等

    • 组件复用:工厂方法创建可复用组件

    • 配置变化:通过工厂参数定制组件

运行性能分析

前端性能优化:

  1. 虚拟DOM操作:最小化DOM操作,批量更新

  2. 数据缓冲:限制历史数据大小,防止内存泄漏

  3. 事件委托:使用事件委托减少事件监听器数量

  4. 图表优化:限制数据点数量,避免性能下降

内存管理:

  1. 缓存策略:设备数据缓存,减少网络请求

  2. 垃圾回收:及时清理不再需要的DOM元素

  3. 对象池:复用Toast、Alert等UI组件

网络优化:

  1. WebSocket重连:指数退避算法

  2. 数据压缩:JSON数据轻量传输

  3. 请求合并:批量请求减少连接数

数据流结构设计

1. WebSocket消息格式:

{
    type: 'device_data',           // 消息类型
    timestamp: 1640995200000,      // Unix时间戳
    device_id: 'sensor_001',       // 设备ID
    device_type: 'temperature',    // 设备类型
    data: {                        // 传感器数据
        temperature: 25.5,
        humidity: 60
    },
    metadata: {                    // 元数据
        signal_strength: -50,
        battery_level: 85
    }
}

2. 前端数据缓存结构:

设备缓存结构:
Map {
    "device_id": {
        id: "sensor_001",
        type: "temperature",
        online: true,
        last_seen: 1640995200000,
        data_history: [
            {timestamp: 1640995200000, data: {...}},
            ...
        ],
        metadata: {...}
    },
    ...
}

3. 图表数据缓冲区:

数据缓冲区结构:
{
    temperature: [25.5, 26.0, ...],  // 温度数据数组
    humidity: [60, 61, ...],         // 湿度数据数组
    power: [120, 125, ...],          // 功率数据数组
    timestamp: [Date, Date, ...]     // 时间戳数组
}

突发事件处理策略

突发事件 处理策略 设计模式应用
网络断开 WebSocket重连,指数退避 观察者模式通知连接状态
数据风暴 数据缓冲,限制更新频率 策略模式的流控策略
内存泄漏 定期清理,对象池 工厂模式的资源管理
UI卡顿 虚拟滚动,分批渲染 策略模式的渲染优化
浏览器兼容 特性检测,降级方案 工厂模式的组件创建

第六部分总结:实现了完整的Web前端仪表板,采用单例模式管理全局状态,观察者模式处理事件通知,策略模式处理不同类型的数据。前端具备实时数据展示、设备管理、告警通知和图表可视化功能,与后端服务器通过WebSocket进行实时通信。


整个IoT物联网平台项目现已完成,包含:

  1. C++后端服务器:高性能WebSocket处理、设备管理、消息路由

  2. 协议定义:二进制消息格式,优化网络传输

  3. Web前端:响应式仪表板,实时数据可视化

  4. 设计模式应用:反应器模式、责任链模式、发布-订阅模式、单例模式等

系统具备完整的IoT平台功能,支持大规模设备连接、实时数据监控和设备控制,适用于工业物联网、智能家居、环境监测等多种场景。

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐