feat(admin-portal): 完成参考项目差距闭环 - 4 批次补齐 + Vitest 测试

差距分析闭环(workline §7-§8):
- P0:审计子模块 3 页 + 学校组织 7 页
- P1:系统设置 4 Card + 公告管理 + 邀请码管理
- P2:文件管理 + AI 配置
- P6:5 测试文件 / 69 用例全过

共享基础设施:
- permissions +11 权限点 +14 路由
- view-models +20 接口
- i18n +100 key
- graphql-client +25 operation
- fixtures/handlers +15 mock +20 handler

质量:typecheck + lint 零错误,vitest 69/69 通过,arch.db 已更新
This commit is contained in:
SpecialX
2026-07-13 13:05:29 +08:00
parent fad6e43b47
commit 7cf9aec20e
40 changed files with 6707 additions and 7 deletions

View File

@@ -0,0 +1,106 @@
"use client";
import { type ReactNode } from "react";
import { useAcademicYears } from "@/hooks/use-school";
import {
PaperCard,
LoadingState,
ErrorState,
EmptyState,
Table,
TableRow,
TableCell,
} from "@/components/ui";
import { t } from "@/lib/i18n";
import type { AcademicYearViewModel } from "@/types/view-models";
function CurrentBadge(): ReactNode {
const color = "var(--color-success)";
return (
<span
style={{
display: "inline-block",
padding: "2px 8px",
fontSize: 11,
borderRadius: 10,
background: `${color}20`,
color,
border: `1px solid ${color}40`,
fontWeight: 500,
}}
>
{t("admin.school.currentYear")}
</span>
);
}
export default function AcademicYearPage(): ReactNode {
const { data, loading, error } = useAcademicYears();
return (
<PaperCard className="p-4">
{loading && <LoadingState />}
{error && <ErrorState message={error.message} />}
{!loading && !error && (
<>
{data.length === 0 ? (
<EmptyState message={t("admin.common.empty")} />
) : (
<Table
headers={[
"学年",
t("admin.school.startDate"),
t("admin.school.endDate"),
"第一学期",
"第二学期",
t("admin.common.status"),
]}
>
{data.map((year: AcademicYearViewModel) => (
<TableRow key={year.id}>
<TableCell
style={{
fontWeight: year.isCurrent ? 600 : 500,
color: year.isCurrent
? "var(--color-accent)"
: "var(--color-ink)",
}}
>
{year.name}
</TableCell>
<TableCell style={{ whiteSpace: "nowrap" }}>
{year.startDate}
</TableCell>
<TableCell style={{ whiteSpace: "nowrap" }}>
{year.endDate}
</TableCell>
<TableCell
style={{
whiteSpace: "nowrap",
fontFamily: "var(--font-mono)",
fontSize: 12,
}}
>
{year.firstSemesterStart} ~ {year.firstSemesterEnd}
</TableCell>
<TableCell
style={{
whiteSpace: "nowrap",
fontFamily: "var(--font-mono)",
fontSize: 12,
}}
>
{year.secondSemesterStart} ~ {year.secondSemesterEnd}
</TableCell>
<TableCell>
{year.isCurrent ? <CurrentBadge /> : "—"}
</TableCell>
</TableRow>
))}
</Table>
)}
</>
)}
</PaperCard>
);
}