import type { JSX } from "react"; import { BookOpen } from "lucide-react"; import { getTranslations } from "next-intl/server"; import { requirePermission } from "@/shared/lib/auth-guard"; import { Permissions } from "@/shared/types/permissions"; import { getLessonPlans } from "@/modules/lesson-preparation/data-access"; import { getLessonPlanById } from "@/modules/lesson-preparation/data-access"; import { getKnowledgePointsByTextbookId, getTextbooks } from "@/modules/textbooks/data-access"; import { CurriculumHeatmap } from "@/modules/lesson-preparation/components/curriculum-heatmap"; import type { PlanKpLink } from "@/modules/lesson-preparation/lib/curriculum-coverage"; import type { LessonPlanDocument } from "@/modules/lesson-preparation/types"; import { isRecord } from "@/shared/lib/type-guards"; import { EmptyState } from "@/shared/components/ui/empty-state"; export const dynamic = "force-dynamic"; /** * V5-20 T4:教师端课标热力图页面。 * * 展示教师所有课案对所选教材知识点的覆盖情况。 * 从教师所有课案中提取 knowledgePointIds,统计覆盖率。 */ export default async function HeatmapPage(): Promise { const t = await getTranslations("lessonPreparation"); const ctx = await requirePermission(Permissions.LESSON_PLAN_READ); // 教师所有课案(含草稿和已发布) const [plans, textbooks] = await Promise.all([ getLessonPlans({}, ctx.dataScope, ctx.userId), getTextbooks(), ]); // 仅保留有教材的课案 const plansWithTextbook = plans.filter((p) => p.textbookId); // 默认展示教师第一本教材(按使用频率) const textbookCounts = new Map(); for (const p of plansWithTextbook) { if (p.textbookId) { textbookCounts.set(p.textbookId, (textbookCounts.get(p.textbookId) ?? 0) + 1); } } const defaultTextbookId = [...textbookCounts.entries()].sort((a, b) => b[1] - a[1])[0]?.[0] ?? textbooks[0]?.id ?? ""; if (!defaultTextbookId || textbooks.length === 0) { return (
); } // 获取该教材的所有知识点 const allKps = await getKnowledgePointsByTextbookId(defaultTextbookId); // 提取该教材下所有课案的知识点关联 const planLinks: PlanKpLink[] = []; const chapterNames: Record = {}; for (const plan of plansWithTextbook.filter((p) => p.textbookId === defaultTextbookId)) { try { const fullPlan = await getLessonPlanById(plan.id, ctx.userId); if (!fullPlan) continue; const doc = fullPlan.content as LessonPlanDocument; const kpSet = new Set(); for (const node of doc.nodes) { if (!isRecord(node.data)) continue; const dataKps = (node.data as { knowledgePointIds?: unknown }).knowledgePointIds; if (Array.isArray(dataKps)) { for (const kp of dataKps) { if (typeof kp === "string") kpSet.add(kp); } } } if (kpSet.size > 0) { planLinks.push({ planId: plan.id, knowledgePointIds: [...kpSet] }); } } catch { // 单个课案加载失败不阻塞整体 } } // 章节名映射(从 textbooks 模块获取) // 此处简化:使用知识点自身的 chapterId,章节名从 textbooks data-access 获取 // 为避免 N+1 查询,此处仅用 chapterId 作为 key,章节名由前端未知章节兜底 // 完整实现可在 textbooks data-access 添加批量查询函数 for (const kp of allKps) { if (kp.chapterId) { chapterNames[kp.chapterId] = kp.chapterId; // 占位,实际应查询章节名 } } return (

{t("heatmap.description")}

{allKps.length === 0 ? ( ) : ( )}
); }