feat(teacher-portal): 完成参考项目差距闭环 P3-P7 全量实现

- P3 考试/作业/成绩 mutation + 详情页 + 批改界面 + 乐观更新 + 多 Tab 同步
- P4 知识图谱 SVG 可视化 + 学情分析仪表盘 + parent-portal Remote
- P5 WebSocket 通知中心 + AI 出题(SSE) + AI 教案 + AI 学情报告
- P6 可观测性硬化:Sentry + WebVitals + OTel + A11y + 性能配置 + Cookie 迁移
- P7 参考项目差距闭环:新增 35 个页面覆盖 13 个缺失模块
  - attendance(考勤 4 页)/questions(题库)/textbooks(教材 2 页)
  - classes/[id] 详情 + classes/schedule 课表
  - course-plans(2 页)/diagnostic(2 页)/error-book/practice
  - exams/[id]/build 组卷 + exams/[id]/analytics 考后分析
  - exams/[id]/edit-rich 富文本编辑 + exams/[id]/proctoring 监考
  - grades/entry 批量录入 + grades/stats 统计 + grades/analytics 分析 + grades/report-card 报告卡
  - homework/submissions 列表 + assignments/[id]/submissions 批量批改
  - homework/submissions/[submissionId] 单份批改 + scan-grading 扫描批改
  - lesson-plans 编辑器 + library + calendar + heatmap 5 页
  - elective 选修课 3 页 /leave 请假 /schedule-changes 调课
- P7 基础设施:61 GraphQL operations + 5 handlers + 13 fixtures + 11 viewports
- 集成 browser.ts/server.ts 注册所有 p7 handlers(fallthrough 顺序)
- viewports.ts 扩展 11 个新导航项
- 验证:tsc --noEmit 零错误 + eslint 零错误
- 文档:workline.md 新增 §5 P7 参考项目差距闭环(含完整文件清单)
This commit is contained in:
SpecialX
2026-07-13 14:27:04 +08:00
parent f13ca612e6
commit d49d211425
117 changed files with 30867 additions and 108 deletions

View File

@@ -1,29 +1,97 @@
"use client";
/**
* Settings 页面 - 个人设置
* Settings 页面 - 个人设置P3 启用编辑模式)
*
* 数据来源ARB-001 §1.2GraphQL MeQuery只读展示
* - P2 阶段仅展示当前用户信息P2 纯读,无 Mutation
* - P3+ 启用 UpdateUserMutation 后开启编辑功能
* 数据来源ARB-001 §1.2
* - GraphQL MeQuery读取当前用户信息
* - GraphQL UpdateUserMutationP3 扩展MSW mock更新姓名/邮箱
* - 编辑按钮切换只读/编辑模式
* - 乐观更新:本地立即展示新值;失败回滚由错误提示
*
* 维护者ai13teacher-portal
*/
import { useQuery } from "urql";
import { useState, useEffect } from "react";
import { useQuery, useMutation } from "urql";
import { Loading } from "@edu/ui-components";
import { MeQuery } from "@/lib/graphql";
import { MeQuery, UpdateUserMutation } from "@/lib/graphql";
export default function SettingsPage() {
const [result] = useQuery({ query: MeQuery });
const [result, reexecuteQuery] = useQuery({ query: MeQuery });
const [updateResult, updateUser] = useMutation(UpdateUserMutation);
const [editing, setEditing] = useState(false);
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
// 进入编辑模式时用当前用户数据预填表单
useEffect(() => {
if (editing && result.data?.me) {
setName(result.data.me.name);
setEmail(result.data.me.email);
setError(null);
}
}, [editing, result.data?.me]);
const handleStartEdit = () => {
setEditing(true);
};
const handleCancel = () => {
setEditing(false);
setError(null);
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
if (!name.trim()) {
setError("姓名不能为空");
return;
}
if (!email.trim() || !email.includes("@")) {
setError("邮箱格式不正确");
return;
}
setSubmitting(true);
// 乐观更新:本地立即显示新值(更新成功后 reexecuteQuery 拉取最新数据)
const res = await updateUser({
input: { name: name.trim(), email: email.trim() },
});
setSubmitting(false);
if (res.error) {
setError(res.error.message);
return;
}
// 成功后重新拉取 me 刷新展示
reexecuteQuery({ requestPolicy: "network-only" });
setEditing(false);
};
return (
<div className="px-10 py-10">
<header className="mb-8">
<h1 className="text-3xl font-serif text-ink"></h1>
<p className="mt-1 text-sm text-ink-muted">
GraphQL MeQuery · P2 P3
</p>
<header className="mb-8 flex items-baseline justify-between">
<div>
<h1 className="text-3xl font-serif text-ink"></h1>
<p className="mt-1 text-sm text-ink-muted">
GraphQL MeQuery + UpdateUserMutation · P3
</p>
</div>
{!editing && result.data?.me && (
<button
type="button"
onClick={handleStartEdit}
className="px-4 py-2 text-sm text-ink-on-accent bg-accent rounded-button hover:bg-accent-hover"
>
</button>
)}
</header>
<div className="rule-thin mb-8" />
@@ -38,6 +106,66 @@ export default function SettingsPage() {
</div>
) : !result.data?.me ? (
<p className="text-sm italic text-ink-muted"></p>
) : editing ? (
<section className="max-w-2xl">
<div className="mb-6">
<h2 className="text-xl font-serif text-ink mb-2"></h2>
<div className="rule-thin mb-4" />
</div>
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<label className="block text-tiny uppercase tracking-wide text-ink-muted mb-1">
<span className="text-danger">*</span>
</label>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
className="w-full px-3 py-2 bg-transparent border-b border-rule text-sm text-ink focus:outline-none focus:border-b-2"
required
/>
</div>
<div>
<label className="block text-tiny uppercase tracking-wide text-ink-muted mb-1">
<span className="text-danger">*</span>
</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full px-3 py-2 bg-transparent border-b border-rule text-sm text-ink focus:outline-none focus:border-b-2"
required
/>
</div>
{error && (
<div className="mark-left py-2 border-l-2 border-danger pl-md">
<p className="text-sm px-3 text-danger">{error}</p>
</div>
)}
<div className="flex items-center gap-4 pt-2">
<button
type="submit"
disabled={submitting}
className="px-4 py-2 text-sm text-ink-on-accent bg-accent rounded-button hover:bg-accent-hover disabled:opacity-50"
>
{submitting ? "保存中..." : "保存"}
</button>
<button
type="button"
onClick={handleCancel}
disabled={submitting}
className="text-tiny uppercase tracking-wide text-ink-muted hover:opacity-70 disabled:opacity-50"
>
</button>
{updateResult.data && !error && !submitting && (
<span className="text-tiny text-success"></span>
)}
</div>
</form>
</section>
) : (
<section className="max-w-2xl">
<div className="mb-6">
@@ -89,7 +217,7 @@ export default function SettingsPage() {
<div className="mt-8 p-4 border border-rule rounded-card bg-subtle">
<p className="text-sm text-ink-muted">
P2 updateUser Mutation P3
P3 UpdateUserMutation"编辑"
</p>
</div>
</section>