This commit is contained in:
xingyu4j
2025-11-24 11:47:31 +08:00
185 changed files with 2651 additions and 939 deletions

View File

@@ -92,7 +92,7 @@ const rightSlots = computed(() => {
list.push({ index: Number(name[2]), name: key });
}
});
return list.sort((a, b) => a.index - b.index);
return list.toSorted((a, b) => a.index - b.index);
});
const leftSlots = computed(() => {
@@ -111,7 +111,7 @@ const leftSlots = computed(() => {
list.push({ index: Number(name[2]), name: key });
}
});
return list.sort((a, b) => a.index - b.index);
return list.toSorted((a, b) => a.index - b.index);
});
function clearPreferencesAndLogout() {

View File

@@ -1,7 +1,9 @@
<script lang="ts" setup>
import type { NotificationItem } from './types';
import { Bell, MailCheck } from '@vben/icons';
import { useRouter } from 'vue-router';
import { Bell, CircleCheckBig, CircleX, MailCheck } from '@vben/icons';
import { $t } from '@vben/locales';
import {
@@ -36,9 +38,11 @@ const emit = defineEmits<{
makeAll: [];
open: [boolean];
read: [NotificationItem];
remove: [NotificationItem];
viewAll: [];
}>();
const router = useRouter();
const [open, toggle] = useToggle();
function close() {
@@ -59,7 +63,28 @@ function handleClear() {
}
function handleClick(item: NotificationItem) {
emit('read', item);
// 如果通知项有链接,点击时跳转
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,
});
}
}
function handleOpen() {
@@ -97,7 +122,7 @@ function handleOpen() {
</div>
<VbenScrollbar v-if="notifications.length > 0">
<ul class="!flex max-h-[360px] w-full flex-col">
<template v-for="item in notifications" :key="item.title">
<template v-for="item in notifications" :key="item.id ?? item.title">
<li
class="hover:bg-accent border-border relative flex w-full cursor-pointer items-start gap-5 border-t px-3 py-3"
@click="handleClick(item)"
@@ -113,7 +138,6 @@ function handleOpen() {
<img
:src="item.avatar"
class="aspect-square h-full w-full object-cover"
role="img"
/>
</span>
<div class="flex flex-col gap-1 leading-none">
@@ -125,6 +149,30 @@ function handleOpen() {
{{ item.date }}
</p>
</div>
<div
class="absolute right-3 top-1/2 flex -translate-y-1/2 flex-col gap-2"
>
<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="text-destructive h-6 px-2"
:tooltip="$t('common.delete')"
@click.stop="emit('remove', item)"
>
<CircleX class="size-4" />
</VbenIconButton>
</div>
</li>
</template>
</ul>

View File

@@ -1,10 +1,17 @@
interface NotificationItem {
id: any;
avatar: string;
date: string;
isRead?: boolean;
message: string;
title: string;
id?: number;
/**
* 跳转链接,可以是路由路径或完整 URL
* @example '/dashboard' 或 'https://example.com'
*/
link?: string;
query?: Record<string, any>;
state?: Record<string, any>;
}
export type { NotificationItem };

View File

@@ -66,7 +66,7 @@ function toggleTheme(event: MouseEvent) {
];
const animate = document.documentElement.animate(
{
clipPath: isDark.value ? [...clipPath].reverse() : clipPath,
clipPath: isDark.value ? [...clipPath].toReversed() : clipPath,
},
{
duration: 450,