You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

618 lines
20 KiB

<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>