feat(lesson-preparation): add AI evaluation, analytics, attachments, calendar, comments, review, substitutes, formative, and version diff

- Add actions-ai-evaluation, actions-analytics, actions-attachments, actions-calendar, actions-comments, actions-formative, actions-questions, actions-review, actions-substitutes

- Add corresponding data-access layers for each new action module

- Add calendar-view, curriculum-map-view, version-diff-viewer components

- Add editor-slice, selection-slice, version-slice hooks for state management

- Add document-diff and scope-check lib utilities

- Add default-question-service and external-questions-bridge services
This commit is contained in:
SpecialX
2026-07-03 10:25:21 +08:00
parent a16f09d3c3
commit 20023e13fd
75 changed files with 5131 additions and 1186 deletions

View File

@@ -0,0 +1,145 @@
/**
* M2 协同备课 - 评论 Server Actions
*/
"use server";
import { revalidatePath } from "next/cache";
import { z } from "zod";
import {
getCommentsByPlanId,
getCommentsByBlockId,
createComment,
updateCommentContent,
toggleCommentResolved,
deleteComment,
countUnresolvedComments,
} from "./data-access-comments";
import type { LessonPlanComment } from "./data-access-comments";
import type { ActionState } from "@/shared/types/action-state";
import { getAuthContext, requirePermission } from "@/shared/lib/auth-guard";
import { handleActionError } from "@/shared/lib/action-utils";
import { Permissions } from "@/shared/types/permissions";
const createCommentSchema = z.object({
planId: z.string().min(1, "error.planIdRequired"),
blockId: z.string().min(1, "error.blockIdRequired"),
content: z.string().min(1, "error.contentRequired").max(2000, "error.contentTooLong"),
parentCommentId: z.string().optional(),
});
const updateCommentSchema = z.object({
commentId: z.string().min(1, "error.commentIdRequired"),
content: z.string().min(1, "error.contentRequired").max(2000, "error.contentTooLong"),
});
/**
* 查询课案评论
*/
export async function getLessonPlanCommentsAction(
planId: string,
blockId?: string,
): Promise<ActionState<{ items: LessonPlanComment[] }>> {
try {
await requirePermission(Permissions.LESSON_PLAN_READ);
const items = blockId
? await getCommentsByBlockId(planId, blockId)
: await getCommentsByPlanId(planId);
return { success: true, data: { items } };
} catch (e) {
return handleActionError(e);
}
}
/**
* 创建评论
*/
export async function createLessonPlanCommentAction(
input: Record<string, unknown>,
): Promise<ActionState<{ comment: LessonPlanComment }>> {
try {
await requirePermission(Permissions.LESSON_PLAN_UPDATE);
const parseResult = createCommentSchema.safeParse(input);
if (!parseResult.success) {
return {
success: false,
message: "Invalid input",
errors: Object.fromEntries(
Object.entries(parseResult.error.flatten().fieldErrors),
),
};
}
const auth = await getAuthContext();
if (!auth.userId) {
return { success: false, message: "Unauthorized" };
}
const comment = await createComment(parseResult.data, auth.userId);
revalidatePath(`/teacher/lesson-plans/${parseResult.data.planId}/edit`);
return { success: true, data: { comment } };
} catch (e) {
return handleActionError(e);
}
}
/**
* 更新评论
*/
export async function updateLessonPlanCommentAction(
input: Record<string, unknown>,
): Promise<ActionState<null>> {
try {
await requirePermission(Permissions.LESSON_PLAN_UPDATE);
const parseResult = updateCommentSchema.safeParse(input);
if (!parseResult.success) {
return { success: false, message: "Invalid input" };
}
await updateCommentContent(parseResult.data.commentId, parseResult.data.content);
return { success: true, data: null };
} catch (e) {
return handleActionError(e);
}
}
/**
* 切换评论解决状态
*/
export async function toggleCommentResolvedAction(
commentId: string,
): Promise<ActionState<null>> {
try {
await requirePermission(Permissions.LESSON_PLAN_UPDATE);
await toggleCommentResolved(commentId);
return { success: true, data: null };
} catch (e) {
return handleActionError(e);
}
}
/**
* 删除评论
*/
export async function deleteLessonPlanCommentAction(
commentId: string,
): Promise<ActionState<null>> {
try {
await requirePermission(Permissions.LESSON_PLAN_UPDATE);
await deleteComment(commentId);
return { success: true, data: null };
} catch (e) {
return handleActionError(e);
}
}
/**
* 统计未解决评论数(用于列表卡片徽章)
*/
export async function getUnresolvedCommentCountAction(
planId: string,
): Promise<ActionState<{ count: number }>> {
try {
await requirePermission(Permissions.LESSON_PLAN_READ);
const count = await countUnresolvedComments(planId);
return { success: true, data: { count } };
} catch (e) {
return handleActionError(e);
}
}