ec-admin/src/components/ty-notice.vue
liuyun 64bbb0a987 feat(notice): 顶栏通知铃铛 + 通知管理页
- ty-notice 铃铛(未读角标 + 列表 + 已读)+ notice store 轮询
- 通知管理页(ty-crud)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-11 22:25:22 +08:00

129 lines
3.4 KiB
Vue

<template>
<el-popover placement="bottom" :width="340" trigger="click" @show="onOpen">
<template #reference>
<el-badge :value="unreadCount" :hidden="unreadCount === 0" :max="99" class="ty-notice__badge">
<button class="ty-notice__btn" type="button" title="通知">
<icon type="iconify" icon="mdi:bell-outline" />
</button>
</el-badge>
</template>
<div class="ty-notice">
<div class="ty-notice__head">
<span class="ty-notice__head-title">通知</span>
<el-button v-if="unreadCount > 0" link type="primary" @click="onReadAll">全部已读</el-button>
</div>
<el-scrollbar max-height="320px">
<div v-if="list.length === 0" class="ty-notice__empty">暂无通知</div>
<div
v-for="n in list"
:key="n.id"
class="ty-notice__item"
:class="{ 'is-unread': n.readAt == null }"
@click="onRead(n)"
>
<div class="ty-notice__item-title">
<span v-if="n.readAt == null" class="ty-notice__dot" />
{{ n.title }}
</div>
<div class="ty-notice__item-content">{{ n.content }}</div>
<div class="ty-notice__item-time">{{ formatTime(n.createdAt) }}</div>
</div>
</el-scrollbar>
</div>
</el-popover>
</template>
<script setup lang="ts">
import { computed, onMounted, onUnmounted } from 'vue'
import { useNoticeStore } from '@/stores/modules/notice'
import type { MyNotice } from '@/api/notice'
defineOptions({ name: 'TyNotice' })
const noticeStore = useNoticeStore()
const unreadCount = computed(() => noticeStore.unreadCount)
const list = computed(() => noticeStore.list)
function formatTime(v: number): string {
const d = new Date(v)
const pad = (x: number) => String(x).padStart(2, '0')
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`
}
function onOpen(): void {
void noticeStore.fetchList()
}
async function onRead(n: MyNotice): Promise<void> {
if (n.readAt == null) await noticeStore.readOne(n.id)
}
async function onReadAll(): Promise<void> {
await noticeStore.readAll()
}
onMounted(() => {
void noticeStore.fetchUnread()
noticeStore.startPolling()
})
onUnmounted(() => {
noticeStore.stopPolling()
})
</script>
<style scoped lang="scss">
.ty-notice__badge {
@apply inline-flex items-center;
}
.ty-notice__btn {
@apply inline-flex items-center justify-center w-[34px] h-[34px] border-none rounded-base bg-transparent text-[18px] text-text-regular cursor-pointer transition-colors duration-200;
&:hover {
@apply bg-bg-page;
color: var(--el-color-primary);
}
}
.ty-notice__head {
@apply flex items-center justify-between px-3 py-2 border-b border-solid;
border-color: var(--wm-border-color-light);
}
.ty-notice__head-title {
@apply font-semibold text-text-primary;
}
.ty-notice__empty {
@apply py-8 text-center text-sm text-text-secondary;
}
.ty-notice__item {
@apply px-3 py-2.5 cursor-pointer transition-colors duration-150;
&:hover {
@apply bg-bg-page;
}
}
.ty-notice__item-title {
@apply flex items-center gap-1.5 text-sm text-text-primary;
}
.ty-notice__item-content {
@apply mt-1 truncate text-xs text-text-secondary;
}
.ty-notice__item-time {
@apply mt-1 text-xs;
color: var(--wm-text-color-placeholder, #909399);
}
.ty-notice__dot {
@apply inline-block w-2 h-2 shrink-0 rounded-full;
background: var(--el-color-danger);
}
</style>