From 18d94c0cc2723f5c0e1b13fd85b0ff6b25dc817c Mon Sep 17 00:00:00 2001 From: SpecialX <47072643+wangxiner55@users.noreply.github.com> Date: Mon, 13 Jul 2026 15:12:56 +0800 Subject: [PATCH] =?UTF-8?q?feat(parent-portal):=20=E5=AE=8C=E6=88=90?= =?UTF-8?q?=E5=8F=82=E8=80=83=E9=A1=B9=E7=9B=AE(CICD)=E5=AE=B6=E9=95=BF?= =?UTF-8?q?=E7=AB=AF=E5=8A=9F=E8=83=BD=E5=85=A8=E9=87=8F=E8=A6=86=E7=9B=96?= =?UTF-8?q?=20-=209=20=E6=96=B0=E9=A1=B5=E9=9D=A2=20+=203=20=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2=E5=A2=9E=E5=BC=BA=20+=20763=20=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E9=80=9A=E8=BF=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 变更内容 ### 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 --- .../src/app/parent/attendance/page.tsx | 36 +- .../app/parent/children/[studentId]/page.tsx | 131 ++++ .../src/app/parent/course-plans/[id]/page.tsx | 76 ++ .../src/app/parent/course-plans/page.tsx | 46 ++ .../src/app/parent/dashboard/page.tsx | 140 +++- .../src/app/parent/diagnostic/page.tsx | 60 ++ .../src/app/parent/elective/page.tsx | 43 ++ .../src/app/parent/error-book/page.tsx | 127 +++ .../src/app/parent/grades/page.tsx | 28 +- .../app/parent/grades/report-card/page.tsx | 106 +++ .../src/app/parent/leave/page.tsx | 44 ++ .../lesson-plans/[planId]/view/page.tsx | 76 ++ .../src/app/parent/lesson-plans/page.tsx | 81 ++ .../src/app/parent/practice/page.tsx | 60 ++ .../components/AttendanceCalendar.test.tsx | 87 ++- .../src/components/AttendanceCalendar.tsx | 68 +- .../components/AttendanceRateCard.test.tsx | 152 ++++ .../src/components/AttendanceRateCard.tsx | 68 ++ .../AttendanceWarningBanner.test.tsx | 124 +++ .../components/AttendanceWarningBanner.tsx | 125 +++ .../src/components/ChildDetailPanel.test.tsx | 416 ++++++++++ .../src/components/ChildDetailPanel.tsx | 324 ++++++++ .../src/components/ChildGradeChart.test.tsx | 54 +- .../src/components/ChildGradeChart.tsx | 18 +- .../src/components/ChildSummaryCard.test.tsx | 71 +- .../src/components/ChildSummaryCard.tsx | 143 ++-- .../src/components/CoursePlanDetail.test.tsx | 115 +++ .../src/components/CoursePlanDetail.tsx | 125 +++ .../src/components/CoursePlanList.test.tsx | 129 ++++ .../src/components/CoursePlanList.tsx | 115 +++ .../components/DiagnosticReportList.test.tsx | 217 ++++++ .../src/components/DiagnosticReportList.tsx | 140 ++++ .../components/ElectiveSelectionList.test.tsx | 131 ++++ .../src/components/ElectiveSelectionList.tsx | 126 +++ .../components/ErrorBookStatsCard.test.tsx | 101 +++ .../src/components/ErrorBookStatsCard.tsx | 97 +++ .../components/ExportGradesButton.test.tsx | 205 +++++ .../src/components/ExportGradesButton.tsx | 66 ++ .../components/GrowthArchiveChart.test.tsx | 145 ++++ .../src/components/GrowthArchiveChart.tsx | 80 ++ .../src/components/LeaveRequestForm.test.tsx | 296 +++++++ .../src/components/LeaveRequestForm.tsx | 214 ++++++ .../src/components/LeaveRequestList.test.tsx | 208 +++++ .../src/components/LeaveRequestList.tsx | 138 ++++ .../src/components/LessonPlanList.test.tsx | 112 +++ .../src/components/LessonPlanList.tsx | 117 +++ .../LessonPlanReadonlyView.test.tsx | 127 +++ .../src/components/LessonPlanReadonlyView.tsx | 133 ++++ .../components/MasterySummaryCard.test.tsx | 134 ++++ .../src/components/MasterySummaryCard.tsx | 96 +++ .../components/ParentAttentionBanner.test.tsx | 152 ++++ .../src/components/ParentAttentionBanner.tsx | 105 +++ .../components/PracticeSessionList.test.tsx | 252 ++++++ .../src/components/PracticeSessionList.tsx | 125 +++ .../src/components/PracticeStatsCard.test.tsx | 116 +++ .../src/components/PracticeStatsCard.tsx | 82 ++ .../src/components/ReportCardView.test.tsx | 232 ++++++ .../src/components/ReportCardView.tsx | 119 +++ .../components/TopWrongQuestionList.test.tsx | 158 ++++ .../src/components/TopWrongQuestionList.tsx | 92 +++ .../src/hooks/useAcademicYears.test.tsx | 84 ++ .../src/hooks/useAcademicYears.ts | 28 + .../src/hooks/useChildAttendance.ts | 40 +- .../hooks/useChildCoursePlanDetail.test.tsx | 131 ++++ .../src/hooks/useChildCoursePlanDetail.ts | 34 + .../src/hooks/useChildCoursePlans.test.tsx | 121 +++ .../src/hooks/useChildCoursePlans.ts | 34 + .../src/hooks/useChildDetail.test.tsx | 144 ++++ .../parent-portal/src/hooks/useChildDetail.ts | 30 + .../src/hooks/useChildDiagnostic.test.tsx | 149 ++++ .../src/hooks/useChildDiagnostic.ts | 49 ++ .../src/hooks/useChildElective.test.tsx | 120 +++ .../src/hooks/useChildElective.ts | 34 + .../src/hooks/useChildErrorBook.test.tsx | 174 +++++ .../src/hooks/useChildErrorBook.ts | 65 ++ .../src/hooks/useChildGrowthArchive.test.tsx | 206 +++++ .../src/hooks/useChildGrowthArchive.ts | 47 ++ .../src/hooks/useChildLeaveRequests.test.tsx | 142 ++++ .../src/hooks/useChildLeaveRequests.ts | 35 + .../hooks/useChildLessonPlanDetail.test.tsx | 133 ++++ .../src/hooks/useChildLessonPlanDetail.ts | 34 + .../src/hooks/useChildLessonPlans.test.tsx | 163 ++++ .../src/hooks/useChildLessonPlans.ts | 34 + .../src/hooks/useChildPractice.test.tsx | 147 ++++ .../src/hooks/useChildPractice.ts | 49 ++ .../src/hooks/useChildSummary.ts | 7 +- .../src/hooks/useCreateLeaveRequest.test.tsx | 143 ++++ .../src/hooks/useCreateLeaveRequest.ts | 43 ++ .../src/hooks/useExportChildGrades.test.tsx | 170 ++++ .../src/hooks/useExportChildGrades.ts | 57 ++ .../src/hooks/useReportCard.test.tsx | 157 ++++ apps/parent-portal/src/hooks/useReportCard.ts | 34 + .../src/lib/graphql/operations.ts | 372 +++++++++ .../src/lib/schemas/leave-request.ts | 27 + apps/parent-portal/src/test/mocks/fixtures.ts | 723 ++++++++++++++++++ apps/parent-portal/src/test/mocks/handlers.ts | 249 +++++- apps/parent-portal/src/types/index.ts | 270 +++++++ .../worklines/parent-portal_workline.md | 106 ++- 98 files changed, 11977 insertions(+), 112 deletions(-) create mode 100644 apps/parent-portal/src/app/parent/children/[studentId]/page.tsx create mode 100644 apps/parent-portal/src/app/parent/course-plans/[id]/page.tsx create mode 100644 apps/parent-portal/src/app/parent/course-plans/page.tsx create mode 100644 apps/parent-portal/src/app/parent/diagnostic/page.tsx create mode 100644 apps/parent-portal/src/app/parent/elective/page.tsx create mode 100644 apps/parent-portal/src/app/parent/error-book/page.tsx create mode 100644 apps/parent-portal/src/app/parent/grades/report-card/page.tsx create mode 100644 apps/parent-portal/src/app/parent/leave/page.tsx create mode 100644 apps/parent-portal/src/app/parent/lesson-plans/[planId]/view/page.tsx create mode 100644 apps/parent-portal/src/app/parent/lesson-plans/page.tsx create mode 100644 apps/parent-portal/src/app/parent/practice/page.tsx create mode 100644 apps/parent-portal/src/components/AttendanceRateCard.test.tsx create mode 100644 apps/parent-portal/src/components/AttendanceRateCard.tsx create mode 100644 apps/parent-portal/src/components/AttendanceWarningBanner.test.tsx create mode 100644 apps/parent-portal/src/components/AttendanceWarningBanner.tsx create mode 100644 apps/parent-portal/src/components/ChildDetailPanel.test.tsx create mode 100644 apps/parent-portal/src/components/ChildDetailPanel.tsx create mode 100644 apps/parent-portal/src/components/CoursePlanDetail.test.tsx create mode 100644 apps/parent-portal/src/components/CoursePlanDetail.tsx create mode 100644 apps/parent-portal/src/components/CoursePlanList.test.tsx create mode 100644 apps/parent-portal/src/components/CoursePlanList.tsx create mode 100644 apps/parent-portal/src/components/DiagnosticReportList.test.tsx create mode 100644 apps/parent-portal/src/components/DiagnosticReportList.tsx create mode 100644 apps/parent-portal/src/components/ElectiveSelectionList.test.tsx create mode 100644 apps/parent-portal/src/components/ElectiveSelectionList.tsx create mode 100644 apps/parent-portal/src/components/ErrorBookStatsCard.test.tsx create mode 100644 apps/parent-portal/src/components/ErrorBookStatsCard.tsx create mode 100644 apps/parent-portal/src/components/ExportGradesButton.test.tsx create mode 100644 apps/parent-portal/src/components/ExportGradesButton.tsx create mode 100644 apps/parent-portal/src/components/GrowthArchiveChart.test.tsx create mode 100644 apps/parent-portal/src/components/GrowthArchiveChart.tsx create mode 100644 apps/parent-portal/src/components/LeaveRequestForm.test.tsx create mode 100644 apps/parent-portal/src/components/LeaveRequestForm.tsx create mode 100644 apps/parent-portal/src/components/LeaveRequestList.test.tsx create mode 100644 apps/parent-portal/src/components/LeaveRequestList.tsx create mode 100644 apps/parent-portal/src/components/LessonPlanList.test.tsx create mode 100644 apps/parent-portal/src/components/LessonPlanList.tsx create mode 100644 apps/parent-portal/src/components/LessonPlanReadonlyView.test.tsx create mode 100644 apps/parent-portal/src/components/LessonPlanReadonlyView.tsx create mode 100644 apps/parent-portal/src/components/MasterySummaryCard.test.tsx create mode 100644 apps/parent-portal/src/components/MasterySummaryCard.tsx create mode 100644 apps/parent-portal/src/components/ParentAttentionBanner.test.tsx create mode 100644 apps/parent-portal/src/components/ParentAttentionBanner.tsx create mode 100644 apps/parent-portal/src/components/PracticeSessionList.test.tsx create mode 100644 apps/parent-portal/src/components/PracticeSessionList.tsx create mode 100644 apps/parent-portal/src/components/PracticeStatsCard.test.tsx create mode 100644 apps/parent-portal/src/components/PracticeStatsCard.tsx create mode 100644 apps/parent-portal/src/components/ReportCardView.test.tsx create mode 100644 apps/parent-portal/src/components/ReportCardView.tsx create mode 100644 apps/parent-portal/src/components/TopWrongQuestionList.test.tsx create mode 100644 apps/parent-portal/src/components/TopWrongQuestionList.tsx create mode 100644 apps/parent-portal/src/hooks/useAcademicYears.test.tsx create mode 100644 apps/parent-portal/src/hooks/useAcademicYears.ts create mode 100644 apps/parent-portal/src/hooks/useChildCoursePlanDetail.test.tsx create mode 100644 apps/parent-portal/src/hooks/useChildCoursePlanDetail.ts create mode 100644 apps/parent-portal/src/hooks/useChildCoursePlans.test.tsx create mode 100644 apps/parent-portal/src/hooks/useChildCoursePlans.ts create mode 100644 apps/parent-portal/src/hooks/useChildDetail.test.tsx create mode 100644 apps/parent-portal/src/hooks/useChildDetail.ts create mode 100644 apps/parent-portal/src/hooks/useChildDiagnostic.test.tsx create mode 100644 apps/parent-portal/src/hooks/useChildDiagnostic.ts create mode 100644 apps/parent-portal/src/hooks/useChildElective.test.tsx create mode 100644 apps/parent-portal/src/hooks/useChildElective.ts create mode 100644 apps/parent-portal/src/hooks/useChildErrorBook.test.tsx create mode 100644 apps/parent-portal/src/hooks/useChildErrorBook.ts create mode 100644 apps/parent-portal/src/hooks/useChildGrowthArchive.test.tsx create mode 100644 apps/parent-portal/src/hooks/useChildGrowthArchive.ts create mode 100644 apps/parent-portal/src/hooks/useChildLeaveRequests.test.tsx create mode 100644 apps/parent-portal/src/hooks/useChildLeaveRequests.ts create mode 100644 apps/parent-portal/src/hooks/useChildLessonPlanDetail.test.tsx create mode 100644 apps/parent-portal/src/hooks/useChildLessonPlanDetail.ts create mode 100644 apps/parent-portal/src/hooks/useChildLessonPlans.test.tsx create mode 100644 apps/parent-portal/src/hooks/useChildLessonPlans.ts create mode 100644 apps/parent-portal/src/hooks/useChildPractice.test.tsx create mode 100644 apps/parent-portal/src/hooks/useChildPractice.ts create mode 100644 apps/parent-portal/src/hooks/useCreateLeaveRequest.test.tsx create mode 100644 apps/parent-portal/src/hooks/useCreateLeaveRequest.ts create mode 100644 apps/parent-portal/src/hooks/useExportChildGrades.test.tsx create mode 100644 apps/parent-portal/src/hooks/useExportChildGrades.ts create mode 100644 apps/parent-portal/src/hooks/useReportCard.test.tsx create mode 100644 apps/parent-portal/src/hooks/useReportCard.ts create mode 100644 apps/parent-portal/src/lib/schemas/leave-request.ts diff --git a/apps/parent-portal/src/app/parent/attendance/page.tsx b/apps/parent-portal/src/app/parent/attendance/page.tsx index 9970c7c..fa97b2e 100644 --- a/apps/parent-portal/src/app/parent/attendance/page.tsx +++ b/apps/parent-portal/src/app/parent/attendance/page.tsx @@ -1,16 +1,35 @@ // 考勤页面 // 依据:02-architecture-design.md §3.1 路由结构 // 路由:/parent/attendance +// - 月份导航(前一月/后一月切换) +// - 聚合出勤率卡片 + 异常预警横幅 +// - 月度考勤日历 "use client"; +import { useState, useMemo } from "react"; import { useChildAttendance } from "@/hooks/useChildAttendance"; import { useChildSwitcher } from "@/hooks/useChildSwitcher"; import { AttendanceCalendar } from "@/components/AttendanceCalendar"; +import { AttendanceRateCard } from "@/components/AttendanceRateCard"; +import { AttendanceWarningBanner } from "@/components/AttendanceWarningBanner"; + +function getCurrentMonth(): string { + const now = new Date(); + return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}`; +} export default function AttendancePage() { const { currentChild } = useChildSwitcher(); - const { attendance, loading, error } = useChildAttendance(); + const [currentMonth, setCurrentMonth] = useState(getCurrentMonth); + const { attendance, loading, error } = useChildAttendance({ + month: currentMonth, + }); + + const monthLabel = useMemo(() => { + const [year, month] = currentMonth.split("-"); + return `${year ?? ""}年${parseInt(month ?? "1", 10)}月`; + }, [currentMonth]); if (loading) { return ; @@ -27,7 +46,20 @@ export default function AttendancePage() { return (

