## 变更内容 ### P0 高优先级新页面 - /parent/leave: 请假表单(react-hook-form + Zod) + 历史列表(状态筛选) - /parent/grades/report-card: 报告卡(学年/学期筛选 + 打印 + 教师评语) - /parent/children/[studentId]: 子女详情聚合页(5 Tab: overview/homework/grades/exams/schedule) ### P1 中优先级新页面 - /parent/error-book: 错题本(5 项统计 + Top 错题 + 薄弱知识点) - /parent/diagnostic: 诊断报告(掌握度摘要 + 已发布诊断报告) - /parent/practice: 练习统计(4 项统计 + 练习历史) ### P2 低优先级新页面 - /parent/course-plans + [id]: 课程计划列表 + 详情(章节列表) - /parent/lesson-plans + [planId]/view: 备课列表(学科筛选) + 只读详情 - /parent/elective: 选修课(分类色点 + 状态徽标) ### P3 现有页面增强 - /parent/dashboard: ParentAttentionBanner + 多子女卡片网格 + 趋势图标 + 逾期高亮 - /parent/attendance: AttendanceRateCard + AttendanceWarningBanner + 月份导航 - /parent/grades: GrowthArchiveChart + ExportGradesButton + 班级均对比线 ### 基础设施 - types/index.ts: +20 新类型(LeaveRequest/ReportCard/ErrorBookStats/DiagnosticReport/PracticeStats/CoursePlan/LessonPlan/ElectiveSelection/ChildDetail/ScheduleItem 等) - operations.ts: +19 GraphQL operations(17 query + 2 mutation) - fixtures.ts: +18 mock 数据集 - handlers.ts: +19 MSW GraphQL handler ### 质量校验 - typecheck: 0 错误 - lint: 0 错误 - test: 71 文件 / 763 测试全部通过(从 413 增至 763, +350 测试) ### 文档 - workline.md: 新增 §7 参考项目(CICD)差距分析与实现安排 + §7.3-7.5 实现进度与覆盖完成度 ### 设计决策 - 保留当前"单子女切换"范式(MultiChildTabBar + useChildSwitcher),不迁移到"多子女同屏对比" - 仪表盘除外:已增强为多子女卡片网格并列展示 - 参考项目所有 11 个家长端页面功能已 100% 覆盖 Refs: ARB-020 §22, ARB-022 §24.4
144 lines
4.1 KiB
TypeScript
144 lines
4.1 KiB
TypeScript
// 仪表盘页面
|
||
// 依据:02-architecture-design.md §3.1 路由结构、§15 仪表盘设计
|
||
// 路由:/parent/dashboard
|
||
// - 多子女并列展示 ChildSummaryCard
|
||
// - 顶部关注横幅(逾期作业 / 出勤率低预警)
|
||
// - 当前选中子女的考勤日历
|
||
|
||
"use client";
|
||
|
||
import { useState, useCallback, useEffect } from "react";
|
||
import { useChildSwitcher } from "@/hooks/useChildSwitcher";
|
||
import { useChildSummary } from "@/hooks/useChildSummary";
|
||
import { useChildAttendance } from "@/hooks/useChildAttendance";
|
||
import { ChildSummaryCard } from "@/components/ChildSummaryCard";
|
||
import { AttendanceCalendar } from "@/components/AttendanceCalendar";
|
||
import { ParentAttentionBanner } from "@/components/ParentAttentionBanner";
|
||
import type { ChildInfo, ChildSummary } from "@/types";
|
||
|
||
export default function DashboardPage() {
|
||
const { children, currentChild, loading } = useChildSwitcher();
|
||
const { attendance, loading: attLoading } = useChildAttendance();
|
||
const [summaries, setSummaries] = useState<Record<string, ChildSummary>>({});
|
||
|
||
const handleSummaryLoaded = useCallback(
|
||
(childId: string, summary: ChildSummary) => {
|
||
setSummaries((prev) => {
|
||
if (prev[childId]?.avgScore === summary.avgScore) return prev;
|
||
return { ...prev, [childId]: summary };
|
||
});
|
||
},
|
||
[],
|
||
);
|
||
|
||
if (loading) {
|
||
return (
|
||
<div className="space-y-4">
|
||
<span className="skeleton h-8 w-full rounded" />
|
||
<span className="skeleton h-32 w-full rounded" />
|
||
<span className="skeleton h-48 w-full rounded" />
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (children.length === 0) {
|
||
return (
|
||
<p className="rounded border border-rule bg-paper-elevated p-4 text-sm text-ink-muted">
|
||
暂无绑定子女
|
||
</p>
|
||
);
|
||
}
|
||
|
||
const attentionItems = children.map((c) => ({
|
||
childId: c.id,
|
||
childName: c.name,
|
||
pendingHomeworkCount: summaries[c.id]?.pendingHomeworkCount ?? 0,
|
||
attendanceRate: summaries[c.id]?.attendanceRate ?? 1,
|
||
}));
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
<ParentAttentionBanner items={attentionItems} />
|
||
|
||
<h1 className="font-serif text-2xl">学习概览</h1>
|
||
|
||
{/* 多子女并列卡片 */}
|
||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
||
{children.map((child) => (
|
||
<ChildDashboardCard
|
||
key={child.id}
|
||
child={child}
|
||
onSummaryLoaded={handleSummaryLoaded}
|
||
/>
|
||
))}
|
||
</div>
|
||
|
||
{/* 当前选中子女的考勤日历 */}
|
||
{currentChild && (
|
||
<div>
|
||
<h2 className="mb-3 font-serif text-lg">{currentChild.name}的考勤</h2>
|
||
{attLoading ? (
|
||
<span className="skeleton h-64 w-full rounded" />
|
||
) : (
|
||
<AttendanceCalendar records={attendance} />
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// 子女仪表盘卡片:独立获取每个子女的 summary
|
||
function ChildDashboardCard({
|
||
child,
|
||
onSummaryLoaded,
|
||
}: {
|
||
child: ChildInfo;
|
||
onSummaryLoaded: (childId: string, summary: ChildSummary) => void;
|
||
}) {
|
||
const { summary, loading, error } = useChildSummary(child.id);
|
||
|
||
useEffect(() => {
|
||
if (summary) {
|
||
onSummaryLoaded(child.id, summary);
|
||
}
|
||
}, [summary, child.id, onSummaryLoaded]);
|
||
|
||
if (loading) {
|
||
return (
|
||
<div className="space-y-4">
|
||
<h2 className="font-serif text-lg">{child.name}</h2>
|
||
<span className="skeleton h-32 w-full rounded" />
|
||
<span className="skeleton h-48 w-full rounded" />
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (error) {
|
||
return (
|
||
<div>
|
||
<h2 className="mb-2 font-serif text-lg">{child.name}</h2>
|
||
<div className="rounded border border-danger p-4 text-sm text-danger">
|
||
加载失败:{error.message}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (!summary) {
|
||
return (
|
||
<div>
|
||
<h2 className="mb-2 font-serif text-lg">{child.name}</h2>
|
||
<p className="text-sm text-ink-muted">暂无数据</p>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div>
|
||
<h2 className="mb-3 font-serif text-lg">{child.name}</h2>
|
||
<ChildSummaryCard summary={summary} />
|
||
</div>
|
||
);
|
||
}
|