# Admin 前端文件规范核查报告 v2 > 版本:v2(基于 v1 报告的二次复查) > 核查范围:`src/app/(dashboard)/admin/` 下全部 26 个 `page.tsx` 文件 > 核查依据: > - `.trae/rules/project_rules.md`(项目规则) > - `docs/standards/coding-standards.md`(编码规范 v1.0) > - `docs/architecture/004_architecture_impact_map.md`(架构影响地图) > - React / Next.js 16 最佳实践 > - Web 界面设计规范(WCAG 2.2 AA) > 核查日期:2026-06-18(v2) > 上次核查:2026-06-18(v1) --- ## 〇、v1 → v2 修复状态追踪 **重要说明**:本次复查发现,自 v1 报告(`bugs/admin_bug.md`)输出后,`src/app/(dashboard)/admin/` 下全部 26 个 `page.tsx` 文件**内容均未发生任何修改**,`src/shared/lib/utils.ts` 也未新增共享工具函数。v1 报告提出的所有问题**全部未修复**。 ### v1 问题修复状态对照表 | v1 编号 | 问题 | 严重级别 | v2 状态 | 备注 | |---------|------|---------|---------|------| | P0-1 | 全部 26 个页面缺少 `error.tsx` / `loading.tsx` | P0 | ❌ 未修复 | 仍无任何 error/loading 边界文件 | | P0-2 | `attendance/page.tsx` 缺少权限校验 | P0 | ❌ 未修复 | 第 26 行仍为 `getAuthContext()`,未加 `requirePermission` | | P1-1 | 全部 26 个页面组件缺少返回类型标注 | P1 | ❌ 未修复 | 全部页面函数仍无 `: Promise` | | P1-2 | `getParam` 工具函数在 27 个文件中重复 | P1 | ❌ 未修复 | `shared/lib/utils.ts` 未新增 `getSearchParam` | | P1-3 | 4 个文件使用 `as` 类型断言 | P1 | ❌ 未修复 | `audit-logs/*`、`attendance` 仍用 `as` | | P1-4 | UI 文案中英文混用 | P1 | ❌ 未修复 | 仅 `users/import` 为中文,其余仍英文 | | P2-1 | `school/grades/insights` 使用原生 `` | | P2-2 | `users/import` 使用原生 `` | P2 | ❌ 未修复 | 第 93-128 行仍为原生 `
` | | P2-3 | Tailwind 任意值违规 | P2 | ❌ 未修复 | `md:w-[360px]`、`h-[360px]` 仍存在 | | P2-4 | `users/import` 硬编码颜色 `text-amber-500` | P2 | ❌ 未修复 | 第 67 行未变 | | P2-5 | `school/grades/insights` 导入顺序违规 | P2 | ❌ 未修复 | `lucide-react` 仍在最后 | | P2-6 | `course-plans/[id]/edit` 同模块重复导入 | P2 | ❌ 未修复 | 第 3-4 行仍分两行 | | P2-7 | `scheduling/*` 从 `actions` 取数 | P2 | ❌ 未修复 | 仍从 `@/modules/scheduling/actions` 导入 | **结论**:v1 提出的 **2 个 P0 + 4 个 P1 + 7 个 P2 = 13 个问题,0 个已修复**。 --- ## 一、v2 新增发现(v1 遗漏的问题) 本次复查在 v1 基础上深度审查,新发现 **10 个问题**。 ### P1 重要问题(v2 新增) #### P1-5(v2 新增)`attendance/page.tsx` 第 39 行违反 Prettier `printWidth: 100` **文件**:[src/app/(dashboard)/admin/attendance/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/attendance/page.tsx#L39) **违反规范**: - `.prettierrc` 配置 `"printWidth": 100` - 编码规范 §十五:「Prettier 自动保证格式一致」 **现状**:第 39 行单行长度约 115 字符,超出 100 字符限制: ```tsx status: status && status !== "all" ? (status as "present" | "absent" | "late" | "early_leave" | "excused") : undefined, ``` **说明**:项目 `.prettierrc` 已配置 `printWidth: 100`,但此行未触发格式化,可能是因为该文件未经过 `prettier --write` 处理,或 ESLint 未强制 Prettier 规则。 **修复建议**:抽取状态类型守卫后自然换行(同时解决 P1-3 的 `as` 断言问题): ```tsx const isValidAttendanceStatus = (v?: string): v is AttendanceStatus => v === "present" || v === "absent" || v === "late" || v === "early_leave" || v === "excused" // 在组件内 status: status && status !== "all" && isValidAttendanceStatus(status) ? status : undefined, ``` --- #### P1-6(v2 新增)`school/grades/insights/page.tsx` 的 `getParam` 实现与其他文件不一致 **文件**:[src/app/(dashboard)/admin/school/grades/insights/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/school/grades/insights/page.tsx#L17-L22) **现状**:该文件的 `getParam` 实现与其他 8 个 admin 页面**逻辑等价但写法不同**: ```tsx // school/grades/insights/page.tsx(第 17-22 行)—— 三分支写法 const getParam = (params: SearchParams, key: string) => { const v = params[key] if (typeof v === "string") return v if (Array.isArray(v)) return v[0] return undefined } // 其他 8 个 admin 页面 —— 三元写法 const getParam = (params: SearchParams, key: string) => { const v = params[key] return Array.isArray(v) ? v[0] : v } ``` **影响**:加剧 P1-2 的 DRY 问题,两种实现并存增加维护成本,且 `v[0]` 在 `noUncheckedIndexedAccess` 开启后返回 `string | undefined`,两种写法的类型推导行为可能不同。 **修复建议**:与 P1-2 一并解决,抽取到 `shared/lib/utils.ts` 统一实现。 --- #### P1-7(v2 新增)`attendance/page.tsx` 第 39 行使用内联字面量类型而非 `AttendanceStatus` 类型 **文件**:[src/app/(dashboard)/admin/attendance/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/attendance/page.tsx#L39) **违反规范**: - 编码规范 §4.2:「优先 `interface` 描述对象形状,`type` 用于联合、交叉、映射类型」 - DRY 原则 **现状**:第 39 行内联了 5 个字面量类型,而非引用 `AttendanceStatus` 类型: ```tsx status as "present" | "absent" | "late" | "early_leave" | "excused" ``` **说明**:`@/modules/attendance/types` 应已定义 `AttendanceStatus` 类型(其他模块如 `announcements`、`scheduling`、`course-plans`、`elective` 均有对应 status 类型导出)。内联字面量导致类型定义重复,若枚举值变更需多处修改。 **修复建议**: ```tsx import type { AttendanceStatus } from "@/modules/attendance/types" const isValidAttendanceStatus = (v?: string): v is AttendanceStatus => v === "present" || v === "absent" || v === "late" || v === "early_leave" || v === "excused" ``` --- ### P2 一般问题(v2 新增) #### P2-8(v2 新增)`school/grades/insights/page.tsx` 第 24 行 `fmt` 工具函数内联定义 **文件**:[src/app/(dashboard)/admin/school/grades/insights/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/school/grades/insights/page.tsx#L24) **违反规范**: - 编码规范 §一:「单一职责」 - 编码规范 §5.3:「工具函数 ≤ 40 行」(此函数 1 行,但属于通用工具应抽取) **现状**:第 24 行内联定义数字格式化函数: ```tsx const fmt = (v: number | null, digits = 1) => (typeof v === "number" && Number.isFinite(v) ? v.toFixed(digits) : "-") ``` **影响**:该函数为通用数字格式化工具,可能在其他统计页面(如 `teacher/grades/stats`、`management/grade/insights`)重复出现。 **修复建议**:抽取到 `shared/lib/utils.ts`: ```tsx export function formatNumber(v: number | null | undefined, digits = 1): string { if (typeof v !== "number" || !Number.isFinite(v)) return "-" return v.toFixed(digits) } ``` --- #### P2-9(v2 新增)`school/grades/insights/page.tsx` 第 137 行可用可选链简化 **文件**:[src/app/(dashboard)/admin/school/grades/insights/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/school/grades/insights/page.tsx#L137) **现状**:第 137 行使用三元表达式而非可选链: ```tsx
{insights.latest ? insights.latest.title : "-"}
``` **修复建议**:使用可选链 + 空值合并: ```tsx
{insights.latest?.title ?? "-"}
``` **说明**:同文件第 136 行已使用 `insights.latest?.scoreStats.avg ?? null`,写法不一致。 --- #### P2-10(v2 新增)`school/page.tsx` 缺少 `export const dynamic` 声明 **文件**:[src/app/(dashboard)/admin/school/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/school/page.tsx) **现状**:该文件仅 5 行,使用 `redirect()` 跳转,但**未声明** `export const dynamic = "force-dynamic"`: ```tsx import { redirect } from "next/navigation" export default function AdminSchoolPage() { redirect("/admin/school/classes") } ``` **对比**:admin 目录下其他 25 个页面均声明了 `export const dynamic = "force-dynamic"`,仅此文件缺失。 **影响**:Next.js 可能在构建时尝试静态生成此页面,`redirect()` 在静态生成阶段的行为与运行时不同,可能导致构建警告或行为不一致。 **修复建议**:补充声明: ```tsx import { redirect } from "next/navigation" export const dynamic = "force-dynamic" export default function AdminSchoolPage(): never { redirect("/admin/school/classes") } ``` **注**:`redirect()` 抛出异常永不返回,返回类型应标注为 `never`。 --- #### P2-11(v2 新增)`users/import/page.tsx` 是同步函数但无 `dynamic` 导出,与其他页面不一致 **文件**:[src/app/(dashboard)/admin/users/import/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/users/import/page.tsx#L14) **现状**:第 14 行为同步函数组件,且无 `export const dynamic` 声明: ```tsx export default function UserImportPage() { return ( /* ... */ ) } ``` **对比**:admin 目录下其他 24 个数据获取页面均声明 `export const dynamic = "force-dynamic"`,仅此文件与 `school/page.tsx` 缺失。 **说明**:该页面为纯静态内容(无数据获取),理论上可静态生成,但与 admin 路由组整体策略不一致。需明确决策: - 若 admin 路由组统一 `force-dynamic`(因权限校验需运行时),则此页面应补充声明 - 若允许静态页面,则应在架构文档中说明例外 **修复建议**:为保持一致性,补充 `export const dynamic = "force-dynamic"`,或显式注释说明为何例外。 --- #### P2-12(v2 新增)多个编辑页缺少返回上一页的导航 **违反规范**: - Web 界面设计规范:「焦点管理必须合理」 - 用户体验最佳实践:「始终提供返回路径」 **现状**:以下编辑/创建页面**未提供返回按钮**,用户只能通过浏览器后退或侧边栏导航: | 文件 | 是否有返回按钮 | |------|--------------| | `announcements/[id]/page.tsx` | ❌ 无 | | `course-plans/create/page.tsx` | ❌ 无(仅 `CoursePlanForm` 的 `backHref` prop) | | `course-plans/[id]/page.tsx` | ❌ 无(仅 `CoursePlanDetail` 的 `backHref` prop) | | `course-plans/[id]/edit/page.tsx` | ❌ 无(仅 `CoursePlanForm` 的 `backHref` prop) | | `elective/create/page.tsx` | ❌ 无(仅 `ElectiveCourseForm` 的 `backHref` prop) | | `elective/[id]/edit/page.tsx` | ❌ 无(仅 `ElectiveCourseForm` 的 `backHref` prop) | | `users/import/page.tsx` | ✅ 有(第 20-25 行 `ArrowLeft` 返回按钮) | **说明**:`users/import/page.tsx` 在页面顶部提供了显式的返回按钮(``),是正确的做法。其他编辑页虽通过子组件的 `backHref` prop 传递了返回路径,但返回入口依赖子组件内部实现,页面层未统一控制。 **修复建议**:在所有编辑/创建页面顶部统一添加返回按钮,与 `users/import/page.tsx` 保持一致;或将返回按钮抽取为共享组件 `PageBackButton`。 --- #### P2-13(v2 新增)大部分页面缺少 `metadata` 导出 **违反规范**: - Next.js 16 最佳实践:「页面应导出 `metadata` 用于 SEO 与标签页标题」 - 编码规范 §十四:「文档与交付物」 **现状**: | 文件 | 是否导出 `metadata` | |------|-------------------| | `users/import/page.tsx` | ✅ 有(第 9-12 行) | | 其余 25 个页面 | ❌ 无 | **影响**:浏览器标签页标题默认显示全局标题,无法区分当前所在 admin 子页面,影响用户体验(多个标签页难以区分)。 **修复建议**:为每个页面补充 `metadata` 导出: ```tsx import type { Metadata } from "next" export const metadata: Metadata = { title: "审计日志 - Next_Edu", description: "查看系统所有用户操作记录", } ``` --- #### P2-14(v2 新增)`school/grades/insights/page.tsx` 使用原生 `` 导致整页刷新 **文件**:[src/app/(dashboard)/admin/school/grades/insights/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/school/grades/insights/page.tsx#L55-L72) **现状**:第 55-72 行使用原生 HTML `` 提交筛选器,会导致**整页刷新**,丢失当前滚动位置与页面状态。 **违反规范**: - 编码规范 §7.3:「URL 状态:使用 `nuqs`(已集成)」 - React 最佳实践:「避免不必要的整页刷新」 **影响**: - 用户体验差:每次筛选都触发整页白屏加载(叠加 P0-1 缺少 `loading.tsx` 问题更严重) - 与项目已集成的 `nuqs` URL 状态管理方案不一致 - 其他筛选页(`audit-logs/*`、`attendance`)使用子组件内的客户端筛选,此页面是唯一使用原生 form 提交的 **修复建议**: 1. **方案 A(推荐)**:将筛选器提取为客户端组件,使用 `nuqs` 的 `useQueryState` 管理 `gradeId` 参数,实现无刷新筛选 2. **方案 B(最小改动)**:保持服务端筛选,但补充 `loading.tsx` 缓解白屏问题 --- ## 二、v2 核查概览(含 v1 + v2 全部问题) | 维度 | 文件数 | 通过 | 待改进 | v2 新增 | |------|--------|------|--------|---------| | 架构分层 | 26 | 24 | 2 | 0 | | TypeScript 规范 | 26 | 4 | 22 | +3 | | 安全与权限 | 26 | 3 | 23 | 0 | | UI 一致性与设计令牌 | 26 | 18 | 8 | +1 | | 错误与加载边界 | 26 | 0 | 26 | 0 | | 代码复用(DRY) | 26 | 0 | 26 | +2 | | 格式化(Prettier) | 26 | 25 | 1 | +1 | | 导航与 UX | 26 | 1 | 25 | +2 | | SEO(metadata) | 26 | 1 | 25 | +1 | **累计问题数**:v1 的 13 个 + v2 新增 10 个 = **23 个问题**,全部未修复。 --- ## 三、v2 问题清单汇总(按严重程度排序) ### P0 严重(必须立即修复) | 编号 | 问题 | v1/v2 | 文件 | |------|------|-------|------| | P0-1 | 全部 26 个页面缺少 `error.tsx` / `loading.tsx` | v1 | 全部 | | P0-2 | `attendance/page.tsx` 缺少 `requirePermission` 权限校验 | v1 | `attendance/page.tsx` | ### P1 重要(应尽快修复) | 编号 | 问题 | v1/v2 | 文件 | |------|------|-------|------| | P1-1 | 全部 26 个页面缺少返回类型 `Promise` | v1 | 全部 | | P1-2 | `getParam` 在 27 个文件重复定义 | v1 | 9 个 admin 文件 | | P1-3 | 4 个文件使用 `as` 类型断言 | v1 | `audit-logs/*`、`attendance` | | P1-4 | UI 文案中英文混用 | v1 | ~20 个文件 | | P1-5 | `attendance` 第 39 行超 `printWidth: 100` | **v2** | `attendance/page.tsx` | | P1-6 | `school/grades/insights` 的 `getParam` 实现不一致 | **v2** | `school/grades/insights/page.tsx` | | P1-7 | `attendance` 使用内联字面量而非 `AttendanceStatus` 类型 | **v2** | `attendance/page.tsx` | ### P2 一般(建议修复) | 编号 | 问题 | v1/v2 | 文件 | |------|------|-------|------| | P2-1 | `school/grades/insights` 使用原生 `
` | v1 | `users/import/page.tsx` | | P2-3 | Tailwind 任意值 `w-[360px]`、`h-[360px]` | v1 | `school/grades/insights/page.tsx` | | P2-4 | `users/import` 硬编码颜色 `text-amber-500` | v1 | `users/import/page.tsx` | | P2-5 | `school/grades/insights` 导入顺序违规 | v1 | `school/grades/insights/page.tsx` | | P2-6 | `course-plans/[id]/edit` 同模块重复导入 | v1 | `course-plans/[id]/edit/page.tsx` | | P2-7 | `scheduling/*` 从 `actions` 取数 | v1 | `scheduling/*` | | P2-8 | `fmt` 工具函数内联定义 | **v2** | `school/grades/insights/page.tsx` | | P2-9 | 第 137 行可用可选链简化 | **v2** | `school/grades/insights/page.tsx` | | P2-10 | `school/page.tsx` 缺少 `export const dynamic` | **v2** | `school/page.tsx` | | P2-11 | `users/import` 缺少 `dynamic` 声明(不一致) | **v2** | `users/import/page.tsx` | | P2-12 | 多个编辑页缺少返回按钮 | **v2** | 6 个编辑/创建页 | | P2-13 | 25 个页面缺少 `metadata` 导出 | **v2** | 25 个文件 | | P2-14 | 原生 `` 整页刷新 | **v2** | `school/grades/insights/page.tsx` | --- ## 四、React 性能优化建议(v2 更新) ### R1 利用 Suspense 流式渲染(v1 提出,未实施) **现状**:所有页面使用 `export const dynamic = "force-dynamic"` 整页动态渲染。 **建议**:对数据量大的页面(`audit-logs/*`、`school/grades/insights`、`attendance`)拆分 Suspense 边界。详见 v1 报告 R1。 ### R2 `school/grades/insights/page.tsx` 串行查询可并行(v1 提出,未实施) **现状**:第 30-33 行 `getGrades()` 与 `getGradeHomeworkInsights()` 串行执行,但两者无数据依赖。 **建议**:改为 `Promise.all` 并行。详见 v1 报告 R2。 ### R3 列表页 `classOptions` 映射可下沉至 data-access(v1 提出,未实施) 详见 v1 报告 R3。 ### R4(v2 新增)`school/grades/insights/page.tsx` 表格未虚拟化,大数据量下性能风险 **文件**:[src/app/(dashboard)/admin/school/grades/insights/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/school/grades/insights/page.tsx#L164-L180) **现状**:第 164-180 行与第 208-220 行使用 `insights.assignments.map()` 与 `insights.classes.map()` 直接渲染整张表格,无分页或虚拟化。 **说明**:`getGradeHomeworkInsights({ limit: 50 })` 限制为 50 条,但 `insights.classes` 无限制,大型学校(如 50+ 班级的年级)可能渲染数百行 DOM 节点。 **修复建议**: - 短期:在 data-access 层对 `classes` 也加 `limit` - 长期:引入 `@tanstack/react-virtual` 虚拟化长列表 --- ## 五、Web 界面设计规范建议(v2 更新) ### W1-W5(v1 提出,未实施) 详见 v1 报告第四部分:`
`、标题层级、`aria-live`、`EmptyState` 图标语义。 ### W6(v2 新增)`school/grades/insights/page.tsx` 原生 `` 缺少 `aria-label` 或 `aria-labelledby`,且 `