- 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
205 lines
5.9 KiB
TypeScript
205 lines
5.9 KiB
TypeScript
"use client";
|
||
|
||
import type { ReactElement } from "react";
|
||
import type {
|
||
BlockData,
|
||
BlockType,
|
||
} from "../types";
|
||
import {
|
||
isBlackboardBlockData,
|
||
isExerciseBlockData,
|
||
isHomeworkBlockData,
|
||
isImportBlockData,
|
||
isKeyPointBlockData,
|
||
isNewTeachingBlockData,
|
||
isObjectiveBlockData,
|
||
isReflectionBlockData,
|
||
isRichTextBlockData,
|
||
isSummaryBlockData,
|
||
isTextStudyBlockData,
|
||
} from "../lib/type-guards";
|
||
import { RichTextBlock } from "../components/blocks/rich-text-block";
|
||
import { ExerciseBlock } from "../components/blocks/exercise-block";
|
||
import { TextStudyBlock } from "../components/blocks/text-study-block";
|
||
import { ObjectiveBlock } from "../components/blocks/objective-block";
|
||
import { KeyPointBlock } from "../components/blocks/key-point-block";
|
||
import { ImportBlock } from "../components/blocks/import-block";
|
||
import { NewTeachingBlock } from "../components/blocks/new-teaching-block";
|
||
import { SummaryBlock } from "../components/blocks/summary-block";
|
||
import { HomeworkBlock } from "../components/blocks/homework-block";
|
||
import { BlackboardBlock } from "../components/blocks/blackboard-block";
|
||
import { ReflectionBlock } from "../components/blocks/reflection-block";
|
||
|
||
/**
|
||
* Block 注册表:配置驱动渲染。
|
||
* 新增 Block 类型只需在此注册,无需修改 NodeEditPanel。
|
||
*/
|
||
|
||
export interface BlockRenderProps {
|
||
blockId: string;
|
||
data: BlockData;
|
||
textbookId?: string;
|
||
chapterId?: string;
|
||
classes?: { id: string; name: string }[];
|
||
/** V5-5:当前课案 ID(仅 RichTextBlock 用于素材库 picker 关联) */
|
||
planId?: string;
|
||
onUpdate: (data: BlockData) => void;
|
||
}
|
||
|
||
export interface BlockRegistryEntry {
|
||
/** 是否为富文本类(共享 RichTextBlock 编辑器) */
|
||
isRichText?: boolean;
|
||
}
|
||
|
||
/**
|
||
* Block 注册表元数据(单一数据源,P0-9 修复)。
|
||
* isRichText 属性是判断富文本 block 的唯一权威来源。
|
||
* 组件渲染由 BlockRenderer 统一处理,避免在 render 中动态获取组件引用。
|
||
*/
|
||
export const BLOCK_REGISTRY: Record<BlockType, BlockRegistryEntry> = {
|
||
objective: {},
|
||
key_point: {},
|
||
import: {},
|
||
new_teaching: {},
|
||
consolidation: { isRichText: true },
|
||
summary: {},
|
||
homework: {},
|
||
blackboard: {},
|
||
text_study: {},
|
||
exercise: {},
|
||
rich_text: { isRichText: true },
|
||
reflection: {},
|
||
};
|
||
|
||
/**
|
||
* 判断某 BlockType 是否为富文本类。
|
||
* P0-9 修复:统一以 BLOCK_REGISTRY.isRichText 为唯一数据源,
|
||
* 删除 constants.ts 中的 RICH_TEXT_BLOCK_TYPES 和此处的 RICH_TEXT_TYPES 冗余定义。
|
||
*/
|
||
export function isRichTextBlock(type: BlockType): boolean {
|
||
return BLOCK_REGISTRY[type]?.isRichText === true;
|
||
}
|
||
|
||
/**
|
||
* 静态 Block 渲染组件。
|
||
* 根据 type 从注册表查找并渲染对应 Block,所有组件引用均为模块顶层静态声明,
|
||
* 满足 react-hooks/static-components 规则。
|
||
* 新增 Block 类型时,在此 switch 中添加对应 case 即可。
|
||
*
|
||
* V3 修复:使用类型守卫替代 `as` 断言,安全收窄 BlockData 联合类型。
|
||
* 类型守卫失败时返回 null(理论上不会发生,因为 type 与 data 由调用方保证一致)。
|
||
*/
|
||
export function BlockRenderer(props: BlockRenderProps & { type: BlockType }): ReactElement | null {
|
||
const { type, ...rest } = props;
|
||
switch (type) {
|
||
case "objective": {
|
||
if (!isObjectiveBlockData(rest.data)) return null;
|
||
return (
|
||
<ObjectiveBlock
|
||
data={rest.data}
|
||
onUpdate={(d) => rest.onUpdate(d)}
|
||
/>
|
||
);
|
||
}
|
||
case "key_point": {
|
||
if (!isKeyPointBlockData(rest.data)) return null;
|
||
return (
|
||
<KeyPointBlock
|
||
data={rest.data}
|
||
onUpdate={(d) => rest.onUpdate(d)}
|
||
/>
|
||
);
|
||
}
|
||
case "import": {
|
||
if (!isImportBlockData(rest.data)) return null;
|
||
return (
|
||
<ImportBlock
|
||
data={rest.data}
|
||
onUpdate={(d) => rest.onUpdate(d)}
|
||
/>
|
||
);
|
||
}
|
||
case "new_teaching": {
|
||
if (!isNewTeachingBlockData(rest.data)) return null;
|
||
return (
|
||
<NewTeachingBlock
|
||
data={rest.data}
|
||
textbookId={rest.textbookId}
|
||
chapterId={rest.chapterId}
|
||
onUpdate={(d) => rest.onUpdate(d)}
|
||
/>
|
||
);
|
||
}
|
||
case "summary": {
|
||
if (!isSummaryBlockData(rest.data)) return null;
|
||
return (
|
||
<SummaryBlock
|
||
data={rest.data}
|
||
onUpdate={(d) => rest.onUpdate(d)}
|
||
/>
|
||
);
|
||
}
|
||
case "homework": {
|
||
if (!isHomeworkBlockData(rest.data)) return null;
|
||
return (
|
||
<HomeworkBlock
|
||
data={rest.data}
|
||
onUpdate={(d) => rest.onUpdate(d)}
|
||
/>
|
||
);
|
||
}
|
||
case "blackboard": {
|
||
if (!isBlackboardBlockData(rest.data)) return null;
|
||
return (
|
||
<BlackboardBlock
|
||
data={rest.data}
|
||
textbookId={rest.textbookId}
|
||
chapterId={rest.chapterId}
|
||
onUpdate={(d) => rest.onUpdate(d)}
|
||
/>
|
||
);
|
||
}
|
||
case "reflection": {
|
||
if (!isReflectionBlockData(rest.data)) return null;
|
||
return (
|
||
<ReflectionBlock
|
||
data={rest.data}
|
||
onUpdate={(d) => rest.onUpdate(d)}
|
||
/>
|
||
);
|
||
}
|
||
case "exercise": {
|
||
if (!isExerciseBlockData(rest.data)) return null;
|
||
return (
|
||
<ExerciseBlock
|
||
blockId={rest.blockId}
|
||
data={rest.data}
|
||
classes={rest.classes ?? []}
|
||
textbookId={rest.textbookId}
|
||
chapterId={rest.chapterId}
|
||
/>
|
||
);
|
||
}
|
||
case "text_study": {
|
||
if (!isTextStudyBlockData(rest.data)) return null;
|
||
return <TextStudyBlock blockId={rest.blockId} data={rest.data} />;
|
||
}
|
||
case "rich_text":
|
||
case "consolidation": {
|
||
if (!isRichTextBlockData(rest.data)) return null;
|
||
return (
|
||
<RichTextBlock
|
||
data={rest.data}
|
||
textbookId={rest.textbookId}
|
||
chapterId={rest.chapterId}
|
||
planId={rest.planId}
|
||
blockId={rest.blockId}
|
||
onUpdate={(d) => rest.onUpdate(d)}
|
||
/>
|
||
);
|
||
}
|
||
default:
|
||
return null;
|
||
}
|
||
}
|