前言一个完善的通知系统可以显著提升用户体验让用户及时了解新评论回复文章被点赞系统公告签到奖励今天分享如何实现一个优雅的通知中心功能设计通知类型// src/types/notification.tsexporttypeNotificationType|comment// 评论通知|reply// 回复通知|like// 点赞通知|follow// 关注通知|system// 系统通知|achievement// 成就通知exportinterfaceNotification{id:stringtype:NotificationType title:stringcontent:stringavatar?:stringlink?:stringread:booleancreateTime:number}核心实现1. 通知服务// src/services/notification.tsimport{defineStore}frompiniaimport{ref,computed}fromvueimporttype{Notification,NotificationType}from/types/notificationexportconstuseNotificationStoredefineStore(notification,(){constnotificationsrefNotification[]([])// 加载通知functionloadNotifications(){constdatalocalStorage.getItem(blog_notifications)if(data){notifications.valueJSON.parse(data)}}// 保存通知functionsaveNotifications(){localStorage.setItem(blog_notifications,JSON.stringify(notifications.value))}// 添加通知functionaddNotification(notification:OmitNotification,id|read|createTime){constnewNotification:Notification{...notification,id:notif_${Date.now()}_${Math.random().toString(36).slice(2)},read:false,createTime:Date.now()}notifications.value.unshift(newNotification)saveNotifications()// 触发浏览器通知if(Notification.permissiongranted){newNotification(newNotification.title,{body:newNotification.content,icon:newNotification.avatar})}returnnewNotification}// 标记已读functionmarkAsRead(id:string){constnotificationnotifications.value.find(nn.idid)if(notification){notification.readtruesaveNotifications()}}// 全部已读functionmarkAllAsRead(){notifications.value.forEach(n{n.readtrue})saveNotifications()}// 删除通知functiondeleteNotification(id:string){constindexnotifications.value.findIndex(nn.idid)if(index-1){notifications.value.splice(index,1)saveNotifications()}}// 未读数量constunreadCountcomputed((){returnnotifications.value.filter(n!n.read).length})// 按类型分组constgroupedNotificationscomputed((){constgroups:RecordNotificationType,Notification[]{comment:[],reply:[],like:[],follow:[],system:[],achievement:[]}notifications.value.forEach(n{groups[n.type].push(n)})returngroups})// 请求通知权限asyncfunctionrequestPermission(){if(Notificationinwindow){constpermissionawaitNotification.requestPermission()returnpermissiongranted}returnfalse}loadNotifications()return{notifications,unreadCount,groupedNotifications,addNotification,markAsRead,markAllAsRead,deleteNotification,requestPermission}})2. 通知中心组件!-- src/components/notification/NotificationCenter.vue -- template el-popover v-model:visiblevisible placementbottom-end :width360 triggerclick template #reference div classnotification-trigger el-badge :valueunreadCount :hiddenunreadCount 0 :max99 el-button :iconBell circle / /el-badge !-- 红点提醒 -- span v-ifhasNewNotification classnew-dot / /div /template template #default div classnotification-center !-- 头部 -- div classheader h3通知中心/h3 el-button v-ifunreadCount 0 text sizesmall clickhandleMarkAllRead 全部已读 /el-button /div !-- 标签页 -- el-tabs v-modelactiveTab classnotification-tabs el-tab-pane label全部 nameall / el-tab-pane label评论 namecomment / el-tab-pane label点赞 namelike / el-tab-pane label系统 namesystem / /el-tabs !-- 通知列表 -- div classnotification-list div v-fornotification in filteredNotifications :keynotification.id classnotification-item :class{ unread: !notification.read } clickhandleClick(notification) el-avatar :srcnotification.avatar || defaultAvatar :size40 / div classcontent div classtitle{{ notification.title }}/div div classmessage{{ notification.content }}/div div classtime{{ formatTime(notification.createTime) }}/div /div div classactions el-button v-if!notification.read text sizesmall click.stophandleMarkRead(notification.id) 标记已读 /el-button el-button text sizesmall click.stophandleDelete(notification.id) 删除 /el-button /div /div el-empty v-iffilteredNotifications.length 0 description暂无通知 / /div /div /template /el-popover /template script setup langts import { ref, computed, watch, onMounted } from vue import { Bell } from element-plus/icons-vue import { useNotificationStore } from /services/notification import type { Notification } from /types/notification import { ElMessage } from element-plus const notificationStore useNotificationStore() const visible ref(false) const activeTab ref(all) const unreadCount computed(() notificationStore.unreadCount) const hasNewNotification computed(() unreadCount.value 0) const defaultAvatar /default-avatar.png const filteredNotifications computed(() { if (activeTab.value all) { return notificationStore.notifications } return notificationStore.notifications.filter(n n.type activeTab.value) }) function formatTime(timestamp: number) { const date new Date(timestamp) const now new Date() const diff now.getTime() - date.getTime() if (diff 60000) return 刚刚 if (diff 3600000) return ${Math.floor(diff / 60000)}分钟前 if (diff 86400000) return ${Math.floor(diff / 3600000)}小时前 if (diff 604800000) return ${Math.floor(diff / 86400000)}天前 return date.toLocaleDateString() } function handleClick(notification: Notification) { notificationStore.markAsRead(notification.id) if (notification.link) { window.location.href notification.link } visible.value false } function handleMarkRead(id: string) { notificationStore.markAsRead(id) } function handleMarkAllRead() { notificationStore.markAllAsRead() ElMessage.success(已全部标记为已读) } function handleDelete(id: string) { notificationStore.deleteNotification(id) } // 监听新通知 watch(() notificationStore.unreadCount, (newCount, oldCount) { if (newCount oldCount) { // 播放提示音 const audio new Audio(/notification.mp3) audio.play().catch(() {}) } }) onMounted(() { notificationStore.requestPermission() }) /script style scoped .notification-trigger { position: relative; display: inline-block; } .new-dot { position: absolute; top: 0; right: 0; width: 8px; height: 8px; background: #f56c6c; border-radius: 50%; animation: pulse 2s infinite; } keyframes pulse { 0%, 100% { transform: scale(1); opacity: 1; } 50% { transform: scale(1.2); opacity: 0.8; } } .notification-center { margin: -12px; } .header { display: flex; justify-content: space-between; align-items: center; padding: 12px 16px; border-bottom: 1px solid var(--el-border-color); } .header h3 { margin: 0; font-size: 16px; } .notification-tabs { padding: 0 8px; } .notification-list { max-height: 400px; overflow-y: auto; padding: 8px; } .notification-item { display: flex; gap: 12px; padding: 12px; border-radius: 8px; cursor: pointer; transition: background 0.2s; } .notification-item:hover { background: var(--el-fill-color-light); } .notification-item.unread { background: var(--el-color-primary-light-9); } .notification-item.unread::before { content: ; position: absolute; left: 4px; top: 50%; transform: translateY(-50%); width: 6px; height: 6px; background: var(--el-color-primary); border-radius: 50%; } .content { flex: 1; min-width: 0; } .title { font-weight: 600; margin-bottom: 4px; } .message { font-size: 13px; color: var(--el-text-color-secondary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .time { font-size: 12px; color: var(--el-text-color-placeholder); margin-top: 4px; } .actions { display: flex; flex-direction: column; gap: 4px; } /style使用示例!-- 在 Header 中使用 -- template header div classheader-content !-- 其他内容 -- NotificationCenter / /div /header /template script setup langts import NotificationCenter from /components/notification/NotificationCenter.vue import { useNotificationStore } from /services/notification const notificationStore useNotificationStore() // 模拟收到新评论 function simulateNewComment() { notificationStore.addNotification({ type: comment, title: 新评论, content: 用户前端小白评论了你的文章《Vue 3 入门指南》, avatar: https://api.dicebear.com/7.x/avataaars/svg?seeduser1, link: /article/vue3-guide }) } /script浏览器通知// 在需要时请求权限并发送通知asyncfunctionsendBrowserNotification(title:string,options?:NotificationOptions){if(NotificationinwindowNotification.permissiongranted){newNotification(title,{icon:/logo.png,badge:/badge.png,...options})}}进阶功能接入 WebSocket 实现实时推送添加通知免打扰模式支持通知折叠和展开