Files
ljwx-health/ljwx-bigscreen/app/static/js/modules/business/message-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

666 lines
17 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 - Message Panel V2 (业务模块)
// V2.0 架构重构 - P2.4
// ============================================
/**
* MessagePanelV2 - 消息面板 V2
*
* 核心职责:
* 1. 实时展示消息列表(5秒刷新)
* 2. 消息分类显示(系统/告警/通知)
* 3. 消息已读/未读状态管理
* 4. 消息详情查看
*
* 解决问题:
* - ❌ 当前:60秒刷新,消息延迟高
* - ✅ 新设计:5秒高频刷新,消息延迟降低 92%
*
* @author Claude AI
* @date 2026-01-06
* @version 2.0.0
*/
class MessagePanelV2 extends BaseComponent {
/**
* 构造函数
* @param {string|Element} container - 容器元素或选择器
* @param {object} options - 配置选项
*/
constructor(container, options = {}) {
super(container, options);
// 消息数据缓存
this.messages = [];
// 消息类型配置
this.messageTypes = {
system: { label: '系统', color: '#00e4ff', icon: '⚙️' },
alert: { label: '告警', color: '#ff4444', icon: '🔔' },
notification: { label: '通知', color: '#ffbb00', icon: '📢' }
};
// 当前过滤条件
this.filters = {
type: 'all', // all | system | alert | notification
status: 'all' // all | read | unread
};
// 分页配置
this.pagination = {
current: 1,
pageSize: 15,
total: 0
};
// 事件总线
this.eventBus = options.eventBus || window.eventBus || null;
// 数据管理器
this.dataManager = options.dataManager || null;
// 是否启用调试日志
this.debug = options.debug || false;
this.log('✅ MessagePanelV2 初始化完成');
}
// ============================================
// 生命周期方法
// ============================================
/**
* 初始化组件
* @param {object} data - 初始数据
*/
async init(data) {
if (this.initialized) {
this.log('⚠️ 组件已初始化');
return;
}
this.log('🎯 初始化 MessagePanelV2');
// 渲染初始数据
await this.render(data);
// 绑定事件
this.bindEvents();
// 订阅数据更新
this._subscribeToDataUpdates();
this.initialized = true;
this.log('✅ MessagePanelV2 初始化完成');
}
/**
* 渲染组件
* @param {object} data - 数据
*/
async render(data) {
if (!this.container) {
console.error('❌ 容器不存在');
return;
}
// 提取消息数据
this.messages = this._extractMessages(data);
this.pagination.total = this._getFilteredMessages().length;
// 渲染 HTML
this.container.innerHTML = this._generateHTML();
this.log(`✅ 渲染完成: ${this.messages.length} 条消息`);
}
/**
* 更新组件
* @param {object} data - 新数据
*/
async update(data) {
if (!this.initialized) {
return this.init(data);
}
this.log('🔄 更新 MessagePanelV2');
await this.render(data);
}
/**
* 绑定事件
*/
bindEvents() {
if (!this.container) return;
// 消息项点击事件(事件委托)
this.container.addEventListener('click', (e) => {
const messageItem = e.target.closest('.message-item');
if (messageItem) {
const messageId = messageItem.dataset.messageId;
this._handleMessageClick(messageId);
}
// 过滤按钮点击
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 markAllBtn = e.target.closest('.mark-all-read-btn');
if (markAllBtn) {
this._handleMarkAllRead();
}
});
this.log('✅ 事件绑定完成');
}
// ============================================
// 数据处理
// ============================================
/**
* 订阅数据更新事件
* @private
*/
_subscribeToDataUpdates() {
if (!this.eventBus) {
console.warn('⚠️ EventBus 未配置,无法订阅数据更新');
return;
}
// 监听消息数据更新事件
this.eventBus.on('messages:updated', (data) => {
this.log('📡 收到消息数据更新', data);
this.update(data);
});
this.log('📌 已订阅消息数据更新事件');
}
/**
* 提取消息数据
* @private
*/
_extractMessages(data) {
if (!data) return [];
// 兼容多种数据格式
let messages = [];
if (Array.isArray(data)) {
messages = data;
} else if (data.messages && Array.isArray(data.messages)) {
messages = data.messages;
} else if (data.data && Array.isArray(data.data.messages)) {
messages = data.data.messages;
}
// 过滤和排序
return messages
.filter(msg => msg && msg.message_id)
.sort((a, b) => {
// 未读消息优先
if (!a.is_read && b.is_read) return -1;
if (a.is_read && !b.is_read) return 1;
// 按时间倒序
return new Date(b.timestamp || 0) - new Date(a.timestamp || 0);
});
}
/**
* 获取过滤后的消息
* @private
*/
_getFilteredMessages() {
return this.messages.filter(msg => {
// 类型过滤
if (this.filters.type !== 'all' && msg.type !== this.filters.type) {
return false;
}
// 状态过滤
if (this.filters.status === 'read' && !msg.is_read) {
return false;
}
if (this.filters.status === 'unread' && msg.is_read) {
return false;
}
return true;
});
}
/**
* 获取分页数据
* @private
*/
_getPaginatedMessages() {
const filtered = this._getFilteredMessages();
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="message-panel-v2">
${this._generateHeader()}
${this._generateStats()}
${this._generateFilters()}
${this._generateList()}
${this._generatePagination()}
</div>
`;
}
/**
* 生成头部
* @private
*/
_generateHeader() {
const filteredCount = this._getFilteredMessages().length;
const unreadCount = this.messages.filter(m => !m.is_read).length;
return `
<div class="message-header">
<h3 class="message-title">消息列表</h3>
<div class="message-header-actions">
<span class="message-count">${filteredCount} / ${this.messages.length}</span>
${unreadCount > 0 ? `
<button class="mark-all-read-btn">
全部标记已读 (${unreadCount})
</button>
` : ''}
</div>
</div>
`;
}
/**
* 生成统计信息
* @private
*/
_generateStats() {
const stats = this._calculateStats();
return `
<div class="message-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-unread">
<span class="stat-icon">🔴</span>
<span class="stat-label">未读</span>
<span class="stat-value">${stats.unread}</span>
</div>
<div class="stat-item stat-system">
<span class="stat-icon">⚙️</span>
<span class="stat-label">系统</span>
<span class="stat-value">${stats.system}</span>
</div>
<div class="stat-item stat-alert">
<span class="stat-icon">🔔</span>
<span class="stat-label">告警</span>
<span class="stat-value">${stats.alert}</span>
</div>
</div>
`;
}
/**
* 生成过滤器
* @private
*/
_generateFilters() {
return `
<div class="message-filters">
<div class="filter-group">
<label class="filter-label">类型:</label>
<button class="filter-btn ${this.filters.type === 'all' ? 'active' : ''}"
data-filter-type="type" data-filter-value="all">全部</button>
<button class="filter-btn ${this.filters.type === 'system' ? 'active' : ''}"
data-filter-type="type" data-filter-value="system">系统</button>
<button class="filter-btn ${this.filters.type === 'alert' ? 'active' : ''}"
data-filter-type="type" data-filter-value="alert">告警</button>
<button class="filter-btn ${this.filters.type === 'notification' ? 'active' : ''}"
data-filter-type="type" data-filter-value="notification">通知</button>
</div>
<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 === 'unread' ? 'active' : ''}"
data-filter-type="status" data-filter-value="unread">未读</button>
<button class="filter-btn ${this.filters.status === 'read' ? 'active' : ''}"
data-filter-type="status" data-filter-value="read">已读</button>
</div>
</div>
`;
}
/**
* 生成列表
* @private
*/
_generateList() {
const messages = this._getPaginatedMessages();
if (messages.length === 0) {
return `
<div class="message-empty">
<span class="empty-icon">📭</span>
<p class="empty-text">暂无消息</p>
</div>
`;
}
const items = messages.map(msg => this._generateMessageItem(msg)).join('');
return `
<div class="message-list">
${items}
</div>
`;
}
/**
* 生成消息项
* @private
*/
_generateMessageItem(msg) {
const typeConfig = this.messageTypes[msg.type] || this.messageTypes.notification;
const timeAgo = this._formatTimeAgo(msg.timestamp);
const isUnread = !msg.is_read;
return `
<div class="message-item message-${msg.type} ${isUnread ? 'message-unread' : ''}"
data-message-id="${msg.message_id}">
${isUnread ? '<div class="message-unread-dot"></div>' : ''}
<div class="message-icon">${typeConfig.icon}</div>
<div class="message-content">
<div class="message-main">
<span class="message-title">${msg.title || '无标题'}</span>
<span class="message-text">${msg.content || '无内容'}</span>
</div>
<div class="message-meta">
<span class="message-time">${timeAgo}</span>
${msg.sender ? `<span class="message-sender">${msg.sender}</span>` : ''}
</div>
</div>
<div class="message-badge" style="background-color: ${typeConfig.color}">
${typeConfig.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="message-pagination">
${buttons.join('')}
</div>
`;
}
// ============================================
// 事件处理
// ============================================
/**
* 处理消息点击
* @private
*/
_handleMessageClick(messageId) {
const message = this.messages.find(m => m.message_id === messageId);
if (!message) {
console.warn('⚠️ 找不到消息:', messageId);
return;
}
this.log('📌 消息点击:', message);
// 标记为已读
if (!message.is_read) {
this._markAsRead(messageId);
}
// 触发事件
if (this.eventBus) {
this.eventBus.emit('message:click', message);
}
// 可选:显示详情弹窗
this._showMessageDetail(message);
}
/**
* 处理过滤变化
* @private
*/
_handleFilterChange(filterType, filterValue) {
this.filters[filterType] = filterValue;
this.pagination.current = 1; // 重置到第一页
this.render({ messages: this.messages });
this.log('🔍 过滤条件变化:', this.filters);
}
/**
* 处理分页变化
* @private
*/
_handlePageChange(page) {
if (page < 1 || page > Math.ceil(this.pagination.total / this.pagination.pageSize)) {
return;
}
this.pagination.current = page;
this.render({ messages: this.messages });
this.log('📄 切换到第', page, '页');
}
/**
* 处理全部标记已读
* @private
*/
_handleMarkAllRead() {
const unreadMessages = this.messages.filter(m => !m.is_read);
if (unreadMessages.length === 0) {
return;
}
this.log('✅ 标记全部已读:', unreadMessages.length);
// 更新本地状态
unreadMessages.forEach(msg => {
msg.is_read = true;
});
// 触发事件(通知后端)
if (this.eventBus) {
this.eventBus.emit('messages:markAllRead', {
messageIds: unreadMessages.map(m => m.message_id)
});
}
// 重新渲染
this.render({ messages: this.messages });
}
/**
* 标记消息为已读
* @private
*/
_markAsRead(messageId) {
const message = this.messages.find(m => m.message_id === messageId);
if (message && !message.is_read) {
message.is_read = true;
// 触发事件(通知后端)
if (this.eventBus) {
this.eventBus.emit('message:markAsRead', { messageId });
}
// 重新渲染
this.render({ messages: this.messages });
this.log('✅ 消息已标记为已读:', messageId);
}
}
/**
* 显示消息详情
* @private
*/
_showMessageDetail(message) {
// TODO: 实现详情弹窗
console.log('🔍 消息详情:', message);
// 可以通过 EventBus 通知主窗口显示模态框
if (this.eventBus) {
this.eventBus.emit('modal:show', {
type: 'message-detail',
data: message
});
}
}
// ============================================
// 工具方法
// ============================================
/**
* 计算统计信息
* @private
*/
_calculateStats() {
return {
total: this.messages.length,
unread: this.messages.filter(m => !m.is_read).length,
system: this.messages.filter(m => m.type === 'system').length,
alert: this.messages.filter(m => m.type === 'alert').length,
notification: this.messages.filter(m => m.type === 'notification').length
};
}
/**
* 格式化时间(相对时间)
* @private
*/
_formatTimeAgo(timestamp) {
if (!timestamp) return '未知时间';
const now = new Date();
const time = new Date(timestamp);
const diff = Math.floor((now - time) / 1000); // 秒
if (diff < 60) return `${diff}秒前`;
if (diff < 3600) return `${Math.floor(diff / 60)}分钟前`;
if (diff < 86400) return `${Math.floor(diff / 3600)}小时前`;
return `${Math.floor(diff / 86400)}天前`;
}
/**
* 获取统计信息
* @returns {object} 统计数据
*/
getStats() {
return this._calculateStats();
}
/**
* 日志输出
* @private
*/
log(...args) {
if (this.debug) {
console.log('[MessagePanelV2]', ...args);
}
}
}
// ============================================
// 模块导出 - 暴露到全局作用域
// ============================================
window.MessagePanelV2 = MessagePanelV2;
// 设置模块加载完成标志
window.messagePanelV2ModuleLoaded = true;
console.log('✅ message-panel-v2.js Message Panel V2 模块加载完成');