feat(lesson-preparation): 备课模块审计重构 — 跨模块解耦 + i18n + 纯函数抽取 + 错误边界

P0-1 跨模块直查修复:publish-service 不再直查 examQuestions 表,新增 exams/data-access.addExamQuestions 接口,复用 classes/data-access.getStudentIdsByClassIds

P0-2 i18n 接入:新增 zh-CN/en 翻译文件,注册 lessonPreparation 命名空间,17 个组件改造为 useTranslations/getTranslations

P1 纯函数抽取:lib/document-migration.ts(类型守卫替代 as 断言)、lib/node-summary.ts(翻译函数注入)、lib/rf-mappers.ts

P1 错误边界+骨架屏:新增 LessonPlanErrorBoundary 和 4 个 Skeleton 组件

P1 Block 注册表:新增 config/block-registry.tsx(BlockRenderer 组件),node-edit-panel 重构为配置驱动渲染

P1 其他修复:exercise-block 改用 router.refresh(),node-editor/lesson-node 复用 lib/ 纯函数

架构图同步:更新 004 和 005 文档

Refs: docs/architecture/audit/lesson-preparation-audit-report.md
This commit is contained in:
SpecialX
2026-06-22 16:17:58 +08:00
parent 4833930834
commit 20691f53ce
32 changed files with 1456 additions and 360 deletions

View File

@@ -1,6 +1,7 @@
"use client";
import { useCallback, useEffect, useRef, useState } from "react";
import { useTranslations } from "next-intl";
import { useLessonPlanEditor } from "../hooks/use-lesson-plan-editor";
import { NodeEditor } from "./node-editor";
import { NodeEditPanel } from "./node-edit-panel";
@@ -10,7 +11,6 @@ import {
saveLessonPlanVersionAction,
getLessonPlanByIdAction,
} from "../actions";
import { BLOCK_TYPE_LABELS } from "../constants";
import type { BlockType } from "../types";
import { Button } from "@/shared/components/ui/button";
import { Plus, Save, History } from "lucide-react";
@@ -47,10 +47,10 @@ export function LessonPlanEditor({
chapterId,
classes,
}: Props) {
const t = useTranslations("lessonPreparation");
const editor = useLessonPlanEditor();
const [showVersions, setShowVersions] = useState(false);
const [showAddMenu, setShowAddMenu] = useState(false);
const [panelOpen, setPanelOpen] = useState(false);
const autoSaveTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
const versionTimer = useRef<ReturnType<typeof setInterval> | null>(null);
const addMenuRef = useRef<HTMLDivElement>(null);
@@ -62,11 +62,6 @@ export function LessonPlanEditor({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [initKey]);
// 选中节点时打开侧边面板
useEffect(() => {
if (editor.selectedNodeId) setPanelOpen(true);
}, [editor.selectedNodeId]);
// 自动保存debounce 3s- 用 getState() 获取最新值(修复 P1-4
useEffect(() => {
if (!editor.isDirty) return;
@@ -95,13 +90,13 @@ export function LessonPlanEditor({
await saveLessonPlanVersionAction({
planId: state.planId,
content: state.doc,
label: "自动版本",
label: t("version.autoLabel"),
});
}, 30 * 60 * 1000);
return () => {
if (versionTimer.current) clearInterval(versionTimer.current);
};
}, [planId]);
}, [planId, t]);
// 离开未保存提示P3-1
useEffect(() => {
@@ -158,20 +153,20 @@ export function LessonPlanEditor({
/>
<span className="text-on-surface-variant text-sm">
{editor.isSaving
? "保存中..."
? t("status.saving")
: editor.isDirty
? "未保存"
: "已保存"}
? t("status.unsaved")
: t("status.saved")}
</span>
<Button
variant="outline"
size="sm"
onClick={() => setShowVersions(true)}
>
<History className="w-4 h-4 mr-1" />
<History className="w-4 h-4 mr-1" /> {t("action.versions")}
</Button>
<Button size="sm" onClick={handleManualSave} disabled={editor.isSaving}>
<Save className="w-4 h-4 mr-1" />
<Save className="w-4 h-4 mr-1" /> {t("action.saveVersion")}
</Button>
</div>
@@ -179,31 +174,27 @@ export function LessonPlanEditor({
<div className="flex-1 flex overflow-hidden">
{/* 节点画布 */}
<div className="flex-1 relative">
<NodeEditor
textbookId={textbookId}
chapterId={chapterId}
classes={classes}
/>
<NodeEditor />
{/* 添加节点浮动按钮 */}
<div className="absolute bottom-4 left-4 z-10" ref={addMenuRef}>
<Button
variant="default"
onClick={() => setShowAddMenu(!showAddMenu)}
>
<Plus className="w-4 h-4 mr-1" />
<Plus className="w-4 h-4 mr-1" /> {t("action.addNode")}
</Button>
{showAddMenu && (
<div className="absolute bottom-12 left-0 bg-surface border border-outline-variant rounded-lg shadow-lg p-2 grid grid-cols-2 gap-1 w-72 max-h-[60vh] overflow-y-auto">
{BLOCK_TYPES_TO_ADD.map((t) => (
{BLOCK_TYPES_TO_ADD.map((blockType) => (
<button
key={t}
key={blockType}
onClick={() => {
editor.addNode(t);
editor.addNode(blockType);
setShowAddMenu(false);
}}
className="text-left px-2 py-1 text-sm hover:bg-surface-container-highest rounded"
>
{BLOCK_TYPE_LABELS[t]}
{t(`blockType.${blockType}`)}
</button>
))}
</div>
@@ -211,8 +202,8 @@ export function LessonPlanEditor({
</div>
</div>
{/* 侧边内容编辑面板 */}
{panelOpen && editor.selectedNodeId && (
{/* 侧边内容编辑面板:直接用 selectedNodeId 控制显示(修复 P1-2 */}
{editor.selectedNodeId && (
<div className="w-[420px] flex-shrink-0">
<NodeEditPanel
textbookId={textbookId}