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:
@@ -1,11 +1,14 @@
|
||||
// ChildSummaryCard:子女概览卡片
|
||||
// 依据:02-architecture-design.md §15.4 ChildSummaryCard 设计
|
||||
// - 平均分 / 班级排名 / 出勤率 / 待完成作业
|
||||
// - 成绩趋势指示
|
||||
// - 成绩趋势指示(图标 + 文案)
|
||||
// - 逾期作业高亮徽标
|
||||
// - 即将到来的事件
|
||||
// - 点击跳转子女详情
|
||||
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import type { ChildSummary } from "@/types";
|
||||
import { formatPercent, formatNumber, formatDate } from "@/lib/utils";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -15,82 +18,93 @@ interface ChildSummaryCardProps {
|
||||
}
|
||||
|
||||
const TREND_LABELS = {
|
||||
up: { text: "上升", color: "text-success" },
|
||||
down: { text: "下降", color: "text-danger" },
|
||||
stable: { text: "稳定", color: "text-ink-muted" },
|
||||
up: { text: "上升", color: "text-success", icon: "↑" },
|
||||
down: { text: "下降", color: "text-danger", icon: "↓" },
|
||||
stable: { text: "稳定", color: "text-ink-muted", icon: "→" },
|
||||
} as const;
|
||||
|
||||
const DEFAULT_TREND = { text: "稳定", color: "text-ink-muted" };
|
||||
const DEFAULT_TREND = { text: "稳定", color: "text-ink-muted", icon: "→" };
|
||||
|
||||
export function ChildSummaryCard({ summary }: ChildSummaryCardProps) {
|
||||
const trend = TREND_LABELS[summary.recentGradeTrend] ?? DEFAULT_TREND;
|
||||
const hasPending = summary.pendingHomeworkCount > 0;
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* 指标网格 */}
|
||||
<div className="grid grid-cols-2 gap-4 lg:grid-cols-4">
|
||||
<Metric label="平均分" value={formatNumber(summary.avgScore)} />
|
||||
<Metric
|
||||
label="班级排名"
|
||||
value={`${summary.classRank} / ${summary.classSize}`}
|
||||
/>
|
||||
<Metric label="出勤率" value={formatPercent(summary.attendanceRate)} />
|
||||
<Metric
|
||||
label="待完成作业"
|
||||
value={formatNumber(summary.pendingHomeworkCount)}
|
||||
highlight={summary.pendingHomeworkCount > 0}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 成绩趋势 */}
|
||||
<div className="rounded border border-rule bg-paper-elevated p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="font-serif text-base">成绩趋势</h3>
|
||||
<span className={cn("text-sm", trend.color)}>{trend.text}</span>
|
||||
<Link
|
||||
href={`/parent/children/${summary.childId}`}
|
||||
className="block focus-visible:outline-2"
|
||||
aria-label={`查看 ${summary.childId} 详情`}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
{/* 指标网格 */}
|
||||
<div className="grid grid-cols-2 gap-4 lg:grid-cols-4">
|
||||
<Metric label="平均分" value={formatNumber(summary.avgScore)} />
|
||||
<Metric
|
||||
label="班级排名"
|
||||
value={`${summary.classRank} / ${summary.classSize}`}
|
||||
/>
|
||||
<Metric label="出勤率" value={formatPercent(summary.attendanceRate)} />
|
||||
<Metric
|
||||
label="待完成作业"
|
||||
value={formatNumber(summary.pendingHomeworkCount)}
|
||||
highlight={hasPending}
|
||||
badge={hasPending ? `${summary.pendingHomeworkCount} 份待办` : undefined}
|
||||
/>
|
||||
</div>
|
||||
<ul className="mt-3 space-y-2">
|
||||
{summary.recentScores.slice(0, 5).map((score) => (
|
||||
<li
|
||||
key={score.examId}
|
||||
className="flex items-center justify-between border-b border-rule pb-2 text-sm last:border-b-0 last:pb-0"
|
||||
>
|
||||
<div>
|
||||
<span className="font-medium">{score.subject}</span>
|
||||
<span className="ml-2 text-ink-muted">{score.examName}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="font-mono">{score.studentScore}</span>
|
||||
{score.classAverage !== undefined && (
|
||||
<span className="text-xs text-ink-muted">
|
||||
班均 {score.classAverage}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* 即将到来事件 */}
|
||||
{summary.upcomingEvents && summary.upcomingEvents.length > 0 && (
|
||||
{/* 成绩趋势 */}
|
||||
<div className="rounded border border-rule bg-paper-elevated p-4">
|
||||
<h3 className="font-serif text-base">即将到来</h3>
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="font-serif text-base">成绩趋势</h3>
|
||||
<span className={cn("flex items-center gap-1 text-sm", trend.color)}>
|
||||
<span aria-hidden="true">{trend.icon}</span>
|
||||
{trend.text}
|
||||
</span>
|
||||
</div>
|
||||
<ul className="mt-3 space-y-2">
|
||||
{summary.upcomingEvents.map((event) => (
|
||||
{summary.recentScores.slice(0, 5).map((score) => (
|
||||
<li
|
||||
key={event.id}
|
||||
className="flex items-center justify-between text-sm"
|
||||
key={score.examId}
|
||||
className="flex items-center justify-between border-b border-rule pb-2 text-sm last:border-b-0 last:pb-0"
|
||||
>
|
||||
<span className="mark-left">{event.title}</span>
|
||||
<span className="text-ink-muted">
|
||||
{formatDate(event.dueDate)}
|
||||
</span>
|
||||
<div>
|
||||
<span className="font-medium">{score.subject}</span>
|
||||
<span className="ml-2 text-ink-muted">{score.examName}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="font-mono">{score.studentScore}</span>
|
||||
{score.classAverage !== undefined && (
|
||||
<span className="text-xs text-ink-muted">
|
||||
班均 {score.classAverage}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 即将到来事件 */}
|
||||
{summary.upcomingEvents && summary.upcomingEvents.length > 0 && (
|
||||
<div className="rounded border border-rule bg-paper-elevated p-4">
|
||||
<h3 className="font-serif text-base">即将到来</h3>
|
||||
<ul className="mt-3 space-y-2">
|
||||
{summary.upcomingEvents.map((event) => (
|
||||
<li
|
||||
key={event.id}
|
||||
className="flex items-center justify-between text-sm"
|
||||
>
|
||||
<span className="mark-left">{event.title}</span>
|
||||
<span className="text-ink-muted">
|
||||
{formatDate(event.dueDate)}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -98,20 +112,27 @@ function Metric({
|
||||
label,
|
||||
value,
|
||||
highlight,
|
||||
badge,
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
highlight?: boolean;
|
||||
badge?: string;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"rounded border border-rule bg-paper-elevated p-4",
|
||||
"relative rounded border border-rule bg-paper-elevated p-4",
|
||||
highlight && "border-warning",
|
||||
)}
|
||||
>
|
||||
<p className="text-xs text-ink-muted">{label}</p>
|
||||
<p className="mt-1 font-serif text-2xl">{value}</p>
|
||||
{badge && (
|
||||
<span className="absolute right-2 top-2 rounded bg-warning px-1.5 py-0.5 text-xs text-paper">
|
||||
{badge}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user