|
|
|
@ -1,8 +1,6 @@ |
|
|
|
<script lang="ts" setup> |
|
|
|
import type { NotificationItem } from './types'; |
|
|
|
|
|
|
|
import { useRouter } from 'vue-router'; |
|
|
|
|
|
|
|
import { Bell, CircleCheckBig, CircleX, MailCheck } from '@vben/icons'; |
|
|
|
import { $t } from '@vben/locales'; |
|
|
|
|
|
|
|
@ -15,76 +13,48 @@ import { |
|
|
|
|
|
|
|
import { useToggle } from '@vueuse/core'; |
|
|
|
|
|
|
|
interface Props { |
|
|
|
/** |
|
|
|
* 显示圆点 |
|
|
|
*/ |
|
|
|
dot?: boolean; |
|
|
|
/** |
|
|
|
* 消息列表 |
|
|
|
*/ |
|
|
|
notifications?: NotificationItem[]; |
|
|
|
} |
|
|
|
|
|
|
|
defineOptions({ name: 'NotificationPopup' }); |
|
|
|
|
|
|
|
withDefaults(defineProps<Props>(), { |
|
|
|
dot: false, |
|
|
|
notifications: () => [], |
|
|
|
}); |
|
|
|
withDefaults( |
|
|
|
defineProps<{ |
|
|
|
/** 显示圆点 */ |
|
|
|
dot?: boolean; |
|
|
|
/** 消息列表 */ |
|
|
|
notifications?: NotificationItem[]; |
|
|
|
}>(), |
|
|
|
{ |
|
|
|
dot: false, |
|
|
|
notifications: () => [], |
|
|
|
}, |
|
|
|
); |
|
|
|
|
|
|
|
const emit = defineEmits<{ |
|
|
|
clear: []; |
|
|
|
makeAll: []; |
|
|
|
onClick: [NotificationItem]; |
|
|
|
read: [NotificationItem]; |
|
|
|
remove: [NotificationItem]; |
|
|
|
viewAll: []; |
|
|
|
}>(); |
|
|
|
|
|
|
|
const router = useRouter(); |
|
|
|
const [open, toggle] = useToggle(); |
|
|
|
|
|
|
|
function close() { |
|
|
|
const close = () => { |
|
|
|
open.value = false; |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
function handleViewAll() { |
|
|
|
const handleViewAll = () => { |
|
|
|
emit('viewAll'); |
|
|
|
close(); |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
function handleMakeAll() { |
|
|
|
const handleMakeAll = () => { |
|
|
|
emit('makeAll'); |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
function handleClear() { |
|
|
|
const handleClear = () => { |
|
|
|
emit('clear'); |
|
|
|
} |
|
|
|
|
|
|
|
function handleClick(item: NotificationItem) { |
|
|
|
// 如果通知项有链接,点击时跳转 |
|
|
|
if (item.link) { |
|
|
|
navigateTo(item.link, item.query, item.state); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
function navigateTo( |
|
|
|
link: string, |
|
|
|
query?: Record<string, any>, |
|
|
|
state?: Record<string, any>, |
|
|
|
) { |
|
|
|
if (link.startsWith('http://') || link.startsWith('https://')) { |
|
|
|
// 外部链接,在新标签页打开 |
|
|
|
window.open(link, '_blank'); |
|
|
|
} else { |
|
|
|
// 内部路由链接,支持 query 参数和 state |
|
|
|
router.push({ |
|
|
|
path: link, |
|
|
|
query: query || {}, |
|
|
|
state, |
|
|
|
}); |
|
|
|
} |
|
|
|
} |
|
|
|
}; |
|
|
|
</script> |
|
|
|
<template> |
|
|
|
<VbenPopover v-model:open="open" content-class="relative right-2 w-90 p-0"> |
|
|
|
@ -104,66 +74,72 @@ function navigateTo( |
|
|
|
<div class="flex items-center justify-between p-4 py-3"> |
|
|
|
<div class="text-foreground">{{ $t('ui.widgets.notifications') }}</div> |
|
|
|
<VbenIconButton |
|
|
|
:disabled="notifications.length <= 0" |
|
|
|
:disabled="!notifications || notifications.length <= 0" |
|
|
|
:tooltip="$t('ui.widgets.markAllAsRead')" |
|
|
|
@click="handleMakeAll" |
|
|
|
> |
|
|
|
<MailCheck class="size-4" /> |
|
|
|
</VbenIconButton> |
|
|
|
</div> |
|
|
|
<VbenScrollbar v-if="notifications.length > 0"> |
|
|
|
<VbenScrollbar v-if="!notifications || notifications.length > 0"> |
|
|
|
<ul class="flex! max-h-90 w-full flex-col"> |
|
|
|
<template v-for="item in notifications" :key="item.id ?? item.title"> |
|
|
|
<li |
|
|
|
class="relative flex w-full cursor-pointer items-start gap-5 border-t border-border p-3 hover:bg-accent" |
|
|
|
@click="handleClick(item)" |
|
|
|
@click="emit('onClick', item)" |
|
|
|
> |
|
|
|
<span |
|
|
|
v-if="!item.isRead" |
|
|
|
class="absolute top-2 right-2 size-2 rounded-sm bg-primary" |
|
|
|
></span> |
|
|
|
|
|
|
|
<span |
|
|
|
class="relative flex size-10 shrink-0 overflow-hidden rounded-full" |
|
|
|
> |
|
|
|
<img |
|
|
|
:src="item.avatar" |
|
|
|
class="aspect-square size-full object-cover" |
|
|
|
/> |
|
|
|
</span> |
|
|
|
<div class="flex flex-col gap-1 leading-none"> |
|
|
|
<p class="font-semibold">{{ item.title }}</p> |
|
|
|
<p class="my-1 line-clamp-2 text-xs text-muted-foreground"> |
|
|
|
{{ item.message }} |
|
|
|
</p> |
|
|
|
<p class="line-clamp-2 text-xs text-muted-foreground"> |
|
|
|
{{ item.date }} |
|
|
|
</p> |
|
|
|
</div> |
|
|
|
<div |
|
|
|
class="absolute top-1/2 right-3 flex -translate-y-1/2 flex-col gap-2" |
|
|
|
> |
|
|
|
<VbenIconButton |
|
|
|
<slot name="content" :item="item"> |
|
|
|
<span |
|
|
|
v-if="!item.isRead" |
|
|
|
size="xs" |
|
|
|
variant="ghost" |
|
|
|
class="h-6 px-2" |
|
|
|
:tooltip="$t('common.confirm')" |
|
|
|
@click.stop="emit('read', item)" |
|
|
|
class="absolute top-2 right-2 size-2 rounded-sm bg-primary" |
|
|
|
></span> |
|
|
|
|
|
|
|
<span |
|
|
|
class="relative flex size-10 shrink-0 overflow-hidden rounded-full" |
|
|
|
> |
|
|
|
<CircleCheckBig class="size-4" /> |
|
|
|
</VbenIconButton> |
|
|
|
<VbenIconButton |
|
|
|
v-if="item.isRead" |
|
|
|
size="xs" |
|
|
|
variant="ghost" |
|
|
|
class="h-6 px-2 text-destructive" |
|
|
|
:tooltip="$t('common.delete')" |
|
|
|
@click.stop="emit('remove', item)" |
|
|
|
<img |
|
|
|
:src="item.avatar" |
|
|
|
class="aspect-square size-full object-cover" |
|
|
|
/> |
|
|
|
</span> |
|
|
|
<div class="flex flex-col gap-1 leading-none"> |
|
|
|
<p class="font-semibold">{{ item.title }}</p> |
|
|
|
<p class="my-1 line-clamp-2 text-xs text-muted-foreground"> |
|
|
|
{{ item.message }} |
|
|
|
</p> |
|
|
|
<p class="line-clamp-2 text-xs text-muted-foreground"> |
|
|
|
{{ item.date }} |
|
|
|
</p> |
|
|
|
</div> |
|
|
|
<div |
|
|
|
class="absolute top-1/2 right-3 flex -translate-y-1/2 flex-row gap-1" |
|
|
|
> |
|
|
|
<CircleX class="size-4" /> |
|
|
|
</VbenIconButton> |
|
|
|
</div> |
|
|
|
<slot name="action" :item="item"> |
|
|
|
<slot name="action-prepend" :item="item"></slot> |
|
|
|
<VbenIconButton |
|
|
|
v-if="!item.isRead" |
|
|
|
size="xs" |
|
|
|
variant="ghost" |
|
|
|
class="h-6 px-2" |
|
|
|
:tooltip="$t('common.confirm')" |
|
|
|
@click.stop="emit('read', item)" |
|
|
|
> |
|
|
|
<CircleCheckBig class="size-4" /> |
|
|
|
</VbenIconButton> |
|
|
|
<VbenIconButton |
|
|
|
v-if="item.isRead" |
|
|
|
size="xs" |
|
|
|
variant="ghost" |
|
|
|
class="h-6 px-2 text-destructive" |
|
|
|
:tooltip="$t('common.delete')" |
|
|
|
@click.stop="emit('remove', item)" |
|
|
|
> |
|
|
|
<CircleX class="size-4" /> |
|
|
|
</VbenIconButton> |
|
|
|
<slot name="action-append" :item="item"></slot> |
|
|
|
</slot> |
|
|
|
</div> |
|
|
|
</slot> |
|
|
|
</li> |
|
|
|
</template> |
|
|
|
</ul> |
|
|
|
@ -179,7 +155,7 @@ function navigateTo( |
|
|
|
class="flex items-center justify-between border-t border-border px-4 py-3" |
|
|
|
> |
|
|
|
<VbenButton |
|
|
|
:disabled="notifications.length <= 0" |
|
|
|
:disabled="!notifications || notifications.length <= 0" |
|
|
|
size="sm" |
|
|
|
variant="ghost" |
|
|
|
@click="handleClear" |
|
|
|
|