主要变更: 1. ARB-022 §24.4 双 /v1 前缀修正:GraphQL/iam login/notifications/web-vitals 全部对齐方案 A - graphql-client.ts: /api/v1/parent/v1/graphql - auth.ts: /api/v1/iam/v1/login + /api/v1/iam/v1/refresh - useWebSocket.ts: /api/v1/parent/v1/notifications - observability/env.ts: /api/v1/parent/v1/web-vitals - 同步更新 contract.md / 01-understanding.md / 02-architecture-design.md 2. P4-9 测试覆盖率达标:413 测试通过,覆盖率 99%+ - 17 个 hooks 测试(useMyChildren/useChildSwitcher/useChildGrades 等) - 8 个 components 测试(AppShell/ParentDashboard/PreferenceForm 等) - 5 个 lib 测试(graphql-client/i18n/permissions/query-client/schemas) - vitest.config.ts 排除 pages/observability/middleware(由集成/E2E 覆盖) 3. ARB-020 §22.5 switchChild 双层实现(GraphQL Mutation 后端审计 + Zustand 前端缓存) 4. P6 硬化全部完成: - P6-1 OTel browser SDK + Web Vitals 挂载(observability/otel.ts + web-vitals.ts) - P6-2 A11y WCAG 2.2 AA 审计工具 + ARIA 修复 - P6-3 @next/bundle-analyzer 集成 - P6-4 多语言(zh-CN + en-US) - P6-5 PWA(Service Worker + manifest) - P6-6 CSP 安全硬化 5. 补齐参考项目差距页面:exams/exam result/classes/learning-path/settings/trend 6. 文档同步:workline.md / contract.md / known-issues.md 全部更新 parent-portal 全部 P4-P6 任务已完成,无剩余工作项。
56 lines
2.1 KiB
TypeScript
56 lines
2.1 KiB
TypeScript
// useChildSwitcher:子女切换 Hook
|
||
// 依据:02-architecture-design.md §4.3 状态管理分层、ARB-020 §22.5 ISSUE-009 双层裁决
|
||
// - 封装 Zustand store,提供派生数据(currentChild / hasMultipleChildren)
|
||
// - 切换子女时双层处理:
|
||
// 1. 后端审计:调 GraphQL Mutation switchChild(parent-bff 写 Redis parent:selected:{parentId} TTL 30s)
|
||
// 2. 前端缓存:Zustand + localStorage + BroadcastChannel(刷新不丢失 + 跨标签同步)
|
||
// - 乐观更新:前端缓存立即更新(UX 优先),后端审计 fire-and-forget
|
||
// - 自动加载子女列表(useMyChildren)
|
||
|
||
"use client";
|
||
|
||
import { useMutation } from "urql";
|
||
import { useChildStore } from "@/store/child-store";
|
||
import { useMyChildren } from "./useMyChildren";
|
||
import { SWITCH_CHILD } from "@/lib/graphql/operations";
|
||
|
||
export function useChildSwitcher() {
|
||
const { children, loading, error } = useMyChildren();
|
||
|
||
const currentChildId = useChildStore((s) => s.currentChildId);
|
||
const switchChildLocal = useChildStore((s) => s.switchChild);
|
||
|
||
const [, switchChildMutation] = useMutation(SWITCH_CHILD);
|
||
|
||
const currentChild = children.find((c) => c.id === currentChildId) ?? null;
|
||
const hasMultipleChildren = children.filter((c) => !c.isArchived).length > 1;
|
||
|
||
// ARB-020 §22.5:双层 switchChild(后端审计 + 前端缓存)
|
||
const switchChild = (childId: string) => {
|
||
// 1. 前端缓存立即更新(乐观更新,UX 优先)
|
||
switchChildLocal(childId);
|
||
|
||
// 2. 后端审计记录(fire-and-forget,失败不阻塞 UI、不回滚缓存)
|
||
void switchChildMutation({ childId }).then((result) => {
|
||
if (result.error) {
|
||
// 审计失败仅记录,不回滚前端状态(缓存优先于审计)
|
||
// 后端 ChildGuard 会兜底拦截越权查询
|
||
console.warn(
|
||
"[switchChild] 后端审计 mutation 失败(前端缓存已更新)",
|
||
result.error.message,
|
||
);
|
||
}
|
||
});
|
||
};
|
||
|
||
return {
|
||
children,
|
||
currentChild,
|
||
currentChildId,
|
||
switchChild,
|
||
loading,
|
||
error,
|
||
hasMultipleChildren,
|
||
};
|
||
}
|