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:
@@ -1,6 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useLessonPlanEditor } from "../../hooks/use-lesson-plan-editor";
|
||||
import { QuestionBankPicker } from "../question-bank-picker";
|
||||
import { InlineQuestionEditor } from "../inline-question-editor";
|
||||
@@ -10,15 +12,20 @@ import { Plus, Trash2 } from "lucide-react";
|
||||
import type {
|
||||
ExerciseBlockData,
|
||||
ExerciseItem,
|
||||
ExercisePurpose,
|
||||
} from "../../types";
|
||||
|
||||
interface Props {
|
||||
blockId: string;
|
||||
data: ExerciseBlockData;
|
||||
classes: { id: string; name: string }[];
|
||||
textbookId?: string;
|
||||
chapterId?: string;
|
||||
}
|
||||
|
||||
export function ExerciseBlock({ blockId, data, classes }: Props) {
|
||||
export function ExerciseBlock({ blockId, data, classes, textbookId, chapterId }: Props) {
|
||||
const t = useTranslations("lessonPreparation");
|
||||
const router = useRouter();
|
||||
const { updateNode, planId } = useLessonPlanEditor();
|
||||
const [showBank, setShowBank] = useState(false);
|
||||
const [showInline, setShowInline] = useState(false);
|
||||
@@ -49,34 +56,34 @@ export function ExerciseBlock({ blockId, data, classes }: Props) {
|
||||
<select
|
||||
value={data.purpose}
|
||||
onChange={(e) =>
|
||||
update({ purpose: e.target.value as never })
|
||||
update({ purpose: e.target.value as ExercisePurpose })
|
||||
}
|
||||
className="border rounded px-2 py-1 text-sm"
|
||||
>
|
||||
<option value="class_practice">课堂练习</option>
|
||||
<option value="after_class_homework">课后作业</option>
|
||||
<option value="class_practice">{t("exercise.purpose.class_practice")}</option>
|
||||
<option value="after_class_homework">{t("exercise.purpose.after_class_homework")}</option>
|
||||
</select>
|
||||
</div>
|
||||
{data.items.length === 0 ? (
|
||||
<p className="text-on-surface-variant text-sm p-4 text-center border border-dashed rounded">
|
||||
暂无题目,点击下方按钮添加
|
||||
{t("questionBank.empty")}
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
{data.items.map((item, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
key={item.questionId}
|
||||
className="flex items-center gap-2 border rounded p-2"
|
||||
>
|
||||
<span className="text-xs bg-surface-container-highest px-2 py-0.5 rounded">
|
||||
{item.source === "bank" ? "题库" : "新建"}
|
||||
{item.source === "bank" ? t("questionBank.source.bank") : t("questionBank.source.inline")}
|
||||
</span>
|
||||
<span className="text-sm flex-1 truncate">
|
||||
{item.source === "bank"
|
||||
? `题目 ${item.questionId.slice(0, 8)}`
|
||||
: "课案内新建题目"}
|
||||
? t("questionBank.questionId", { id: item.questionId.slice(0, 8) })
|
||||
: t("questionBank.inlineQuestion")}
|
||||
</span>
|
||||
<span className="text-xs">{item.score}分</span>
|
||||
<span className="text-xs">{t("questionBank.score", { score: item.score })}</span>
|
||||
<button onClick={() => removeItem(idx)}>
|
||||
<Trash2 className="w-3 h-3 text-error" />
|
||||
</button>
|
||||
@@ -91,7 +98,7 @@ export function ExerciseBlock({ blockId, data, classes }: Props) {
|
||||
onClick={() => setShowBank(true)}
|
||||
>
|
||||
<Plus className="w-3 h-3 mr-1" />
|
||||
从题库添加
|
||||
{t("questionBank.fromBank")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -99,18 +106,18 @@ export function ExerciseBlock({ blockId, data, classes }: Props) {
|
||||
onClick={() => setShowInline(true)}
|
||||
>
|
||||
<Plus className="w-3 h-3 mr-1" />
|
||||
新建题目
|
||||
{t("questionBank.inlineNew")}
|
||||
</Button>
|
||||
{data.publishedAssignmentId ? (
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<span className="bg-tertiary-container/20 text-tertiary px-2 py-1 rounded">
|
||||
已发布为作业
|
||||
{t("status.publishedAsHomework")}
|
||||
</span>
|
||||
<a
|
||||
href="/teacher/homework"
|
||||
className="text-primary underline"
|
||||
>
|
||||
查看
|
||||
{t("action.viewHomework")}
|
||||
</a>
|
||||
</div>
|
||||
) : (
|
||||
@@ -120,7 +127,7 @@ export function ExerciseBlock({ blockId, data, classes }: Props) {
|
||||
size="sm"
|
||||
onClick={() => setShowPublish(true)}
|
||||
>
|
||||
发布为作业
|
||||
{t("action.publish")}
|
||||
</Button>
|
||||
)
|
||||
)}
|
||||
@@ -134,6 +141,8 @@ export function ExerciseBlock({ blockId, data, classes }: Props) {
|
||||
)}
|
||||
{showInline && (
|
||||
<InlineQuestionEditor
|
||||
textbookId={textbookId}
|
||||
chapterId={chapterId}
|
||||
onAdd={(item) => {
|
||||
addItems([item]);
|
||||
setShowInline(false);
|
||||
@@ -147,7 +156,7 @@ export function ExerciseBlock({ blockId, data, classes }: Props) {
|
||||
blockId={blockId}
|
||||
classes={classes}
|
||||
onClose={() => setShowPublish(false)}
|
||||
onPublished={() => window.location.reload()}
|
||||
onPublished={() => router.refresh()}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user