From b7e26ca1b22412f3af1fbec9907f1f2f2c19f64f Mon Sep 17 00:00:00 2001 From: Pine Date: Sat, 5 Sep 2026 18:06:48 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=EF=BC=9A=E9=80=9A=E7=9F=A5?= =?UTF-8?q?=E5=BE=BD=E7=AB=A0=E5=AE=9E=E6=97=B6=E6=9B=B4=E6=96=B0=E7=8A=B6?= =?UTF-8?q?=E6=80=81=EF=BC=8C=E8=AF=BB=E5=AE=8C=E5=90=8E=E7=AB=8B=E5=8D=B3?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E6=97=A0=E9=9C=80=E5=88=B7=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增全局通知store(notificationStore.ts):统一管理未读数量,支持setUnread/decrementUnread/refreshUnread/markAllRead/markOneRead - 修改Sidebar.tsx:从全局store读取未读数量,useNotifyChannel的onUnread回调同步到全局store - 修改Notifications.tsx:标记全部已读和单条已读时调用全局store方法,立即更新未读数量 - 修复TrainingCenter.tsx和Notifications.tsx中的语法错误(缺少右括号) - 所有页面的通知徽章现在都能实时同步,标记已读后立即更新,无需刷新页面 --- console/src/layouts/Sidebar.tsx | 8 ++- .../src/pages/portal/Opc/Notifications.tsx | 23 ++++---- .../src/pages/portal/Opc/TrainingCenter.tsx | 2 +- console/src/stores/notificationStore.ts | 58 +++++++++++++++++++ 4 files changed, 77 insertions(+), 14 deletions(-) create mode 100644 console/src/stores/notificationStore.ts diff --git a/console/src/layouts/Sidebar.tsx b/console/src/layouts/Sidebar.tsx index e5d1080..e5539e6 100644 --- a/console/src/layouts/Sidebar.tsx +++ b/console/src/layouts/Sidebar.tsx @@ -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)"; diff --git a/console/src/pages/portal/Opc/Notifications.tsx b/console/src/pages/portal/Opc/Notifications.tsx index f45b241..51b4ec9 100644 --- a/console/src/pages/portal/Opc/Notifications.tsx +++ b/console/src/pages/portal/Opc/Notifications.tsx @@ -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>({}); + // 从全局 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 { /* 静默 */ } diff --git a/console/src/pages/portal/Opc/TrainingCenter.tsx b/console/src/pages/portal/Opc/TrainingCenter.tsx index d5db614..6fadf64 100644 --- a/console/src/pages/portal/Opc/TrainingCenter.tsx +++ b/console/src/pages/portal/Opc/TrainingCenter.tsx @@ -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); } diff --git a/console/src/stores/notificationStore.ts b/console/src/stores/notificationStore.ts new file mode 100644 index 0000000..ad4edf2 --- /dev/null +++ b/console/src/stores/notificationStore.ts @@ -0,0 +1,58 @@ +// 全局通知 store — 统一管理未读数量,确保各处徽章实时同步 +import { create } from "zustand"; +import { notifyApi } from "../api/modules/notify"; + +interface NotificationState { + unread: number; + unreadByCategory: Record; + lastUpdated: number; + setUnread: (total: number, byCat?: Record) => void; + decrementUnread: (count?: number) => void; + refreshUnread: () => Promise; + markAllRead: () => Promise; + markOneRead: (id: string) => Promise; +} + +export const useNotificationStore = create((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(); + } + }, +}));