feat(parent-portal): 完整实现 P4+P5+P6 家长端微前端

实现内容(仲裁裁决驱动,首次即最终方案):

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
This commit is contained in:
SpecialX
2026-07-10 17:40:27 +08:00
parent b54bfd101b
commit 5661938cc0
76 changed files with 9525 additions and 70 deletions

View File

@@ -0,0 +1,69 @@
// 客户端 ProvidersQueryClient + Urql + MSW 初始化
// 依据02-architecture-design.md §4.3 状态管理分层、§8 MSW
// - NEXT_PUBLIC_API_MOCKING=enabled 时启动 MSW worker
// - QueryClient 单例useState 保持稳定)
// - Urql client 单例
"use client";
import { useState, useEffect, type ReactNode } from "react";
import { QueryClientProvider } from "@tanstack/react-query";
import { Provider as UrqlProvider } from "urql";
import { createQueryClient } from "@/lib/query-client";
import { getGraphQLClient } from "@/lib/graphql-client";
const isMockingEnabled = process.env.NEXT_PUBLIC_API_MOCKING === "enabled";
interface ProvidersProps {
children: ReactNode;
}
export function Providers({ children }: ProvidersProps) {
const [queryClient] = useState(() => createQueryClient());
const [urqlClient] = useState(() => getGraphQLClient());
const [mswReady, setMswReady] = useState(!isMockingEnabled);
// MSW 初始化(仅浏览器 + mock 开启时)
useEffect(() => {
if (!isMockingEnabled || typeof window === "undefined") return;
let active = true;
(async () => {
try {
const { worker } = await import("@/test/mocks/browser");
await worker.start({
onUnhandledRequest: "bypass",
serviceWorker: {
url: "/mockServiceWorker.js",
},
});
if (active) setMswReady(true);
} catch (err) {
// MSW 启动失败不阻塞应用
console.warn("[MSW] 启动失败,将使用真实 API", err);
if (active) setMswReady(true);
}
})();
return () => {
active = false;
};
}, []);
// MSW 未就绪时显示加载态(避免 mock 数据未注入时闪烁)
if (!mswReady) {
return (
<div
role="status"
aria-live="polite"
className="flex min-h-screen items-center justify-center"
>
<span className="skeleton h-8 w-48 rounded" />
</div>
);
}
return (
<QueryClientProvider client={queryClient}>
<UrqlProvider value={urqlClient}>{children}</UrqlProvider>
</QueryClientProvider>
);
}