初始版本
This commit is contained in:
165
app/composables/useNotificationCenter.ts
Normal file
165
app/composables/useNotificationCenter.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user