|
|
<template> <!-- 全局审批通知管理组件 - 无界面,纯逻辑 --> <div style="display: none;"></div></template>
<script>import { getPendingApplyList, getPendingTriConfirmList } from '@/api/erf/erf'import approvalConfig from '@/config/approval-notification.config'
/** * 全局审批通知管理器 * * 功能: * 1. 登录后自动检查待审批项(经理审批、计划员排产、三方确认) * 2. 定时轮询检查新的待审批项(默认5分钟,可在配置文件中修改) * 3. 在浏览器右下角弹出合并通知提示 * 4. 点击不同类型的提醒跳转到对应页面 * * 配置文件:src/config/approval-notification.config.js */export default { name: 'ApprovalNotificationManager',
data() { return { pollingTimer: null, // 轮询定时器
pollingInterval: approvalConfig.polling.interval, // 轮询间隔(从配置文件读取)
lastCheckTime: null, // 上次检查时间
notifiedApplications: new Set(), // 已通知过的申请单号集合(避免重复通知)
isInitialized: false, // 是否已初始化
audioContext: null, // 音频上下文(用于提示音)
config: approvalConfig, // 配置对象
isChecking: false, // 是否正在检查中(防止重复调用)
firstCheckTimeout: null, // 首次检查的定时器
activeNotifications: [], // 当前显示的所有通知实例
// 上一次各类型待办数量(用于轮询时检测新增)
lastCounts: { manager: 0, planner: 0, triConfirm: 0 } } },
computed: { /** * 当前登录用户ID */ currentUserId() { return this.$store.state.user.id },
/** * 当前登录用户名称 */ currentUserName() { return this.$store.state.user.name },
/** * 当前站点 */ currentSite() { return this.$store.state.user.site },
/** * 工程试验消息通知标志('Y'=启用通知, 'N'=禁用通知) */ erfMsgFlag() { return this.$store.state.user.erfMsgFlag },
/** * 是否已登录 */ isLoggedIn() { return this.currentUserId && this.currentUserId !== 0 },
/** * 是否需要显示通知(已登录且erfMsgFlag='Y') */ shouldShowNotification() { return this.isLoggedIn && this.erfMsgFlag === 'Y' } },
watch: { /** * 监听用户登录状态变化 */ isLoggedIn(newVal, oldVal) { console.log(`[审批通知] 登录状态变化: ${oldVal} -> ${newVal}, erfMsgFlag: ${this.erfMsgFlag}, 已初始化: ${this.isInitialized}`)
// 只有真正的登录状态变化才处理
if (oldVal === newVal) { console.log('[审批通知] 登录状态未变化,跳过') return }
if (newVal && !this.isInitialized) { // 用户已登录且未初始化,但需要检查 erfMsgFlag
if (this.erfMsgFlag === 'Y') { console.log('[审批通知] 用户已登录且启用通知(watch触发),初始化通知系统') // 延迟执行,避免与 mounted 冲突
this.$nextTick(() => { if (!this.isInitialized && this.shouldShowNotification) { this.initializeNotificationSystem() } }) } else { console.log('[审批通知] 用户已登录但未启用通知功能(erfMsgFlag=N),等待erfMsgFlag变化') } } else if (!newVal && this.isInitialized) { console.log('[审批通知] 用户已登出,停止通知系统') this.stopNotificationSystem() } },
/** * 监听 erfMsgFlag 变化 */ erfMsgFlag(newVal, oldVal) { console.log(`[审批通知] erfMsgFlag变化: ${oldVal} -> ${newVal}, 登录状态: ${this.isLoggedIn}, 已初始化: ${this.isInitialized}`)
if (newVal === 'Y' && this.shouldShowNotification && !this.isInitialized) { // erfMsgFlag 变为 'Y',且满足显示通知条件但未初始化,则初始化通知系统
console.log('[审批通知] erfMsgFlag启用且用户已登录,初始化通知系统') this.$nextTick(() => { if (!this.isInitialized && this.shouldShowNotification) { this.initializeNotificationSystem() } }) } else if (newVal === 'Y' && !this.isLoggedIn) { // erfMsgFlag 为 'Y' 但用户未登录
console.log('[审批通知] erfMsgFlag启用但用户未登录,等待登录') } else if (newVal === 'N' && this.isInitialized) { // erfMsgFlag 变为 'N',且已初始化,则停止通知系统
console.log('[审批通知] erfMsgFlag禁用,停止通知系统') this.stopNotificationSystem() } } },
mounted() { console.log(`[审批通知] 组件挂载, 登录状态: ${this.isLoggedIn}, erfMsgFlag: ${this.erfMsgFlag}, 已初始化: ${this.isInitialized}`) // 组件挂载时,如果需要显示通知且未初始化则初始化
// 使用延迟确保只执行一次
this.$nextTick(() => { if (this.shouldShowNotification && !this.isInitialized) { console.log('[审批通知] 组件挂载时初始化通知系统') this.initializeNotificationSystem() } else if (!this.shouldShowNotification) { console.log(`[审批通知] 用户未启用通知功能(erfMsgFlag=${this.erfMsgFlag}),不初始化`) } }) },
beforeDestroy() { // 组件销毁时清理资源
console.log('[审批通知] 组件销毁,清理所有资源') this.stopNotificationSystem() },
methods: { /** * 初始化通知系统 */ initializeNotificationSystem() { // 检查是否需要显示通知
if (!this.shouldShowNotification) { console.log(`[审批通知] 用户未启用通知功能(erfMsgFlag=${this.erfMsgFlag}),跳过初始化`) return }
// 双重检查锁
if (this.isInitialized) { console.log('[审批通知] 系统已初始化,跳过重复初始化') return }
console.log('[审批通知] 开始初始化通知系统...')
// 立即设置标志,防止并发调用
this.isInitialized = true
// 先清理可能存在的旧资源
this.stopPolling() this.closeAllNotifications()
// 清理可能存在的首次检查定时器
if (this.firstCheckTimeout) { clearTimeout(this.firstCheckTimeout) this.firstCheckTimeout = null }
// 延迟后首次检查(避免登录时的接口压力)
this.firstCheckTimeout = setTimeout(() => { if (this.isInitialized && this.isLoggedIn) { this.checkPendingApprovals(true) } this.firstCheckTimeout = null }, this.config.polling.firstCheckDelay)
// 启动定时轮询
this.startPolling()
console.log('[审批通知] 通知系统初始化完成') },
/** * 停止通知系统 */ stopNotificationSystem() { console.log('[审批通知] 停止通知系统...')
// 关闭所有已显示的通知窗口
this.closeAllNotifications()
// 停止轮询
this.stopPolling()
// 清理首次检查定时器
if (this.firstCheckTimeout) { clearTimeout(this.firstCheckTimeout) this.firstCheckTimeout = null }
// 重置状态
this.isInitialized = false this.isChecking = false this.notifiedApplications.clear() this.lastCheckTime = null this.lastCounts = { manager: 0, planner: 0, triConfirm: 0 }
console.log('[审批通知] 通知系统已停止') },
/** * 关闭所有活动的通知窗口 */ closeAllNotifications() { console.log(`[审批通知] 关闭所有通知窗口,当前数量: ${this.activeNotifications.length}`)
// 关闭所有通知
this.activeNotifications.forEach(notification => { try { if (notification && typeof notification.close === 'function') { notification.close() } } catch (error) { console.error('[审批通知] 关闭通知失败:', error) } })
// 清空通知列表
this.activeNotifications = [] },
/** * 从活动列表中移除指定的通知实例 * @param {Object} notification - 要移除的通知实例 */ removeNotification(notification) { const index = this.activeNotifications.indexOf(notification) if (index > -1) { this.activeNotifications.splice(index, 1) console.log(`[审批通知] 已移除通知实例,剩余活动通知数: ${this.activeNotifications.length}`) } },
/** * 启动定时轮询 */ startPolling() { if (this.pollingTimer) { console.log('[审批通知] 定时器已存在,跳过创建') return }
this.pollingTimer = setInterval(() => { if (this.isLoggedIn && this.isInitialized) { this.checkPendingApprovals(false) } }, this.pollingInterval)
console.log(`[审批通知] 定时轮询已启动,定时器ID: ${this.pollingTimer}, 间隔 ${this.pollingInterval / 1000} 秒`) },
/** * 停止定时轮询 */ stopPolling() { if (this.pollingTimer) { console.log(`[审批通知] 清除定时器,ID: ${this.pollingTimer}`) clearInterval(this.pollingTimer) this.pollingTimer = null } },
/** * 检查所有类型的待办事项(经理审批、计划员排产、三方确认) * 三个接口并行调用,结果合并后显示一个通知 * @param {boolean} isFirstCheck - 是否首次检查 */ checkPendingApprovals(isFirstCheck = false) { // 检查是否需要显示通知
if (!this.shouldShowNotification) { console.log(`[审批通知] 用户未启用通知功能(erfMsgFlag=${this.erfMsgFlag}),跳过检查`) return }
if (!this.isLoggedIn) { console.log('[审批通知] 用户未登录,跳过检查') return }
// 防止重复调用
if (this.isChecking) { console.log('[审批通知] 正在检查中,跳过本次调用') return }
this.isChecking = true console.log(`[审批通知] 开始检查所有待办事项... (首次检查: ${isFirstCheck})`)
// 1. 经理审批查询
const managerRequest = getPendingApplyList({ site: this.currentSite, currentUserId: this.currentUserId, userName: this.currentUserName, pageType: 'MANAGER', pendingStatus: '已下达', page: 1, limit: 20 }).then(({data}) => { if (data && data.code === 0) { const list = data.rows || (data.page && data.page.list) || [] return { count: data.totalCount || (data.page && data.page.totalCount) || list.length, list: list } } return { count: 0, list: [] } }).catch(() => ({ count: 0, list: [] }))
// 2. 计划员排产查询
const plannerRequest = getPendingApplyList({ site: this.currentSite, currentUserId: this.currentUserId, userName: this.currentUserName, pageType: 'PLANNER', pendingStatus: '已批准', page: 1, limit: 20 }).then(({data}) => { if (data && data.code === 0) { const list = data.rows || (data.page && data.page.list) || [] return { count: data.totalCount || (data.page && data.page.totalCount) || list.length, list: list } } return { count: 0, list: [] } }).catch(() => ({ count: 0, list: [] }))
// 3. 三方确认查询
const triConfirmRequest = getPendingTriConfirmList({ currentUserId: this.currentUserId, site: this.currentSite }).then(({data}) => { if (data && data.code === 0) { const list = data.list || [] return { count: list.length, list: list } } return { count: 0, list: [] } }).catch(() => ({ count: 0, list: [] }))
// 并行执行三个查询
Promise.all([managerRequest, plannerRequest, triConfirmRequest]).then(([managerResult, plannerResult, triConfirmResult]) => { const counts = { manager: managerResult.count, planner: plannerResult.count, triConfirm: triConfirmResult.count }
console.log(`[审批通知] 查询结果 - 经理审批: ${counts.manager}, 计划员排产: ${counts.planner}, 三方确认: ${counts.triConfirm}`)
const totalCount = counts.manager + counts.planner + counts.triConfirm
if (totalCount > 0) { if (isFirstCheck) { // 首次检查:显示合并通知
this.showCombinedNotification(counts) } else { // 轮询检查:只在有新增时显示通知
const hasNewItems = counts.manager > this.lastCounts.manager || counts.planner > this.lastCounts.planner || counts.triConfirm > this.lastCounts.triConfirm
if (hasNewItems) { // 关闭旧通知,显示新通知
this.closeAllNotifications() this.showCombinedNotification(counts) } } }
// 更新上次的数量记录
this.lastCounts = { ...counts } this.lastCheckTime = new Date() this.isChecking = false }).catch(error => { console.error('[审批通知] 检查待办事项失败:', error) this.isChecking = false }) },
/** * 显示合并通知(包含经理审批、计划员排产、三方确认) * 使用 VNode 创建可点击的分区通知 * @param {Object} counts - 各类型待办数量 { manager, planner, triConfirm } */ showCombinedNotification(counts) { const h = this.$createElement let notificationRef = null
// 关闭通知并跳转的辅助函数
const navigateAndClose = (path) => { if (notificationRef) { this.removeNotification(notificationRef) notificationRef.close() } this.$router.push({ path: path }).catch(err => { this.log('路由跳转失败: ' + err.message, 'warn') }) }
// 构建通知内容的各个区块
const sections = []
// 经理审批区块
if (counts.manager > 0) { sections.push( h('div', { class: 'notification-item manager-item', on: { click: (e) => { e.stopPropagation(); navigateAndClose('erf-expApplyApproval') } } }, [ h('div', { class: 'notification-item-icon' }, [ h('i', { class: 'el-icon-s-check', style: { color: '#E6A23C', fontSize: '18px' } }) ]), h('div', { class: 'notification-item-content' }, [ h('p', { class: 'notification-item-text' }, [ '您有 ', h('span', { class: 'count-highlight', style: { color: '#E6A23C' } }, counts.manager), ' 个工程试验申请单待审批' ]), h('p', { class: 'notification-item-link' }, '点击此通知查看详情') ]) ]) ) }
// 计划员排产区块
if (counts.planner > 0) { sections.push( h('div', { class: 'notification-item planner-item', on: { click: (e) => { e.stopPropagation(); navigateAndClose('erf-plannerSchedule') } } }, [ h('div', { class: 'notification-item-icon' }, [ h('i', { class: 'el-icon-date', style: { color: '#409EFF', fontSize: '18px' } }) ]), h('div', { class: 'notification-item-content' }, [ h('p', { class: 'notification-item-text' }, [ '您有 ', h('span', { class: 'count-highlight', style: { color: '#409EFF' } }, counts.planner), ' 个工程试验申请单待排产' ]), h('p', { class: 'notification-item-link' }, '点击此通知查看详情') ]) ]) ) }
// 三方确认区块
if (counts.triConfirm > 0) { sections.push( h('div', { class: 'notification-item tri-confirm-item', on: { click: (e) => { e.stopPropagation(); navigateAndClose('erf-triConfirm') } } }, [ h('div', { class: 'notification-item-icon' }, [ h('i', { class: 'el-icon-finished', style: { color: '#67C23A', fontSize: '18px' } }) ]), h('div', { class: 'notification-item-content' }, [ h('p', { class: 'notification-item-text' }, [ '您有 ', h('span', { class: 'count-highlight', style: { color: '#67C23A' } }, counts.triConfirm), ' 个三方确认工序待确认' ]), h('p', { class: 'notification-item-link' }, '点击此通知查看详情') ]) ]) ) }
// 如果没有任何待办,不显示通知
if (sections.length === 0) { return }
// 创建完整的通知消息 VNode
const message = h('div', { class: 'combined-notification-body' }, sections)
// 创建通知实例
notificationRef = this.$notify({ title: '待审批提醒', customClass: 'approval-notification combined-notification', message: message, type: 'warning', position: this.config.notification.position, duration: this.config.notification.summaryDuration, showClose: this.config.notification.showClose, onClose: () => { this.removeNotification(notificationRef) } })
// 保存通知实例
this.activeNotifications.push(notificationRef) console.log(`[审批通知] 已创建合并通知(经理:${counts.manager}, 排产:${counts.planner}, 三方:${counts.triConfirm}),当前活动通知数: ${this.activeNotifications.length}`) },
/** * 播放通知提示音 */ playNotificationSound() { // 检查是否启用提示音
if (!this.config.sound.enabled || !this.config.features.playSound) { return }
try { // 使用 Web Audio API 生成简单的提示音
const audioContext = new (window.AudioContext || window.webkitAudioContext)() const oscillator = audioContext.createOscillator() const gainNode = audioContext.createGain()
oscillator.connect(gainNode) gainNode.connect(audioContext.destination)
oscillator.frequency.value = this.config.sound.frequency // 频率
oscillator.type = 'sine' // 正弦波
const duration = this.config.sound.duration const volume = this.config.sound.volume
gainNode.gain.setValueAtTime(volume, audioContext.currentTime) gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + duration)
oscillator.start(audioContext.currentTime) oscillator.stop(audioContext.currentTime + duration) } catch (error) { this.log('播放提示音失败: ' + error.message, 'warn') } },
/** * 手动触发检查(供外部调用) */ manualCheck() { console.log('[审批通知] 手动触发检查') this.checkPendingApprovals(false) },
/** * 清除指定申请单的通知记录(审批完成后调用) * @param {string} applyNo - 申请单号 */ clearNotification(applyNo) { this.notifiedApplications.delete(applyNo) this.log(`已清除申请单 ${applyNo} 的通知记录`, 'info') },
/** * 日志输出辅助方法 * @param {string} message - 日志消息 * @param {string} level - 日志级别 ('info', 'warn', 'error', 'debug') */ log(message, level = 'info') { if (!this.config.logging.enabled) { return }
const prefix = this.config.logging.prefix const fullMessage = `${prefix} ${message}`
switch (level) { case 'error': console.error(fullMessage) break case 'warn': console.warn(fullMessage) break case 'debug': if (this.config.debug.enabled) { console.debug(fullMessage) } break case 'info': default: console.log(fullMessage) break } } }}</script>
<style scoped>/* 无需样式 */</style>
|