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

3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
  1. <template>
  2. <!-- 全局审批通知管理组件 - 无界面纯逻辑 -->
  3. <div style="display: none;"></div>
  4. </template>
  5. <script>
  6. import { getPendingApplyList, getPendingTriConfirmList } from '@/api/erf/erf'
  7. import approvalConfig from '@/config/approval-notification.config'
  8. /**
  9. * 全局审批通知管理器
  10. *
  11. * 功能
  12. * 1. 登录后自动检查待审批项经理审批计划员排产三方确认
  13. * 2. 定时轮询检查新的待审批项默认5分钟可在配置文件中修改
  14. * 3. 在浏览器右下角弹出合并通知提示
  15. * 4. 点击不同类型的提醒跳转到对应页面
  16. *
  17. * 配置文件src/config/approval-notification.config.js
  18. */
  19. export default {
  20. name: 'ApprovalNotificationManager',
  21. data() {
  22. return {
  23. pollingTimer: null, // 轮询定时器
  24. pollingInterval: approvalConfig.polling.interval, // 轮询间隔(从配置文件读取)
  25. lastCheckTime: null, // 上次检查时间
  26. notifiedApplications: new Set(), // 已通知过的申请单号集合(避免重复通知)
  27. isInitialized: false, // 是否已初始化
  28. audioContext: null, // 音频上下文(用于提示音)
  29. config: approvalConfig, // 配置对象
  30. isChecking: false, // 是否正在检查中(防止重复调用)
  31. firstCheckTimeout: null, // 首次检查的定时器
  32. activeNotifications: [], // 当前显示的所有通知实例
  33. // 上一次各类型待办数量(用于轮询时检测新增)
  34. lastCounts: {
  35. manager: 0,
  36. planner: 0,
  37. triConfirm: 0
  38. }
  39. }
  40. },
  41. computed: {
  42. /**
  43. * 当前登录用户ID
  44. */
  45. currentUserId() {
  46. return this.$store.state.user.id
  47. },
  48. /**
  49. * 当前登录用户名称
  50. */
  51. currentUserName() {
  52. return this.$store.state.user.name
  53. },
  54. /**
  55. * 当前站点
  56. */
  57. currentSite() {
  58. return this.$store.state.user.site
  59. },
  60. /**
  61. * 工程试验消息通知标志'Y'=启用通知, 'N'=禁用通知
  62. */
  63. erfMsgFlag() {
  64. return this.$store.state.user.erfMsgFlag
  65. },
  66. /**
  67. * 是否已登录
  68. */
  69. isLoggedIn() {
  70. return this.currentUserId && this.currentUserId !== 0
  71. },
  72. /**
  73. * 是否需要显示通知已登录且erfMsgFlag='Y'
  74. */
  75. shouldShowNotification() {
  76. return this.isLoggedIn && this.erfMsgFlag === 'Y'
  77. }
  78. },
  79. watch: {
  80. /**
  81. * 监听用户登录状态变化
  82. */
  83. isLoggedIn(newVal, oldVal) {
  84. console.log(`[审批通知] 登录状态变化: ${oldVal} -> ${newVal}, erfMsgFlag: ${this.erfMsgFlag}, 已初始化: ${this.isInitialized}`)
  85. // 只有真正的登录状态变化才处理
  86. if (oldVal === newVal) {
  87. console.log('[审批通知] 登录状态未变化,跳过')
  88. return
  89. }
  90. if (newVal && !this.isInitialized) {
  91. // 用户已登录且未初始化,但需要检查 erfMsgFlag
  92. if (this.erfMsgFlag === 'Y') {
  93. console.log('[审批通知] 用户已登录且启用通知(watch触发),初始化通知系统')
  94. // 延迟执行,避免与 mounted 冲突
  95. this.$nextTick(() => {
  96. if (!this.isInitialized && this.shouldShowNotification) {
  97. this.initializeNotificationSystem()
  98. }
  99. })
  100. } else {
  101. console.log('[审批通知] 用户已登录但未启用通知功能(erfMsgFlag=N),等待erfMsgFlag变化')
  102. }
  103. } else if (!newVal && this.isInitialized) {
  104. console.log('[审批通知] 用户已登出,停止通知系统')
  105. this.stopNotificationSystem()
  106. }
  107. },
  108. /**
  109. * 监听 erfMsgFlag 变化
  110. */
  111. erfMsgFlag(newVal, oldVal) {
  112. console.log(`[审批通知] erfMsgFlag变化: ${oldVal} -> ${newVal}, 登录状态: ${this.isLoggedIn}, 已初始化: ${this.isInitialized}`)
  113. if (newVal === 'Y' && this.shouldShowNotification && !this.isInitialized) {
  114. // erfMsgFlag 变为 'Y',且满足显示通知条件但未初始化,则初始化通知系统
  115. console.log('[审批通知] erfMsgFlag启用且用户已登录,初始化通知系统')
  116. this.$nextTick(() => {
  117. if (!this.isInitialized && this.shouldShowNotification) {
  118. this.initializeNotificationSystem()
  119. }
  120. })
  121. } else if (newVal === 'Y' && !this.isLoggedIn) {
  122. // erfMsgFlag 为 'Y' 但用户未登录
  123. console.log('[审批通知] erfMsgFlag启用但用户未登录,等待登录')
  124. } else if (newVal === 'N' && this.isInitialized) {
  125. // erfMsgFlag 变为 'N',且已初始化,则停止通知系统
  126. console.log('[审批通知] erfMsgFlag禁用,停止通知系统')
  127. this.stopNotificationSystem()
  128. }
  129. }
  130. },
  131. mounted() {
  132. console.log(`[审批通知] 组件挂载, 登录状态: ${this.isLoggedIn}, erfMsgFlag: ${this.erfMsgFlag}, 已初始化: ${this.isInitialized}`)
  133. // 组件挂载时,如果需要显示通知且未初始化则初始化
  134. // 使用延迟确保只执行一次
  135. this.$nextTick(() => {
  136. if (this.shouldShowNotification && !this.isInitialized) {
  137. console.log('[审批通知] 组件挂载时初始化通知系统')
  138. this.initializeNotificationSystem()
  139. } else if (!this.shouldShowNotification) {
  140. console.log(`[审批通知] 用户未启用通知功能(erfMsgFlag=${this.erfMsgFlag}),不初始化`)
  141. }
  142. })
  143. },
  144. beforeDestroy() {
  145. // 组件销毁时清理资源
  146. console.log('[审批通知] 组件销毁,清理所有资源')
  147. this.stopNotificationSystem()
  148. },
  149. methods: {
  150. /**
  151. * 初始化通知系统
  152. */
  153. initializeNotificationSystem() {
  154. // 检查是否需要显示通知
  155. if (!this.shouldShowNotification) {
  156. console.log(`[审批通知] 用户未启用通知功能(erfMsgFlag=${this.erfMsgFlag}),跳过初始化`)
  157. return
  158. }
  159. // 双重检查锁
  160. if (this.isInitialized) {
  161. console.log('[审批通知] 系统已初始化,跳过重复初始化')
  162. return
  163. }
  164. console.log('[审批通知] 开始初始化通知系统...')
  165. // 立即设置标志,防止并发调用
  166. this.isInitialized = true
  167. // 先清理可能存在的旧资源
  168. this.stopPolling()
  169. this.closeAllNotifications()
  170. // 清理可能存在的首次检查定时器
  171. if (this.firstCheckTimeout) {
  172. clearTimeout(this.firstCheckTimeout)
  173. this.firstCheckTimeout = null
  174. }
  175. // 延迟后首次检查(避免登录时的接口压力)
  176. this.firstCheckTimeout = setTimeout(() => {
  177. if (this.isInitialized && this.isLoggedIn) {
  178. this.checkPendingApprovals(true)
  179. }
  180. this.firstCheckTimeout = null
  181. }, this.config.polling.firstCheckDelay)
  182. // 启动定时轮询
  183. this.startPolling()
  184. console.log('[审批通知] 通知系统初始化完成')
  185. },
  186. /**
  187. * 停止通知系统
  188. */
  189. stopNotificationSystem() {
  190. console.log('[审批通知] 停止通知系统...')
  191. // 关闭所有已显示的通知窗口
  192. this.closeAllNotifications()
  193. // 停止轮询
  194. this.stopPolling()
  195. // 清理首次检查定时器
  196. if (this.firstCheckTimeout) {
  197. clearTimeout(this.firstCheckTimeout)
  198. this.firstCheckTimeout = null
  199. }
  200. // 重置状态
  201. this.isInitialized = false
  202. this.isChecking = false
  203. this.notifiedApplications.clear()
  204. this.lastCheckTime = null
  205. this.lastCounts = { manager: 0, planner: 0, triConfirm: 0 }
  206. console.log('[审批通知] 通知系统已停止')
  207. },
  208. /**
  209. * 关闭所有活动的通知窗口
  210. */
  211. closeAllNotifications() {
  212. console.log(`[审批通知] 关闭所有通知窗口,当前数量: ${this.activeNotifications.length}`)
  213. // 关闭所有通知
  214. this.activeNotifications.forEach(notification => {
  215. try {
  216. if (notification && typeof notification.close === 'function') {
  217. notification.close()
  218. }
  219. } catch (error) {
  220. console.error('[审批通知] 关闭通知失败:', error)
  221. }
  222. })
  223. // 清空通知列表
  224. this.activeNotifications = []
  225. },
  226. /**
  227. * 从活动列表中移除指定的通知实例
  228. * @param {Object} notification - 要移除的通知实例
  229. */
  230. removeNotification(notification) {
  231. const index = this.activeNotifications.indexOf(notification)
  232. if (index > -1) {
  233. this.activeNotifications.splice(index, 1)
  234. console.log(`[审批通知] 已移除通知实例,剩余活动通知数: ${this.activeNotifications.length}`)
  235. }
  236. },
  237. /**
  238. * 启动定时轮询
  239. */
  240. startPolling() {
  241. if (this.pollingTimer) {
  242. console.log('[审批通知] 定时器已存在,跳过创建')
  243. return
  244. }
  245. this.pollingTimer = setInterval(() => {
  246. if (this.isLoggedIn && this.isInitialized) {
  247. this.checkPendingApprovals(false)
  248. }
  249. }, this.pollingInterval)
  250. console.log(`[审批通知] 定时轮询已启动,定时器ID: ${this.pollingTimer}, 间隔 ${this.pollingInterval / 1000}`)
  251. },
  252. /**
  253. * 停止定时轮询
  254. */
  255. stopPolling() {
  256. if (this.pollingTimer) {
  257. console.log(`[审批通知] 清除定时器,ID: ${this.pollingTimer}`)
  258. clearInterval(this.pollingTimer)
  259. this.pollingTimer = null
  260. }
  261. },
  262. /**
  263. * 检查所有类型的待办事项经理审批计划员排产三方确认
  264. * 三个接口并行调用结果合并后显示一个通知
  265. * @param {boolean} isFirstCheck - 是否首次检查
  266. */
  267. checkPendingApprovals(isFirstCheck = false) {
  268. // 检查是否需要显示通知
  269. if (!this.shouldShowNotification) {
  270. console.log(`[审批通知] 用户未启用通知功能(erfMsgFlag=${this.erfMsgFlag}),跳过检查`)
  271. return
  272. }
  273. if (!this.isLoggedIn) {
  274. console.log('[审批通知] 用户未登录,跳过检查')
  275. return
  276. }
  277. // 防止重复调用
  278. if (this.isChecking) {
  279. console.log('[审批通知] 正在检查中,跳过本次调用')
  280. return
  281. }
  282. this.isChecking = true
  283. console.log(`[审批通知] 开始检查所有待办事项... (首次检查: ${isFirstCheck})`)
  284. // 1. 经理审批查询
  285. const managerRequest = getPendingApplyList({
  286. site: this.currentSite,
  287. currentUserId: this.currentUserId,
  288. userName: this.currentUserName,
  289. pageType: 'MANAGER',
  290. pendingStatus: '已下达',
  291. page: 1,
  292. limit: 20
  293. }).then(({data}) => {
  294. if (data && data.code === 0) {
  295. const list = data.rows || (data.page && data.page.list) || []
  296. return { count: data.totalCount || (data.page && data.page.totalCount) || list.length, list: list }
  297. }
  298. return { count: 0, list: [] }
  299. }).catch(() => ({ count: 0, list: [] }))
  300. // 2. 计划员排产查询
  301. const plannerRequest = getPendingApplyList({
  302. site: this.currentSite,
  303. currentUserId: this.currentUserId,
  304. userName: this.currentUserName,
  305. pageType: 'PLANNER',
  306. pendingStatus: '已批准',
  307. page: 1,
  308. limit: 20
  309. }).then(({data}) => {
  310. if (data && data.code === 0) {
  311. const list = data.rows || (data.page && data.page.list) || []
  312. return { count: data.totalCount || (data.page && data.page.totalCount) || list.length, list: list }
  313. }
  314. return { count: 0, list: [] }
  315. }).catch(() => ({ count: 0, list: [] }))
  316. // 3. 三方确认查询
  317. const triConfirmRequest = getPendingTriConfirmList({
  318. currentUserId: this.currentUserId,
  319. site: this.currentSite
  320. }).then(({data}) => {
  321. if (data && data.code === 0) {
  322. const list = data.list || []
  323. return { count: list.length, list: list }
  324. }
  325. return { count: 0, list: [] }
  326. }).catch(() => ({ count: 0, list: [] }))
  327. // 并行执行三个查询
  328. Promise.all([managerRequest, plannerRequest, triConfirmRequest]).then(([managerResult, plannerResult, triConfirmResult]) => {
  329. const counts = {
  330. manager: managerResult.count,
  331. planner: plannerResult.count,
  332. triConfirm: triConfirmResult.count
  333. }
  334. console.log(`[审批通知] 查询结果 - 经理审批: ${counts.manager}, 计划员排产: ${counts.planner}, 三方确认: ${counts.triConfirm}`)
  335. const totalCount = counts.manager + counts.planner + counts.triConfirm
  336. if (totalCount > 0) {
  337. if (isFirstCheck) {
  338. // 首次检查:显示合并通知
  339. this.showCombinedNotification(counts)
  340. } else {
  341. // 轮询检查:只在有新增时显示通知
  342. const hasNewItems = counts.manager > this.lastCounts.manager ||
  343. counts.planner > this.lastCounts.planner ||
  344. counts.triConfirm > this.lastCounts.triConfirm
  345. if (hasNewItems) {
  346. // 关闭旧通知,显示新通知
  347. this.closeAllNotifications()
  348. this.showCombinedNotification(counts)
  349. }
  350. }
  351. }
  352. // 更新上次的数量记录
  353. this.lastCounts = { ...counts }
  354. this.lastCheckTime = new Date()
  355. this.isChecking = false
  356. }).catch(error => {
  357. console.error('[审批通知] 检查待办事项失败:', error)
  358. this.isChecking = false
  359. })
  360. },
  361. /**
  362. * 显示合并通知包含经理审批计划员排产三方确认
  363. * 使用 VNode 创建可点击的分区通知
  364. * @param {Object} counts - 各类型待办数量 { manager, planner, triConfirm }
  365. */
  366. showCombinedNotification(counts) {
  367. const h = this.$createElement
  368. let notificationRef = null
  369. // 关闭通知并跳转的辅助函数
  370. const navigateAndClose = (path) => {
  371. if (notificationRef) {
  372. this.removeNotification(notificationRef)
  373. notificationRef.close()
  374. }
  375. this.$router.push({ path: path }).catch(err => {
  376. this.log('路由跳转失败: ' + err.message, 'warn')
  377. })
  378. }
  379. // 构建通知内容的各个区块
  380. const sections = []
  381. // 经理审批区块
  382. if (counts.manager > 0) {
  383. sections.push(
  384. h('div', {
  385. class: 'notification-item manager-item',
  386. on: { click: (e) => { e.stopPropagation(); navigateAndClose('erf-expApplyApproval') } }
  387. }, [
  388. h('div', { class: 'notification-item-icon' }, [
  389. h('i', { class: 'el-icon-s-check', style: { color: '#E6A23C', fontSize: '18px' } })
  390. ]),
  391. h('div', { class: 'notification-item-content' }, [
  392. h('p', { class: 'notification-item-text' }, [
  393. '您有 ',
  394. h('span', { class: 'count-highlight', style: { color: '#E6A23C' } }, counts.manager),
  395. ' 个工程试验申请单待审批'
  396. ]),
  397. h('p', { class: 'notification-item-link' }, '点击此通知查看详情')
  398. ])
  399. ])
  400. )
  401. }
  402. // 计划员排产区块
  403. if (counts.planner > 0) {
  404. sections.push(
  405. h('div', {
  406. class: 'notification-item planner-item',
  407. on: { click: (e) => { e.stopPropagation(); navigateAndClose('erf-plannerSchedule') } }
  408. }, [
  409. h('div', { class: 'notification-item-icon' }, [
  410. h('i', { class: 'el-icon-date', style: { color: '#409EFF', fontSize: '18px' } })
  411. ]),
  412. h('div', { class: 'notification-item-content' }, [
  413. h('p', { class: 'notification-item-text' }, [
  414. '您有 ',
  415. h('span', { class: 'count-highlight', style: { color: '#409EFF' } }, counts.planner),
  416. ' 个工程试验申请单待排产'
  417. ]),
  418. h('p', { class: 'notification-item-link' }, '点击此通知查看详情')
  419. ])
  420. ])
  421. )
  422. }
  423. // 三方确认区块
  424. if (counts.triConfirm > 0) {
  425. sections.push(
  426. h('div', {
  427. class: 'notification-item tri-confirm-item',
  428. on: { click: (e) => { e.stopPropagation(); navigateAndClose('erf-triConfirm') } }
  429. }, [
  430. h('div', { class: 'notification-item-icon' }, [
  431. h('i', { class: 'el-icon-finished', style: { color: '#67C23A', fontSize: '18px' } })
  432. ]),
  433. h('div', { class: 'notification-item-content' }, [
  434. h('p', { class: 'notification-item-text' }, [
  435. '您有 ',
  436. h('span', { class: 'count-highlight', style: { color: '#67C23A' } }, counts.triConfirm),
  437. ' 个三方确认工序待确认'
  438. ]),
  439. h('p', { class: 'notification-item-link' }, '点击此通知查看详情')
  440. ])
  441. ])
  442. )
  443. }
  444. // 如果没有任何待办,不显示通知
  445. if (sections.length === 0) {
  446. return
  447. }
  448. // 创建完整的通知消息 VNode
  449. const message = h('div', { class: 'combined-notification-body' }, sections)
  450. // 创建通知实例
  451. notificationRef = this.$notify({
  452. title: '待审批提醒',
  453. customClass: 'approval-notification combined-notification',
  454. message: message,
  455. type: 'warning',
  456. position: this.config.notification.position,
  457. duration: this.config.notification.summaryDuration,
  458. showClose: this.config.notification.showClose,
  459. onClose: () => {
  460. this.removeNotification(notificationRef)
  461. }
  462. })
  463. // 保存通知实例
  464. this.activeNotifications.push(notificationRef)
  465. console.log(`[审批通知] 已创建合并通知(经理:${counts.manager}, 排产:${counts.planner}, 三方:${counts.triConfirm}),当前活动通知数: ${this.activeNotifications.length}`)
  466. },
  467. /**
  468. * 播放通知提示音
  469. */
  470. playNotificationSound() {
  471. // 检查是否启用提示音
  472. if (!this.config.sound.enabled || !this.config.features.playSound) {
  473. return
  474. }
  475. try {
  476. // 使用 Web Audio API 生成简单的提示音
  477. const audioContext = new (window.AudioContext || window.webkitAudioContext)()
  478. const oscillator = audioContext.createOscillator()
  479. const gainNode = audioContext.createGain()
  480. oscillator.connect(gainNode)
  481. gainNode.connect(audioContext.destination)
  482. oscillator.frequency.value = this.config.sound.frequency // 频率
  483. oscillator.type = 'sine' // 正弦波
  484. const duration = this.config.sound.duration
  485. const volume = this.config.sound.volume
  486. gainNode.gain.setValueAtTime(volume, audioContext.currentTime)
  487. gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + duration)
  488. oscillator.start(audioContext.currentTime)
  489. oscillator.stop(audioContext.currentTime + duration)
  490. } catch (error) {
  491. this.log('播放提示音失败: ' + error.message, 'warn')
  492. }
  493. },
  494. /**
  495. * 手动触发检查供外部调用
  496. */
  497. manualCheck() {
  498. console.log('[审批通知] 手动触发检查')
  499. this.checkPendingApprovals(false)
  500. },
  501. /**
  502. * 清除指定申请单的通知记录审批完成后调用
  503. * @param {string} applyNo - 申请单号
  504. */
  505. clearNotification(applyNo) {
  506. this.notifiedApplications.delete(applyNo)
  507. this.log(`已清除申请单 ${applyNo} 的通知记录`, 'info')
  508. },
  509. /**
  510. * 日志输出辅助方法
  511. * @param {string} message - 日志消息
  512. * @param {string} level - 日志级别 ('info', 'warn', 'error', 'debug')
  513. */
  514. log(message, level = 'info') {
  515. if (!this.config.logging.enabled) {
  516. return
  517. }
  518. const prefix = this.config.logging.prefix
  519. const fullMessage = `${prefix} ${message}`
  520. switch (level) {
  521. case 'error':
  522. console.error(fullMessage)
  523. break
  524. case 'warn':
  525. console.warn(fullMessage)
  526. break
  527. case 'debug':
  528. if (this.config.debug.enabled) {
  529. console.debug(fullMessage)
  530. }
  531. break
  532. case 'info':
  533. default:
  534. console.log(fullMessage)
  535. break
  536. }
  537. }
  538. }
  539. }
  540. </script>
  541. <style scoped>
  542. /* 无需样式 */
  543. </style>