CI/CD Pipeline / security-scan (push) Successful in 1m4s
CI/CD Pipeline / test-build-boot (push) Successful in 5m25s
CI/CD Pipeline / build-pr (ljwx-admin) (push) Has been skipped
CI/CD Pipeline / build-pr (ljwx-bigscreen) (push) Has been skipped
CI/CD Pipeline / build-pr (ljwx-boot) (push) Has been skipped
CI/CD Pipeline / test-admin (push) Successful in 8m14s
CI/CD Pipeline / test-bigscreen (push) Successful in 53s
CI/CD Pipeline / build-push (ljwx-admin) (push) Successful in 22s
CI/CD Pipeline / build-push (ljwx-bigscreen) (push) Successful in 23s
CI/CD Pipeline / build-push (ljwx-boot) (push) Successful in 12s
CI/CD Pipeline / update-deploy (push) Successful in 1s
611 lines
15 KiB
JavaScript
611 lines
15 KiB
JavaScript
// ============================================
|
||
// LJWX BigScreen - Device Panel V2 (业务模块)
|
||
// V2.0 架构重构 - P2.3
|
||
// ============================================
|
||
|
||
/**
|
||
* DevicePanelV2 - 设备面板 V2
|
||
*
|
||
* 核心职责:
|
||
* 1. 展示设备列表(在线/离线状态)
|
||
* 2. 设备分组统计
|
||
* 3. 设备搜索和过滤
|
||
* 4. 设备详情查看
|
||
*
|
||
* 解决问题:
|
||
* - ❌ 当前:设备数据混杂在 get_total_info 中,刷新频率不合理
|
||
* - ✅ 新设计:30秒独立刷新,设备状态实时性提升
|
||
*
|
||
* @author Claude AI
|
||
* @date 2026-01-06
|
||
* @version 2.0.0
|
||
*/
|
||
class DevicePanelV2 extends BaseComponent {
|
||
/**
|
||
* 构造函数
|
||
* @param {string|Element} container - 容器元素或选择器
|
||
* @param {object} options - 配置选项
|
||
*/
|
||
constructor(container, options = {}) {
|
||
super(container, options);
|
||
|
||
// 设备数据缓存
|
||
this.devices = [];
|
||
|
||
// 当前过滤条件
|
||
this.filters = {
|
||
status: 'all', // all | online | offline
|
||
type: 'all', // all | watch | band | etc.
|
||
searchText: ''
|
||
};
|
||
|
||
// 设备状态配置
|
||
this.deviceStatus = {
|
||
online: { label: '在线', color: '#00e4ff', icon: '🟢' },
|
||
offline: { label: '离线', color: '#999999', icon: '⚫' }
|
||
};
|
||
|
||
// 分页配置
|
||
this.pagination = {
|
||
current: 1,
|
||
pageSize: 20,
|
||
total: 0
|
||
};
|
||
|
||
// 事件总线
|
||
this.eventBus = options.eventBus || window.eventBus || null;
|
||
|
||
// 数据管理器
|
||
this.dataManager = options.dataManager || null;
|
||
|
||
// 是否启用调试日志
|
||
this.debug = options.debug || false;
|
||
|
||
this.log('✅ DevicePanelV2 初始化完成');
|
||
}
|
||
|
||
// ============================================
|
||
// 生命周期方法
|
||
// ============================================
|
||
|
||
/**
|
||
* 初始化组件
|
||
* @param {object} data - 初始数据
|
||
*/
|
||
async init(data) {
|
||
if (this.initialized) {
|
||
this.log('⚠️ 组件已初始化');
|
||
return;
|
||
}
|
||
|
||
this.log('🎯 初始化 DevicePanelV2');
|
||
|
||
// 渲染初始数据
|
||
await this.render(data);
|
||
|
||
// 绑定事件
|
||
this.bindEvents();
|
||
|
||
// 订阅数据更新
|
||
this._subscribeToDataUpdates();
|
||
|
||
this.initialized = true;
|
||
this.log('✅ DevicePanelV2 初始化完成');
|
||
}
|
||
|
||
/**
|
||
* 渲染组件
|
||
* @param {object} data - 数据
|
||
*/
|
||
async render(data) {
|
||
if (!this.container) {
|
||
console.error('❌ 容器不存在');
|
||
return;
|
||
}
|
||
|
||
// 提取设备数据
|
||
this.devices = this._extractDevices(data);
|
||
this.pagination.total = this._getFilteredDevices().length;
|
||
|
||
// 渲染 HTML
|
||
this.container.innerHTML = this._generateHTML();
|
||
|
||
this.log(`✅ 渲染完成: ${this.devices.length} 台设备`);
|
||
}
|
||
|
||
/**
|
||
* 更新组件
|
||
* @param {object} data - 新数据
|
||
*/
|
||
async update(data) {
|
||
if (!this.initialized) {
|
||
return this.init(data);
|
||
}
|
||
|
||
this.log('🔄 更新 DevicePanelV2');
|
||
await this.render(data);
|
||
}
|
||
|
||
/**
|
||
* 绑定事件
|
||
*/
|
||
bindEvents() {
|
||
if (!this.container) return;
|
||
|
||
// 设备项点击事件(事件委托)
|
||
this.container.addEventListener('click', (e) => {
|
||
const deviceItem = e.target.closest('.device-item');
|
||
if (deviceItem) {
|
||
const deviceId = deviceItem.dataset.deviceId;
|
||
this._handleDeviceClick(deviceId);
|
||
}
|
||
|
||
// 过滤按钮点击
|
||
const filterBtn = e.target.closest('.filter-btn');
|
||
if (filterBtn) {
|
||
const filterType = filterBtn.dataset.filterType;
|
||
const filterValue = filterBtn.dataset.filterValue;
|
||
this._handleFilterChange(filterType, filterValue);
|
||
}
|
||
|
||
// 分页按钮点击
|
||
const pageBtn = e.target.closest('.page-btn');
|
||
if (pageBtn) {
|
||
const page = parseInt(pageBtn.dataset.page);
|
||
this._handlePageChange(page);
|
||
}
|
||
});
|
||
|
||
// 搜索框输入事件
|
||
const searchInput = this.container.querySelector('.device-search-input');
|
||
if (searchInput) {
|
||
searchInput.addEventListener('input', (e) => {
|
||
this._handleSearch(e.target.value);
|
||
});
|
||
}
|
||
|
||
this.log('✅ 事件绑定完成');
|
||
}
|
||
|
||
// ============================================
|
||
// 数据处理
|
||
// ============================================
|
||
|
||
/**
|
||
* 订阅数据更新事件
|
||
* @private
|
||
*/
|
||
_subscribeToDataUpdates() {
|
||
if (!this.eventBus) {
|
||
console.warn('⚠️ EventBus 未配置,无法订阅数据更新');
|
||
return;
|
||
}
|
||
|
||
// 监听设备数据更新事件
|
||
this.eventBus.on('devices:updated', (data) => {
|
||
this.log('📡 收到设备数据更新', data);
|
||
this.update(data);
|
||
});
|
||
|
||
this.log('📌 已订阅设备数据更新事件');
|
||
}
|
||
|
||
/**
|
||
* 提取设备数据
|
||
* @private
|
||
*/
|
||
_extractDevices(data) {
|
||
if (!data) return [];
|
||
|
||
// 兼容多种数据格式
|
||
let devices = [];
|
||
|
||
if (Array.isArray(data)) {
|
||
devices = data;
|
||
} else if (data.devices && Array.isArray(data.devices)) {
|
||
devices = data.devices;
|
||
} else if (data.data && Array.isArray(data.data.devices)) {
|
||
devices = data.data.devices;
|
||
}
|
||
|
||
// 过滤和排序
|
||
return devices
|
||
.filter(device => device && device.device_id)
|
||
.sort((a, b) => {
|
||
// 在线设备优先
|
||
if (a.status === 'online' && b.status !== 'online') return -1;
|
||
if (a.status !== 'online' && b.status === 'online') return 1;
|
||
|
||
// 按名称排序
|
||
return (a.device_name || '').localeCompare(b.device_name || '');
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 获取过滤后的设备
|
||
* @private
|
||
*/
|
||
_getFilteredDevices() {
|
||
return this.devices.filter(device => {
|
||
// 状态过滤
|
||
if (this.filters.status !== 'all' && device.status !== this.filters.status) {
|
||
return false;
|
||
}
|
||
|
||
// 类型过滤
|
||
if (this.filters.type !== 'all' && device.device_type !== this.filters.type) {
|
||
return false;
|
||
}
|
||
|
||
// 搜索文本过滤
|
||
if (this.filters.searchText) {
|
||
const searchLower = this.filters.searchText.toLowerCase();
|
||
const deviceName = (device.device_name || '').toLowerCase();
|
||
const userName = (device.user_name || '').toLowerCase();
|
||
const deviceId = (device.device_id || '').toLowerCase();
|
||
|
||
if (!deviceName.includes(searchLower) &&
|
||
!userName.includes(searchLower) &&
|
||
!deviceId.includes(searchLower)) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
return true;
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 获取分页数据
|
||
* @private
|
||
*/
|
||
_getPaginatedDevices() {
|
||
const filtered = this._getFilteredDevices();
|
||
const start = (this.pagination.current - 1) * this.pagination.pageSize;
|
||
const end = start + this.pagination.pageSize;
|
||
return filtered.slice(start, end);
|
||
}
|
||
|
||
// ============================================
|
||
// HTML 生成
|
||
// ============================================
|
||
|
||
/**
|
||
* 生成 HTML
|
||
* @private
|
||
*/
|
||
_generateHTML() {
|
||
return `
|
||
<div class="device-panel-v2">
|
||
${this._generateHeader()}
|
||
${this._generateStats()}
|
||
${this._generateFilters()}
|
||
${this._generateList()}
|
||
${this._generatePagination()}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
/**
|
||
* 生成头部
|
||
* @private
|
||
*/
|
||
_generateHeader() {
|
||
const filteredCount = this._getFilteredDevices().length;
|
||
|
||
return `
|
||
<div class="device-header">
|
||
<h3 class="device-title">设备列表</h3>
|
||
<span class="device-count">${filteredCount} / ${this.devices.length}</span>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
/**
|
||
* 生成统计信息
|
||
* @private
|
||
*/
|
||
_generateStats() {
|
||
const stats = this._calculateStats();
|
||
|
||
return `
|
||
<div class="device-stats">
|
||
<div class="stat-item stat-total">
|
||
<span class="stat-icon">📱</span>
|
||
<span class="stat-label">总数</span>
|
||
<span class="stat-value">${stats.total}</span>
|
||
</div>
|
||
<div class="stat-item stat-online">
|
||
<span class="stat-icon">🟢</span>
|
||
<span class="stat-label">在线</span>
|
||
<span class="stat-value">${stats.online}</span>
|
||
</div>
|
||
<div class="stat-item stat-offline">
|
||
<span class="stat-icon">⚫</span>
|
||
<span class="stat-label">离线</span>
|
||
<span class="stat-value">${stats.offline}</span>
|
||
</div>
|
||
<div class="stat-item stat-rate">
|
||
<span class="stat-icon">📊</span>
|
||
<span class="stat-label">在线率</span>
|
||
<span class="stat-value">${stats.onlineRate}%</span>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
/**
|
||
* 生成过滤器
|
||
* @private
|
||
*/
|
||
_generateFilters() {
|
||
return `
|
||
<div class="device-filters">
|
||
<div class="filter-group">
|
||
<label class="filter-label">状态:</label>
|
||
<button class="filter-btn ${this.filters.status === 'all' ? 'active' : ''}"
|
||
data-filter-type="status" data-filter-value="all">全部</button>
|
||
<button class="filter-btn ${this.filters.status === 'online' ? 'active' : ''}"
|
||
data-filter-type="status" data-filter-value="online">在线</button>
|
||
<button class="filter-btn ${this.filters.status === 'offline' ? 'active' : ''}"
|
||
data-filter-type="status" data-filter-value="offline">离线</button>
|
||
</div>
|
||
<div class="filter-search">
|
||
<input type="text" class="device-search-input" placeholder="搜索设备或用户..."
|
||
value="${this.filters.searchText}">
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
/**
|
||
* 生成列表
|
||
* @private
|
||
*/
|
||
_generateList() {
|
||
const devices = this._getPaginatedDevices();
|
||
|
||
if (devices.length === 0) {
|
||
return `
|
||
<div class="device-empty">
|
||
<span class="empty-icon">📱</span>
|
||
<p class="empty-text">暂无设备</p>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
const items = devices.map(device => this._generateDeviceItem(device)).join('');
|
||
|
||
return `
|
||
<div class="device-list">
|
||
${items}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
/**
|
||
* 生成设备项
|
||
* @private
|
||
*/
|
||
_generateDeviceItem(device) {
|
||
const statusConfig = this.deviceStatus[device.status] || this.deviceStatus.offline;
|
||
const batteryLevel = device.battery_level || 0;
|
||
const batteryIcon = this._getBatteryIcon(batteryLevel);
|
||
|
||
return `
|
||
<div class="device-item device-${device.status}" data-device-id="${device.device_id}">
|
||
<div class="device-icon">${statusConfig.icon}</div>
|
||
<div class="device-content">
|
||
<div class="device-main">
|
||
<span class="device-name">${device.device_name || '未命名设备'}</span>
|
||
<span class="device-user">${device.user_name || '未绑定'}</span>
|
||
</div>
|
||
<div class="device-meta">
|
||
<span class="device-model">${device.device_type || '未知型号'}</span>
|
||
<span class="device-battery">${batteryIcon} ${batteryLevel}%</span>
|
||
</div>
|
||
</div>
|
||
<div class="device-badge" style="background-color: ${statusConfig.color}">
|
||
${statusConfig.label}
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
/**
|
||
* 生成分页
|
||
* @private
|
||
*/
|
||
_generatePagination() {
|
||
const totalPages = Math.ceil(this.pagination.total / this.pagination.pageSize);
|
||
|
||
if (totalPages <= 1) {
|
||
return '';
|
||
}
|
||
|
||
const buttons = [];
|
||
|
||
// 上一页
|
||
buttons.push(`
|
||
<button class="page-btn page-prev" data-page="${this.pagination.current - 1}"
|
||
${this.pagination.current === 1 ? 'disabled' : ''}>
|
||
‹
|
||
</button>
|
||
`);
|
||
|
||
// 页码
|
||
for (let i = 1; i <= totalPages; i++) {
|
||
if (
|
||
i === 1 ||
|
||
i === totalPages ||
|
||
(i >= this.pagination.current - 1 && i <= this.pagination.current + 1)
|
||
) {
|
||
buttons.push(`
|
||
<button class="page-btn page-num ${i === this.pagination.current ? 'active' : ''}"
|
||
data-page="${i}">
|
||
${i}
|
||
</button>
|
||
`);
|
||
} else if (
|
||
i === this.pagination.current - 2 ||
|
||
i === this.pagination.current + 2
|
||
) {
|
||
buttons.push(`<span class="page-ellipsis">...</span>`);
|
||
}
|
||
}
|
||
|
||
// 下一页
|
||
buttons.push(`
|
||
<button class="page-btn page-next" data-page="${this.pagination.current + 1}"
|
||
${this.pagination.current === totalPages ? 'disabled' : ''}>
|
||
›
|
||
</button>
|
||
`);
|
||
|
||
return `
|
||
<div class="device-pagination">
|
||
${buttons.join('')}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
// ============================================
|
||
// 事件处理
|
||
// ============================================
|
||
|
||
/**
|
||
* 处理设备点击
|
||
* @private
|
||
*/
|
||
_handleDeviceClick(deviceId) {
|
||
const device = this.devices.find(d => d.device_id === deviceId);
|
||
|
||
if (!device) {
|
||
console.warn('⚠️ 找不到设备:', deviceId);
|
||
return;
|
||
}
|
||
|
||
this.log('📌 设备点击:', device);
|
||
|
||
// 触发事件
|
||
if (this.eventBus) {
|
||
this.eventBus.emit('device:click', device);
|
||
}
|
||
|
||
// 可选:显示详情弹窗
|
||
this._showDeviceDetail(device);
|
||
}
|
||
|
||
/**
|
||
* 处理过滤变化
|
||
* @private
|
||
*/
|
||
_handleFilterChange(filterType, filterValue) {
|
||
this.filters[filterType] = filterValue;
|
||
this.pagination.current = 1; // 重置到第一页
|
||
this.render({ devices: this.devices });
|
||
|
||
this.log('🔍 过滤条件变化:', this.filters);
|
||
}
|
||
|
||
/**
|
||
* 处理搜索
|
||
* @private
|
||
*/
|
||
_handleSearch(searchText) {
|
||
this.filters.searchText = searchText;
|
||
this.pagination.current = 1; // 重置到第一页
|
||
this.render({ devices: this.devices });
|
||
|
||
this.log('🔍 搜索文本:', searchText);
|
||
}
|
||
|
||
/**
|
||
* 处理分页变化
|
||
* @private
|
||
*/
|
||
_handlePageChange(page) {
|
||
if (page < 1 || page > Math.ceil(this.pagination.total / this.pagination.pageSize)) {
|
||
return;
|
||
}
|
||
|
||
this.pagination.current = page;
|
||
this.render({ devices: this.devices });
|
||
|
||
this.log('📄 切换到第', page, '页');
|
||
}
|
||
|
||
/**
|
||
* 显示设备详情
|
||
* @private
|
||
*/
|
||
_showDeviceDetail(device) {
|
||
// TODO: 实现详情弹窗
|
||
console.log('🔍 设备详情:', device);
|
||
|
||
// 可以通过 EventBus 通知主窗口显示模态框
|
||
if (this.eventBus) {
|
||
this.eventBus.emit('modal:show', {
|
||
type: 'device-detail',
|
||
data: device
|
||
});
|
||
}
|
||
}
|
||
|
||
// ============================================
|
||
// 工具方法
|
||
// ============================================
|
||
|
||
/**
|
||
* 计算统计信息
|
||
* @private
|
||
*/
|
||
_calculateStats() {
|
||
const total = this.devices.length;
|
||
const online = this.devices.filter(d => d.status === 'online').length;
|
||
const offline = total - online;
|
||
const onlineRate = total > 0 ? Math.round((online / total) * 100) : 0;
|
||
|
||
return { total, online, offline, onlineRate };
|
||
}
|
||
|
||
/**
|
||
* 获取电池图标
|
||
* @private
|
||
*/
|
||
_getBatteryIcon(level) {
|
||
if (level >= 80) return '🔋';
|
||
if (level >= 50) return '🔋';
|
||
if (level >= 20) return '🪫';
|
||
return '🪫';
|
||
}
|
||
|
||
/**
|
||
* 获取统计信息
|
||
* @returns {object} 统计数据
|
||
*/
|
||
getStats() {
|
||
return this._calculateStats();
|
||
}
|
||
|
||
/**
|
||
* 日志输出
|
||
* @private
|
||
*/
|
||
log(...args) {
|
||
if (this.debug) {
|
||
console.log('[DevicePanelV2]', ...args);
|
||
}
|
||
}
|
||
}
|
||
|
||
// ============================================
|
||
// 模块导出 - 暴露到全局作用域
|
||
// ============================================
|
||
|
||
window.DevicePanelV2 = DevicePanelV2;
|
||
|
||
// 设置模块加载完成标志
|
||
window.devicePanelV2ModuleLoaded = true;
|
||
console.log('✅ device-panel-v2.js Device Panel V2 模块加载完成');
|