|
|
@@ -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"
|