import { ref, computed } from 'vue' import { getUnreadCount, listRecentNotification, markRead, markAllRead, } from '@/api/app/notification' import type { Notification, UnreadCountResult, NotificationType } from '@/api/app/notification/model' import { message } from 'ant-design-vue' /** 轮询间隔:30 秒 */ const POLL_INTERVAL = 30_000 /** 全局未读数(跨组件共享) */ const unreadTotal = ref(0) const unreadByType = ref>>({}) const recentList = ref([]) const loading = ref(false) /** 轮询定时器 */ let pollTimer: ReturnType | null = null /** 防止重复轮询的计数器 */ let pollRefCount = 0 /** 通知类型 → 显示名称 & 图标 */ export const notificationTypeMap: Record = { ticket: { label: '工单通知', icon: '🎫', color: '#1890ff' }, review: { label: '审核通知', icon: '✅', color: '#52c41a' }, system: { label: '系统通知', icon: '📢', color: '#faad14' }, resource: { label: '资源通知', icon: '🖥️', color: '#722ed1' }, permission: { label: '权限通知', icon: '🔐', color: '#eb2f96' }, member: { label: '成员通知', icon: '👥', color: '#13c2c2' }, payment: { label: '账单通知', icon: '💳', color: '#fa8c16' }, } /** * 通知中心 composable(全局共享状态) */ export function useNotificationCenter() { // ---------- 加载未读数 ---------- async function fetchUnreadCount() { try { const data = await getUnreadCount() unreadTotal.value = data.total ?? 0 unreadByType.value = { ...data } as Partial> ;(unreadByType.value as Record).total = undefined // 去掉 total } catch { // 静默失败,不打扰用户 } } // ---------- 加载最近通知 ---------- async function fetchRecentNotifications(limit = 10) { loading.value = true try { recentList.value = await listRecentNotification({ limit }) } catch { // 静默失败 } finally { loading.value = false } } // ---------- 标记单条已读 ---------- async function markNotificationRead(id: number) { try { await markRead(id) // 乐观更新 const item = recentList.value.find((n) => n.id === id) if (item) item.isRead = 1 if (unreadTotal.value > 0) unreadTotal.value-- } catch { message.error('标记失败,请重试') } } // ---------- 全部已读 ---------- async function markAllAsRead(type?: string) { try { await markAllRead(type ? { type } : undefined) recentList.value.forEach((n) => { if (!type || n.type === type) n.isRead = 1 }) if (type) { unreadTotal.value -= (unreadByType.value[type as NotificationType] ?? 0) unreadByType.value[type as NotificationType] = 0 } else { unreadTotal.value = 0 Object.keys(unreadByType.value).forEach((k) => { unreadByType.value[k as NotificationType] = 0 }) } if (unreadTotal.value < 0) unreadTotal.value = 0 message.success('已全部标记为已读') } catch { message.error('操作失败,请重试') } } // ---------- 启动 / 停止轮询 ---------- function startPolling() { pollRefCount++ if (pollTimer) return // 立即拉取一次 fetchUnreadCount() pollTimer = setInterval(fetchUnreadCount, POLL_INTERVAL) } function stopPolling() { pollRefCount = Math.max(0, pollRefCount - 1) if (pollRefCount === 0 && pollTimer) { clearInterval(pollTimer) pollTimer = null } } // ---------- 时间格式化 ---------- function formatTime(time?: string): string { if (!time) return '' const date = new Date(time) const now = new Date() const diffMs = now.getTime() - date.getTime() const diffSec = Math.floor(diffMs / 1000) const diffMin = Math.floor(diffSec / 60) const diffHour = Math.floor(diffMin / 60) const diffDay = Math.floor(diffHour / 24) if (diffSec < 60) return '刚刚' if (diffMin < 60) return `${diffMin} 分钟前` if (diffHour < 24) return `${diffHour} 小时前` if (diffDay < 7) return `${diffDay} 天前` return `${date.getMonth() + 1}/${date.getDate()} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}` } // ---------- 获取通知跳转链接 ---------- function getNotificationLink(n: Notification): string { if (n.linkUrl) return n.linkUrl switch (n.refType) { case 'ticket': return `/console/tickets` case 'permission_request': return `/developer/requests` default: return '/console/notifications' } } return { // 状态 unreadTotal, unreadByType, recentList, loading, // 方法 fetchUnreadCount, fetchRecentNotifications, markNotificationRead, markAllAsRead, startPolling, stopPolling, // 工具 formatTime, getNotificationLink, } }