Files
NextEdu/src/modules/lesson-preparation/lib/export.ts
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

268 lines
7.7 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.
/**
* V5-4课案导出/打印工具
*
* 将画布式 LessonPlanDocument 扁平化为线性教学环节列表,
* 供打印视图print-view.tsx渲染。支持详细版/简洁版两种模式:
* - detailed: 包含所有 11 种 Block
* - concise: 仅包含 objective / new_teaching / exercise / homework
*/
import type {
BlackboardBlockData,
BlockData,
ExerciseBlockData,
HomeworkBlockData,
ImportBlockData,
KeyPointBlockData,
LessonPlan,
LessonPlanDocument,
NewTeachingBlockData,
ObjectiveBlockData,
ReflectionBlockData,
RichTextBlockData,
SummaryBlockData,
TextStudyBlockData,
TextbookContentNode,
} from "../types";
/** 导出版本 */
export type ExportVariant = "detailed" | "concise";
/** 简洁版包含的 Block 类型 */
const CONCISE_BLOCK_TYPES = new Set([
"objective",
"new_teaching",
"exercise",
"homework",
]);
/** 扁平化后的教学环节 */
export interface PrintableSection {
type: string;
title: string;
/** 已扁平化为字符串数组的内容 */
lines: string[];
}
/** 导出元信息(页眉/页脚用) */
export interface ExportMeta {
planTitle: string;
textbookTitle?: string;
chapterTitle?: string;
teacherName?: string;
className?: string;
/** 备课最后保存时间 ISO */
lastSavedAt?: string;
/** 教学时长(分钟),来自 import 节点 durationMin 求和 */
totalDurationMin: number;
}
/** 导出文档 */
export interface PrintableLessonPlan {
meta: ExportMeta;
/** 课文正文(如有 textbook_content 节点) */
textbookContent: string | null;
/** 教学环节列表(按 order 排序) */
sections: PrintableSection[];
variant: ExportVariant;
}
/**
* 将画布式文档扁平化为可打印的线性结构。
*
* @param plan 课案对象
* @param meta 元信息(教师名、班级等由调用方注入)
* @param variant detailed | concise
*/
export function flattenLessonPlanForPrint(
plan: LessonPlan,
meta: Partial<ExportMeta>,
variant: ExportVariant = "detailed",
): PrintableLessonPlan {
const doc: LessonPlanDocument = plan.content;
const textbookContent = extractTextbookContent(doc);
const teachingNodes = doc.nodes
.filter((n) => n.type !== "textbook_content")
.filter((n) => variant === "detailed" || CONCISE_BLOCK_TYPES.has(n.type))
.sort((a, b) => a.order - b.order);
const sections = teachingNodes.map((node) =>
flattenBlock(node.type, node.title, node.data as BlockData),
);
// V5-4教学时长由 import 节点求和
const totalDurationMin = doc.nodes
.filter((n) => n.type === "import")
.reduce((sum, n) => {
const data = n.data as ImportBlockData;
return sum + (data.durationMin ?? 0);
}, 0);
return {
meta: {
planTitle: plan.title,
lastSavedAt: plan.lastSavedAt ?? undefined,
totalDurationMin,
...meta,
},
textbookContent,
sections,
variant,
};
}
function extractTextbookContent(doc: LessonPlanDocument): string | null {
const node = doc.nodes.find(
(n): n is TextbookContentNode => n.type === "textbook_content",
);
if (!node) return null;
return node.data.content || null;
}
function flattenBlock(
type: string,
title: string,
data: BlockData,
): PrintableSection {
const lines = flattenBlockData(type, data);
return { type, title, lines };
}
function flattenBlockData(type: string, data: BlockData): string[] {
switch (type) {
case "objective":
return flattenObjective(data as ObjectiveBlockData);
case "key_point":
return flattenKeyPoint(data as KeyPointBlockData);
case "import":
return flattenImport(data as ImportBlockData);
case "new_teaching":
return flattenNewTeaching(data as NewTeachingBlockData);
case "summary":
return flattenSummary(data as SummaryBlockData);
case "homework":
return flattenHomework(data as HomeworkBlockData);
case "blackboard":
return flattenBlackboard(data as BlackboardBlockData);
case "reflection":
return flattenReflection(data as ReflectionBlockData);
case "exercise":
return flattenExercise(data as ExerciseBlockData);
case "text_study":
return flattenTextStudy(data as TextStudyBlockData);
case "rich_text":
return flattenRichText(data as RichTextBlockData);
default:
return [];
}
}
function flattenObjective(data: ObjectiveBlockData): string[] {
const dimensionLabel: Record<ObjectiveItem["dimension"], string> = {
knowledge: "知识与技能",
process: "过程与方法",
emotion: "情感态度",
};
return data.objectives.map(
(o) => `[${dimensionLabel[o.dimension]}] ${o.text}`,
);
}
function flattenKeyPoint(data: KeyPointBlockData): string[] {
return data.keyPoints.map((kp) =>
kp.type === "key" ? `[重点] ${kp.text}` : `[难点] ${kp.text}`,
);
}
function flattenImport(data: ImportBlockData): string[] {
const methodLabel: Record<ImportBlockData["method"], string> = {
question: "提问导入",
situation: "情境导入",
review: "复习导入",
other: "其他",
};
return [
`方式:${methodLabel[data.method]}`,
`时长:${data.durationMin} 分钟`,
data.prompt ? `导入语:${data.prompt}` : "",
].filter((s) => s.length > 0);
}
function flattenNewTeaching(data: NewTeachingBlockData): string[] {
const lines: string[] = [];
data.teachingPoints.forEach((p, i) => {
lines.push(`步骤 ${i + 1}`);
if (p.outline) lines.push(` 提纲:${p.outline}`);
if (p.boardNotes) lines.push(` 板书要点:${p.boardNotes}`);
});
return lines;
}
function flattenSummary(data: SummaryBlockData): string[] {
const lines = data.summaryPoints.map((p, i) => `${i + 1}. ${p}`);
if (data.homeworkPreview) lines.push(`作业预览:${data.homeworkPreview}`);
return lines;
}
function flattenHomework(data: HomeworkBlockData): string[] {
const typeLabel: Record<HomeworkAssignment["type"], string> = {
exercise: "练习",
reading: "阅读",
writing: "写作",
};
return data.assignments.map(
(a) => `[${typeLabel[a.type]}] ${a.description}`,
);
}
function flattenBlackboard(data: BlackboardBlockData): string[] {
const layoutLabel: Record<BlackboardBlockData["layout"], string> = {
structure: "结构式",
mindmap: "思维导图",
text: "文字式",
};
return [`形式:${layoutLabel[data.layout]}`, data.content].filter(
(s) => s.length > 0,
);
}
function flattenReflection(data: ReflectionBlockData): string[] {
const aspectLabel: Record<ReflectionItem["aspect"], string> = {
effectiveness: "教学效果",
problems: "存在问题",
improvements: "改进措施",
};
return data.reflection.map(
(r) => `[${aspectLabel[r.aspect]}] ${r.text}`,
);
}
function flattenExercise(data: ExerciseBlockData): string[] {
if (data.items.length === 0) return ["(无题目)"];
return data.items.map((item, i) => {
const source = item.source === "inline" ? "课案内新建" : "题库";
return `${i + 1}. [${source}] 题目 ID: ${item.questionId} ${item.score} 分)`;
});
}
function flattenTextStudy(data: TextStudyBlockData): string[] {
if (data.annotations.length === 0) return ["(无文本研习标注)"];
return data.annotations.map(
(a, i) => `${i + 1}. [${a.title}] ${a.note}`,
);
}
function flattenRichText(data: RichTextBlockData): string[] {
// HTML 简易去标签,仅保留文本(打印友好)
const text = data.html
.replace(/<[^>]+>/g, "")
.replace(/&nbsp;/g, " ")
.trim();
return text.length > 0 ? [text] : [];
}
// 仅用于类型推导的本地导入别名,避免在 switch case 中重复 import
type ObjectiveItem = ObjectiveBlockData["objectives"][number];
type HomeworkAssignment = HomeworkBlockData["assignments"][number];
type ReflectionItem = ReflectionBlockData["reflection"][number];