Files
jczxw-pc/app/composables/useNotificationCenter.ts
2026-04-23 16:30:57 +08:00

166 lines
5.0 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<Partial<Record<NotificationType, number>>>({})
const recentList = ref<Notification[]>([])
const loading = ref(false)
/** 轮询定时器 */
let pollTimer: ReturnType<typeof setInterval> | null = null
/** 防止重复轮询的计数器 */
let pollRefCount = 0
/** 通知类型 → 显示名称 & 图标 */
export const notificationTypeMap: Record<NotificationType, { label: string; icon: string; color: string }> = {
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<Record<NotificationType, number>>
;(unreadByType.value as Record<string, unknown>).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,
}
}