- ty-notice 铃铛(未读角标 + 列表 + 已读)+ notice store 轮询 - 通知管理页(ty-crud) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
129 lines
3.4 KiB
Vue
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>
|