mirror of
https://github.com/imdap/ruoyi-plus-vben5.git
synced 2026-04-23 00:38:34 +08:00
feat: 通知模块自定义加强
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -25,6 +25,7 @@ export {
|
||||
CircleX,
|
||||
Copy,
|
||||
CornerDownLeft,
|
||||
Download,
|
||||
Ellipsis,
|
||||
Eraser,
|
||||
Expand,
|
||||
|
||||
@@ -24,6 +24,7 @@ export {
|
||||
VbenContextMenu,
|
||||
VbenCountToAnimator,
|
||||
VbenFullScreen,
|
||||
VbenIconButton,
|
||||
VbenInputPassword,
|
||||
VbenLoading,
|
||||
VbenLogo,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -12,6 +12,8 @@ interface NotificationItem {
|
||||
link?: string;
|
||||
query?: Record<string, any>;
|
||||
state?: Record<string, any>;
|
||||
/** 业务字段 */
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export type { NotificationItem };
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user