修复:通知徽章实时更新状态,读完后立即更新无需刷新

- 新增全局通知store(notificationStore.ts):统一管理未读数量,支持setUnread/decrementUnread/refreshUnread/markAllRead/markOneRead
- 修改Sidebar.tsx:从全局store读取未读数量,useNotifyChannel的onUnread回调同步到全局store
- 修改Notifications.tsx:标记全部已读和单条已读时调用全局store方法,立即更新未读数量
- 修复TrainingCenter.tsx和Notifications.tsx中的语法错误(缺少右括号)
- 所有页面的通知徽章现在都能实时同步,标记已读后立即更新,无需刷新页面
This commit is contained in:
Pine
2026-09-05 18:06:48 +08:00
parent d4694674b2
commit b7e26ca1b2
4 changed files with 77 additions and 14 deletions
+7 -1
View File
@@ -38,6 +38,7 @@ import { buildSessionPath, getSessionIdFromPath } from "../utils/sessionRoute";
import sessionApi from "../pages/Chat/sessionApi";
import { useInboxWobble } from "../hooks/useInboxWobble";
import { useNotifyChannel } from "../hooks/useNotifyChannel";
import { useNotificationStore } from "../stores/notificationStore";
import styles from "./index.module.less";
import { useTheme } from "../contexts/ThemeContext";
import { useMenuItems, useRoutes } from "../plugins/registry/hooks";
@@ -335,7 +336,12 @@ export default function Sidebar({ selectedKey }: SidebarProps) {
// ── Inbox badge dot & wobble ─────────────────────────────────────────────
const hasInboxUnread = hasUnreadMessages || hasPendingApprovals;
// 实时业务通知:菜单角标 + 全局 toast(SSE 通道)
const { unread: notifyUnread } = useNotifyChannel({ toast: true });
useNotifyChannel({
toast: true,
onUnread: (s) => useNotificationStore.getState().setUnread(s.unread, s.unreadByCategory),
});
// 从全局 store 读取未读数量,确保标记已读后实时更新
const notifyUnread = useNotificationStore((s) => s.unread);
const inboxDotColor = hasPendingApprovals
? "#e04848"
: "rgba(255, 157, 77, 1)";
+11 -12
View File
@@ -10,6 +10,7 @@ import { RequireRole } from "../../../auth/guards";
import { notifyApi, type NotifyItem, type NotifyPayload } from "../../../api/modules/notify";
import { friendlyErrorMessage } from "../../../utils/error";
import { useNotifyChannel } from "../../../hooks/useNotifyChannel";
import { useNotificationStore } from "../../../stores/notificationStore";
import styles from "../Opc/OpcList.module.less";
const CATEGORIES = [
@@ -37,8 +38,9 @@ export default function OpcNotifications() {
const [page, setPage] = useState(1);
const [category, setCategory] = useState("");
const [loading, setLoading] = useState(false);
const [unread, setUnread] = useState(0);
const [unreadByCategory, setUnreadByCategory] = useState<Record<string, number>>({});
// 从全局 store 读取未读数量,确保标记已读后实时更新
const unread = useNotificationStore((s) => s.unread);
const unreadByCategory = useNotificationStore((s) => s.unreadByCategory);
const firstLoadRef = useRef(true);
const load = useCallback(async (cat = category, pg = page) => {
@@ -47,10 +49,10 @@ export default function OpcNotifications() {
const r = await notifyApi.list({ category: cat, page: pg, pageSize: 15 });
setItems(r.items ?? []);
setTotal(r.total ?? 0);
setUnread(r.unread ?? 0);
setUnreadByCategory(r.unread_by_category ?? {});
// 同步未读数量到全局 store
useNotificationStore.getState().setUnread(r.unread ?? 0, r.unread_by_category ?? {});
} catch (e) {
message.error(friendlyErrorMessage(e, "加载失败");
message.error(friendlyErrorMessage(e, "加载失败"));
} finally {
setLoading(false);
}
@@ -81,8 +83,7 @@ export default function OpcNotifications() {
refreshUnread();
},
onUnread: (s) => {
setUnread(s.unread);
setUnreadByCategory(s.unreadByCategory);
useNotificationStore.getState().setUnread(s.unread, s.unreadByCategory);
},
});
@@ -101,21 +102,19 @@ export default function OpcNotifications() {
const markRead = async () => {
try {
await notifyApi.readAll();
await useNotificationStore.getState().markAllRead();
message.success(t("role.opc.allRead"));
load();
refreshUnread();
} catch (err) {
message.error(friendlyErrorMessage(err, "操作失败");
message.error(friendlyErrorMessage(err, "操作失败"));
}
};
const markOne = async (it: NotifyItem) => {
if (it.read) return;
try {
await notifyApi.readOne(it.id);
await useNotificationStore.getState().markOneRead(it.id);
setItems((prev) => prev.map((x) => (x.id === it.id ? { ...x, read: true } : x)));
refreshUnread();
} catch {
/* 静默 */
}
@@ -47,7 +47,7 @@ export default function TrainingCenter() {
setCourses(coursesRes.items);
setCategories(catsRes.items);
} catch (e: any) {
message.error(friendlyErrorMessage(e, "加载课程失败");
message.error(friendlyErrorMessage(e, "加载课程失败"));
} finally {
setLoading(false);
}
+58
View File
@@ -0,0 +1,58 @@
// 全局通知 store — 统一管理未读数量,确保各处徽章实时同步
import { create } from "zustand";
import { notifyApi } from "../api/modules/notify";
interface NotificationState {
unread: number;
unreadByCategory: Record<string, number>;
lastUpdated: number;
setUnread: (total: number, byCat?: Record<string, number>) => void;
decrementUnread: (count?: number) => void;
refreshUnread: () => Promise<void>;
markAllRead: () => Promise<void>;
markOneRead: (id: string) => Promise<void>;
}
export const useNotificationStore = create<NotificationState>((set, get) => ({
unread: 0,
unreadByCategory: {},
lastUpdated: 0,
setUnread: (total, byCat = {}) => {
set({ unread: total, unreadByCategory: byCat, lastUpdated: Date.now() });
},
decrementUnread: (count = 1) => {
const current = get().unread;
set({ unread: Math.max(0, current - count), lastUpdated: Date.now() });
},
refreshUnread: async () => {
try {
const r = await notifyApi.unreadCount();
set({ unread: r.total, unreadByCategory: r.by_category ?? {}, lastUpdated: Date.now() });
} catch {
/* 静默 */
}
},
markAllRead: async () => {
try {
await notifyApi.readAll();
set({ unread: 0, unreadByCategory: {}, lastUpdated: Date.now() });
} catch {
// 失败时刷新一次
get().refreshUnread();
}
},
markOneRead: async (id: string) => {
try {
await notifyApi.readOne(id);
get().decrementUnread(1);
} catch {
// 失败时刷新一次
get().refreshUnread();
}
},
}));