{currentChild?.name}的考勤

- + + + + + + + +

+ 当前查看:{monthLabel} +

); } diff --git a/apps/parent-portal/src/app/parent/children/[studentId]/page.tsx b/apps/parent-portal/src/app/parent/children/[studentId]/page.tsx new file mode 100644 index 0000000..54ceb5e --- /dev/null +++ b/apps/parent-portal/src/app/parent/children/[studentId]/page.tsx @@ -0,0 +1,131 @@ +// 子女详情聚合页 +// 依据:02-architecture-design.md §3.1 路由结构 +// 路由:/parent/children/[studentId] +// 头部:面包屑 + 学生信息;主体:Tab 面板(nuqs 管理 ?tab=) + +"use client"; + +import Link from "next/link"; +import { useParams } from "next/navigation"; +import { useQueryState, parseAsString } from "nuqs"; +import { useChildDetail } from "@/hooks/useChildDetail"; +import { useChildSwitcher } from "@/hooks/useChildSwitcher"; +import { + ChildDetailPanel, + type DetailTab, +} from "@/components/ChildDetailPanel"; +import { cn } from "@/lib/utils"; + +const VALID_TABS: DetailTab[] = [ + "overview", + "homework", + "grades", + "exams", + "schedule", +]; + +function isDetailTab(value: string): value is DetailTab { + return (VALID_TABS as string[]).includes(value); +} + +export default function ChildDetailPage(): JSX.Element { + const params = useParams(); + const studentId = + typeof params?.studentId === "string" + ? params.studentId + : Array.isArray(params?.studentId) + ? (params.studentId[0] ?? "") + : ""; + + const { children } = useChildSwitcher(); + const { childDetail, loading, error } = useChildDetail(studentId); + + // nuqs 管理 ?tab= 参数 + const [tabStr, setTabStr] = useQueryState( + "tab", + parseAsString.withDefault("overview"), + ); + const activeTab: DetailTab = isDetailTab(tabStr) ? tabStr : "overview"; + + // 兄弟姐妹(除当前子女外) + const siblings = children.filter((c) => c.id !== studentId); + + return ( +
+ {/* 面包屑 */} + + + {/* 头部:学生信息 */} +
+ {loading ? ( + + ) : error ? ( +
+ 加载失败:{error.message} +
+ ) : !childDetail ? ( +

+ 暂无子女详情数据 +

+ ) : ( +
+ +
+

+ {childDetail.basicInfo.name} +

+

+ {childDetail.basicInfo.className} · {childDetail.basicInfo.schoolName} · 关系:{childDetail.basicInfo.relation} +

+
+
+ )} +
+ + {/* 兄弟姐妹切换 */} + {siblings.length > 0 && ( +
+ 其他子女: + {siblings.map((s) => ( + + {s.name} + + ))} +
+ )} + + {/* Tab 面板 */} + {childDetail && ( + setTabStr(t)} + /> + )} +
+ ); +} diff --git a/apps/parent-portal/src/app/parent/course-plans/[id]/page.tsx b/apps/parent-portal/src/app/parent/course-plans/[id]/page.tsx new file mode 100644 index 0000000..04f821c --- /dev/null +++ b/apps/parent-portal/src/app/parent/course-plans/[id]/page.tsx @@ -0,0 +1,76 @@ +// 课程计划详情页面(家长查看子女课程计划详情) +// 依据:02-architecture-design.md §3.1 路由结构 +// 路由:/parent/course-plans/[id] + +"use client"; + +import Link from "next/link"; +import { useParams } from "next/navigation"; +import { useChildCoursePlanDetail } from "@/hooks/useChildCoursePlanDetail"; +import { useChildSwitcher } from "@/hooks/useChildSwitcher"; +import { CoursePlanDetail } from "@/components/CoursePlanDetail"; + +export default function CoursePlanDetailPage(): JSX.Element { + const params = useParams(); + const planId = + typeof params?.id === "string" + ? params.id + : Array.isArray(params?.id) + ? (params.id[0] ?? "") + : ""; + const { currentChild } = useChildSwitcher(); + const { coursePlan, loading, error } = useChildCoursePlanDetail(planId); + + if (loading) { + return ( +
+
+ ); + } + + if (error) { + return ( +
+ 加载失败:{error.message} +
+ ); + } + + if (!coursePlan) { + return ( +
+ + ← 返回课程计划 + +

+ 暂无课程计划数据 +

+
+ ); + } + + return ( +
+
+

{currentChild?.name}的课程计划

+ + ← 返回课程计划 + +
+ +
+ ); +} diff --git a/apps/parent-portal/src/app/parent/course-plans/page.tsx b/apps/parent-portal/src/app/parent/course-plans/page.tsx new file mode 100644 index 0000000..6b4da95 --- /dev/null +++ b/apps/parent-portal/src/app/parent/course-plans/page.tsx @@ -0,0 +1,46 @@ +// 课程计划列表页面(家长查看子女课程计划) +// 依据:02-architecture-design.md §3.1 路由结构 +// 路由:/parent/course-plans + +"use client"; + +import { useChildCoursePlans } from "@/hooks/useChildCoursePlans"; +import { useChildSwitcher } from "@/hooks/useChildSwitcher"; +import { CoursePlanList } from "@/components/CoursePlanList"; + +export default function CoursePlansPage(): JSX.Element { + const { currentChild } = useChildSwitcher(); + const { coursePlans, loading, error } = useChildCoursePlans(); + + if (loading) { + return ( +
+
+ ); + } + + if (error) { + return ( +
+ 加载失败:{error.message} +
+ ); + } + + return ( +
+

{currentChild?.name}的课程计划

+ +
+ ); +} diff --git a/apps/parent-portal/src/app/parent/dashboard/page.tsx b/apps/parent-portal/src/app/parent/dashboard/page.tsx index 500fb34..33bc5d9 100644 --- a/apps/parent-portal/src/app/parent/dashboard/page.tsx +++ b/apps/parent-portal/src/app/parent/dashboard/page.tsx @@ -1,9 +1,143 @@ // 仪表盘页面 -// 依据:02-architecture-design.md §3.1 路由结构 +// 依据:02-architecture-design.md §3.1 路由结构、§15 仪表盘设计 // 路由:/parent/dashboard +// - 多子女并列展示 ChildSummaryCard +// - 顶部关注横幅(逾期作业 / 出勤率低预警) +// - 当前选中子女的考勤日历 -import { ParentDashboard } from "@/components/ParentDashboard"; +"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() { - return ; + const { children, currentChild, loading } = useChildSwitcher(); + const { attendance, loading: attLoading } = useChildAttendance(); + const [summaries, setSummaries] = useState>({}); + + const handleSummaryLoaded = useCallback( + (childId: string, summary: ChildSummary) => { + setSummaries((prev) => { + if (prev[childId]?.avgScore === summary.avgScore) return prev; + return { ...prev, [childId]: summary }; + }); + }, + [], + ); + + if (loading) { + return ( +
+ + + +
+ ); + } + + if (children.length === 0) { + return ( +

+ 暂无绑定子女 +

+ ); + } + + const attentionItems = children.map((c) => ({ + childId: c.id, + childName: c.name, + pendingHomeworkCount: summaries[c.id]?.pendingHomeworkCount ?? 0, + attendanceRate: summaries[c.id]?.attendanceRate ?? 1, + })); + + return ( +
+ + +

学习概览

+ + {/* 多子女并列卡片 */} +
+ {children.map((child) => ( + + ))} +
+ + {/* 当前选中子女的考勤日历 */} + {currentChild && ( +
+

{currentChild.name}的考勤

+ {attLoading ? ( + + ) : ( + + )} +
+ )} +
+ ); +} + +// 子女仪表盘卡片:独立获取每个子女的 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 ( +
+

