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 文档
320 lines
11 KiB
TypeScript
320 lines
11 KiB
TypeScript
"use server";
|
||
|
||
import { revalidatePath } from "next/cache";
|
||
import { getTranslations } from "next-intl/server";
|
||
import { requirePermission, PermissionDeniedError } from "@/shared/lib/auth-guard";
|
||
import { Permissions } from "@/shared/types/permissions";
|
||
import {
|
||
getLessonPlans,
|
||
getLessonPlanById,
|
||
createLessonPlan,
|
||
updateLessonPlanContent,
|
||
softDeleteLessonPlan,
|
||
duplicateLessonPlan,
|
||
LessonPlanDataError,
|
||
} from "./data-access";
|
||
import {
|
||
getLessonPlanVersions,
|
||
createLessonPlanVersion,
|
||
revertToVersion,
|
||
pruneAutoVersions,
|
||
} from "./data-access-versions";
|
||
import {
|
||
getLessonPlanTemplates,
|
||
saveAsTemplate,
|
||
deletePersonalTemplate,
|
||
} from "./data-access-templates";
|
||
import {
|
||
createLessonPlanSchema,
|
||
updateLessonPlanContentSchema,
|
||
saveVersionSchema,
|
||
revertVersionSchema,
|
||
saveAsTemplateSchema,
|
||
} from "./schema";
|
||
import type { ActionState, LessonPlanDocument } from "./types";
|
||
|
||
// ---- 课案列表 ----
|
||
export async function getLessonPlansAction(params: {
|
||
query?: string;
|
||
textbookId?: string;
|
||
chapterId?: string;
|
||
subjectId?: string;
|
||
status?: string;
|
||
}): Promise<
|
||
ActionState<{
|
||
items: Awaited<ReturnType<typeof getLessonPlans>>;
|
||
}>
|
||
> {
|
||
const t = await getTranslations("lessonPreparation");
|
||
try {
|
||
const ctx = await requirePermission(Permissions.LESSON_PLAN_READ);
|
||
const items = await getLessonPlans(params, ctx.dataScope, ctx.userId);
|
||
return { success: true, data: { items } };
|
||
} catch (e) {
|
||
if (e instanceof PermissionDeniedError)
|
||
return { success: false, message: e.message };
|
||
return { success: false, message: t("error.getList") };
|
||
}
|
||
}
|
||
|
||
// ---- 单课案 ----
|
||
export async function getLessonPlanByIdAction(
|
||
planId: string,
|
||
): Promise<
|
||
ActionState<{ plan: Awaited<ReturnType<typeof getLessonPlanById>> }>
|
||
> {
|
||
const t = await getTranslations("lessonPreparation");
|
||
try {
|
||
const ctx = await requirePermission(Permissions.LESSON_PLAN_READ);
|
||
const plan = await getLessonPlanById(planId, ctx.userId);
|
||
if (!plan) return { success: false, message: t("error.notFound") };
|
||
return { success: true, data: { plan } };
|
||
} catch (e) {
|
||
if (e instanceof PermissionDeniedError)
|
||
return { success: false, message: e.message };
|
||
return { success: false, message: t("error.getOne") };
|
||
}
|
||
}
|
||
|
||
// ---- 创建 ----
|
||
export async function createLessonPlanAction(
|
||
prevState: ActionState | null,
|
||
formData: FormData,
|
||
): Promise<ActionState<{ planId: string }>> {
|
||
const t = await getTranslations("lessonPreparation");
|
||
try {
|
||
const ctx = await requirePermission(Permissions.LESSON_PLAN_CREATE);
|
||
const parsed = createLessonPlanSchema.safeParse({
|
||
title: formData.get("title"),
|
||
textbookId: formData.get("textbookId") || undefined,
|
||
chapterId: formData.get("chapterId") || undefined,
|
||
subjectId: formData.get("subjectId") || undefined,
|
||
gradeId: formData.get("gradeId") || undefined,
|
||
templateId: formData.get("templateId"),
|
||
});
|
||
if (!parsed.success) {
|
||
return { success: false, errors: parsed.error.flatten().fieldErrors };
|
||
}
|
||
const { planId } = await createLessonPlan({
|
||
...parsed.data,
|
||
creatorId: ctx.userId,
|
||
// V2-2 修复:传入翻译函数,将 SYSTEM_TEMPLATES 中的 i18n 键翻译为实际文本
|
||
translateTitle: (key: string) => {
|
||
// 支持 "blockType.objective" 和 "template.blocks.tpl_review.1" 两种键
|
||
if (key.startsWith("blockType.")) {
|
||
const blockKey = key.replace("blockType.", "");
|
||
return t(`blockType.${blockKey}`);
|
||
}
|
||
if (key.startsWith("template.blocks.")) {
|
||
const parts = key.split(".");
|
||
const templateId = parts[2];
|
||
const blockIndex = parts[3];
|
||
return t(`template.blocks.${templateId}.${blockIndex}`);
|
||
}
|
||
return key;
|
||
},
|
||
});
|
||
revalidatePath("/teacher/lesson-plans");
|
||
return { success: true, data: { planId } };
|
||
} catch (e) {
|
||
if (e instanceof PermissionDeniedError)
|
||
return { success: false, message: e.message };
|
||
if (e instanceof LessonPlanDataError && e.code === "TEMPLATE_NOT_FOUND")
|
||
return { success: false, message: t("error.templateNotFound") };
|
||
return { success: false, message: t("error.create") };
|
||
}
|
||
}
|
||
|
||
// ---- 更新 content(自动保存)----
|
||
export async function updateLessonPlanAction(input: {
|
||
planId: string;
|
||
title?: string;
|
||
content: LessonPlanDocument;
|
||
}): Promise<ActionState> {
|
||
const t = await getTranslations("lessonPreparation");
|
||
try {
|
||
const ctx = await requirePermission(Permissions.LESSON_PLAN_UPDATE);
|
||
const parsed = updateLessonPlanContentSchema.safeParse(input);
|
||
if (!parsed.success)
|
||
return { success: false, errors: parsed.error.flatten().fieldErrors };
|
||
await updateLessonPlanContent(parsed.data.planId, ctx.userId, {
|
||
...(parsed.data.title ? { title: parsed.data.title } : {}),
|
||
content: parsed.data.content as LessonPlanDocument,
|
||
});
|
||
return { success: true };
|
||
} catch (e) {
|
||
if (e instanceof PermissionDeniedError)
|
||
return { success: false, message: e.message };
|
||
return { success: false, message: t("error.save") };
|
||
}
|
||
}
|
||
|
||
// ---- 手动保存版本 ----
|
||
export async function saveLessonPlanVersionAction(input: {
|
||
planId: string;
|
||
content: LessonPlanDocument;
|
||
label?: string;
|
||
}): Promise<ActionState<{ versionNo: number }>> {
|
||
const t = await getTranslations("lessonPreparation");
|
||
try {
|
||
const ctx = await requirePermission(Permissions.LESSON_PLAN_UPDATE);
|
||
const parsed = saveVersionSchema.safeParse(input);
|
||
if (!parsed.success)
|
||
return { success: false, errors: parsed.error.flatten().fieldErrors };
|
||
const { versionNo } = await createLessonPlanVersion({
|
||
planId: parsed.data.planId,
|
||
content: input.content,
|
||
userId: ctx.userId,
|
||
isAuto: false,
|
||
label: parsed.data.label,
|
||
});
|
||
await pruneAutoVersions(parsed.data.planId);
|
||
return { success: true, data: { versionNo } };
|
||
} catch (e) {
|
||
if (e instanceof PermissionDeniedError)
|
||
return { success: false, message: e.message };
|
||
return { success: false, message: t("error.saveVersion") };
|
||
}
|
||
}
|
||
|
||
// ---- 版本列表 ----
|
||
export async function getLessonPlanVersionsAction(
|
||
planId: string,
|
||
): Promise<
|
||
ActionState<{
|
||
versions: Awaited<ReturnType<typeof getLessonPlanVersions>>;
|
||
}>
|
||
> {
|
||
const t = await getTranslations("lessonPreparation");
|
||
try {
|
||
const ctx = await requirePermission(Permissions.LESSON_PLAN_READ);
|
||
const versions = await getLessonPlanVersions(planId, ctx.userId);
|
||
return { success: true, data: { versions } };
|
||
} catch (e) {
|
||
if (e instanceof PermissionDeniedError)
|
||
return { success: false, message: e.message };
|
||
return { success: false, message: t("error.getVersions") };
|
||
}
|
||
}
|
||
|
||
// ---- 回退版本 ----
|
||
export async function revertLessonPlanVersionAction(input: {
|
||
planId: string;
|
||
versionNo: number;
|
||
}): Promise<ActionState<{ newVersionNo: number }>> {
|
||
const t = await getTranslations("lessonPreparation");
|
||
try {
|
||
const ctx = await requirePermission(Permissions.LESSON_PLAN_UPDATE);
|
||
const parsed = revertVersionSchema.safeParse(input);
|
||
if (!parsed.success)
|
||
return { success: false, errors: parsed.error.flatten().fieldErrors };
|
||
const result = await revertToVersion(
|
||
parsed.data.planId,
|
||
parsed.data.versionNo,
|
||
ctx.userId,
|
||
);
|
||
if (!result) return { success: false, message: t("error.versionNotFound") };
|
||
revalidatePath(`/teacher/lesson-plans/${parsed.data.planId}/edit`);
|
||
return { success: true, data: { newVersionNo: result.newVersionNo } };
|
||
} catch (e) {
|
||
if (e instanceof PermissionDeniedError)
|
||
return { success: false, message: e.message };
|
||
return { success: false, message: t("error.revert") };
|
||
}
|
||
}
|
||
|
||
// ---- 删除(软删除)----
|
||
export async function deleteLessonPlanAction(
|
||
planId: string,
|
||
): Promise<ActionState> {
|
||
const t = await getTranslations("lessonPreparation");
|
||
try {
|
||
const ctx = await requirePermission(Permissions.LESSON_PLAN_DELETE);
|
||
await softDeleteLessonPlan(planId, ctx.userId);
|
||
revalidatePath("/teacher/lesson-plans");
|
||
return { success: true };
|
||
} catch (e) {
|
||
if (e instanceof PermissionDeniedError)
|
||
return { success: false, message: e.message };
|
||
return { success: false, message: t("error.delete") };
|
||
}
|
||
}
|
||
|
||
// ---- 复制 ----
|
||
export async function duplicateLessonPlanAction(
|
||
planId: string,
|
||
): Promise<ActionState<{ newPlanId: string }>> {
|
||
const t = await getTranslations("lessonPreparation");
|
||
try {
|
||
const ctx = await requirePermission(Permissions.LESSON_PLAN_CREATE);
|
||
const { newPlanId } = await duplicateLessonPlan(planId, ctx.userId);
|
||
revalidatePath("/teacher/lesson-plans");
|
||
return { success: true, data: { newPlanId } };
|
||
} catch (e) {
|
||
if (e instanceof PermissionDeniedError)
|
||
return { success: false, message: e.message };
|
||
if (e instanceof LessonPlanDataError && e.code === "NOT_FOUND")
|
||
return { success: false, message: t("error.notFound") };
|
||
return { success: false, message: t("error.duplicate") };
|
||
}
|
||
}
|
||
|
||
// ---- 模板列表 ----
|
||
export async function getLessonPlanTemplatesAction(): Promise<
|
||
ActionState<{
|
||
templates: Awaited<ReturnType<typeof getLessonPlanTemplates>>;
|
||
}>
|
||
> {
|
||
const t = await getTranslations("lessonPreparation");
|
||
try {
|
||
const ctx = await requirePermission(Permissions.LESSON_PLAN_READ);
|
||
const templates = await getLessonPlanTemplates(ctx.userId);
|
||
return { success: true, data: { templates } };
|
||
} catch (e) {
|
||
if (e instanceof PermissionDeniedError)
|
||
return { success: false, message: e.message };
|
||
return { success: false, message: t("error.getTemplates") };
|
||
}
|
||
}
|
||
|
||
// ---- 另存为模板 ----
|
||
export async function saveAsTemplateAction(input: {
|
||
sourcePlanId: string;
|
||
name: string;
|
||
}): Promise<ActionState<{ templateId: string }>> {
|
||
const t = await getTranslations("lessonPreparation");
|
||
try {
|
||
const ctx = await requirePermission(Permissions.LESSON_PLAN_CREATE);
|
||
const parsed = saveAsTemplateSchema.safeParse(input);
|
||
if (!parsed.success)
|
||
return { success: false, errors: parsed.error.flatten().fieldErrors };
|
||
const { templateId } = await saveAsTemplate({
|
||
...parsed.data,
|
||
userId: ctx.userId,
|
||
});
|
||
return { success: true, data: { templateId } };
|
||
} catch (e) {
|
||
if (e instanceof PermissionDeniedError)
|
||
return { success: false, message: e.message };
|
||
if (e instanceof LessonPlanDataError && e.code === "NOT_FOUND")
|
||
return { success: false, message: t("error.notFound") };
|
||
return { success: false, message: t("error.saveTemplate") };
|
||
}
|
||
}
|
||
|
||
// ---- 删除模板 ----
|
||
export async function deleteTemplateAction(
|
||
templateId: string,
|
||
): Promise<ActionState> {
|
||
const t = await getTranslations("lessonPreparation");
|
||
try {
|
||
const ctx = await requirePermission(Permissions.LESSON_PLAN_DELETE);
|
||
await deletePersonalTemplate(templateId, ctx.userId);
|
||
return { success: true };
|
||
} catch (e) {
|
||
if (e instanceof PermissionDeniedError)
|
||
return { success: false, message: e.message };
|
||
return { success: false, message: t("error.deleteTemplate") };
|
||
}
|
||
}
|