menu-badge-registry.ts 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. import { advanceApi, paymentApi, reimburseApi } from '@/api/bx'
  2. import { menuBadgeApi } from '@/api/pl'
  3. import { fetchPendingTasks } from '@/api/workflow'
  4. import {
  5. MENU_BADGE_CODES,
  6. MENU_BADGE_CODE_SET,
  7. PL_MENU_BADGE_CODES,
  8. type MenuBadgeCode
  9. } from '@/constants/menu-badge-codes'
  10. import { useMenuBadgeStore } from '@/store/modules/menu-badge'
  11. import { useMenuStore } from '@/store/modules/menu'
  12. import type { AppRouteRecord } from '@/types/router'
  13. function unwrapData<T = any>(res: unknown): T {
  14. return ((res as any)?.data ?? res) as T
  15. }
  16. /** 从侧栏菜单树收集 menuCode */
  17. export function collectMenuCodes(items: AppRouteRecord[]): Set<string> {
  18. const codes = new Set<string>()
  19. const walk = (list: AppRouteRecord[]) => {
  20. for (const item of list) {
  21. const code = String(item.meta?.menuCode || '').trim()
  22. if (code) codes.add(code)
  23. if (item.children?.length) walk(item.children)
  24. }
  25. }
  26. walk(items || [])
  27. return codes
  28. }
  29. /** 解析菜单项是否绑定数字角标 */
  30. export function resolveMenuBadgeCode(item: AppRouteRecord): MenuBadgeCode | undefined {
  31. const code = String(item.meta?.menuCode || '').trim()
  32. if (code && MENU_BADGE_CODE_SET.has(code)) {
  33. return code as MenuBadgeCode
  34. }
  35. const name = String(item.name || '')
  36. if (name === 'PlDocumentReminder') return MENU_BADGE_CODES.PL_DOCUMENT_REMINDER
  37. const path = String(item.path || '')
  38. if (path.endsWith('/document-reminder') || path === 'document-reminder') {
  39. return MENU_BADGE_CODES.PL_DOCUMENT_REMINDER
  40. }
  41. return undefined
  42. }
  43. export function resolveMenuBadgeText(item: AppRouteRecord, menuOpen = false): string {
  44. const badgeStore = useMenuBadgeStore()
  45. if (!badgeStore.loaded) return ''
  46. const menuCode = resolveMenuBadgeCode(item)
  47. if (menuCode) {
  48. return badgeStore.getBadgeText(menuCode)
  49. }
  50. const staticBadge = item.meta?.showTextBadge
  51. if (staticBadge && menuOpen) {
  52. return String(staticBadge)
  53. }
  54. return ''
  55. }
  56. function pickPlCodes(codes: Set<string>): string[] {
  57. return PL_MENU_BADGE_CODES.filter((code) => codes.has(code))
  58. }
  59. async function refreshPlMenuBadges(codes: Set<string>) {
  60. const badgeStore = useMenuBadgeStore()
  61. const plCodes = pickPlCodes(codes)
  62. if (!plCodes.length) {
  63. badgeStore.clearCounts([...PL_MENU_BADGE_CODES])
  64. return
  65. }
  66. try {
  67. const res = await menuBadgeApi.stats(plCodes)
  68. const data = unwrapData<{ counts?: Record<string, number> }>(res)
  69. badgeStore.setCounts(data?.counts || {})
  70. } catch {
  71. const zero: Record<string, number> = {}
  72. plCodes.forEach((code) => {
  73. zero[code] = 0
  74. })
  75. badgeStore.setCounts(zero)
  76. }
  77. }
  78. async function refreshBxApproveBadges(codes: Set<string>) {
  79. const badgeStore = useMenuBadgeStore()
  80. const needAdvance =
  81. codes.has(MENU_BADGE_CODES.BX_ADVANCE_APPROVE) || codes.has(MENU_BADGE_CODES.BX_VIEW_TODO)
  82. const needReimburse =
  83. codes.has(MENU_BADGE_CODES.BX_REIMBURSE_APPROVE) || codes.has(MENU_BADGE_CODES.BX_VIEW_TODO)
  84. let advancePending = 0
  85. let reimbursePending = 0
  86. const tasks: Promise<void>[] = []
  87. if (needAdvance) {
  88. tasks.push(
  89. advanceApi.pendingApprovalStats().then((res) => {
  90. const d = unwrapData(res)
  91. advancePending = Number(d?.pendingTotal ?? 0)
  92. if (codes.has(MENU_BADGE_CODES.BX_ADVANCE_APPROVE)) {
  93. badgeStore.setCount(MENU_BADGE_CODES.BX_ADVANCE_APPROVE, advancePending)
  94. }
  95. })
  96. )
  97. } else {
  98. badgeStore.clearCounts([MENU_BADGE_CODES.BX_ADVANCE_APPROVE])
  99. }
  100. if (needReimburse) {
  101. tasks.push(
  102. reimburseApi.pendingApprovalStats().then((res) => {
  103. const d = unwrapData(res)
  104. reimbursePending = Number(d?.pendingTotal ?? 0)
  105. if (codes.has(MENU_BADGE_CODES.BX_REIMBURSE_APPROVE)) {
  106. badgeStore.setCount(MENU_BADGE_CODES.BX_REIMBURSE_APPROVE, reimbursePending)
  107. }
  108. })
  109. )
  110. } else {
  111. badgeStore.clearCounts([MENU_BADGE_CODES.BX_REIMBURSE_APPROVE])
  112. }
  113. await Promise.allSettled(tasks)
  114. if (codes.has(MENU_BADGE_CODES.BX_VIEW_TODO)) {
  115. badgeStore.setCount(MENU_BADGE_CODES.BX_VIEW_TODO, advancePending + reimbursePending)
  116. } else {
  117. badgeStore.clearCounts([MENU_BADGE_CODES.BX_VIEW_TODO])
  118. }
  119. }
  120. async function refreshBxFinanceBadges(codes: Set<string>) {
  121. const badgeStore = useMenuBadgeStore()
  122. const tasks: Promise<void>[] = []
  123. if (codes.has(MENU_BADGE_CODES.BX_ADVANCE_ISSUE)) {
  124. tasks.push(
  125. paymentApi.advanceIssueStats().then((res) => {
  126. const d = unwrapData(res)
  127. badgeStore.setCount(MENU_BADGE_CODES.BX_ADVANCE_ISSUE, Number(d?.pendingCount ?? 0))
  128. })
  129. )
  130. } else {
  131. badgeStore.clearCounts([MENU_BADGE_CODES.BX_ADVANCE_ISSUE])
  132. }
  133. if (codes.has(MENU_BADGE_CODES.BX_PAYMENT)) {
  134. tasks.push(
  135. paymentApi.financeStats().then((res) => {
  136. const d = unwrapData(res)
  137. badgeStore.setCount(MENU_BADGE_CODES.BX_PAYMENT, Number(d?.pendingCount ?? 0))
  138. })
  139. )
  140. } else {
  141. badgeStore.clearCounts([MENU_BADGE_CODES.BX_PAYMENT])
  142. }
  143. await Promise.allSettled(tasks)
  144. }
  145. async function refreshWorkflowPendingBadge(codes: Set<string>) {
  146. const badgeStore = useMenuBadgeStore()
  147. if (!codes.has(MENU_BADGE_CODES.WORKFLOW_PENDING)) {
  148. badgeStore.clearCounts([MENU_BADGE_CODES.WORKFLOW_PENDING])
  149. return
  150. }
  151. try {
  152. const res = await fetchPendingTasks({ page: 1, pageSize: 1 })
  153. const data = unwrapData<{ total?: number }>(res)
  154. badgeStore.setCount(MENU_BADGE_CODES.WORKFLOW_PENDING, Number(data?.total ?? 0))
  155. } catch {
  156. badgeStore.setCount(MENU_BADGE_CODES.WORKFLOW_PENDING, 0)
  157. }
  158. }
  159. /** 按当前用户可见菜单刷新全部数字角标 */
  160. export async function refreshMenuBadges(menuList?: AppRouteRecord[]) {
  161. const menuStore = useMenuStore()
  162. const badgeStore = useMenuBadgeStore()
  163. const codes = collectMenuCodes(menuList ?? menuStore.menuList)
  164. if (!codes.size) {
  165. badgeStore.clearCounts()
  166. return
  167. }
  168. await Promise.allSettled([
  169. refreshPlMenuBadges(codes),
  170. refreshBxApproveBadges(codes),
  171. refreshBxFinanceBadges(codes),
  172. refreshWorkflowPendingBadge(codes)
  173. ])
  174. badgeStore.markLoaded()
  175. }
  176. /** @deprecated 请使用 refreshMenuBadges */
  177. export async function refreshPlDocumentMenuBadges() {
  178. await refreshMenuBadges()
  179. }