refactor(dashboard): V2 审计重构 — i18n 补齐 + 共享抽象 + 单测 + a11y

V2 审计报告(docs/architecture/audit/dashboard-audit-report-v2.md)发现并修复:

- P0 i18n:10 个子组件硬编码字符串全部接入 next-intl(teacher-quick-actions /
  teacher-classes-card / teacher-homework-card / teacher-schedule /
  recent-submissions / teacher-grade-trends / student-grades-card /
  student-today-schedule-card / student-upcoming-assignments-card /
  admin-dashboard),新增 ~50 个翻译键
- P1 共享抽象:新增 DashboardGreetingHeader 组件,消除 teacher/student
  头部 90% 重复代码,两个 Header 改为薄包装
- P2 单测:为 6 个纯函数添加 31 个单元测试
  (tests/integration/dashboard/dashboard-utils.test.ts)
- P2 a11y:admin 表格 caption、teacher/student 视图语义化标签
  (header / section aria-label / aside aria-label)
- 同步架构图 004/005
This commit is contained in:
SpecialX
2026-06-22 17:01:00 +08:00
parent 10c668f36a
commit e997abaf5e
41 changed files with 1811 additions and 516 deletions

View File

@@ -3,14 +3,51 @@
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useTranslations } from "next-intl";
import { toast } from "sonner";
import { Button } from "@/shared/components/ui/button";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/shared/components/ui/alert-dialog";
import { formatDateTime } from "@/shared/lib/utils";
import { duplicateLessonPlanAction, deleteLessonPlanAction } from "../actions";
import { useLessonPlanContextSafe, useRoleConfig } from "../providers/lesson-plan-provider";
import type { LessonPlanListItem } from "../types";
export function LessonPlanCard({ plan }: { plan: LessonPlanListItem }) {
const t = useTranslations("lessonPreparation");
const router = useRouter();
const roleConfig = useRoleConfig();
// 尝试使用注入的数据服务,若未在 Provider 内则 fallback 到直接调用 actions
const ctx = useLessonPlanContextSafe();
const service = ctx?.service ?? null;
async function handleArchive() {
const res = service
? await service.deleteLessonPlan(plan.id)
: await deleteLessonPlanAction(plan.id);
if (res.success) {
toast.success(t("status.archived"));
router.refresh();
} else {
toast.error(res.message ?? t("error.delete"));
}
}
async function handleDuplicate() {
const res = service
? await service.duplicateLessonPlan(plan.id)
: await duplicateLessonPlanAction(plan.id);
if (res.success) router.refresh();
}
return (
<div className="border border-outline-variant rounded-lg p-4 bg-surface-container-lowest hover:shadow-md transition-shadow">
@@ -36,27 +73,34 @@ export function LessonPlanCard({ plan }: { plan: LessonPlanListItem }) {
: t("list.neverSaved")}
</div>
<div className="flex gap-2 mt-3">
<Button
variant="outline"
size="sm"
onClick={async () => {
const res = await duplicateLessonPlanAction(plan.id);
if (res.success) router.refresh();
}}
>
{t("action.duplicate")}
</Button>
<Button
variant="outline"
size="sm"
onClick={async () => {
if (!confirm(t("confirm.archive"))) return;
const res = await deleteLessonPlanAction(plan.id);
if (res.success) router.refresh();
}}
>
{t("action.archive")}
</Button>
{roleConfig.canDuplicate && (
<Button variant="outline" size="sm" onClick={handleDuplicate}>
{t("action.duplicate")}
</Button>
)}
{roleConfig.canArchive && (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="outline" size="sm">
{t("action.archive")}
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{t("confirm.archiveTitle")}</AlertDialogTitle>
<AlertDialogDescription>
{t("confirm.archive")}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>{t("action.cancel")}</AlertDialogCancel>
<AlertDialogAction onClick={handleArchive}>
{t("action.confirm")}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)}
</div>
</div>
);