Files
NextEdu/src/app/(dashboard)/teacher/lesson-plans/heatmap/page.tsx
SpecialX 25dca843be feat(lesson-preparation): major update with AI features, schedules, and new components
- Add actions-schedules.ts and data-access-schedules.ts for schedule management

- Add AI differentiation, AI feedback, consistency check dialogs

- Add attachment-picker, curriculum-heatmap, print-view, version-diff-view

- Add lesson-plan-mobile-view and schedule-dialog components

- Add lib: ai-differentiation, ai-feedback, auto-layout, consistency-check,

  curriculum-coverage, export, version-diff

- Add history-slice hook for version history

- Update existing components, hooks, providers, services, types

- Add teacher lesson-plans heatmap and library pages
2026-07-04 10:22:10 +08:00

125 lines
4.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<JSX.Element> {
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<string, number>();
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 (
<div className="p-6">
<EmptyState
icon={BookOpen}
title={t("heatmap.title")}
description={t("heatmap.selectTextbook")}
className="border-none shadow-none"
/>
</div>
);
}
// 获取该教材的所有知识点
const allKps = await getKnowledgePointsByTextbookId(defaultTextbookId);
// 提取该教材下所有课案的知识点关联
const planLinks: PlanKpLink[] = [];
const chapterNames: Record<string, string> = {};
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<string>();
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 (
<div className="p-6 space-y-4">
<div>
<h1 className="text-2xl font-bold tracking-tight flex items-center gap-2">
<BookOpen className="h-6 w-6" aria-hidden="true" />
{t("heatmap.title")}
</h1>
<p className="text-muted-foreground">{t("heatmap.description")}</p>
</div>
{allKps.length === 0 ? (
<EmptyState
icon={BookOpen}
title={t("heatmap.noData")}
description={t("heatmap.description")}
className="border-none shadow-none"
/>
) : (
<CurriculumHeatmap
allKps={allKps}
planLinks={planLinks}
chapterNames={chapterNames}
/>
)}
</div>
);
}