feat(portal-shell): 学生域全页面迁移与规范合规修复

- 学生域 32 页全量迁移(含作答/自动保存/提交/诊断)

- 补齐 4 个 MSW mock 缺口,修 diagnostic case 名

- 修 4 处 Tailwind 任意值;新增共享组件与路由
This commit is contained in:
SpecialX
2026-08-31 11:25:21 +08:00
parent 039db5efdd
commit 04b7a40bdc
493 changed files with 70985 additions and 2112 deletions

View File

@@ -26,8 +26,26 @@ import {
type ClassInvitationCode,
} from "@/lib/api";
import { notify } from "@/shared/lib/notify";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/shared/components/ui/alert-dialog";
import { Badge } from "@/shared/components/ui/badge";
import { Button } from "@/shared/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/shared/components/ui/dialog";
import { Input } from "@/shared/components/ui/input";
import { FormField } from "@/features/admin/school/schools-client";
@@ -97,23 +115,25 @@ export function ClassInvitationManagerDialog({
if (!open) return null;
return (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4"
onClick={onClose}
<Dialog
open={open}
onOpenChange={(o) => {
if (!o) onClose();
}}
>
<div
className="flex max-h-[90vh] w-full max-w-3xl flex-col rounded-xl border bg-background p-6 shadow-lg"
onClick={(e) => e.stopPropagation()}
>
<div className="mb-4 flex items-center justify-between">
<div>
<h2 className="text-lg font-semibold">{t("title")}</h2>
{className ? (
<p className="mt-1 text-xs text-muted-foreground">
{t("classLabel")}: {className}
</p>
) : null}
</div>
<DialogContent className="flex max-h-[90vh] max-w-3xl flex-col p-6">
<DialogHeader>
<DialogTitle>{t("title")}</DialogTitle>
<DialogDescription className="sr-only">
{t("title")}
</DialogDescription>
{className ? (
<p className="text-xs text-muted-foreground">
{t("classLabel")}: {className}
</p>
) : null}
</DialogHeader>
<div className="flex justify-end">
<Button size="sm" onClick={() => setGenerateOpen(true)}>
<Plus className="mr-1.5 size-4" />
{t("generate")}
@@ -174,7 +194,7 @@ export function ClassInvitationManagerDialog({
? new Date(record.expiresAt).toLocaleString()
: t("neverExpires")}
</td>
<td className="max-w-[200px] truncate p-3 text-sm text-muted-foreground">
<td className="max-w-xs truncate p-3 text-sm text-muted-foreground">
{record.note ?? "-"}
</td>
<td className="p-3 text-right">
@@ -211,11 +231,11 @@ export function ClassInvitationManagerDialog({
)}
</div>
<div className="mt-4 flex justify-end">
<DialogFooter className="mt-4">
<Button variant="outline" onClick={onClose}>
{tCommon("form.close")}
</Button>
</div>
</DialogFooter>
<GenerateCodeDialog
open={generateOpen}
@@ -225,43 +245,40 @@ export function ClassInvitationManagerDialog({
/>
{revokeTarget ? (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4"
onClick={() => setRevokeTarget(null)}
<AlertDialog
open={true}
onOpenChange={(o) => {
if (!o && !revokeMutation.loading) setRevokeTarget(null);
}}
>
<div
className="w-full max-w-sm rounded-xl border bg-background p-6 shadow-lg"
onClick={(e) => e.stopPropagation()}
>
<h2 className="mb-2 text-lg font-semibold">{t("revoke")}</h2>
<p className="mb-4 text-sm text-muted-foreground">
{t("revokeConfirm")}
</p>
<div className="flex justify-end gap-2">
<Button
type="button"
variant="outline"
onClick={() => setRevokeTarget(null)}
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{t("revoke")}</AlertDialogTitle>
<AlertDialogDescription>
{t("revokeConfirm")}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel
disabled={revokeMutation.loading}
onClick={() => setRevokeTarget(null)}
>
{tCommon("form.cancel")}
</Button>
<Button
type="button"
variant="destructive"
</AlertDialogCancel>
<AlertDialogAction
disabled={revokeMutation.loading}
onClick={() => void handleRevoke()}
>
{revokeMutation.loading
? tCommon("form.processing")
: t("revoke")}
</Button>
</div>
</div>
</div>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
) : null}
</div>
</div>
</DialogContent>
</Dialog>
);
}
@@ -344,20 +361,19 @@ function GenerateCodeDialog({
if (!open) return null;
return (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4"
onClick={onClose}
<Dialog
open={open}
onOpenChange={(o) => {
if (!o) onClose();
}}
>
<div
className="w-full max-w-md rounded-xl border bg-background p-6 shadow-lg"
onClick={(e) => e.stopPropagation()}
>
<h2 className="mb-1 text-lg font-semibold">
{t("generateWithCustom")}
</h2>
<p className="mb-4 text-xs text-muted-foreground">
{t("defaultDuration")} · {t("defaultMaxUses")}
</p>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>{t("generateWithCustom")}</DialogTitle>
<DialogDescription>
{t("defaultDuration")} · {t("defaultMaxUses")}
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-4">
<FormField label={t("expiresInHours")}>
<Input
@@ -386,7 +402,7 @@ function GenerateCodeDialog({
maxLength={255}
/>
</FormField>
<div className="flex justify-end gap-2">
<DialogFooter>
<Button
type="button"
variant="outline"
@@ -400,9 +416,9 @@ function GenerateCodeDialog({
? tCommon("form.processing")
: t("generate")}
</Button>
</div>
</DialogFooter>
</form>
</div>
</div>
</DialogContent>
</Dialog>
);
}