- Add anchor injector for canvas-based anchor positioning - Add new block components: blackboard, homework, import, key-point, new-teaching, objective, summary - Add textbook content node for React Flow canvas - Update actions (kp, publish, main), data-access (templates, versions, main) - Update editor, node-editor, block-renderer, and picker components - Update schema, types, hooks, and lib utilities (document-migration, node-summary, rf-mappers)
130 lines
3.6 KiB
TypeScript
130 lines
3.6 KiB
TypeScript
import "server-only";
|
||
|
||
import { and, eq } from "drizzle-orm";
|
||
import { createId } from "@paralleldrive/cuid2";
|
||
|
||
import { db } from "@/shared/db";
|
||
import { lessonPlanTemplates, lessonPlans } from "@/shared/db/schema";
|
||
import { SYSTEM_TEMPLATES } from "./constants";
|
||
import { normalizeDocument, LessonPlanDataError } from "./data-access";
|
||
import type {
|
||
LessonPlanTemplate,
|
||
TemplateBlockSkeleton,
|
||
TemplateType,
|
||
TemplateScope,
|
||
} from "./types";
|
||
|
||
// ---- 类型守卫:安全地将 DB string 收窄为联合类型 ----
|
||
const TEMPLATE_TYPES = ["system", "personal"] as const;
|
||
function isTemplateType(v: string): v is TemplateType {
|
||
return (TEMPLATE_TYPES as readonly string[]).includes(v);
|
||
}
|
||
const TEMPLATE_SCOPES = ["regular", "review", "experiment", "inquiry", "blank", "custom"] as const;
|
||
function isTemplateScope(v: string): v is TemplateScope {
|
||
return (TEMPLATE_SCOPES as readonly string[]).includes(v);
|
||
}
|
||
|
||
// ---- 类型映射:Drizzle 行 → LessonPlanTemplate(Date → ISO string)----
|
||
function mapRowToTemplate(row: {
|
||
id: string;
|
||
name: string;
|
||
type: string;
|
||
scope: string;
|
||
blocks: unknown;
|
||
creatorId: string | null;
|
||
createdAt: Date;
|
||
updatedAt: Date;
|
||
}): LessonPlanTemplate {
|
||
return {
|
||
id: row.id,
|
||
name: row.name,
|
||
type: isTemplateType(row.type) ? row.type : "personal",
|
||
scope: isTemplateScope(row.scope) ? row.scope : "custom",
|
||
// 从 unknown 转换为 TemplateBlockSkeleton[](DB JSON 字段)
|
||
blocks: row.blocks as LessonPlanTemplate["blocks"],
|
||
creatorId: row.creatorId,
|
||
createdAt: row.createdAt.toISOString(),
|
||
updatedAt: row.updatedAt.toISOString(),
|
||
};
|
||
}
|
||
|
||
export async function getLessonPlanTemplates(
|
||
userId: string,
|
||
): Promise<LessonPlanTemplate[]> {
|
||
// system 模板(内存)+ personal 模板(DB)
|
||
const systemTemplates: LessonPlanTemplate[] = SYSTEM_TEMPLATES.map((t) => ({
|
||
id: t.id,
|
||
name: t.name,
|
||
type: "system",
|
||
scope: t.scope,
|
||
blocks: t.blocks,
|
||
creatorId: null,
|
||
createdAt: "",
|
||
updatedAt: "",
|
||
}));
|
||
|
||
const personalRows = await db
|
||
.select()
|
||
.from(lessonPlanTemplates)
|
||
.where(
|
||
and(
|
||
eq(lessonPlanTemplates.type, "personal"),
|
||
eq(lessonPlanTemplates.creatorId, userId),
|
||
),
|
||
);
|
||
const personalTemplates = personalRows.map(mapRowToTemplate);
|
||
|
||
return [...systemTemplates, ...personalTemplates];
|
||
}
|
||
|
||
export async function saveAsTemplate(input: {
|
||
sourcePlanId: string;
|
||
name: string;
|
||
userId: string;
|
||
}): Promise<{ templateId: string }> {
|
||
// 从课案 content 提取 block 骨架
|
||
const plan = await db
|
||
.select({ content: lessonPlans.content })
|
||
.from(lessonPlans)
|
||
.where(
|
||
and(
|
||
eq(lessonPlans.id, input.sourcePlanId),
|
||
eq(lessonPlans.creatorId, input.userId),
|
||
),
|
||
)
|
||
.limit(1);
|
||
if (plan.length === 0) throw new LessonPlanDataError("NOT_FOUND");
|
||
|
||
const doc = normalizeDocument(plan[0].content);
|
||
const skeleton: TemplateBlockSkeleton[] = doc.nodes
|
||
.filter((b): b is import("./types").LessonPlanNode => b.type !== "textbook_content")
|
||
.map((b) => ({
|
||
type: b.type,
|
||
title: b.title,
|
||
}));
|
||
|
||
const templateId = createId();
|
||
await db.insert(lessonPlanTemplates).values({
|
||
id: templateId,
|
||
name: input.name,
|
||
type: "personal",
|
||
scope: "custom",
|
||
blocks: skeleton,
|
||
creatorId: input.userId,
|
||
});
|
||
return { templateId };
|
||
}
|
||
|
||
export async function deletePersonalTemplate(
|
||
templateId: string,
|
||
userId: string,
|
||
): Promise<void> {
|
||
await db.delete(lessonPlanTemplates).where(
|
||
and(
|
||
eq(lessonPlanTemplates.id, templateId),
|
||
eq(lessonPlanTemplates.type, "personal"),
|
||
eq(lessonPlanTemplates.creatorId, userId),
|
||
),
|
||
);
|
||
}
|