refactor(school,classes): 完成 school/grade/class 审计全量改进项
P0-1/P0-2: 删除 grade-management 死模块,年级 CRUD 统一由 school 模块负责 P0-3: classes/actions.ts 从 974 行拆分为 6 个职责文件 + barrel re-export P0-5: 13 个页面 i18n 全量接入(grades/departments/academic-year/classes/insights) P1-1: 角色硬编码改为 hasAdminScope/hasTeacherScope/hasStudentScope 基于 dataScope.type P1-3: 新增 SchoolErrorBoundary + SchoolListSkeleton/SchoolCardSkeleton,4 个页面包裹 Error Boundary P1-4: classes/types.ts 跨领域类型添加归属决策注释 P1-5: schools-view.tsx 拆分为组合模式(SchoolFormDialog + SchoolDeleteDialog + SchoolListToolbar) P1-6: 新增 getSchoolsForUser/getGradesForUser 权限感知查询函数 P2-1: 抽取 useSchoolData hook,对话框状态管理与 UI 分离 同步更新架构图文档 004/005
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useMemo, useState } from "react"
|
||||
import { useCallback, useEffect, useMemo, useState } from "react"
|
||||
import { MoreHorizontal, Pencil, Plus, Trash2 } from "lucide-react"
|
||||
import { toast } from "sonner"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { parseAsString, useQueryState } from "nuqs"
|
||||
import { useTranslations } from "next-intl"
|
||||
|
||||
import type { GradeListItem, SchoolListItem, StaffOption } from "../types"
|
||||
import { createGradeAction, deleteGradeAction, updateGradeAction } from "../actions"
|
||||
@@ -66,43 +67,6 @@ const parseOrder = (raw: string) => {
|
||||
return n
|
||||
}
|
||||
|
||||
const validateForm = (
|
||||
state: FormState,
|
||||
params: { grades: GradeListItem[]; excludeGradeId?: string }
|
||||
): { ok: boolean; errors: FormErrors } => {
|
||||
const errors: FormErrors = {}
|
||||
|
||||
const schoolId = state.schoolId.trim()
|
||||
if (!schoolId) errors.schoolId = "请选择学校"
|
||||
|
||||
const name = normalizeName(state.name)
|
||||
if (!name) errors.name = "请输入年级名称"
|
||||
if (name.length > 100) errors.name = "年级名称最多 100 个字符"
|
||||
|
||||
const order = parseOrder(state.order)
|
||||
if (order === null) errors.order = "Order 必须是非负整数"
|
||||
|
||||
if (schoolId && name) {
|
||||
const dup = params.grades.find((g) => {
|
||||
if (params.excludeGradeId && g.id === params.excludeGradeId) return false
|
||||
return g.school.id === schoolId && normalizeName(g.name).toLowerCase() === name.toLowerCase()
|
||||
})
|
||||
if (dup) errors.name = "该学校下已存在同名年级"
|
||||
}
|
||||
|
||||
return { ok: Object.keys(errors).length === 0, errors }
|
||||
}
|
||||
|
||||
const formatStaffDetail = (u: StaffOption | null) => {
|
||||
if (!u) return <Badge variant="outline">未设置</Badge>
|
||||
return (
|
||||
<div className="min-w-0">
|
||||
<div className="truncate">{u.name}</div>
|
||||
<div className="truncate text-xs text-muted-foreground">{u.email}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function GradesClient({
|
||||
grades,
|
||||
schools,
|
||||
@@ -112,6 +76,7 @@ export function GradesClient({
|
||||
schools: SchoolListItem[]
|
||||
staff: StaffOption[]
|
||||
}) {
|
||||
const t = useTranslations("school")
|
||||
const router = useRouter()
|
||||
const [isWorking, setIsWorking] = useState(false)
|
||||
const [createOpen, setCreateOpen] = useState(false)
|
||||
@@ -149,6 +114,46 @@ export function GradesClient({
|
||||
})
|
||||
}, [staff])
|
||||
|
||||
const validateForm = useCallback(
|
||||
(state: FormState, params: { grades: GradeListItem[]; excludeGradeId?: string }): {
|
||||
ok: boolean
|
||||
errors: FormErrors
|
||||
} => {
|
||||
const errors: FormErrors = {}
|
||||
|
||||
const schoolId = state.schoolId.trim()
|
||||
if (!schoolId) errors.schoolId = t("grades.validation.selectSchool")
|
||||
|
||||
const name = normalizeName(state.name)
|
||||
if (!name) errors.name = t("grades.validation.enterName")
|
||||
if (name.length > 100) errors.name = t("grades.validation.nameTooLong")
|
||||
|
||||
const order = parseOrder(state.order)
|
||||
if (order === null) errors.order = t("grades.validation.orderInvalid")
|
||||
|
||||
if (schoolId && name) {
|
||||
const dup = params.grades.find((g) => {
|
||||
if (params.excludeGradeId && g.id === params.excludeGradeId) return false
|
||||
return g.school.id === schoolId && normalizeName(g.name).toLowerCase() === name.toLowerCase()
|
||||
})
|
||||
if (dup) errors.name = t("grades.validation.duplicateName")
|
||||
}
|
||||
|
||||
return { ok: Object.keys(errors).length === 0, errors }
|
||||
},
|
||||
[t]
|
||||
)
|
||||
|
||||
const formatStaffDetail = (u: StaffOption | null) => {
|
||||
if (!u) return <Badge variant="outline">{t("grades.notSet")}</Badge>
|
||||
return (
|
||||
<div className="min-w-0">
|
||||
<div className="truncate">{u.name}</div>
|
||||
<div className="truncate text-xs text-muted-foreground">{u.email}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const filteredGrades = useMemo(() => {
|
||||
const needle = q.trim().toLowerCase()
|
||||
const bySchool = school === "all" ? "" : school
|
||||
@@ -202,10 +207,13 @@ export function GradesClient({
|
||||
setCreateOpen(true)
|
||||
}
|
||||
|
||||
const createValidation = useMemo(() => validateForm(createState, { grades }), [createState, grades])
|
||||
const createValidation = useMemo(
|
||||
() => validateForm(createState, { grades }),
|
||||
[createState, grades, validateForm]
|
||||
)
|
||||
const editValidation = useMemo(
|
||||
() => validateForm(editState, { grades, excludeGradeId: editItem?.id }),
|
||||
[editItem?.id, editState, grades]
|
||||
[editItem?.id, editState, grades, validateForm]
|
||||
)
|
||||
|
||||
const isEditDirty = useMemo(() => {
|
||||
@@ -236,7 +244,7 @@ export function GradesClient({
|
||||
const handleCreate = async () => {
|
||||
const validation = validateForm(createState, { grades })
|
||||
if (!validation.ok) {
|
||||
toast.error(Object.values(validation.errors)[0] || "请完善表单信息")
|
||||
toast.error(Object.values(validation.errors)[0] || t("grades.validation.fixForm"))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -255,10 +263,10 @@ export function GradesClient({
|
||||
setCreateOpen(false)
|
||||
router.refresh()
|
||||
} else {
|
||||
toast.error(res.message || "Failed to create grade")
|
||||
toast.error(res.message || t("grades.failedCreate"))
|
||||
}
|
||||
} catch {
|
||||
toast.error("Failed to create grade")
|
||||
toast.error(t("grades.failedCreate"))
|
||||
} finally {
|
||||
setIsWorking(false)
|
||||
}
|
||||
@@ -268,11 +276,11 @@ export function GradesClient({
|
||||
if (!editItem) return
|
||||
const validation = validateForm(editState, { grades, excludeGradeId: editItem.id })
|
||||
if (!validation.ok) {
|
||||
toast.error(Object.values(validation.errors)[0] || "请完善表单信息")
|
||||
toast.error(Object.values(validation.errors)[0] || t("grades.validation.fixForm"))
|
||||
return
|
||||
}
|
||||
if (!isEditDirty) {
|
||||
toast.message("没有可保存的变更")
|
||||
toast.message(t("grades.validation.noChanges"))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -291,10 +299,10 @@ export function GradesClient({
|
||||
setEditItem(null)
|
||||
router.refresh()
|
||||
} else {
|
||||
toast.error(res.message || "Failed to update grade")
|
||||
toast.error(res.message || t("grades.failedUpdate"))
|
||||
}
|
||||
} catch {
|
||||
toast.error("Failed to update grade")
|
||||
toast.error(t("grades.failedUpdate"))
|
||||
} finally {
|
||||
setIsWorking(false)
|
||||
}
|
||||
@@ -310,10 +318,10 @@ export function GradesClient({
|
||||
setDeleteItem(null)
|
||||
router.refresh()
|
||||
} else {
|
||||
toast.error(res.message || "Failed to delete grade")
|
||||
toast.error(res.message || t("grades.failedDelete"))
|
||||
}
|
||||
} catch {
|
||||
toast.error("Failed to delete grade")
|
||||
toast.error(t("grades.failedDelete"))
|
||||
} finally {
|
||||
setIsWorking(false)
|
||||
}
|
||||
@@ -324,15 +332,15 @@ export function GradesClient({
|
||||
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||
<div className="flex flex-1 flex-col gap-2 md:flex-row md:items-center">
|
||||
<div className="flex-1 md:max-w-sm">
|
||||
<Input placeholder="搜索年级/学校/组长..." value={q} onChange={(e) => setQ(e.target.value || null)} />
|
||||
<Input placeholder={t("grades.filters.search")} value={q} onChange={(e) => setQ(e.target.value || null)} />
|
||||
</div>
|
||||
|
||||
<Select value={school} onValueChange={(v) => setSchool(v === "all" ? null : v)}>
|
||||
<SelectTrigger className="w-full md:w-[220px]">
|
||||
<SelectValue placeholder="学校" />
|
||||
<SelectValue placeholder={t("grades.filters.school")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">全部学校</SelectItem>
|
||||
<SelectItem value="all">{t("grades.filters.allSchools")}</SelectItem>
|
||||
{schools.map((s) => (
|
||||
<SelectItem key={s.id} value={s.id}>
|
||||
{s.name}
|
||||
@@ -343,28 +351,28 @@ export function GradesClient({
|
||||
|
||||
<Select value={head} onValueChange={(v) => setHead(v === "all" ? null : v)}>
|
||||
<SelectTrigger className="w-full md:w-[220px]">
|
||||
<SelectValue placeholder="负责人" />
|
||||
<SelectValue placeholder={t("grades.filters.head")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">全部</SelectItem>
|
||||
<SelectItem value="missing">两者都未设置</SelectItem>
|
||||
<SelectItem value="missing_grade_head">未设置年级组长</SelectItem>
|
||||
<SelectItem value="missing_teaching_head">未设置教研组长</SelectItem>
|
||||
<SelectItem value="all">{t("grades.filters.allHeads")}</SelectItem>
|
||||
<SelectItem value="missing">{t("grades.filters.missing")}</SelectItem>
|
||||
<SelectItem value="missing_grade_head">{t("grades.filters.missingGradeHead")}</SelectItem>
|
||||
<SelectItem value="missing_teaching_head">{t("grades.filters.missingTeachingHead")}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Select value={sort} onValueChange={(v) => setSort(v === "default" ? null : v)}>
|
||||
<SelectTrigger className="w-full md:w-[220px]">
|
||||
<SelectValue placeholder="排序" />
|
||||
<SelectValue placeholder={t("grades.filters.sort")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="default">默认排序</SelectItem>
|
||||
<SelectItem value="updated_desc">更新时间(新→旧)</SelectItem>
|
||||
<SelectItem value="updated_asc">更新时间(旧→新)</SelectItem>
|
||||
<SelectItem value="name_asc">年级名称(A→Z)</SelectItem>
|
||||
<SelectItem value="name_desc">年级名称(Z→A)</SelectItem>
|
||||
<SelectItem value="order_asc">Order(小→大)</SelectItem>
|
||||
<SelectItem value="order_desc">Order(大→小)</SelectItem>
|
||||
<SelectItem value="default">{t("grades.filters.defaultSort")}</SelectItem>
|
||||
<SelectItem value="updated_desc">{t("grades.filters.updatedDesc")}</SelectItem>
|
||||
<SelectItem value="updated_asc">{t("grades.filters.updatedAsc")}</SelectItem>
|
||||
<SelectItem value="name_asc">{t("grades.filters.nameAsc")}</SelectItem>
|
||||
<SelectItem value="name_desc">{t("grades.filters.nameDesc")}</SelectItem>
|
||||
<SelectItem value="order_asc">{t("grades.filters.orderAsc")}</SelectItem>
|
||||
<SelectItem value="order_desc">{t("grades.filters.orderDesc")}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
@@ -378,20 +386,20 @@ export function GradesClient({
|
||||
setSort(null)
|
||||
}}
|
||||
>
|
||||
重置
|
||||
{t("grades.filters.reset")}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<Button onClick={openCreate} disabled={isWorking || schools.length === 0}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
新建年级
|
||||
{t("grades.new")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Card className="shadow-none">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0">
|
||||
<CardTitle className="text-base">年级列表</CardTitle>
|
||||
<CardTitle className="text-base">{t("grades.list.title")}</CardTitle>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="secondary" className="tabular-nums">
|
||||
{filteredGrades.length}
|
||||
@@ -404,26 +412,30 @@ export function GradesClient({
|
||||
<CardContent>
|
||||
{schools.length === 0 ? (
|
||||
<EmptyState
|
||||
title="暂无学校"
|
||||
description="请先创建学校,再在学校下创建年级。"
|
||||
title={t("grades.list.noSchools")}
|
||||
description={t("grades.list.noSchoolsDescription")}
|
||||
className="h-auto border-none shadow-none"
|
||||
/>
|
||||
) : filteredGrades.length === 0 ? (
|
||||
<EmptyState
|
||||
title={grades.length === 0 ? "暂无年级" : "没有匹配结果"}
|
||||
description={grades.length === 0 ? "创建年级以便管理负责人和班级。" : "尝试修改筛选条件或清空搜索。"}
|
||||
title={grades.length === 0 ? t("grades.list.noGrades") : t("grades.list.noMatch")}
|
||||
description={
|
||||
grades.length === 0
|
||||
? t("grades.list.noGradesDescription")
|
||||
: t("grades.list.noMatchDescription")
|
||||
}
|
||||
className="h-auto border-none shadow-none"
|
||||
/>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>学校</TableHead>
|
||||
<TableHead>年级</TableHead>
|
||||
<TableHead>Order</TableHead>
|
||||
<TableHead>年级组长</TableHead>
|
||||
<TableHead>教研组长</TableHead>
|
||||
<TableHead>更新时间</TableHead>
|
||||
<TableHead>{t("grades.column.school")}</TableHead>
|
||||
<TableHead>{t("grades.column.grade")}</TableHead>
|
||||
<TableHead>{t("grades.column.order")}</TableHead>
|
||||
<TableHead>{t("grades.column.gradeHead")}</TableHead>
|
||||
<TableHead>{t("grades.column.teachingHead")}</TableHead>
|
||||
<TableHead>{t("grades.column.updated")}</TableHead>
|
||||
<TableHead className="w-[60px]" />
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
@@ -449,19 +461,19 @@ export function GradesClient({
|
||||
router.push(`/admin/school/grades/insights?gradeId=${encodeURIComponent(g.id)}`)
|
||||
}
|
||||
>
|
||||
学情
|
||||
{t("grades.actions.insights")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => openEdit(g)}>
|
||||
<Pencil className="mr-2 h-4 w-4" />
|
||||
编辑
|
||||
{t("grades.actions.edit")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="text-destructive focus:text-destructive"
|
||||
onClick={() => setDeleteItem(g)}
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
删除
|
||||
{t("grades.actions.delete")}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
@@ -477,7 +489,7 @@ export function GradesClient({
|
||||
<Dialog open={createOpen} onOpenChange={setCreateOpen}>
|
||||
<DialogContent className="sm:max-w-[560px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>新建年级</DialogTitle>
|
||||
<DialogTitle>{t("grades.form.createTitle")}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<form
|
||||
className="space-y-4"
|
||||
@@ -487,14 +499,14 @@ export function GradesClient({
|
||||
}}
|
||||
>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label className="text-right">School</Label>
|
||||
<Label className="text-right">{t("grades.form.school")}</Label>
|
||||
<div className="col-span-3">
|
||||
<Select
|
||||
value={createState.schoolId}
|
||||
onValueChange={(v) => setCreateState((p) => ({ ...p, schoolId: v }))}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a school" />
|
||||
<SelectValue placeholder={t("grades.form.school")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{schools.map((s) => (
|
||||
@@ -514,14 +526,14 @@ export function GradesClient({
|
||||
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="create-grade-name" className="text-right">
|
||||
Grade
|
||||
{t("grades.form.name")}
|
||||
</Label>
|
||||
<Input
|
||||
id="create-grade-name"
|
||||
className="col-span-3"
|
||||
value={createState.name}
|
||||
onChange={(e) => setCreateState((p) => ({ ...p, name: e.target.value }))}
|
||||
placeholder="e.g. Grade 10"
|
||||
placeholder={t("grades.form.name")}
|
||||
autoFocus
|
||||
/>
|
||||
{createValidation.errors.name ? (
|
||||
@@ -533,7 +545,7 @@ export function GradesClient({
|
||||
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="create-grade-order" className="text-right">
|
||||
Order
|
||||
{t("grades.form.order")}
|
||||
</Label>
|
||||
<Input
|
||||
id="create-grade-order"
|
||||
@@ -553,7 +565,7 @@ export function GradesClient({
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label className="text-right">年级组长</Label>
|
||||
<Label className="text-right">{t("grades.form.gradeHead")}</Label>
|
||||
<div className="col-span-3">
|
||||
<Select
|
||||
value={createState.gradeHeadId}
|
||||
@@ -562,7 +574,7 @@ export function GradesClient({
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Optional" />
|
||||
<SelectValue placeholder={t("grades.optional")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={NONE_SELECT_VALUE}>-</SelectItem>
|
||||
@@ -577,7 +589,7 @@ export function GradesClient({
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label className="text-right">教研组长</Label>
|
||||
<Label className="text-right">{t("grades.form.teachingHead")}</Label>
|
||||
<div className="col-span-3">
|
||||
<Select
|
||||
value={createState.teachingHeadId}
|
||||
@@ -586,7 +598,7 @@ export function GradesClient({
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Optional" />
|
||||
<SelectValue placeholder={t("grades.optional")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={NONE_SELECT_VALUE}>-</SelectItem>
|
||||
@@ -602,10 +614,10 @@ export function GradesClient({
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="outline" onClick={() => setCreateOpen(false)} disabled={isWorking}>
|
||||
Cancel
|
||||
{t("grades.form.cancel")}
|
||||
</Button>
|
||||
<Button type="submit" disabled={isWorking}>
|
||||
创建
|
||||
{t("grades.form.create")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
@@ -620,7 +632,7 @@ export function GradesClient({
|
||||
>
|
||||
<DialogContent className="sm:max-w-[560px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>编辑年级</DialogTitle>
|
||||
<DialogTitle>{t("grades.form.editTitle")}</DialogTitle>
|
||||
</DialogHeader>
|
||||
{editItem ? (
|
||||
<form
|
||||
@@ -631,14 +643,14 @@ export function GradesClient({
|
||||
}}
|
||||
>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label className="text-right">School</Label>
|
||||
<Label className="text-right">{t("grades.form.school")}</Label>
|
||||
<div className="col-span-3">
|
||||
<Select
|
||||
value={editState.schoolId}
|
||||
onValueChange={(v) => setEditState((p) => ({ ...p, schoolId: v }))}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a school" />
|
||||
<SelectValue placeholder={t("grades.form.school")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{schools.map((s) => (
|
||||
@@ -658,7 +670,7 @@ export function GradesClient({
|
||||
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="edit-grade-name" className="text-right">
|
||||
Grade
|
||||
{t("grades.form.name")}
|
||||
</Label>
|
||||
<Input
|
||||
id="edit-grade-name"
|
||||
@@ -675,7 +687,7 @@ export function GradesClient({
|
||||
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="edit-grade-order" className="text-right">
|
||||
Order
|
||||
{t("grades.form.order")}
|
||||
</Label>
|
||||
<Input
|
||||
id="edit-grade-order"
|
||||
@@ -695,7 +707,7 @@ export function GradesClient({
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label className="text-right">年级组长</Label>
|
||||
<Label className="text-right">{t("grades.form.gradeHead")}</Label>
|
||||
<div className="col-span-3">
|
||||
<Select
|
||||
value={editState.gradeHeadId}
|
||||
@@ -704,7 +716,7 @@ export function GradesClient({
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Optional" />
|
||||
<SelectValue placeholder={t("grades.optional")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={NONE_SELECT_VALUE}>-</SelectItem>
|
||||
@@ -719,7 +731,7 @@ export function GradesClient({
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label className="text-right">教研组长</Label>
|
||||
<Label className="text-right">{t("grades.form.teachingHead")}</Label>
|
||||
<div className="col-span-3">
|
||||
<Select
|
||||
value={editState.teachingHeadId}
|
||||
@@ -728,7 +740,7 @@ export function GradesClient({
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Optional" />
|
||||
<SelectValue placeholder={t("grades.optional")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={NONE_SELECT_VALUE}>-</SelectItem>
|
||||
@@ -744,10 +756,10 @@ export function GradesClient({
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="outline" onClick={() => setEditItem(null)} disabled={isWorking}>
|
||||
Cancel
|
||||
{t("grades.form.cancel")}
|
||||
</Button>
|
||||
<Button type="submit" disabled={isWorking}>
|
||||
保存
|
||||
{t("grades.form.save")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
@@ -763,13 +775,15 @@ export function GradesClient({
|
||||
>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>删除年级</AlertDialogTitle>
|
||||
<AlertDialogDescription>将永久删除 {deleteItem?.name || "该年级"}。</AlertDialogDescription>
|
||||
<AlertDialogTitle>{t("grades.delete.title")}</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{t("grades.delete.description", { name: deleteItem?.name || "" })}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={isWorking}>取消</AlertDialogCancel>
|
||||
<AlertDialogCancel disabled={isWorking}>{t("grades.delete.cancel")}</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleDelete} disabled={isWorking}>
|
||||
删除
|
||||
{t("grades.delete.confirm")}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
|
||||
Reference in New Issue
Block a user