mirror of https://github.com/halo-dev/halo
feat: add batch delete and mark-as-read supports for notifications (#7282)
#### What type of PR is this? /area ui /kind feature /milestone 2.20.x #### What this PR does / why we need it: Add support for batch deletion and batch marking as read for notifications in the UC. <img width="763" alt="image" src="https://github.com/user-attachments/assets/a470ae2d-c4d2-4e6c-8c05-76f9f29e378d" /> #### Which issue(s) this PR fixes: Fixes https://github.com/halo-dev/halo/issues/7164 #### Special notes for your reviewer: #### Does this PR introduce a user-facing change? ```release-note 个人中心的消息管理支持批量删除和批量标记为已读 ```pull/7288/head
parent
ddbbe09c2d
commit
ca8bc52079
|
@ -463,6 +463,10 @@ core:
|
||||||
revoke:
|
revoke:
|
||||||
title: Revoke device
|
title: Revoke device
|
||||||
description: Are you sure you want to revoke this device? After revoking, this device will be logged out
|
description: Are you sure you want to revoke this device? After revoking, this device will be logged out
|
||||||
|
revoke_others:
|
||||||
|
title: Revoke all other devices
|
||||||
|
description: Are you sure you want to revoke all other devices? After you revoke, other devices will be logged out
|
||||||
|
toast_success: Login status of other devices has been revoked
|
||||||
uc_notification:
|
uc_notification:
|
||||||
title: Notifications
|
title: Notifications
|
||||||
tabs:
|
tabs:
|
||||||
|
@ -478,6 +482,12 @@ core:
|
||||||
delete:
|
delete:
|
||||||
description: Are you sure you want to delete this notification?
|
description: Are you sure you want to delete this notification?
|
||||||
title: Delete
|
title: Delete
|
||||||
|
delete_all:
|
||||||
|
title: Delete all current notifications
|
||||||
|
description: Are you sure you want to delete all current notifications? You cannot restore them after deletion.
|
||||||
|
mark_all_as_read:
|
||||||
|
title: Mark all current notifications as read
|
||||||
|
description: Mark all current messages as read. Do you want to continue?
|
||||||
overview:
|
overview:
|
||||||
fields:
|
fields:
|
||||||
activated_theme: Activated theme
|
activated_theme: Activated theme
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
{}
|
|
@ -1398,6 +1398,14 @@ core:
|
||||||
delete:
|
delete:
|
||||||
description: Are you sure you want to delete this notification?
|
description: Are you sure you want to delete this notification?
|
||||||
title: Delete
|
title: Delete
|
||||||
|
delete_all:
|
||||||
|
title: Delete all current notifications
|
||||||
|
description: >-
|
||||||
|
Are you sure you want to delete all current notifications? You cannot
|
||||||
|
restore them after deletion.
|
||||||
|
mark_all_as_read:
|
||||||
|
title: Mark all current notifications as read
|
||||||
|
description: Mark all current messages as read. Do you want to continue?
|
||||||
setting:
|
setting:
|
||||||
title: Settings
|
title: Settings
|
||||||
overview:
|
overview:
|
||||||
|
|
|
@ -1170,8 +1170,7 @@ core:
|
||||||
manual:
|
manual:
|
||||||
label: 如果无法扫描二维码,点击查看代替步骤
|
label: 如果无法扫描二维码,点击查看代替步骤
|
||||||
help: 使用以下代码手动配置验证器应用:
|
help: 使用以下代码手动配置验证器应用:
|
||||||
tips: >-
|
tips: 请妥善保管您的两步验证设备,如果设备丢失或损坏,你将无法登录系统。建议你在多个设备上安装验证器应用,或保存好密钥的备份,以防主要设备无法使用。
|
||||||
请妥善保管您的两步验证设备,如果设备丢失或损坏,你将无法登录系统。建议你在多个设备上安装验证器应用,或保存好密钥的备份,以防主要设备无法使用。
|
|
||||||
pat:
|
pat:
|
||||||
operations:
|
operations:
|
||||||
delete:
|
delete:
|
||||||
|
@ -1303,6 +1302,12 @@ core:
|
||||||
delete:
|
delete:
|
||||||
title: 删除消息
|
title: 删除消息
|
||||||
description: 确定要删除该消息吗?
|
description: 确定要删除该消息吗?
|
||||||
|
delete_all:
|
||||||
|
title: 删除当前所有通知
|
||||||
|
description: 确定要删除当前所有通知吗?删除后将无法恢复。
|
||||||
|
mark_all_as_read:
|
||||||
|
title: 将当前所有通知标记为已读
|
||||||
|
description: 将当前所有通知标记为已读,是否继续?
|
||||||
setting:
|
setting:
|
||||||
title: 设置
|
title: 设置
|
||||||
overview:
|
overview:
|
||||||
|
|
|
@ -1287,6 +1287,12 @@ core:
|
||||||
delete:
|
delete:
|
||||||
description: 確定要刪除該訊息嗎?
|
description: 確定要刪除該訊息嗎?
|
||||||
title: 刪除訊息
|
title: 刪除訊息
|
||||||
|
delete_all:
|
||||||
|
title: 刪除目前所有通知
|
||||||
|
description: 確定要刪除目前所有通知嗎?刪除後將無法復原。
|
||||||
|
mark_all_as_read:
|
||||||
|
title: 將目前所有通知標示為已讀
|
||||||
|
description: 將目前所有通知標示為已讀,是否繼續?
|
||||||
setting:
|
setting:
|
||||||
title: 設置
|
title: 設置
|
||||||
overview:
|
overview:
|
||||||
|
|
|
@ -2,7 +2,11 @@
|
||||||
import { useUserStore } from "@/stores/user";
|
import { useUserStore } from "@/stores/user";
|
||||||
import { ucApiClient } from "@halo-dev/api-client";
|
import { ucApiClient } from "@halo-dev/api-client";
|
||||||
import {
|
import {
|
||||||
|
Dialog,
|
||||||
|
IconCheckboxCircle,
|
||||||
|
IconDeleteBin,
|
||||||
IconNotificationBadgeLine,
|
IconNotificationBadgeLine,
|
||||||
|
Toast,
|
||||||
VButton,
|
VButton,
|
||||||
VCard,
|
VCard,
|
||||||
VEmpty,
|
VEmpty,
|
||||||
|
@ -10,13 +14,16 @@ import {
|
||||||
VPageHeader,
|
VPageHeader,
|
||||||
VTabbar,
|
VTabbar,
|
||||||
} from "@halo-dev/components";
|
} from "@halo-dev/components";
|
||||||
import { useQuery } from "@tanstack/vue-query";
|
import { useQuery, useQueryClient } from "@tanstack/vue-query";
|
||||||
import { useRouteQuery } from "@vueuse/router";
|
import { useRouteQuery } from "@vueuse/router";
|
||||||
import { OverlayScrollbarsComponent } from "overlayscrollbars-vue";
|
import { OverlayScrollbarsComponent } from "overlayscrollbars-vue";
|
||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
import NotificationContent from "./components/NotificationContent.vue";
|
import NotificationContent from "./components/NotificationContent.vue";
|
||||||
import NotificationListItem from "./components/NotificationListItem.vue";
|
import NotificationListItem from "./components/NotificationListItem.vue";
|
||||||
|
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const { t } = useI18n();
|
||||||
const { currentUser } = useUserStore();
|
const { currentUser } = useUserStore();
|
||||||
|
|
||||||
const activeTab = useRouteQuery("tab", "unread");
|
const activeTab = useRouteQuery("tab", "unread");
|
||||||
|
@ -54,6 +61,71 @@ const selectedNotification = computed(() => {
|
||||||
(item) => item.metadata.name === selectedNotificationName.value
|
(item) => item.metadata.name === selectedNotificationName.value
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function handleDeleteNotifications() {
|
||||||
|
Dialog.warning({
|
||||||
|
title: t("core.uc_notification.operations.delete_all.title"),
|
||||||
|
description: t("core.uc_notification.operations.delete_all.description"),
|
||||||
|
confirmText: t("core.common.buttons.confirm"),
|
||||||
|
cancelText: t("core.common.buttons.cancel"),
|
||||||
|
confirmType: "danger",
|
||||||
|
onConfirm: async () => {
|
||||||
|
if (!notifications.value || notifications.value.items.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currentUser) {
|
||||||
|
throw new Error("Current user is not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const notification of notifications.value.items) {
|
||||||
|
await ucApiClient.notification.notification.deleteSpecifiedNotification(
|
||||||
|
{
|
||||||
|
username: currentUser.metadata.name,
|
||||||
|
name: notification.metadata.name,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ["user-notifications"] });
|
||||||
|
|
||||||
|
Toast.success(t("core.common.toast.delete_success"));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMarkAllAsRead() {
|
||||||
|
Dialog.warning({
|
||||||
|
title: t("core.uc_notification.operations.mark_all_as_read.title"),
|
||||||
|
description: t(
|
||||||
|
"core.uc_notification.operations.mark_all_as_read.description"
|
||||||
|
),
|
||||||
|
confirmText: t("core.common.buttons.confirm"),
|
||||||
|
cancelText: t("core.common.buttons.cancel"),
|
||||||
|
onConfirm: async () => {
|
||||||
|
if (!notifications.value || notifications.value.items.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currentUser) {
|
||||||
|
throw new Error("Current user is not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const names = notifications.value?.items.map(
|
||||||
|
(notification) => notification.metadata.name
|
||||||
|
);
|
||||||
|
|
||||||
|
await ucApiClient.notification.notification.markNotificationsAsRead({
|
||||||
|
username: currentUser.metadata.name,
|
||||||
|
markSpecifiedRequest: {
|
||||||
|
names,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ["user-notifications"] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -69,17 +141,12 @@ const selectedNotification = computed(() => {
|
||||||
>
|
>
|
||||||
<div class="grid h-full grid-cols-12 divide-y sm:divide-x sm:divide-y-0">
|
<div class="grid h-full grid-cols-12 divide-y sm:divide-x sm:divide-y-0">
|
||||||
<div
|
<div
|
||||||
class="relative col-span-12 h-full overflow-auto sm:col-span-6 lg:col-span-5 xl:col-span-3"
|
class="relative col-span-12 flex h-full flex-col overflow-hidden sm:col-span-6 lg:col-span-5 xl:col-span-3"
|
||||||
>
|
>
|
||||||
<OverlayScrollbarsComponent
|
<div class="sticky top-0 z-10 flex-none">
|
||||||
element="div"
|
|
||||||
:options="{ scrollbars: { autoHide: 'scroll' } }"
|
|
||||||
class="h-full w-full"
|
|
||||||
defer
|
|
||||||
>
|
|
||||||
<VTabbar
|
<VTabbar
|
||||||
v-model:active-id="activeTab"
|
v-model:active-id="activeTab"
|
||||||
class="sticky top-0 z-10 !rounded-none"
|
class="!rounded-none"
|
||||||
:items="[
|
:items="[
|
||||||
{ id: 'unread', label: $t('core.uc_notification.tabs.unread') },
|
{ id: 'unread', label: $t('core.uc_notification.tabs.unread') },
|
||||||
{ id: 'read', label: $t('core.uc_notification.tabs.read') },
|
{ id: 'read', label: $t('core.uc_notification.tabs.read') },
|
||||||
|
@ -87,6 +154,37 @@ const selectedNotification = computed(() => {
|
||||||
type="outline"
|
type="outline"
|
||||||
@change="selectedNotificationName = undefined"
|
@change="selectedNotificationName = undefined"
|
||||||
></VTabbar>
|
></VTabbar>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="absolute right-4 top-1/2 -translate-y-1/2 flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-if="activeTab === 'unread'"
|
||||||
|
class="flex items-center justify-center h-7 w-7 rounded-full cursor-pointer hover:bg-gray-200 disabled:pointer-events-none disabled:opacity-70"
|
||||||
|
:disabled="!notifications?.items.length"
|
||||||
|
@click="handleMarkAllAsRead"
|
||||||
|
>
|
||||||
|
<IconCheckboxCircle
|
||||||
|
class="w-4 h-4 text-gray-600 group-hover:text-gray-900"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="flex items-center justify-center h-7 w-7 group rounded-full cursor-pointer hover:bg-gray-200 disabled:pointer-events-none disabled:opacity-70"
|
||||||
|
:disabled="!notifications?.items.length"
|
||||||
|
@click="handleDeleteNotifications"
|
||||||
|
>
|
||||||
|
<IconDeleteBin
|
||||||
|
class="h-4 w-4 text-gray-600 group-hover:text-red-600"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<OverlayScrollbarsComponent
|
||||||
|
element="div"
|
||||||
|
:options="{ scrollbars: { autoHide: 'scroll' } }"
|
||||||
|
class="h-full w-full flex-1"
|
||||||
|
defer
|
||||||
|
>
|
||||||
<VLoading v-if="isLoading" />
|
<VLoading v-if="isLoading" />
|
||||||
<Transition
|
<Transition
|
||||||
v-else-if="!notifications?.items.length"
|
v-else-if="!notifications?.items.length"
|
||||||
|
|
|
@ -47,6 +47,9 @@ function handleDelete() {
|
||||||
Dialog.warning({
|
Dialog.warning({
|
||||||
title: t("core.uc_notification.operations.delete.title"),
|
title: t("core.uc_notification.operations.delete.title"),
|
||||||
description: t("core.uc_notification.operations.delete.description"),
|
description: t("core.uc_notification.operations.delete.description"),
|
||||||
|
confirmText: t("core.common.buttons.confirm"),
|
||||||
|
cancelText: t("core.common.buttons.cancel"),
|
||||||
|
confirmType: "danger",
|
||||||
async onConfirm() {
|
async onConfirm() {
|
||||||
await ucApiClient.notification.notification.deleteSpecifiedNotification({
|
await ucApiClient.notification.notification.deleteSpecifiedNotification({
|
||||||
name: props.notification.metadata.name,
|
name: props.notification.metadata.name,
|
||||||
|
|
|
@ -13,6 +13,7 @@ export default definePlugin({
|
||||||
meta: {
|
meta: {
|
||||||
title: "core.uc_notification.title",
|
title: "core.uc_notification.title",
|
||||||
searchable: true,
|
searchable: true,
|
||||||
|
hideFooter: true,
|
||||||
menu: {
|
menu: {
|
||||||
name: "core.uc_sidebar.menu.items.notification",
|
name: "core.uc_sidebar.menu.items.notification",
|
||||||
group: "dashboard",
|
group: "dashboard",
|
||||||
|
|
Loading…
Reference in New Issue