主要变更: 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 任务已完成,无剩余工作项。
108 lines
3.4 KiB
TypeScript
108 lines
3.4 KiB
TypeScript
// 客户端 Providers:QueryClient + Urql + MSW + ErrorBoundary + SW + OTel + WebVitals
|
||
// 依据:02-architecture-design.md §4.3 状态管理分层、§8 MSW、§12 可观测性
|
||
// - NEXT_PUBLIC_API_MOCKING=enabled 时启动 MSW worker
|
||
// - QueryClient 单例(useState 保持稳定)
|
||
// - Urql client 单例
|
||
// - ErrorBoundary 包裹全局渲染异常兜底
|
||
// - Service Worker 注册(PWA 离线缓存,P6-5)
|
||
// - OTel browser SDK 初始化(P6-1,条件启用)
|
||
// - Web Vitals 采集初始化(P6-1,生产环境)
|
||
|
||
"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";
|
||
import { ErrorBoundary } from "@/components/ErrorBoundary";
|
||
import { WebVitalsInitializer } from "@/components/WebVitalsInitializer";
|
||
|
||
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;
|
||
};
|
||
}, []);
|
||
|
||
// Service Worker 注册(PWA,P6-5)
|
||
useEffect(() => {
|
||
if (typeof window === "undefined") return;
|
||
if (process.env.NODE_ENV !== "production") return;
|
||
if (!("serviceWorker" in navigator)) return;
|
||
|
||
const registerSW = async () => {
|
||
try {
|
||
// MSW 启用时不注册 PWA Service Worker(避免冲突)
|
||
if (isMockingEnabled) return;
|
||
await navigator.serviceWorker.register("/sw.js", { scope: "/" });
|
||
} catch {
|
||
// SW 注册失败不阻塞应用
|
||
}
|
||
};
|
||
registerSW();
|
||
}, []);
|
||
|
||
// OTel browser SDK 初始化(P6-1,条件启用:NEXT_PUBLIC_OTEL_ENABLED=true)
|
||
useEffect(() => {
|
||
if (typeof window === "undefined") return;
|
||
if (process.env.NODE_ENV !== "production") return;
|
||
void import("@/lib/observability/otel").then(({ initOTel }) => {
|
||
void initOTel();
|
||
});
|
||
}, []);
|
||
|
||
// 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 (
|
||
<ErrorBoundary>
|
||
<QueryClientProvider client={queryClient}>
|
||
<UrqlProvider value={urqlClient}>
|
||
<WebVitalsInitializer />
|
||
{children}
|
||
</UrqlProvider>
|
||
</QueryClientProvider>
|
||
</ErrorBoundary>
|
||
);
|
||
}
|