实现内容(仲裁裁决驱动,首次即最终方案): P4 核心功能 - 认证:localStorage token 存储(F12)+ REST 登录(ISSUE-004)+ refreshAccessToken 竞态防护 - 子女切换:ChildSwitcher(Tab ≤3 / 下拉 ≥4)+ Zustand store(ISSUE-009 纯前端切换) - 数据查询:urql GraphQL 消费 parent-bff(F9)+ TanStack Query 缓存 - 通知中心:NotificationFeed + 已读/全部已读 mutations - 通知偏好:三维矩阵 + ISSUE-033 localStorage 降级 - 5 层状态管理:URL/Server/Client/Global UI/Form - 跨标签同步:BroadcastChannel + storage 事件 P5 实时推送 - WebSocket 连接 push-gateway + 指数退避重连 - HTTP 轮询降级(60s)+ 实时通知 Hook P6 硬化 - Web Vitals 上报 + OTel trace - i18n 5 语言(zh-CN/en-US/zh-TW/ja-JP/ar-SA 含 RTL) - PWA manifest + Service Worker - CSP 安全头 + 权限点 F7 命名 + 设计令牌三层 测试与构建 - Vitest 92 测试全通过(utils/auth/child-store/ChildSwitcher/NotificationFeed/login) - MSW mock 未就绪上游(parent-bff GraphQL + iam REST + iam GetChildrenByParent P0 阻塞用 fixtures) - Dockerfile 多阶段构建(G1,端口 4002,HEALTHCHECK /api/health) - typecheck + lint 零错误 经验沉淀 - known-issues.md §2.13 追加 12 条实现期经验(无 AI 身份标注) - arch.db 已更新(15 TS 模块 / 482 符号 / 138 proto) 依据:02-architecture-design.md(回写总裁裁决)、coord-final-decisions.md、 president-final-rulings.md、parent-portal_workline.md、parent-portal_contract.md
71 lines
2.1 KiB
TypeScript
71 lines
2.1 KiB
TypeScript
// useRealtimeNotifications:实时通知 Hook(P5)
|
||
// 依据:02-architecture-design.md §5 实时推送
|
||
// - WebSocket 接收 NotificationRequested 事件
|
||
// - 收到新通知时更新 urql 缓存(触发 NotificationFeed 重新渲染)
|
||
// - 收到 GradeRecorded / SchoolAnnouncement 事件时触发相应页面刷新
|
||
|
||
"use client";
|
||
|
||
import { useCallback } from "react";
|
||
import { useWebSocket } from "./useWebSocket";
|
||
import type { WebSocketEvent } from "@/types";
|
||
|
||
export function useRealtimeNotifications() {
|
||
const handleEvent = useCallback((event: WebSocketEvent) => {
|
||
switch (event.type) {
|
||
case "NotificationRequested":
|
||
// 新通知:触发 NotificationFeed 刷新
|
||
// urql 会自动重新请求(staleTime 到期或手动 invalidate)
|
||
// 这里通过 dispatchEvent 通知 NotificationFeed 组件
|
||
window.dispatchEvent(
|
||
new CustomEvent("realtime-notification", {
|
||
detail: event.notification,
|
||
}),
|
||
);
|
||
break;
|
||
|
||
case "GradeRecorded":
|
||
// 成绩发布:触发成绩页面刷新
|
||
window.dispatchEvent(
|
||
new CustomEvent("realtime-grade", {
|
||
detail: {
|
||
childId: event.childId,
|
||
examId: event.examId,
|
||
},
|
||
}),
|
||
);
|
||
break;
|
||
|
||
case "SchoolAnnouncement":
|
||
// 学校公告:显示全局通知
|
||
window.dispatchEvent(
|
||
new CustomEvent("realtime-announcement", {
|
||
detail: { title: event.title, body: event.body },
|
||
}),
|
||
);
|
||
break;
|
||
|
||
case "ChildBound":
|
||
// 子女绑定:刷新子女列表
|
||
window.dispatchEvent(new CustomEvent("realtime-child-bound"));
|
||
break;
|
||
|
||
case "ChildUnbound":
|
||
// 子女解绑:刷新子女列表
|
||
window.dispatchEvent(
|
||
new CustomEvent("realtime-child-unbound", {
|
||
detail: { childId: event.childId },
|
||
}),
|
||
);
|
||
break;
|
||
}
|
||
}, []);
|
||
|
||
const { status } = useWebSocket({
|
||
onEvent: handleEvent,
|
||
enabled: true,
|
||
});
|
||
|
||
return { status };
|
||
}
|