feat(textbooks): 教材模块审计重构 — 跨模块解耦 + 权限 + i18n + 错误边界 + 纯函数抽取
P0 修复: - 解耦跨模块 UI 依赖:knowledge-point-dialogs 不再直接 import questions, 改为 renderQuestionCreator render prop 由页面注入 - 接入 usePermission Hook 替换 canEdit 硬编码 - 全模块 i18n 改造:新增 en/zh-CN 翻译文件,替换所有硬编码文案 - Server Action 资源归属校验:新增 verifyChapterBelongsToTextbook/ verifyKnowledgePointBelongsToTextbook,在 reorder/update/delete/create 中校验 P1 改进: - 补齐 Error Boundary:4 个 error.tsx + TextbookSectionErrorBoundary 区块包裹 - 抽取纯函数到 utils.ts/graph-layout.ts/constants.ts 并补单测(26 用例全通过) - 消除重复组件:删除 knowledge-point-panel/create-knowledge-point-dialog - 修复类型断言:chapter.children! → 守卫式访问 - 图谱 a11y:添加 role/aria-label/aria-pressed - 统一删除确认:confirm() → AlertDialog - 数据范围过滤:getTextbooksWithScope 支持学生端按年级过滤 P2 预留: - TextbookAnalytics 埋点接口 + Provider + Hook 同步 005 架构数据 JSON:补充 getTextbooksWithScope/verify*/ChapterTreeNode 等
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
import { useState } from "react"
|
||||
import { Plus } from "lucide-react"
|
||||
import { useFormStatus } from "react-dom"
|
||||
import { useTranslations } from "next-intl"
|
||||
import { Button } from "@/shared/components/ui/button"
|
||||
import {
|
||||
Dialog,
|
||||
@@ -23,22 +24,23 @@ import {
|
||||
SelectValue,
|
||||
} from "@/shared/components/ui/select"
|
||||
import { createTextbookAction } from "../actions"
|
||||
import { SUBJECTS, GRADES } from "../constants"
|
||||
import { toast } from "sonner"
|
||||
|
||||
function SubmitButton() {
|
||||
const { pending } = useFormStatus()
|
||||
const t = useTranslations("textbooks")
|
||||
return (
|
||||
<Button type="submit" disabled={pending}>
|
||||
{pending ? "Saving..." : "Save changes"}
|
||||
{pending ? t("dialog.create.saving") : t("dialog.create.submit")}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
export function TextbookFormDialog() {
|
||||
const t = useTranslations("textbooks")
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
// Using simple form action without useActionState hook for simplicity in this demo environment
|
||||
// In production with React 19/Next 15, we'd use useActionState
|
||||
const handleSubmit = async (formData: FormData) => {
|
||||
const result = await createTextbookAction(null, formData)
|
||||
if (result.success) {
|
||||
@@ -54,72 +56,70 @@ export function TextbookFormDialog() {
|
||||
<DialogTrigger asChild>
|
||||
<Button>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Add Textbook
|
||||
{t("list.add")}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add New Textbook</DialogTitle>
|
||||
<DialogDescription>
|
||||
Create a new digital textbook. Click save when you're done.
|
||||
</DialogDescription>
|
||||
<DialogTitle>{t("dialog.create.title")}</DialogTitle>
|
||||
<DialogDescription>{t("dialog.create.description")}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<form action={handleSubmit}>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="title" className="text-right">
|
||||
Title
|
||||
{t("field.title")}
|
||||
</Label>
|
||||
<Input
|
||||
id="title"
|
||||
name="title"
|
||||
placeholder="e.g. Advanced Calculus"
|
||||
placeholder={t("field.titlePlaceholder")}
|
||||
className="col-span-3"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="subject" className="text-right">
|
||||
Subject
|
||||
{t("field.subject")}
|
||||
</Label>
|
||||
<Select name="subject" required>
|
||||
<SelectTrigger className="col-span-3">
|
||||
<SelectValue placeholder="Select subject" />
|
||||
<SelectValue placeholder={t("field.subjectPlaceholder")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="Mathematics">Mathematics</SelectItem>
|
||||
<SelectItem value="Physics">Physics</SelectItem>
|
||||
<SelectItem value="Chemistry">Chemistry</SelectItem>
|
||||
<SelectItem value="Biology">Biology</SelectItem>
|
||||
<SelectItem value="English">English</SelectItem>
|
||||
<SelectItem value="History">History</SelectItem>
|
||||
<SelectItem value="Geography">Geography</SelectItem>
|
||||
{SUBJECTS.map((s) => (
|
||||
<SelectItem key={s.value} value={s.value}>
|
||||
{t(`subject.${s.labelKey}`)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="grade" className="text-right">
|
||||
Grade
|
||||
{t("field.grade")}
|
||||
</Label>
|
||||
<Select name="grade" required>
|
||||
<SelectTrigger className="col-span-3">
|
||||
<SelectValue placeholder="Select grade" />
|
||||
<SelectValue placeholder={t("field.gradePlaceholder")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="Grade 10">Grade 10</SelectItem>
|
||||
<SelectItem value="Grade 11">Grade 11</SelectItem>
|
||||
<SelectItem value="Grade 12">Grade 12</SelectItem>
|
||||
{GRADES.map((g) => (
|
||||
<SelectItem key={g.value} value={g.value}>
|
||||
{t(`grade.${g.labelKey}`)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="publisher" className="text-right">
|
||||
Publisher
|
||||
{t("field.publisher")}
|
||||
</Label>
|
||||
<Input
|
||||
id="publisher"
|
||||
name="publisher"
|
||||
placeholder="e.g. Next Education"
|
||||
placeholder={t("field.publisherPlaceholder")}
|
||||
className="col-span-3"
|
||||
/>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user