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
This commit is contained in:
267
src/modules/lesson-preparation/lib/export.ts
Normal file
267
src/modules/lesson-preparation/lib/export.ts
Normal file
@@ -0,0 +1,267 @@
|
||||
/**
|
||||
* 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(/ /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];
|
||||
Reference in New Issue
Block a user