feat(parent-portal): 完成参考项目(CICD)家长端功能全量覆盖 - 9 新页面 + 3 页面增强 + 763 测试通过

## 变更内容

### 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
This commit is contained in:
SpecialX
2026-07-13 15:12:56 +08:00
parent d49d211425
commit 18d94c0cc2
98 changed files with 11977 additions and 112 deletions

View File

@@ -1,9 +1,9 @@
// AttendanceCalendar 组件单测
// 依据02-architecture-design.md §15.5 AttendanceCalendar 设计
// 覆盖:日历网格渲染 / 状态颜色标记 / 统计 / 空数据 / 月份标题
// 覆盖:日历网格渲染 / 状态颜色标记 / 统计 / 空数据 / 月份标题 / 月份导航
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import { render, screen, fireEvent } from "@testing-library/react";
import { AttendanceCalendar } from "./AttendanceCalendar";
import type { AttendanceRecord } from "@/types";
@@ -160,3 +160,86 @@ describe("AttendanceCalendar 空数据", () => {
expect(screen.getByText("15")).toBeInTheDocument();
});
});
describe("AttendanceCalendar 月份导航", () => {
it("未传 onMonthChange 时不渲染导航按钮(向后兼容)", () => {
render(<AttendanceCalendar records={[]} />);
expect(screen.queryByRole("button", { name: "上一月" })).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: "下一月" })).not.toBeInTheDocument();
});
it("传入 onMonthChange 时渲染上一月/下一月按钮", () => {
render(
<AttendanceCalendar
records={[]}
month="2026-06"
onMonthChange={vi.fn()}
/>,
);
expect(screen.getByRole("button", { name: "上一月" })).toBeInTheDocument();
expect(screen.getByRole("button", { name: "下一月" })).toBeInTheDocument();
});
it("传入 month prop 时渲染对应月份标题", () => {
render(
<AttendanceCalendar
records={[]}
month="2026-05"
onMonthChange={vi.fn()}
/>,
);
expect(screen.getByText(/2026年5月考勤/)).toBeInTheDocument();
});
it("点击上一月按钮调用 onMonthChange 传 2026-05", () => {
const onMonthChange = vi.fn();
render(
<AttendanceCalendar
records={[]}
month="2026-06"
onMonthChange={onMonthChange}
/>,
);
fireEvent.click(screen.getByRole("button", { name: "上一月" }));
expect(onMonthChange).toHaveBeenCalledWith("2026-05");
});
it("点击下一月按钮调用 onMonthChange 传 2026-07", () => {
const onMonthChange = vi.fn();
render(
<AttendanceCalendar
records={[]}
month="2026-06"
onMonthChange={onMonthChange}
/>,
);
fireEvent.click(screen.getByRole("button", { name: "下一月" }));
expect(onMonthChange).toHaveBeenCalledWith("2026-07");
});
it("跨年导航2026-01 上一月为 2025-12", () => {
const onMonthChange = vi.fn();
render(
<AttendanceCalendar
records={[]}
month="2026-01"
onMonthChange={onMonthChange}
/>,
);
fireEvent.click(screen.getByRole("button", { name: "上一月" }));
expect(onMonthChange).toHaveBeenCalledWith("2025-12");
});
it("跨年导航2026-12 下一月为 2027-01", () => {
const onMonthChange = vi.fn();
render(
<AttendanceCalendar
records={[]}
month="2026-12"
onMonthChange={onMonthChange}
/>,
);
fireEvent.click(screen.getByRole("button", { name: "下一月" }));
expect(onMonthChange).toHaveBeenCalledWith("2027-01");
});
});