refactor(lesson-preparation): V2 审计深度修复 — Server Actions i18n + 错误码模式 + 类型断言清零 + a11y 深度修复 + Tracker 埋点接入
V2-1: 12 个 Server Action 通过 getTranslations 翻译错误消息;Service/DataAccess 层抛出错误码异常(PublishServiceError/LessonPlanDataError),Actions 层通过 PUBLISH_ERROR_KEY_MAP 翻译为 i18n 消息 V2-2: SYSTEM_TEMPLATES name/title 改为 i18n 键,createLessonPlan 接受 translateTitle 函数在服务端翻译后存储到 DB V2-3: 8 处 as unknown as 断言替换为显式类型映射函数(mapRowToLessonPlan/mapRowToListItem/mapRowToTemplate/mapRowToVersion)+ 类型守卫(isLessonPlanStatus/isTemplateType/isTemplateScope) V2-4: MiniMap nodeColor 复用 lib/node-summary.ts 的 getNodeColor V2-5: a11y 深度修复 — lesson-plan-filters/exercise-block/inline-question-editor 的 select 添加 label htmlFor 关联;exercise-block 题目列表改为 ul/li;node-editor 画布添加 role=application + 键盘导航配置 V2-6: Tracker 埋点接入 — 新增 useLessonPlanTrackerSafe hook,在 create/save/publish/revert/duplicate/archive 6 处调用 tracker.track 同步更新架构图 004 和 005 文档
This commit is contained in:
@@ -6,12 +6,48 @@ import { createId } from "@paralleldrive/cuid2";
|
||||
import { db } from "@/shared/db";
|
||||
import { lessonPlanTemplates, lessonPlans } from "@/shared/db/schema";
|
||||
import { SYSTEM_TEMPLATES } from "./constants";
|
||||
import { normalizeDocument } from "./data-access";
|
||||
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[]> {
|
||||
@@ -36,8 +72,7 @@ export async function getLessonPlanTemplates(
|
||||
eq(lessonPlanTemplates.creatorId, userId),
|
||||
),
|
||||
);
|
||||
const personalTemplates =
|
||||
personalRows as unknown as LessonPlanTemplate[];
|
||||
const personalTemplates = personalRows.map(mapRowToTemplate);
|
||||
|
||||
return [...systemTemplates, ...personalTemplates];
|
||||
}
|
||||
@@ -58,7 +93,7 @@ export async function saveAsTemplate(input: {
|
||||
),
|
||||
)
|
||||
.limit(1);
|
||||
if (plan.length === 0) throw new Error("课案不存在或无权访问");
|
||||
if (plan.length === 0) throw new LessonPlanDataError("NOT_FOUND");
|
||||
|
||||
const doc = normalizeDocument(plan[0].content);
|
||||
const skeleton: TemplateBlockSkeleton[] = doc.nodes.map((b) => ({
|
||||
|
||||
Reference in New Issue
Block a user