Merge branch 'fork/xueyitt/main'

This commit is contained in:
Jin Mao
2026-04-15 17:20:10 +08:00
10 changed files with 256 additions and 98 deletions

View File

@@ -147,6 +147,34 @@ function remove(id: number | string) {
function handleMakeAll() {
notifications.value.forEach((item) => (item.isRead = true));
}
const viewAll = () => {};
const 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,
});
}
}
watch(
() => ({
enable: preferences.app.watermark,
@@ -189,6 +217,8 @@ watch(
@read="(item) => item.id && markRead(item.id)"
@remove="(item) => item.id && remove(item.id)"
@make-all="handleMakeAll"
@on-click="handleClick"
@view-all="viewAll"
/>
</template>
<template #extra>

View File

@@ -147,6 +147,34 @@ function remove(id: number | string) {
function handleMakeAll() {
notifications.value.forEach((item) => (item.isRead = true));
}
const viewAll = () => {};
const 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,
});
}
}
watch(
() => ({
enable: preferences.app.watermark,
@@ -189,6 +217,8 @@ watch(
@read="(item) => item.id && markRead(item.id)"
@remove="(item) => item.id && remove(item.id)"
@make-all="handleMakeAll"
@on-click="handleClick"
@view-all="viewAll"
/>
</template>
<template #extra>

View File

@@ -147,6 +147,34 @@ function remove(id: number | string) {
function handleMakeAll() {
notifications.value.forEach((item) => (item.isRead = true));
}
const viewAll = () => {};
const 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,
});
}
}
watch(
() => ({
enable: preferences.app.watermark,
@@ -189,6 +217,8 @@ watch(
@read="(item) => item.id && markRead(item.id)"
@remove="(item) => item.id && remove(item.id)"
@make-all="handleMakeAll"
@on-click="handleClick"
@view-all="viewAll"
/>
</template>
<template #extra>

View File

@@ -148,6 +148,33 @@ function handleMakeAll() {
notifications.value.forEach((item) => (item.isRead = true));
}
const viewAll = () => {};
const 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,
});
}
}
watch(
() => ({
enable: preferences.app.watermark,
@@ -190,6 +217,8 @@ watch(
@read="(item) => item.id && markRead(item.id)"
@remove="(item) => item.id && remove(item.id)"
@make-all="handleMakeAll"
@on-click="handleClick"
@view-all="viewAll"
/>
</template>
<template #extra>

View File

@@ -147,6 +147,34 @@ function remove(id: number | string) {
function handleMakeAll() {
notifications.value.forEach((item) => (item.isRead = true));
}
const viewAll = () => {};
const 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,
});
}
}
watch(
() => ({
enable: preferences.app.watermark,
@@ -189,6 +217,8 @@ watch(
@read="(item) => item.id && markRead(item.id)"
@remove="(item) => item.id && remove(item.id)"
@make-all="handleMakeAll"
@on-click="handleClick"
@view-all="viewAll"
/>
</template>
<template #extra>

View File

@@ -26,6 +26,7 @@ export {
CircleX,
Copy,
CornerDownLeft,
Download,
Ellipsis,
Eraser,
Expand,

View File

@@ -24,6 +24,7 @@ export {
VbenContextMenu,
VbenCountToAnimator,
VbenFullScreen,
VbenIconButton,
VbenInputPassword,
VbenLoading,
VbenLogo,

View File

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

View File

@@ -12,6 +12,8 @@ interface NotificationItem {
link?: string;
query?: Record<string, any>;
state?: Record<string, any>;
/** 业务字段 */
[key: string]: any;
}
export type { NotificationItem };

View File

@@ -163,6 +163,33 @@ function handleMakeAll() {
function handleClickLogo() {}
const viewAll = () => {};
const 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,
});
}
}
watch(
() => ({
enable: preferences.app.watermark,
@@ -215,6 +242,8 @@ onBeforeMount(() => {
@read="(item) => item.id && markRead(item.id)"
@remove="(item) => item.id && remove(item.id)"
@make-all="handleMakeAll"
@on-click="handleClick"
@view-all="viewAll"
/>
</template>
<template #extra>