Files
ljwx-health/ljwx-bigscreen/app/static/js/modules/business/device-panel-v2.js
T
bruno a0e61587d2
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
commit to gitea
2026-01-07 21:55:37 +08:00

611 lines
15 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ============================================
// 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 模块加载完成');