feat(web): notification bell with polling and dropdown

This commit is contained in:
2026-04-29 10:33:53 +08:00
parent b7b9532a5a
commit 04a19dbcef
2 changed files with 99 additions and 2 deletions
+68 -2
View File
@@ -1,7 +1,73 @@
<template> <template>
<span class="text-sm text-gray-400">🔔</span> <NPopover
trigger="click"
placement="bottom-end"
@update:show="onOpen"
>
<template #trigger>
<NBadge
:value="store.unreadCount"
:max="99"
:show="store.unreadCount > 0"
>
<NButton text>
<span class="text-lg">🔔</span>
</NButton>
</NBadge>
</template>
<div class="w-80 max-h-96 overflow-auto">
<div
v-if="store.items.length === 0"
class="p-4 text-gray-500 text-sm"
>
暂无通知
</div>
<div
v-for="n in store.items"
:key="n.id"
class="p-3 border-b border-gray-100 dark:border-gray-700 last:border-0"
>
<div class="text-sm font-medium">
{{ n.title }}
</div>
<div class="text-xs text-gray-500 mt-1">
{{ n.body }}
</div>
</div>
<div
v-if="store.unreadCount > 0"
class="p-2 border-t border-gray-100 dark:border-gray-700"
>
<NButton
size="small"
text
@click="onMarkAll"
>
全部标记为已读
</NButton>
</div>
</div>
</NPopover>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
// I6 占位:真实未读轮询 + 弹层在 I6 实现。 import { NPopover, NBadge, NButton } from 'naive-ui'
import { notificationsApi } from '@/api/notifications'
import { useNotificationsStore } from '@/stores/notifications'
import { useUnreadPoller } from '@/composables/useNotifications'
const store = useNotificationsStore()
useUnreadPoller()
async function onOpen(open: boolean) {
if (!open) return
const { items } = await notificationsApi.list(false, 50)
store.setItems(items)
}
async function onMarkAll() {
await notificationsApi.markAllRead()
store.setUnreadCount(0)
store.setItems(store.items.map((i) => ({ ...i, read_at: new Date().toISOString() })))
}
</script> </script>
+31
View File
@@ -0,0 +1,31 @@
import { onMounted, onUnmounted, ref } from 'vue'
import { notificationsApi } from '@/api/notifications'
import { useNotificationsStore } from '@/stores/notifications'
const POLL_INTERVAL_MS = 30_000
export function useUnreadPoller() {
const store = useNotificationsStore()
let timer: ReturnType<typeof setInterval> | null = null
const error = ref<string | null>(null)
async function refresh() {
try {
const { count } = await notificationsApi.unreadCount()
store.setUnreadCount(count)
} catch (e) {
error.value = String(e)
}
}
onMounted(() => {
refresh()
timer = setInterval(refresh, POLL_INTERVAL_MS)
})
onUnmounted(() => {
if (timer) clearInterval(timer)
})
return { refresh, error }
}