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
666 lines
17 KiB
JavaScript
666 lines
17 KiB
JavaScript
// ============================================
|
||
// 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 模块加载完成');
|