@@ -1,18 +1,22 @@
< script setup lang = "ts" >
import { computed , onBeforeUnmount , onMounted , reactive , ref , watch } from 'vue' ;
import { useDebounceFn , useInfiniteScroll } from '@vueuse/core' ;
import { NOTIFY _MESSAGE _LEVEL _DICT _CODE } from '@/constants/dict' ;
import {
fetchGetMyNotifyMessagePage ,
fetchGetUnreadNotifyCount ,
fetchUpdateAllNotifyMessageRead ,
fetchUpdateNotifyMessageRead
} from '@/service/api' ;
import { formatRelativeTim e } from '@/utils/datetime ' ;
import { useDictStor e } from '@/store/modules/dict ' ;
import { formatDateTime , formatRelativeTime } from '@/utils/datetime' ;
defineOptions ( { name : 'NotificationBell' } ) ;
const dictStore = useDictStore ( ) ;
const PAGE _SIZE = 10 ;
const UNREAD _COUNT _POLL _INTERVAL = 30 * 1000 ;
const UNREAD _COUNT _POLL _INTERVAL = 15 * 1000 ;
type TabKey = 'unread' | 'read' ;
@@ -43,10 +47,18 @@ const drawerOpen = ref(false);
const activeTab = ref < TabKey > ( 'unread' ) ;
const searchKeyword = ref ( '' ) ;
const detailVisible = ref ( false ) ;
const detailMessage = ref < Api .NotifyMessage.NotifyMessage | null > ( null ) ;
function keywordParam ( ) {
return searchKeyword . value . trim ( ) || undefined ;
}
/** 列表圆点颜色:跟随消息等级(与等级徽标同一字典色源);取不到时回 undefined, 由 CSS 兜底 */
function levelDotColor ( level : number ) {
return dictStore . getDictItem ( NOTIFY _MESSAGE _LEVEL _DICT _CODE , level ) ? . colorType ? ? undefined ;
}
async function refreshUnreadCount ( ) {
const { data , error } = await fetchGetUnreadNotifyCount ( ) ;
if ( ! error && typeof data === 'number' ) {
@@ -180,6 +192,16 @@ async function markRead(item: Api.NotifyMessage.NotifyMessage) {
}
}
function openDetail ( row : Api . NotifyMessage . NotifyMessage ) {
// 弹框持有该行引用,正文不随未读列表移除而消失
detailMessage . value = row ;
detailVisible . value = true ;
// 未读消息「打开即已读」:后台静默标记,避免"看一半就跑到已读"
if ( ! row . readStatus ) {
markRead ( row ) ;
}
}
async function markAllRead ( ) {
const { error } = await fetchUpdateAllNotifyMessageRead ( ) ;
if ( error ) return ;
@@ -193,6 +215,8 @@ async function markAllRead() {
let pollTimer : ReturnType < typeof setInterval > | null = null ;
onMounted ( ( ) => {
// 等级徽标颜色/文案走字典:若未在登录缓存内则按编码补拉一次(已缓存时不发请求)
dictStore . ensureDictData ( NOTIFY _MESSAGE _LEVEL _DICT _CODE ) ;
refreshUnreadCount ( ) ;
pollTimer = setInterval ( ( ) => {
if ( document . hidden ) return ;
@@ -253,12 +277,15 @@ onBeforeUnmount(() => {
v-for=" row in listStates . unread . items "
:key=" row . id "
class=" notification - bell _ _row is - unread "
@click=" markRead ( row ) "
@click=" openDetail ( row ) "
>
<span class=" notification - bell _ _row - dot " />
<span class=" notification - bell _ _row - dot " :style=" { backgroundColor : levelDotColor ( row . level ) } " />
<div class=" notification - bell _ _row - body ">
<div class=" notification - bell _ _row - title ">{{ row.templateContent }}</div>
<div class=" notification - bell _ _row - time ">{{ formatRelativeTime(row.createTime) }}</div >
<div class=" notification - bell _ _row - meta " >
<DictTag :dict-code=" NOTIFY _MESSAGE _LEVEL _DICT _CODE " :value=" row . level " size=" small " round />
<span class=" notification - bell _ _row - time ">{{ formatRelativeTime(row.createTime) }}</span>
</div>
</div>
</li>
</ul>
@@ -273,18 +300,23 @@ onBeforeUnmount(() => {
<ElTabPane name=" read ">
<template #label>
<span class=" notification - bell _ _tab - label ">
已读
<span class=" notification - bell _ _tab - count ">{{ listStates.read.total }}</span>
</span>
<span class=" notification - bell _ _tab - label ">已读</span>
</template>
<ElScrollbar ref=" readScrollbar " class=" notification - bell _ _scroll ">
<ul v-if=" listStates . read . items . length > 0 " class=" notification - bell _ _list ">
<li v-for=" row in listStates . read . items " :key=" row . id " class=" notification - bell _ _row ">
<span class=" notification - bell _ _row - dot " />
<li
v-for=" row in listStates . read . items "
:key=" row . id "
class=" notification - bell _ _row "
@click=" openDetail ( row ) "
>
<span class=" notification - bell _ _row - dot " :style=" { backgroundColor : levelDotColor ( row . level ) } " />
<div class=" notification - bell _ _row - body ">
<div class=" notification - bell _ _row - title ">{{ row.templateContent }}</div>
<div class=" notification - bell _ _row - time ">{{ formatRelativeTime(row.createTime) }}</div >
<div class=" notification - bell _ _row - meta " >
<DictTag :dict-code=" NOTIFY _MESSAGE _LEVEL _DICT _CODE " :value=" row . level " size=" small " round />
<span class=" notification - bell _ _row - time ">{{ formatRelativeTime(row.createTime) }}</span>
</div>
</div>
</li>
</ul>
@@ -303,6 +335,28 @@ onBeforeUnmount(() => {
<ElButton @click=" closeDrawer ">关闭</ElButton>
</template>
</ElDrawer>
<ElDialog v-model=" detailVisible " width=" 520 px " align-center class=" notification - bell _ _detail ">
<template #header>
<div class=" notification - bell _ _detail - head ">
<span class=" notification - bell _ _detail - sender ">{{ detailMessage?.templateNickname || '系统通知' }}</span>
<DictTag
v-if=" detailMessage "
:dict-code=" NOTIFY _MESSAGE _LEVEL _DICT _CODE "
:value=" detailMessage . level "
size=" small "
round
/>
</div>
</template>
<div v-if=" detailMessage " class=" notification - bell _ _detail - body ">
<div class=" notification - bell _ _detail - content ">{{ detailMessage.templateContent }}</div>
<div class=" notification - bell _ _detail - time ">收到于 {{ formatDateTime(detailMessage.createTime) }}</div>
</div>
<template #footer>
<ElButton @click=" detailVisible = false " > 关闭 < / ElButton >
< / template >
< / ElDialog >
< / template >
< style scoped >
@@ -484,18 +538,15 @@ onBeforeUnmount(() => {
gap : 10 px ;
padding : 12 px 4 px ;
border - radius : 8 px ;
cursor : pointer ;
transition : background - color 120 ms ease ;
}
. notification - bell _ _row + . notification - bell _ _row {
border - top : 1 px dashed var ( -- el - border - color - lighter ) ;
}
. notification - bell _ _row . is - unread {
cursor : pointer ;
transition : background - color 120 ms ease ;
}
. notification - bell _ _row . is - unread : hover {
. notification - bell _ _row : hover {
background - color : var ( -- el - fill - color - light ) ;
}
@@ -527,8 +578,14 @@ onBeforeUnmount(() => {
font - weight : 500 ;
}
. notification - bell _ _row - meta {
display : flex ;
align - items : center ;
gap : 8 px ;
margin - top : 6 px ;
}
. notification - bell _ _row - time {
margin - top : 4 px ;
color : var ( -- el - text - color - secondary ) ;
font - size : 12 px ;
}
@@ -547,4 +604,41 @@ onBeforeUnmount(() => {
font - size : 12 px ;
user - select : none ;
}
. notification - bell _ _detail - body {
display : flex ;
flex - direction : column ;
gap : 14 px ;
}
. notification - bell _ _detail - head {
display : flex ;
align - items : center ;
gap : 10 px ;
padding - right : 8 px ;
min - width : 0 ;
}
. notification - bell _ _detail - sender {
min - width : 0 ;
overflow : hidden ;
color : var ( -- el - text - color - primary ) ;
font - size : 16 px ;
font - weight : 600 ;
text - overflow : ellipsis ;
white - space : nowrap ;
}
. notification - bell _ _detail - content {
color : var ( -- el - text - color - regular ) ;
font - size : 14 px ;
line - height : 1.7 ;
white - space : pre - wrap ;
word - break : break - word ;
}
. notification - bell _ _detail - time {
color : var ( -- el - text - color - secondary ) ;
font - size : 12 px ;
}
< / style >