515 lines
12 KiB
Vue
515 lines
12 KiB
Vue
<template>
|
|
<div class="notification-center">
|
|
<!-- 通知按钮 -->
|
|
<el-popover
|
|
:visible="visible"
|
|
:width="360"
|
|
trigger="manual"
|
|
placement="bottom-end"
|
|
popper-class="notification-popover"
|
|
@hide="handleHide"
|
|
>
|
|
<template #reference>
|
|
<div class="notification-trigger" @click="toggleNotifications">
|
|
<el-badge :value="unreadCount" :hidden="unreadCount === 0" :max="99">
|
|
<el-button circle>
|
|
<el-icon :size="18">
|
|
<Bell />
|
|
</el-icon>
|
|
</el-button>
|
|
</el-badge>
|
|
</div>
|
|
</template>
|
|
|
|
<div class="notification-content">
|
|
<!-- 头部 -->
|
|
<div class="notification-header">
|
|
<div class="flex items-center justify-between">
|
|
<h3 class="text-lg font-semibold text-gray-900">通知</h3>
|
|
<div class="flex items-center space-x-2">
|
|
<el-button
|
|
v-if="unreadCount > 0"
|
|
size="small"
|
|
text
|
|
@click="markAllAsRead"
|
|
>
|
|
全部已读
|
|
</el-button>
|
|
<el-button size="small" text @click="clearAll">
|
|
清空
|
|
</el-button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 筛选标签 -->
|
|
<div class="notification-filters">
|
|
<div class="filter-tabs">
|
|
<div
|
|
v-for="filter in filters"
|
|
:key="filter.key"
|
|
class="filter-tab"
|
|
:class="{ active: activeFilter === filter.key }"
|
|
@click="switchFilter(filter.key)"
|
|
>
|
|
{{ filter.label }}
|
|
<span v-if="filter.count > 0" class="filter-count">{{ filter.count }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 通知列表 -->
|
|
<div class="notification-list">
|
|
<div v-if="filteredNotifications.length === 0" class="empty-state">
|
|
<div class="empty-icon">
|
|
<el-icon size="32" class="text-gray-400">
|
|
<Bell />
|
|
</el-icon>
|
|
</div>
|
|
<p class="empty-text">暂无通知</p>
|
|
</div>
|
|
|
|
<div v-else class="notification-items">
|
|
<div
|
|
v-for="notification in filteredNotifications"
|
|
:key="notification.id"
|
|
class="notification-item"
|
|
:class="{ unread: !notification.read }"
|
|
@click="handleNotificationClick(notification)"
|
|
>
|
|
<div class="notification-icon">
|
|
<div
|
|
class="icon-wrapper"
|
|
:class="getNotificationIconClass(notification.type)"
|
|
>
|
|
<el-icon>
|
|
<component :is="getNotificationIcon(notification.type)" />
|
|
</el-icon>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="notification-content">
|
|
<div class="notification-title">{{ notification.title }}</div>
|
|
<div class="notification-message">{{ notification.message }}</div>
|
|
<div class="notification-time">{{ formatTime(notification.createTime) }}</div>
|
|
</div>
|
|
|
|
<div class="notification-actions">
|
|
<el-dropdown @command="handleAction">
|
|
<el-button circle size="small" text>
|
|
<el-icon><MoreFilled /></el-icon>
|
|
</el-button>
|
|
<template #dropdown>
|
|
<el-dropdown-menu>
|
|
<el-dropdown-item
|
|
v-if="!notification.read"
|
|
:command="`read_${notification.id}`"
|
|
>
|
|
标记已读
|
|
</el-dropdown-item>
|
|
<el-dropdown-item
|
|
v-else
|
|
:command="`unread_${notification.id}`"
|
|
>
|
|
标记未读
|
|
</el-dropdown-item>
|
|
<el-dropdown-item
|
|
:command="`delete_${notification.id}`"
|
|
divided
|
|
>
|
|
删除
|
|
</el-dropdown-item>
|
|
</el-dropdown-menu>
|
|
</template>
|
|
</el-dropdown>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 底部 -->
|
|
<div v-if="hasMore" class="notification-footer">
|
|
<el-button text @click="loadMore" :loading="loading">
|
|
加载更多
|
|
</el-button>
|
|
</div>
|
|
</div>
|
|
</el-popover>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
|
import { ElMessage } from 'element-plus'
|
|
import {
|
|
Bell,
|
|
MoreFilled,
|
|
ChatDotRound,
|
|
User,
|
|
Setting,
|
|
Warning,
|
|
InfoFilled
|
|
} from '@element-plus/icons-vue'
|
|
import { formatRelativeTime } from '@/utils/format'
|
|
import { useNotificationStore } from '@/stores/notification'
|
|
|
|
interface NotificationItem {
|
|
id: string
|
|
type: 'message' | 'system' | 'user' | 'warning' | 'info'
|
|
title: string
|
|
message: string
|
|
read: boolean
|
|
createTime: number
|
|
data?: any
|
|
}
|
|
|
|
// 状态管理
|
|
const notificationStore = useNotificationStore()
|
|
|
|
// 响应式数据
|
|
const visible = ref(false)
|
|
const loading = ref(false)
|
|
const activeFilter = ref('all')
|
|
|
|
// 模拟通知数据
|
|
const notifications = ref<NotificationItem[]>([
|
|
{
|
|
id: '1',
|
|
type: 'message',
|
|
title: 'AI助手回复',
|
|
message: '您的问题已经得到回复,请查看',
|
|
read: false,
|
|
createTime: Date.now() - 1000 * 60 * 5
|
|
},
|
|
{
|
|
id: '2',
|
|
type: 'system',
|
|
title: '系统更新',
|
|
message: '系统已更新到最新版本 v1.2.0',
|
|
read: false,
|
|
createTime: Date.now() - 1000 * 60 * 60
|
|
},
|
|
{
|
|
id: '3',
|
|
type: 'user',
|
|
title: '资料完善提醒',
|
|
message: '完善个人资料可以获得更好的服务体验',
|
|
read: true,
|
|
createTime: Date.now() - 1000 * 60 * 60 * 24
|
|
}
|
|
])
|
|
|
|
const hasMore = ref(true)
|
|
|
|
// 筛选选项
|
|
const filters = computed(() => [
|
|
{ key: 'all', label: '全部', count: notifications.value.length },
|
|
{ key: 'unread', label: '未读', count: unreadCount.value },
|
|
{ key: 'message', label: '消息', count: notifications.value.filter(n => n.type === 'message').length },
|
|
{ key: 'system', label: '系统', count: notifications.value.filter(n => n.type === 'system').length }
|
|
])
|
|
|
|
// 计算属性
|
|
const unreadCount = computed(() => {
|
|
return notifications.value.filter(n => !n.read).length
|
|
})
|
|
|
|
const filteredNotifications = computed(() => {
|
|
let filtered = notifications.value
|
|
|
|
switch (activeFilter.value) {
|
|
case 'unread':
|
|
filtered = filtered.filter(n => !n.read)
|
|
break
|
|
case 'message':
|
|
filtered = filtered.filter(n => n.type === 'message')
|
|
break
|
|
case 'system':
|
|
filtered = filtered.filter(n => n.type === 'system')
|
|
break
|
|
case 'user':
|
|
filtered = filtered.filter(n => n.type === 'user')
|
|
break
|
|
}
|
|
|
|
return filtered.sort((a, b) => b.createTime - a.createTime)
|
|
})
|
|
|
|
// 方法
|
|
const toggleNotifications = () => {
|
|
visible.value = !visible.value
|
|
}
|
|
|
|
const handleHide = () => {
|
|
visible.value = false
|
|
}
|
|
|
|
const switchFilter = (filterKey: string) => {
|
|
activeFilter.value = filterKey
|
|
}
|
|
|
|
const handleNotificationClick = (notification: NotificationItem) => {
|
|
// 标记为已读
|
|
if (!notification.read) {
|
|
markAsRead(notification.id)
|
|
}
|
|
|
|
// 处理点击事件
|
|
switch (notification.type) {
|
|
case 'message':
|
|
// 跳转到聊天页面
|
|
break
|
|
case 'system':
|
|
// 显示系统消息详情
|
|
break
|
|
case 'user':
|
|
// 跳转到个人资料页面
|
|
break
|
|
}
|
|
|
|
visible.value = false
|
|
}
|
|
|
|
const markAsRead = (notificationId: string) => {
|
|
const notification = notifications.value.find(n => n.id === notificationId)
|
|
if (notification) {
|
|
notification.read = true
|
|
}
|
|
}
|
|
|
|
const markAsUnread = (notificationId: string) => {
|
|
const notification = notifications.value.find(n => n.id === notificationId)
|
|
if (notification) {
|
|
notification.read = false
|
|
}
|
|
}
|
|
|
|
const markAllAsRead = () => {
|
|
notifications.value.forEach(n => {
|
|
n.read = true
|
|
})
|
|
ElMessage.success('已标记全部为已读')
|
|
}
|
|
|
|
const deleteNotification = (notificationId: string) => {
|
|
const index = notifications.value.findIndex(n => n.id === notificationId)
|
|
if (index > -1) {
|
|
notifications.value.splice(index, 1)
|
|
ElMessage.success('通知已删除')
|
|
}
|
|
}
|
|
|
|
const clearAll = () => {
|
|
notifications.value = []
|
|
ElMessage.success('已清空所有通知')
|
|
}
|
|
|
|
const loadMore = () => {
|
|
loading.value = true
|
|
|
|
// 模拟加载更多
|
|
setTimeout(() => {
|
|
loading.value = false
|
|
hasMore.value = false
|
|
ElMessage.info('没有更多通知了')
|
|
}, 1000)
|
|
}
|
|
|
|
const handleAction = (command: string) => {
|
|
const [action, notificationId] = command.split('_')
|
|
|
|
switch (action) {
|
|
case 'read':
|
|
markAsRead(notificationId)
|
|
break
|
|
case 'unread':
|
|
markAsUnread(notificationId)
|
|
break
|
|
case 'delete':
|
|
deleteNotification(notificationId)
|
|
break
|
|
}
|
|
}
|
|
|
|
const getNotificationIcon = (type: string) => {
|
|
switch (type) {
|
|
case 'message':
|
|
return ChatDotRound
|
|
case 'system':
|
|
return Setting
|
|
case 'user':
|
|
return User
|
|
case 'warning':
|
|
return Warning
|
|
case 'info':
|
|
return InfoFilled
|
|
default:
|
|
return Bell
|
|
}
|
|
}
|
|
|
|
const getNotificationIconClass = (type: string) => {
|
|
switch (type) {
|
|
case 'message':
|
|
return 'text-blue-500 bg-blue-100'
|
|
case 'system':
|
|
return 'text-green-500 bg-green-100'
|
|
case 'user':
|
|
return 'text-purple-500 bg-purple-100'
|
|
case 'warning':
|
|
return 'text-orange-500 bg-orange-100'
|
|
case 'info':
|
|
return 'text-gray-500 bg-gray-100'
|
|
default:
|
|
return 'text-gray-500 bg-gray-100'
|
|
}
|
|
}
|
|
|
|
const formatTime = (timestamp: number) => {
|
|
return formatRelativeTime(timestamp)
|
|
}
|
|
|
|
// 点击外部关闭
|
|
const handleClickOutside = (event: MouseEvent) => {
|
|
const target = event.target as HTMLElement
|
|
if (!target.closest('.notification-center') && !target.closest('.notification-popover')) {
|
|
visible.value = false
|
|
}
|
|
}
|
|
|
|
// 生命周期
|
|
onMounted(() => {
|
|
document.addEventListener('click', handleClickOutside)
|
|
})
|
|
|
|
onUnmounted(() => {
|
|
document.removeEventListener('click', handleClickOutside)
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.notification-trigger {
|
|
@apply cursor-pointer;
|
|
}
|
|
|
|
.notification-content {
|
|
@apply w-full;
|
|
}
|
|
|
|
.notification-header {
|
|
@apply pb-3 border-b border-gray-200 mb-3;
|
|
}
|
|
|
|
.notification-filters {
|
|
@apply mb-3;
|
|
}
|
|
|
|
.filter-tabs {
|
|
@apply flex space-x-1;
|
|
}
|
|
|
|
.filter-tab {
|
|
@apply px-3 py-1 text-sm rounded-full cursor-pointer transition-colors flex items-center space-x-1;
|
|
@apply text-gray-600 hover:bg-gray-100;
|
|
}
|
|
|
|
.filter-tab.active {
|
|
@apply bg-blue-100 text-blue-600;
|
|
}
|
|
|
|
.filter-count {
|
|
@apply bg-current text-white rounded-full px-1.5 py-0.5 text-xs min-w-[1.25rem] text-center;
|
|
}
|
|
|
|
.notification-list {
|
|
@apply max-h-96 overflow-y-auto;
|
|
}
|
|
|
|
.empty-state {
|
|
@apply text-center py-8;
|
|
}
|
|
|
|
.empty-icon {
|
|
@apply mb-3;
|
|
}
|
|
|
|
.empty-text {
|
|
@apply text-gray-500 text-sm;
|
|
}
|
|
|
|
.notification-items {
|
|
@apply space-y-1;
|
|
}
|
|
|
|
.notification-item {
|
|
@apply flex items-start space-x-3 p-3 rounded-lg cursor-pointer transition-colors;
|
|
@apply hover:bg-gray-50;
|
|
}
|
|
|
|
.notification-item.unread {
|
|
@apply bg-blue-50 border-l-4 border-blue-500;
|
|
}
|
|
|
|
.notification-icon {
|
|
@apply flex-shrink-0;
|
|
}
|
|
|
|
.icon-wrapper {
|
|
@apply w-8 h-8 rounded-full flex items-center justify-center;
|
|
}
|
|
|
|
.notification-content {
|
|
@apply flex-1 min-w-0;
|
|
}
|
|
|
|
.notification-title {
|
|
@apply text-sm font-medium text-gray-900 mb-1;
|
|
}
|
|
|
|
.notification-message {
|
|
@apply text-sm text-gray-600 mb-1 line-clamp-2;
|
|
}
|
|
|
|
.notification-time {
|
|
@apply text-xs text-gray-500;
|
|
}
|
|
|
|
.notification-actions {
|
|
@apply flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity;
|
|
}
|
|
|
|
.notification-item:hover .notification-actions {
|
|
@apply opacity-100;
|
|
}
|
|
|
|
.notification-footer {
|
|
@apply text-center pt-3 border-t border-gray-200 mt-3;
|
|
}
|
|
|
|
.line-clamp-2 {
|
|
display: -webkit-box;
|
|
-webkit-line-clamp: 2;
|
|
-webkit-box-orient: vertical;
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* 自定义滚动条 */
|
|
.notification-list::-webkit-scrollbar {
|
|
@apply w-1;
|
|
}
|
|
|
|
.notification-list::-webkit-scrollbar-track {
|
|
@apply bg-gray-100 rounded;
|
|
}
|
|
|
|
.notification-list::-webkit-scrollbar-thumb {
|
|
@apply bg-gray-300 rounded hover:bg-gray-400;
|
|
}
|
|
</style>
|
|
|
|
<style>
|
|
.notification-popover {
|
|
padding: 16px !important;
|
|
}
|
|
</style>
|