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,102 @@
// 登录页(独立模式 / MF 未启用时使用)
// 依据02-architecture-design.md §3.1 路由结构、ISSUE-004 登录走 REST
// MF 启用时由 Shell 提供登录;独立模式由本页提供
// mock 账号parent@example.com / password
"use client";
import { useState, type FormEvent } from "react";
import { useRouter } from "next/navigation";
import { login } from "@/lib/auth";
import { cn } from "@/lib/utils";
export default function LoginPage() {
const router = useRouter();
const [email, setEmail] = useState("parent@example.com");
const [password, setPassword] = useState("password");
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
async function handleSubmit(e: FormEvent<HTMLFormElement>) {
e.preventDefault();
setError(null);
setLoading(true);
try {
await login(email, password);
router.push("/parent/dashboard");
} catch (err) {
setError(err instanceof Error ? err.message : "登录失败");
} finally {
setLoading(false);
}
}
return (
<main className="flex min-h-screen items-center justify-center px-4">
<div className="w-full max-w-sm">
<header className="mb-8 text-center">
<h1 className="font-serif text-3xl">Edu </h1>
<p className="mt-2 text-sm text-ink-muted"></p>
</header>
<form
onSubmit={handleSubmit}
className="space-y-4 rounded-lg border border-rule bg-paper-elevated p-6"
aria-describedby={error ? "login-error" : undefined}
>
<div className="space-y-1">
<label htmlFor="email" className="block text-sm font-medium">
</label>
<input
id="email"
type="email"
required
autoComplete="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full rounded border border-rule bg-paper px-3 py-2 text-sm outline-none focus:border-accent"
/>
</div>
<div className="space-y-1">
<label htmlFor="password" className="block text-sm font-medium">
</label>
<input
id="password"
type="password"
required
autoComplete="current-password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full rounded border border-rule bg-paper px-3 py-2 text-sm outline-none focus:border-accent"
/>
</div>
{error && (
<p id="login-error" role="alert" className="text-sm text-danger">
{error}
</p>
)}
<button
type="submit"
disabled={loading}
className={cn(
"w-full rounded bg-accent px-4 py-2 text-sm font-medium text-paper",
"transition-opacity hover:opacity-90",
"disabled:cursor-not-allowed disabled:opacity-50",
)}
>
{loading ? "登录中..." : "登录"}
</button>
</form>
<p className="mt-4 text-center text-xs text-ink-muted">
mock parent@example.com / password
</p>
</div>
</main>
);
}