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:
@@ -1,29 +1,97 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* Settings 页面 - 个人设置
|
||||
* Settings 页面 - 个人设置(P3 启用编辑模式)
|
||||
*
|
||||
* 数据来源(ARB-001 §1.2):GraphQL MeQuery(只读展示)
|
||||
* - P2 阶段仅展示当前用户信息(P2 纯读,无 Mutation)
|
||||
* - P3+ 启用 UpdateUserMutation 后开启编辑功能
|
||||
* 数据来源(ARB-001 §1.2):
|
||||
* - GraphQL MeQuery(读取当前用户信息)
|
||||
* - GraphQL UpdateUserMutation(P3 扩展,MSW mock)更新姓名/邮箱
|
||||
* - 编辑按钮切换只读/编辑模式
|
||||
* - 乐观更新:本地立即展示新值;失败回滚由错误提示
|
||||
*
|
||||
* 维护者:ai13(teacher-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>
|
||||
|
||||
Reference in New Issue
Block a user