{child.name}

+ + +
+ ); + } + + if (error) { + return ( +
+

{child.name}

+
+ 加载失败:{error.message} +
+
+ ); + } + + if (!summary) { + return ( +
+

{child.name}

+

暂无数据

+
+ ); + } + + return ( +
+

{child.name}

+ +
+ ); } diff --git a/apps/parent-portal/src/app/parent/diagnostic/page.tsx b/apps/parent-portal/src/app/parent/diagnostic/page.tsx new file mode 100644 index 0000000..080a17d --- /dev/null +++ b/apps/parent-portal/src/app/parent/diagnostic/page.tsx @@ -0,0 +1,60 @@ +// 诊断报告页面 +// 依据:02-architecture-design.md §3.1 路由结构 +// 路由:/parent/diagnostic + +"use client"; + +import { useChildDiagnostic } from "@/hooks/useChildDiagnostic"; +import { useChildSwitcher } from "@/hooks/useChildSwitcher"; +import { MasterySummaryCard } from "@/components/MasterySummaryCard"; +import { DiagnosticReportList } from "@/components/DiagnosticReportList"; + +export default function DiagnosticPage() { + const { currentChild } = useChildSwitcher(); + const { masterySummary, reports, loading, error } = useChildDiagnostic(); + + if (loading) { + return ( +
+
+ ); + } + + if (error) { + return ( +
+ 加载失败:{error.message} +
+ ); + } + + const hasData = masterySummary !== null || reports.length > 0; + + return ( +
+

{currentChild?.name}的诊断报告

+ + {!hasData ? ( +

+ 暂无诊断数据 +

+ ) : ( + <> + {masterySummary && } + +
+

已发布诊断报告

+ +
+ + )} +
+ ); +} diff --git a/apps/parent-portal/src/app/parent/elective/page.tsx b/apps/parent-portal/src/app/parent/elective/page.tsx new file mode 100644 index 0000000..e9a9b50 --- /dev/null +++ b/apps/parent-portal/src/app/parent/elective/page.tsx @@ -0,0 +1,43 @@ +// 选修课页面(家长查看子女选修课) +// 依据:02-architecture-design.md §3.1 路由结构 +// 路由:/parent/elective + +"use client"; + +import { useChildElective } from "@/hooks/useChildElective"; +import { useChildSwitcher } from "@/hooks/useChildSwitcher"; +import { ElectiveSelectionList } from "@/components/ElectiveSelectionList"; + +export default function ElectivePage(): JSX.Element { + const { currentChild } = useChildSwitcher(); + const { electiveSelections, loading, error } = useChildElective(); + + if (loading) { + return ( +
+
+ ); + } + + if (error) { + return ( +
+ 加载失败:{error.message} +
+ ); + } + + return ( +
+

{currentChild?.name}的选修课

+ +
+ ); +} diff --git a/apps/parent-portal/src/app/parent/error-book/page.tsx b/apps/parent-portal/src/app/parent/error-book/page.tsx new file mode 100644 index 0000000..11b99ce --- /dev/null +++ b/apps/parent-portal/src/app/parent/error-book/page.tsx @@ -0,0 +1,127 @@ +// 错题本页面 +// 依据:02-architecture-design.md §3.1 路由结构 +// 路由:/parent/error-book + +"use client"; + +import { useChildErrorBook } from "@/hooks/useChildErrorBook"; +import { useChildSwitcher } from "@/hooks/useChildSwitcher"; +import { ErrorBookStatsCard } from "@/components/ErrorBookStatsCard"; +import { TopWrongQuestionList } from "@/components/TopWrongQuestionList"; +import { cn, formatPercent } from "@/lib/utils"; + +export default function ErrorBookPage() { + const { currentChild } = useChildSwitcher(); + const { stats, topWrongQuestions, weakKps, loading, error } = + useChildErrorBook(); + + if (loading) { + return ( +
+
+ ); + } + + if (error) { + return ( +
+ 加载失败:{error.message} +
+ ); + } + + const hasData = + stats !== null || + topWrongQuestions.length > 0 || + weakKps.length > 0; + + return ( +
+

{currentChild?.name}的错题本

+ + {!hasData ? ( +

+ 暂无错题数据 +

+ ) : ( + <> + {stats && } + +
+

高频错题

+ +
+ +
+

薄弱知识点

+ {weakKps.length === 0 ? ( +

+ 暂无薄弱知识点 +

+ ) : ( +
    + {weakKps.map((kp) => { + const masteryPercent = Math.round(kp.masteryRate * 100); + const isWeak = kp.masteryRate < 0.6; + return ( +
  • +
    +
    +

    + {kp.knowledgePointName} +

    +

    + {kp.subject} +

    +
    +
    +

    + 错 {kp.errorCount} 次 +

    +

    + {formatPercent(kp.masteryRate)} +

    +
    +
    +
    +
    +
    +
  • + ); + })} +
+ )} +
+ + )} +
+ ); +} diff --git a/apps/parent-portal/src/app/parent/grades/page.tsx b/apps/parent-portal/src/app/parent/grades/page.tsx index dc3b515..c64346c 100644 --- a/apps/parent-portal/src/app/parent/grades/page.tsx +++ b/apps/parent-portal/src/app/parent/grades/page.tsx @@ -1,17 +1,25 @@ // 成绩页面 // 依据:02-architecture-design.md §3.1 路由结构 // 路由:/parent/grades +// - 顶部导出按钮 +// - 成绩对比图(学生分数柱 + 班级均虚线) +// - 成绩明细表 +// - 底部成长档案折线图(学生 vs 班级均) "use client"; import { useChildGrades } from "@/hooks/useChildGrades"; import { useChildSwitcher } from "@/hooks/useChildSwitcher"; +import { useChildGrowthArchive } from "@/hooks/useChildGrowthArchive"; import { ChildGradeChart } from "@/components/ChildGradeChart"; +import { GrowthArchiveChart } from "@/components/GrowthArchiveChart"; +import { ExportGradesButton } from "@/components/ExportGradesButton"; import { formatDate, scoreToGrade, gradeToChinese, cn } from "@/lib/utils"; export default function GradesPage() { - const { currentChild } = useChildSwitcher(); + const { currentChild, currentChildId } = useChildSwitcher(); const { grades, loading, error } = useChildGrades(); + const { growthArchive, loading: archiveLoading } = useChildGrowthArchive(); if (loading) { return ( @@ -36,7 +44,15 @@ export default function GradesPage() { return (
-

{currentChild?.name}的成绩

+
+

{currentChild?.name}的成绩

+ {currentChildId && ( + + )} +
{grades.length > 0 && } @@ -87,6 +103,14 @@ export default function GradesPage() {
+ + {archiveLoading && ( +