feat(parent-portal): 完整实现 P4+P5+P6 家长端微前端
实现内容(仲裁裁决驱动,首次即最终方案): P4 核心功能 - 认证:localStorage token 存储(F12)+ REST 登录(ISSUE-004)+ refreshAccessToken 竞态防护 - 子女切换:ChildSwitcher(Tab ≤3 / 下拉 ≥4)+ Zustand store(ISSUE-009 纯前端切换) - 数据查询:urql GraphQL 消费 parent-bff(F9)+ TanStack Query 缓存 - 通知中心:NotificationFeed + 已读/全部已读 mutations - 通知偏好:三维矩阵 + ISSUE-033 localStorage 降级 - 5 层状态管理:URL/Server/Client/Global UI/Form - 跨标签同步:BroadcastChannel + storage 事件 P5 实时推送 - WebSocket 连接 push-gateway + 指数退避重连 - HTTP 轮询降级(60s)+ 实时通知 Hook P6 硬化 - Web Vitals 上报 + OTel trace - i18n 5 语言(zh-CN/en-US/zh-TW/ja-JP/ar-SA 含 RTL) - PWA manifest + Service Worker - CSP 安全头 + 权限点 F7 命名 + 设计令牌三层 测试与构建 - Vitest 92 测试全通过(utils/auth/child-store/ChildSwitcher/NotificationFeed/login) - MSW mock 未就绪上游(parent-bff GraphQL + iam REST + iam GetChildrenByParent P0 阻塞用 fixtures) - Dockerfile 多阶段构建(G1,端口 4002,HEALTHCHECK /api/health) - typecheck + lint 零错误 经验沉淀 - known-issues.md §2.13 追加 12 条实现期经验(无 AI 身份标注) - arch.db 已更新(15 TS 模块 / 482 符号 / 138 proto) 依据:02-architecture-design.md(回写总裁裁决)、coord-final-decisions.md、 president-final-rulings.md、parent-portal_workline.md、parent-portal_contract.md
This commit is contained in:
119
apps/parent-portal/src/components/ChildSummaryCard.tsx
Normal file
119
apps/parent-portal/src/components/ChildSummaryCard.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
// ChildSummaryCard:子女概览卡片
|
||||
// 依据:02-architecture-design.md §15.4 ChildSummaryCard 设计
|
||||
// - 平均分 / 班级排名 / 出勤率 / 待完成作业
|
||||
// - 成绩趋势指示
|
||||
// - 即将到来的事件
|
||||
|
||||
"use client";
|
||||
|
||||
import type { ChildSummary } from "@/types";
|
||||
import { formatPercent, formatNumber, formatDate } from "@/lib/utils";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface ChildSummaryCardProps {
|
||||
summary: ChildSummary;
|
||||
}
|
||||
|
||||
const TREND_LABELS = {
|
||||
up: { text: "上升", color: "text-success" },
|
||||
down: { text: "下降", color: "text-danger" },
|
||||
stable: { text: "稳定", color: "text-ink-muted" },
|
||||
} as const;
|
||||
|
||||
const DEFAULT_TREND = { text: "稳定", color: "text-ink-muted" };
|
||||
|
||||
export function ChildSummaryCard({ summary }: ChildSummaryCardProps) {
|
||||
const trend = TREND_LABELS[summary.recentGradeTrend] ?? DEFAULT_TREND;
|
||||
|
||||
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>
|
||||
</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>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
function Metric({
|
||||
label,
|
||||
value,
|
||||
highlight,
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
highlight?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"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>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ChildSummaryCard;
|
||||
Reference in New Issue
Block a user