feat(lesson-preparation): add readonly view, anchor node selector, and type guards
- Add lesson-plan-readonly-view for viewing published plans - Add anchor-node-selector and textbook-segments for canvas anchor positioning - Add i18n-errors and type-guards lib utilities - Add lesson-plan-provider-setup for provider initialization - Update actions, data-access (knowledge, versions, main), publish-service - Update blocks (blackboard, exercise, homework, import, key-point, objective, reflection) - Update editor, node-editor, node-edit-panel, pickers, and providers
This commit is contained in:
@@ -10,7 +10,8 @@ import { persistExamDraft, addExamQuestions } from "@/modules/exams/data-access"
|
||||
import { createHomeworkAssignment } from "@/modules/homework/data-access-write";
|
||||
import { getStudentIdsByClassIds } from "@/modules/classes/data-access";
|
||||
import { normalizeDocument } from "./data-access";
|
||||
import type { LessonPlanDocument, ExerciseBlockData, LessonPlan, LessonPlanStatus } from "./types";
|
||||
import { isExerciseBlockData, isLessonPlanStatus, isValidQuestionType } from "./lib/type-guards";
|
||||
import type { LessonPlanDocument, LessonPlan, LessonPlanStatus } from "./types";
|
||||
|
||||
interface PublishInput {
|
||||
planId: string;
|
||||
@@ -19,6 +20,10 @@ interface PublishInput {
|
||||
classIds: string[];
|
||||
availableAt?: Date;
|
||||
dueAt?: Date;
|
||||
/** 作业标题(由 actions 层 i18n 翻译后传入)*/
|
||||
homeworkTitle: string;
|
||||
/** 作业描述(由 actions 层 i18n 翻译后传入)*/
|
||||
homeworkDescription: string;
|
||||
}
|
||||
|
||||
interface PublishResult {
|
||||
@@ -27,12 +32,6 @@ interface PublishResult {
|
||||
updatedContent: LessonPlanDocument;
|
||||
}
|
||||
|
||||
// 类型守卫:安全地将 string 收窄为 LessonPlanStatus
|
||||
const LESSON_PLAN_STATUSES = ["draft", "published", "archived"] as const;
|
||||
function isLessonPlanStatus(v: string): v is LessonPlanStatus {
|
||||
return (LESSON_PLAN_STATUSES as readonly string[]).includes(v);
|
||||
}
|
||||
|
||||
/**
|
||||
* publish-service 错误:使用错误码替代硬编码中文,
|
||||
* 由 actions 层通过 PUBLISH_ERROR_KEY_MAP 翻译为 i18n 消息。
|
||||
@@ -44,7 +43,8 @@ export type PublishErrorCode =
|
||||
| "NO_QUESTIONS"
|
||||
| "ALREADY_PUBLISHED"
|
||||
| "NO_SUBJECT_OR_GRADE"
|
||||
| "NO_STUDENTS";
|
||||
| "NO_STUDENTS"
|
||||
| "INVALID_QUESTION_TYPE";
|
||||
|
||||
export class PublishServiceError extends Error {
|
||||
constructor(public readonly code: PublishErrorCode) {
|
||||
@@ -64,7 +64,10 @@ export async function publishLessonPlanHomework(
|
||||
.limit(1);
|
||||
if (rows.length === 0) throw new PublishServiceError("PLAN_NOT_FOUND");
|
||||
const row = rows[0];
|
||||
// 类型守卫:从 Drizzle 推导类型收窄为 LessonPlan 所需字段
|
||||
// 使用类型守卫收窄(替代 as 断言)
|
||||
const status: LessonPlanStatus = isLessonPlanStatus(row.status)
|
||||
? row.status
|
||||
: "draft";
|
||||
const plan: LessonPlan = {
|
||||
id: row.id,
|
||||
title: row.title,
|
||||
@@ -76,7 +79,7 @@ export async function publishLessonPlanHomework(
|
||||
templateId: row.templateId,
|
||||
templateName: row.templateName,
|
||||
content: normalizeDocument(row.content),
|
||||
status: isLessonPlanStatus(row.status) ? row.status : "draft",
|
||||
status,
|
||||
creatorId: row.creatorId,
|
||||
lastSavedAt: row.lastSavedAt?.toISOString() ?? null,
|
||||
createdAt: row.createdAt.toISOString(),
|
||||
@@ -85,13 +88,15 @@ export async function publishLessonPlanHomework(
|
||||
if (plan.creatorId !== input.userId)
|
||||
throw new PublishServiceError("NO_PERMISSION");
|
||||
|
||||
// 2. 定位 exercise block
|
||||
// 2. 定位 exercise block(使用类型守卫替代 as 断言)
|
||||
const block = plan.content.nodes.find((b) => b.id === input.blockId);
|
||||
if (!block || block.type !== "exercise")
|
||||
throw new PublishServiceError("NO_EXERCISE_BLOCK");
|
||||
const data = block.data as ExerciseBlockData;
|
||||
if (data.items.length === 0) throw new PublishServiceError("NO_QUESTIONS");
|
||||
if (data.publishedAssignmentId)
|
||||
if (!isExerciseBlockData(block.data))
|
||||
throw new PublishServiceError("NO_EXERCISE_BLOCK");
|
||||
if (block.data.items.length === 0)
|
||||
throw new PublishServiceError("NO_QUESTIONS");
|
||||
if (block.data.publishedAssignmentId)
|
||||
throw new PublishServiceError("ALREADY_PUBLISHED");
|
||||
|
||||
// 3. inline 题目入库,替换占位 ID
|
||||
@@ -99,22 +104,22 @@ export async function publishLessonPlanHomework(
|
||||
const newBlock = newContent.nodes.find((b) => b.id === input.blockId);
|
||||
if (!newBlock || newBlock.type !== "exercise")
|
||||
throw new PublishServiceError("NO_EXERCISE_BLOCK");
|
||||
const newData = newBlock.data as ExerciseBlockData;
|
||||
if (!isExerciseBlockData(newBlock.data))
|
||||
throw new PublishServiceError("NO_EXERCISE_BLOCK");
|
||||
const newData = newBlock.data;
|
||||
|
||||
for (let i = 0; i < newData.items.length; i++) {
|
||||
const item = newData.items[i];
|
||||
if (item.source === "inline" && item.inlineContent) {
|
||||
// 类型守卫:确保 inline 题目类型合法
|
||||
const validTypes = ["single_choice", "multiple_choice", "text", "judgment", "composite"] as const;
|
||||
const qt = item.inlineContent.type;
|
||||
if (!validTypes.includes(qt as typeof validTypes[number])) {
|
||||
throw new Error(`无效的题目类型: ${qt}`);
|
||||
// 使用类型守卫校验题目类型(替代 as 断言 + 硬编码中文错误)
|
||||
if (!isValidQuestionType(qt)) {
|
||||
throw new PublishServiceError("INVALID_QUESTION_TYPE");
|
||||
}
|
||||
const questionType = qt as typeof validTypes[number];
|
||||
const questionId = await createQuestionWithRelations(
|
||||
{
|
||||
content: item.inlineContent.content,
|
||||
type: questionType,
|
||||
type: qt,
|
||||
difficulty: item.inlineContent.difficulty,
|
||||
knowledgePointIds: item.inlineContent.knowledgePointIds,
|
||||
},
|
||||
@@ -128,19 +133,19 @@ export async function publishLessonPlanHomework(
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 打包 exam 草稿
|
||||
// 4. 打包 exam 草稿(标题/描述由 actions 层 i18n 传入)
|
||||
const examId = createId();
|
||||
if (!plan.subjectId || !plan.gradeId) {
|
||||
throw new PublishServiceError("NO_SUBJECT_OR_GRADE");
|
||||
}
|
||||
await persistExamDraft({
|
||||
examId,
|
||||
title: `${plan.title} - 作业`,
|
||||
title: input.homeworkTitle,
|
||||
creatorId: input.userId,
|
||||
subjectId: plan.subjectId,
|
||||
gradeId: plan.gradeId,
|
||||
scheduledAt: undefined,
|
||||
description: `来自课案:${plan.title}`,
|
||||
description: input.homeworkDescription,
|
||||
});
|
||||
// 插入 examQuestions(通过 exams data-access 跨模块接口)
|
||||
await addExamQuestions(
|
||||
@@ -161,8 +166,8 @@ export async function publishLessonPlanHomework(
|
||||
await createHomeworkAssignment({
|
||||
assignmentId,
|
||||
sourceExamId: examId,
|
||||
title: `${plan.title} - 作业`,
|
||||
description: `来自课案:${plan.title}`,
|
||||
title: input.homeworkTitle,
|
||||
description: input.homeworkDescription,
|
||||
structure: null,
|
||||
status: "published",
|
||||
creatorId: input.userId,
|
||||
|
||||
Reference in New Issue
Block a user