/** * 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, 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 = { 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 = { 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 = { exercise: "练习", reading: "阅读", writing: "写作", }; return data.assignments.map( (a) => `[${typeLabel[a.type]}] ${a.description}`, ); } function flattenBlackboard(data: BlackboardBlockData): string[] { const layoutLabel: Record = { structure: "结构式", mindmap: "思维导图", text: "文字式", }; return [`形式:${layoutLabel[data.layout]}`, data.content].filter( (s) => s.length > 0, ); } function flattenReflection(data: ReflectionBlockData): string[] { const aspectLabel: Record = { 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(/ /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];