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