实现内容(仲裁裁决驱动,首次即最终方案): 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
103 lines
3.4 KiB
TypeScript
103 lines
3.4 KiB
TypeScript
// 登录页(独立模式 / 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>
|
||
);
|
||
}
|