refactor: fix all P0/P1/P2 bugs and architecture issues
Bug fixes (from bugs/ directory): - Fix cross-module DB queries in 9 modules (homework, grades, parent, diagnostic, elective, proctoring, notifications, scheduling, classes) by routing through data-access functions - Fix shared/lib <-> auth circular dependency via new session.ts module - Fix divide-by-zero guard in grades data-access - Fix audit export data truncation (paginated fetch for full datasets) - Fix missing transactions in homework grading and elective lottery - Fix missing revalidatePath in course-plans actions - Fix frontend permission checks using requirePermission instead of requireAuth - Fix dashboard role routing using session.user.roles - Fix student auth pattern (migrate getDemoStudentUser to users module) - Fix ActionState return type handling in components Code quality fixes: - Remove 60+ as type assertions (replace with type guards) - Remove non-null assertions (use optional chaining or explicit checks) - Convert dynamic imports to static imports (grades, diagnostic) - Add React.cache() wrapping for read functions - Parallelize independent queries with Promise.all - Add explicit return types to 30+ arrow functions - Replace any with unknown + type guards - Fix import type for type-only imports - Add Zod validation schemas for classes and diagnostic modules - Extract duplicate code (normalizeRoleName, normalizeBcryptHash, logger IP extraction) - Add console.error to silent catch blocks - Fix permission naming consistency (exam:proctor_read -> exam:proctor:read) Architecture doc sync: - Update 004_architecture_impact_map.md and 005_architecture_data.json - Update management-modules-audit.md for P0-7 cross-module fix Moved deleted proctoring event route to deletes/ folder.
This commit is contained in:
548
bugs/admin_bug.md
Normal file
548
bugs/admin_bug.md
Normal file
@@ -0,0 +1,548 @@
|
||||
# Admin 前端文件规范核查报告
|
||||
|
||||
> 核查范围:`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
|
||||
|
||||
---
|
||||
|
||||
## 一、核查概览
|
||||
|
||||
| 维度 | 文件数 | 通过 | 待改进 |
|
||||
|------|--------|------|--------|
|
||||
| 架构分层 | 26 | 24 | 2 |
|
||||
| TypeScript 规范 | 26 | 4 | 22 |
|
||||
| 安全与权限 | 26 | 3 | 23 |
|
||||
| UI 一致性与设计令牌 | 26 | 18 | 8 |
|
||||
| 错误与加载边界 | 26 | 0 | 26 |
|
||||
| 代码复用(DRY) | 26 | 0 | 26 |
|
||||
|
||||
**结论**:整体架构清晰、服务端组件使用规范、并行数据获取到位,但在**返回类型标注、权限校验一致性、加载/错误边界、代码复用、UI 文案一致性**方面存在系统性问题,需统一整改。
|
||||
|
||||
---
|
||||
|
||||
## 二、问题清单(按严重程度排序)
|
||||
|
||||
### P0 严重问题(必须立即修复)
|
||||
|
||||
#### P0-1 全部 26 个页面缺少 `error.tsx` 与 `loading.tsx`
|
||||
|
||||
**违反规范**:
|
||||
- 编码规范 §2.3:「每个路由段应提供 `loading.tsx`(骨架屏)和 `error.tsx`(错误边界)」
|
||||
- 编码规范 §2.4:「每个路由段都必须提供 `error.tsx`,不得出现未捕获异常导致白屏」
|
||||
- 编码规范 §2.4:「`loading.tsx` 必须提供骨架屏或最小可感知的加载状态,不得使用全局 spin 遮罩」
|
||||
|
||||
**现状**:`src/app/(dashboard)/admin/` 及其所有子路由(`dashboard/`、`announcements/`、`school/*`、`audit-logs/*`、`scheduling/*`、`course-plans/*`、`elective/*`、`attendance/`、`files/`、`users/import/`)均**未提供** `loading.tsx` 和 `error.tsx`。
|
||||
|
||||
对比:`teacher/`、`student/` 路由组在关键页面已提供 `loading.tsx`(如 `teacher/exams/all/loading.tsx`、`student/dashboard/loading.tsx`),admin 路由组完全缺失。
|
||||
|
||||
**影响**:
|
||||
- 数据获取失败时整页白屏,用户体验差
|
||||
- 无加载态感知,用户误以为页面卡死
|
||||
- 不符合 Next.js 16 App Router 最佳实践(Suspense 流式渲染)
|
||||
|
||||
**修复建议**:
|
||||
1. 在 `src/app/(dashboard)/admin/` 根目录新增 `error.tsx`(具名导出,客户端组件,含重试按钮)
|
||||
2. 在 `src/app/(dashboard)/admin/` 根目录新增 `loading.tsx`(骨架屏,匹配各页面布局)
|
||||
3. 对数据量大的页面(`audit-logs/*`、`school/grades/insights`、`attendance`)单独提供 `loading.tsx`
|
||||
4. 对动态路由(`[id]/page.tsx`)单独提供 `error.tsx` 处理 `notFound` 以外的异常
|
||||
|
||||
---
|
||||
|
||||
#### P0-2 `attendance/page.tsx` 缺少权限校验
|
||||
|
||||
**文件**:[src/app/(dashboard)/admin/attendance/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/attendance/page.tsx)
|
||||
|
||||
**违反规范**:
|
||||
- 项目规则:「Server Action 必须使用 `requirePermission()` 进行权限校验」
|
||||
- 编码规范 §8.3:「权限校验:Server Action 必须使用 `requirePermission()`」
|
||||
|
||||
**现状**:第 26 行仅调用 `getAuthContext()` 获取上下文,**未调用 `requirePermission()`** 验证用户是否有考勤查看权限。
|
||||
|
||||
```tsx
|
||||
// 当前代码(第 26 行)
|
||||
const ctx = await getAuthContext()
|
||||
```
|
||||
|
||||
**对比**:同类 admin 页面均做了权限校验:
|
||||
- `audit-logs/page.tsx` 第 22 行:`await requirePermission(Permissions.AUDIT_LOG_READ)`
|
||||
- `audit-logs/login-logs/page.tsx` 第 22 行:`await requirePermission(Permissions.AUDIT_LOG_READ)`
|
||||
- `audit-logs/data-changes/page.tsx` 第 26 行:`await requirePermission(Permissions.AUDIT_LOG_READ)`
|
||||
- `files/page.tsx` 第 12 行:`await requirePermission(Permissions.FILE_READ)`
|
||||
|
||||
**影响**:越权风险——无考勤查看权限的用户可直接访问 `/admin/attendance` 查看全校考勤数据。
|
||||
|
||||
**修复建议**:在 `getAuthContext()` 前增加权限校验:
|
||||
```tsx
|
||||
await requirePermission(Permissions.ATTENDANCE_READ)
|
||||
const ctx = await getAuthContext()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### P1 重要问题(应尽快修复)
|
||||
|
||||
#### P1-1 全部 26 个页面组件缺少返回类型标注
|
||||
|
||||
**违反规范**:
|
||||
- 编码规范 §4.2:「函数返回值必须显式标注,特别是 `Promise<T>`」
|
||||
- 项目规则:「函数返回值必须显式标注」
|
||||
|
||||
**现状**:所有 `page.tsx` 的默认导出函数均未标注返回类型,例如:
|
||||
|
||||
```tsx
|
||||
// dashboard/page.tsx
|
||||
export default async function AdminDashboardPage() { // ❌ 缺少 : Promise<JSX.Element>
|
||||
const data = await getAdminDashboardData()
|
||||
return <AdminDashboardView data={data} />
|
||||
}
|
||||
```
|
||||
|
||||
**影响**:26 个文件全部不合规,类型推导依赖 TS 隐式推断,不利于代码审查与维护。
|
||||
|
||||
**修复建议**:统一补充返回类型:
|
||||
```tsx
|
||||
export default async function AdminDashboardPage(): Promise<JSX.Element> {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
涉及文件:admin 目录下全部 26 个 `page.tsx`。
|
||||
|
||||
---
|
||||
|
||||
#### P1-2 `getParam` 工具函数在 27 个文件中重复定义
|
||||
|
||||
**违反规范**:
|
||||
- 编码规范 §一:「单一职责」「工具函数 ≤ 40 行」
|
||||
- DRY 原则
|
||||
|
||||
**现状**:以下 admin 文件各自重复定义了相同的 `getParam` / `SearchParams` 类型与函数:
|
||||
|
||||
| 文件 | 行号 |
|
||||
|------|------|
|
||||
| `announcements/page.tsx` | 8-13 |
|
||||
| `audit-logs/page.tsx` | 10-15 |
|
||||
| `audit-logs/login-logs/page.tsx` | 10-15 |
|
||||
| `audit-logs/data-changes/page.tsx` | 14-19 |
|
||||
| `scheduling/changes/page.tsx` | 16-21 |
|
||||
| `course-plans/page.tsx` | 7-12 |
|
||||
| `elective/page.tsx` | 7-12 |
|
||||
| `attendance/page.tsx` | 13-18 |
|
||||
| `school/grades/insights/page.tsx` | 15-22 |
|
||||
|
||||
全项目共 27 个文件重复(含 teacher / student / management 路由组)。
|
||||
|
||||
**影响**:维护成本高,任何一处逻辑变更需同步修改 27 处。
|
||||
|
||||
**修复建议**:
|
||||
1. 在 `src/shared/lib/utils.ts` 新增共享工具:
|
||||
```tsx
|
||||
export type SearchParams = { [key: string]: string | string[] | undefined }
|
||||
|
||||
export function getSearchParam(params: SearchParams, key: string): string | undefined {
|
||||
const v = params[key]
|
||||
return Array.isArray(v) ? v[0] : v
|
||||
}
|
||||
```
|
||||
2. 全部页面改为 `import { getSearchParam, type SearchParams } from "@/shared/lib/utils"`
|
||||
3. 同步更新架构文档 004 / 005
|
||||
|
||||
---
|
||||
|
||||
#### P1-3 多个页面使用 `as` 类型断言违反 TypeScript 规范
|
||||
|
||||
**违反规范**:
|
||||
- 编码规范 §4.2:「不使用 `as` 断言,除非从 `unknown` 强制转换或在测试中(需注释原因)」
|
||||
- 项目规则:「禁止 `as` 断言(除非从 `unknown` 转换或测试中,需注释原因)」
|
||||
|
||||
**现状**:以下文件使用 `as` 进行类型断言而非类型守卫:
|
||||
|
||||
| 文件 | 行号 | 问题代码 |
|
||||
|------|------|---------|
|
||||
| `audit-logs/page.tsx` | 28 | `(getParam(params, "status") as AuditLogStatus \| undefined)` |
|
||||
| `audit-logs/login-logs/page.tsx` | 26-27 | `as LoginLogAction \| undefined`、`as LoginLogStatus \| undefined` |
|
||||
| `audit-logs/data-changes/page.tsx` | 31 | `as DataChangeAction \| undefined` |
|
||||
| `attendance/page.tsx` | 39 | `as "present" \| "absent" \| "late" \| "early_leave" \| "excused"` |
|
||||
|
||||
**对比(正确示例)**:以下文件已使用类型守卫,应作为模板推广:
|
||||
- `announcements/page.tsx` 第 15-16 行:`isValidStatus` 类型守卫
|
||||
- `scheduling/changes/page.tsx` 第 23-24 行:`isValidStatus` 类型守卫
|
||||
- `course-plans/page.tsx` 第 14-15 行:`isValidStatus` 类型守卫
|
||||
- `elective/page.tsx` 第 14-15 行:`isValidStatus` 类型守卫
|
||||
|
||||
**影响**:运行时无法捕获非法枚举值,类型安全被绕过。
|
||||
|
||||
**修复建议**:为每个枚举类型补充类型守卫,替换 `as` 断言:
|
||||
```tsx
|
||||
const isValidAuditLogStatus = (v?: string): v is AuditLogStatus =>
|
||||
v === "success" || v === "failure" || v === "pending"
|
||||
|
||||
const status = isValidAuditLogStatus(statusParam) ? statusParam : undefined
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### P1-4 UI 文案语言不统一(中英文混用)
|
||||
|
||||
**违反规范**:
|
||||
- 编码规范 §一:「可读性优先」
|
||||
- 项目定位为「Next_Edu K12 智慧教务系统」(中文用户)
|
||||
|
||||
**现状**:
|
||||
|
||||
| 文件 | 文案语言 |
|
||||
|------|---------|
|
||||
| `users/import/page.tsx` | 中文("批量导入用户"、"返回") |
|
||||
| `announcements/[id]/page.tsx` | 英文("Edit Announcement") |
|
||||
| `school/schools/page.tsx` | 英文("Schools"、"Manage schools...") |
|
||||
| `school/classes/page.tsx` | 英文("Classes"、"Manage classes...") |
|
||||
| `school/grades/page.tsx` | 英文("Grades"、"Manage grades...") |
|
||||
| `school/grades/insights/page.tsx` | 英文("Grade Insights"、"Filters") |
|
||||
| `school/academic-year/page.tsx` | 英文("Academic Year") |
|
||||
| `school/departments/page.tsx` | 英文("Departments") |
|
||||
| `audit-logs/page.tsx` | 英文("Audit Logs") |
|
||||
| `audit-logs/login-logs/page.tsx` | 英文("Login Logs") |
|
||||
| `audit-logs/data-changes/page.tsx` | 英文("Data Change Logs") |
|
||||
| `scheduling/auto/page.tsx` | 英文("Auto Schedule") |
|
||||
| `scheduling/changes/page.tsx` | 英文("Schedule Change Requests") |
|
||||
| `scheduling/rules/page.tsx` | 英文("Scheduling Rules") |
|
||||
| `course-plans/page.tsx` | 英文("Course Plans") |
|
||||
| `course-plans/create/page.tsx` | 英文("New Course Plan") |
|
||||
| `course-plans/[id]/edit/page.tsx` | 英文("Edit Course Plan") |
|
||||
| `elective/page.tsx` | 英文("Elective Courses") |
|
||||
| `elective/create/page.tsx` | 英文("New Elective Course") |
|
||||
| `elective/[id]/edit/page.tsx` | 英文("Edit Elective Course") |
|
||||
| `attendance/page.tsx` | 英文("Attendance Overview") |
|
||||
|
||||
**影响**:用户体验割裂,admin 区仅 `users/import` 为中文,其余全英文,与系统定位不符。
|
||||
|
||||
**修复建议**:统一为中文(与 `users/import/page.tsx` 保持一致),或引入 i18n 方案统一管理。建议优先统一为中文。
|
||||
|
||||
---
|
||||
|
||||
### P2 一般问题(建议修复)
|
||||
|
||||
#### P2-1 `school/grades/insights/page.tsx` 使用原生 `<select>` 而非共享组件
|
||||
|
||||
**文件**:[src/app/(dashboard)/admin/school/grades/insights/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/school/grades/insights/page.tsx#L57-L68)
|
||||
|
||||
**现状**:第 57-68 行使用原生 `<select>` 元素,而项目已提供 `@/shared/components/ui/select.tsx`(shadcn Select)。
|
||||
|
||||
```tsx
|
||||
<select
|
||||
name="gradeId"
|
||||
defaultValue={selected || "all"}
|
||||
className="h-10 w-full rounded-md border bg-background px-3 text-sm md:w-[360px]"
|
||||
>
|
||||
```
|
||||
|
||||
**影响**:
|
||||
- UI 风格与其他页面不一致(其他页面使用 shadcn Select)
|
||||
- 原生 `<select>` 样式难以跨浏览器统一
|
||||
- 可访问性较弱(缺少 ARIA 属性)
|
||||
|
||||
**修复建议**:替换为 `@/shared/components/ui/select.tsx` 的 `Select` / `SelectTrigger` / `SelectContent` / `SelectItem` 组合。
|
||||
|
||||
---
|
||||
|
||||
#### P2-2 `users/import/page.tsx` 使用原生 `<table>` 而非共享组件
|
||||
|
||||
**文件**:[src/app/(dashboard)/admin/users/import/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/users/import/page.tsx#L93-L128)
|
||||
|
||||
**现状**:第 93-128 行使用原生 `<table>` 元素手写表格,而项目已提供 `@/shared/components/ui/table.tsx`(shadcn Table)。
|
||||
|
||||
**影响**:与 `school/grades/insights/page.tsx` 等使用 shadcn Table 的页面风格不一致。
|
||||
|
||||
**修复建议**:替换为 `Table` / `TableHeader` / `TableBody` / `TableRow` / `TableHead` / `TableCell` 组合。
|
||||
|
||||
---
|
||||
|
||||
#### P2-3 Tailwind 任意值违规
|
||||
|
||||
**违反规范**:
|
||||
- 编码规范 §6.2:「禁止使用任意值(`w-[137px]`),除非有充分理由并注释说明」
|
||||
- 项目规则:「禁止使用任意值(`w-[137px]`),除非有充分理由并注释」
|
||||
|
||||
**现状**:
|
||||
|
||||
| 文件 | 行号 | 问题类名 |
|
||||
|------|------|---------|
|
||||
| `school/grades/insights/page.tsx` | 60 | `md:w-[360px]` |
|
||||
| `school/grades/insights/page.tsx` | 82, 89, 96 | `h-[360px]` |
|
||||
| `users/import/page.tsx` | 16 | `h-full flex-1 flex-col`(`flex-1` 合理,但整体布局类应复用) |
|
||||
|
||||
**修复建议**:
|
||||
- `md:w-[360px]` → 使用设计令牌宽度类(如 `md:w-72` 或 `md:w-80`)或在 globals.css 定义 `--filter-width` 变量
|
||||
- `h-[360px]` → 使用 `h-80`(320px)或 `h-96`(384px)等标准档位
|
||||
|
||||
---
|
||||
|
||||
#### P2-4 `users/import/page.tsx` 使用硬编码颜色
|
||||
|
||||
**违反规范**:
|
||||
- 编码规范 §6.3:「所有视觉设计决策(颜色、字号、间距)必须体现在设计令牌中,组件中不使用硬编码值」
|
||||
|
||||
**文件**:[src/app/(dashboard)/admin/users/import/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/users/import/page.tsx#L67)
|
||||
|
||||
**现状**:第 67 行使用 `text-amber-500` 硬编码颜色:
|
||||
```tsx
|
||||
<Info className="h-5 w-5 text-amber-500" />
|
||||
```
|
||||
|
||||
**修复建议**:使用设计令牌颜色,如 `text-warning`(若存在)或在 globals.css 定义 `--warning` 变量。如暂无 warning 令牌,可使用 `text-primary` 或 `text-muted-foreground` 保持一致。
|
||||
|
||||
---
|
||||
|
||||
#### P2-5 `school/grades/insights/page.tsx` 导入顺序违规
|
||||
|
||||
**违反规范**:
|
||||
- 编码规范 §4.3:「导入顺序:React → 第三方 → 内部绝对路径 → 相对路径 → 类型导入」
|
||||
- 项目规则引用的 ESLint `import/order` 规则
|
||||
|
||||
**文件**:[src/app/(dashboard)/admin/school/grades/insights/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/school/grades/insights/page.tsx#L1-L11)
|
||||
|
||||
**现状**:第 1-11 行导入顺序混乱,`lucide-react`(第三方库)被放在所有 `@/` 内部导入之后:
|
||||
```tsx
|
||||
import Link from "next/link" // next(外部)
|
||||
import { getGrades } from "@/modules/school/data-access" // 内部
|
||||
import { getGradeHomeworkInsights } from "@/modules/classes/data-access"
|
||||
import { EmptyState } from "@/shared/components/ui/empty-state"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/shared/components/ui/card"
|
||||
import { Badge } from "@/shared/components/ui/badge"
|
||||
import { Button } from "@/shared/components/ui/button"
|
||||
import { Table, TableBody, ... } from "@/shared/components/ui/table"
|
||||
import { formatDate } from "@/shared/lib/utils"
|
||||
import { BarChart3 } from "lucide-react" // ❌ 第三方应在前
|
||||
```
|
||||
|
||||
**修复建议**:调整为 `next` → `lucide-react` → `@/` 内部导入,分组间空一行:
|
||||
```tsx
|
||||
import Link from "next/link"
|
||||
|
||||
import { BarChart3 } from "lucide-react"
|
||||
|
||||
import { getGrades } from "@/modules/school/data-access"
|
||||
// ...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### P2-6 `course-plans/[id]/edit/page.tsx` 同模块重复导入
|
||||
|
||||
**文件**:[src/app/(dashboard)/admin/course-plans/[id]/edit/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/course-plans/[id]/edit/page.tsx#L3-L4)
|
||||
|
||||
**现状**:第 3-4 行从同一模块 `@/modules/course-plans/data-access` 分两行导入:
|
||||
```tsx
|
||||
import { getCoursePlanById } from "@/modules/course-plans/data-access"
|
||||
import { getSubjectOptions } from "@/modules/course-plans/data-access"
|
||||
```
|
||||
|
||||
**修复建议**:合并为单行:
|
||||
```tsx
|
||||
import { getCoursePlanById, getSubjectOptions } from "@/modules/course-plans/data-access"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### P2-7 `scheduling/*` 页面从 `actions` 而非 `data-access` 获取数据
|
||||
|
||||
**违反规范**:
|
||||
- 编码规范 §7.1:「服务端数据获取通过模块的 `data-access.ts` 函数」
|
||||
- 架构影响地图:「actions.ts(编排层:权限 + 调用 data-access + revalidate)」
|
||||
|
||||
**现状**:以下页面从 `@/modules/scheduling/actions` 导入数据查询函数:
|
||||
|
||||
| 文件 | 导入函数 |
|
||||
|------|---------|
|
||||
| `scheduling/auto/page.tsx` | `getAdminClassesForScheduling` |
|
||||
| `scheduling/changes/page.tsx` | `getAdminClassesForScheduling`、`getScheduleChanges` |
|
||||
| `scheduling/rules/page.tsx` | `getAdminClassesForScheduling`、`getSchedulingRules` |
|
||||
|
||||
**说明**:规范允许 `app/` 调用 Server Actions,但 Server Actions 的职责是「编排:权限 + 调用 data-access + revalidate」,主要用于**变更操作**。纯读取操作应通过 `data-access.ts` 暴露,避免在 Server Component 中触发不必要的 `revalidate` 逻辑。
|
||||
|
||||
**修复建议**:将 `getAdminClassesForScheduling`、`getScheduleChanges`、`getSchedulingRules` 等纯查询函数迁移到 `scheduling/data-access.ts`,或在 actions 中明确标注其为只读封装。需同步更新架构文档 004 / 005。
|
||||
|
||||
---
|
||||
|
||||
## 三、React 性能优化建议(基于最佳实践)
|
||||
|
||||
### R1 利用 Suspense 流式渲染提升首屏感知性能
|
||||
|
||||
**现状**:所有页面使用 `export const dynamic = "force-dynamic"` 整页动态渲染,数据获取完成前无任何内容呈现。
|
||||
|
||||
**建议**:对数据量大的页面(`audit-logs/*`、`school/grades/insights`、`attendance`)拆分为多个 Suspense 边界,优先渲染页面骨架,慢查询部分流式注入:
|
||||
|
||||
```tsx
|
||||
import { Suspense } from "react"
|
||||
|
||||
export default async function AuditLogsPage(): Promise<JSX.Element> {
|
||||
return (
|
||||
<div className="flex h-full flex-col space-y-8 p-8">
|
||||
<Header />
|
||||
<Suspense fallback={<FilterSkeleton />}>
|
||||
<Filters />
|
||||
</Suspense>
|
||||
<Suspense fallback={<TableSkeleton />}>
|
||||
<AuditTable />
|
||||
</Suspense>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### R2 `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#L30-L33)
|
||||
|
||||
**现状**:第 30-33 行先 `await getGrades()` 再条件 `await getGradeHomeworkInsights()`,两次串行查询:
|
||||
```tsx
|
||||
const grades = await getGrades()
|
||||
const selected = gradeId && gradeId !== "all" ? gradeId : ""
|
||||
const insights = selected ? await getGradeHomeworkInsights({ gradeId: selected, limit: 50 }) : null
|
||||
```
|
||||
|
||||
**说明**:`insights` 依赖 `selected`(来自 URL 参数,非 `grades` 结果),两者无数据依赖,可并行:
|
||||
```tsx
|
||||
const selected = gradeId && gradeId !== "all" ? gradeId : ""
|
||||
const [grades, insights] = await Promise.all([
|
||||
getGrades(),
|
||||
selected ? getGradeHomeworkInsights({ gradeId: selected, limit: 50 }) : Promise.resolve(null),
|
||||
])
|
||||
```
|
||||
|
||||
### R3 列表页 `classOptions` 映射可下沉至 data-access
|
||||
|
||||
**现状**:`scheduling/auto`、`scheduling/changes`、`scheduling/rules`、`attendance`、`course-plans/create`、`course-plans/[id]/edit`、`elective/create`、`elective/[id]/edit` 等页面均在组件内 `.map()` 转换数据形状:
|
||||
|
||||
```tsx
|
||||
const classOptions = classes.map((c) => ({ id: c.id, name: c.name, grade: c.grade }))
|
||||
```
|
||||
|
||||
**建议**:在对应 `data-access.ts` 提供 `getClassOptions()`、`getStaffOptions()` 等轻量查询函数,仅返回 `{ id, name }` 形状,减少传输数据量与组件层转换逻辑。
|
||||
|
||||
---
|
||||
|
||||
## 四、Web 界面设计规范建议(基于 WCAG 2.2 AA)
|
||||
|
||||
### W1 表单 `<label>` 与控件关联不规范
|
||||
|
||||
**文件**:[src/app/(dashboard)/admin/school/grades/insights/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/school/grades/insights/page.tsx#L56-L57)
|
||||
|
||||
**现状**:第 56-57 行 `<label>` 与 `<select>` 未通过 `htmlFor` / `id` 关联:
|
||||
```tsx
|
||||
<label className="text-sm font-medium">Grade</label>
|
||||
<select name="gradeId" ...>
|
||||
```
|
||||
|
||||
**违反**:WCAG 2.2 SC 1.3.1(信息与关系)、SC 3.3.2(标签或指令)。
|
||||
|
||||
**修复建议**:
|
||||
```tsx
|
||||
<label htmlFor="grade-filter" className="text-sm font-medium">Grade</label>
|
||||
<select id="grade-filter" name="gradeId" ...>
|
||||
```
|
||||
|
||||
### W2 `users/import/page.tsx` 表格缺少 `<caption>` 与语义化标注
|
||||
|
||||
**文件**:[src/app/(dashboard)/admin/users/import/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/users/import/page.tsx#L93-L128)
|
||||
|
||||
**现状**:原生 `<table>` 缺少 `<caption>` 描述表格用途,屏幕阅读器无法快速理解表格主题。
|
||||
|
||||
**修复建议**:增加 `<caption className="sr-only">模板字段说明</caption>`,或替换为 shadcn Table 后通过 `aria-label` 补充。
|
||||
|
||||
### W3 页面标题层级不统一
|
||||
|
||||
**现状**:
|
||||
- 部分页面使用 `<h2>` 作为页面主标题(如 `school/schools`、`audit-logs`)
|
||||
- `users/import/page.tsx` 也使用 `<h2>`
|
||||
- 但页面布局中未见统一的 `<h1>` 主标题层级
|
||||
|
||||
**建议**:确认 `(dashboard)/layout.tsx` 是否提供 `<h1>` 或页面 `<main>` 的 accessible name,若无,建议各页面统一使用 `<h1>` 作为页面主标题,`<h2>` 用于区块标题,保持标题层级连贯。
|
||||
|
||||
### W4 交互式筛选器缺少 `aria-live` 反馈
|
||||
|
||||
**文件**:`school/grades/insights/page.tsx`、`attendance/page.tsx`、`audit-logs/*`
|
||||
|
||||
**现状**:筛选器提交后表格数据刷新,但屏幕阅读器用户无法感知数据已更新。
|
||||
|
||||
**违反**:WCAG 2.2 SC 4.1.3(状态消息)。
|
||||
|
||||
**建议**:在表格容器添加 `aria-live="polite"` 或使用项目已有的 `useAriaLive` Hook 通知「已加载 N 条记录」。
|
||||
|
||||
### W5 `EmptyState` 组件使用一致但图标语义可优化
|
||||
|
||||
**现状**:`scheduling/*`、`attendance`、`school/grades/insights` 均使用 `EmptyState` 组件,图标统一使用 `ClipboardList` / `BarChart3`,体验一致(优点)。
|
||||
|
||||
**建议**:`BarChart3` 用于「无数据」与「选择年级」两种语义略显混淆,建议「等待操作」类空状态使用 `MousePointerClick` 或 `Filter` 图标区分。
|
||||
|
||||
---
|
||||
|
||||
## 五、优秀实践(已符合规范,应保持)
|
||||
|
||||
1. **服务端组件默认化**:全部 26 个页面均为 async 服务端组件,未滥用 `"use client"`,符合 §5.2。
|
||||
2. **并行数据获取**:`announcements/page.tsx`、`audit-logs/*`、`course-plans/create`、`course-plans/[id]/edit`、`elective/create`、`elective/[id]/edit`、`school/grades`、`scheduling/rules` 等均使用 `Promise.all` 并行查询,性能良好。
|
||||
3. **类型守卫正确使用**:`announcements/page.tsx`、`scheduling/changes/page.tsx`、`course-plans/page.tsx`、`elective/page.tsx` 使用 `isValidStatus` 类型守卫,是 `as` 断言的正确替代方案。
|
||||
4. **404 处理**:`announcements/[id]/page.tsx`、`course-plans/[id]/page.tsx`、`course-plans/[id]/edit/page.tsx`、`elective/[id]/edit/page.tsx` 使用 `notFound()` 处理资源不存在场景。
|
||||
5. **权限校验到位**:`audit-logs/*`(3 个文件)、`files/page.tsx` 正确调用 `requirePermission()`。
|
||||
6. **模块化组合**:页面仅负责数据获取与组合,UI 逻辑下沉至 `modules/*/components/`,符合三层架构。
|
||||
7. **`force-dynamic` 标注**:需要实时数据的页面均显式声明 `export const dynamic = "force-dynamic"`。
|
||||
8. **`metadata` 导出**:`users/import/page.tsx` 正确导出 `metadata` 用于 SEO(建议其他页面补充)。
|
||||
|
||||
---
|
||||
|
||||
## 六、修复优先级与建议执行顺序
|
||||
|
||||
| 优先级 | 问题编号 | 建议执行顺序 | 影响范围 |
|
||||
|--------|---------|-------------|---------|
|
||||
| P0 | P0-2 | 立即修复 attendance 权限 | 1 文件 |
|
||||
| P0 | P0-1 | 补充 error.tsx / loading.tsx | 新增 ~6 文件 |
|
||||
| P1 | P1-1 | 补充返回类型标注 | 26 文件 |
|
||||
| P1 | P1-2 | 抽取共享 getSearchParam | 27 文件 |
|
||||
| P1 | P1-3 | 替换 as 断言为类型守卫 | 4 文件 |
|
||||
| P1 | P1-4 | 统一 UI 文案语言 | ~20 文件 |
|
||||
| P2 | P2-1 ~ P2-7 | 逐步整改 | 单文件级 |
|
||||
| R1 ~ R3 | 性能优化 | 迭代优化 | 关键页面 |
|
||||
| W1 ~ W5 | 可访问性 | 迭代优化 | 关键页面 |
|
||||
|
||||
---
|
||||
|
||||
## 七、附:文件清单与合规状态
|
||||
|
||||
| 文件 | P0 | P1 | P2 | 备注 |
|
||||
|------|----|----|----|----|
|
||||
| `dashboard/page.tsx` | - | 缺返回类型 | - | 整体合规 |
|
||||
| `announcements/page.tsx` | - | 缺返回类型、getParam 重复 | - | 类型守卫正确 |
|
||||
| `announcements/[id]/page.tsx` | - | 缺返回类型、英文文案 | - | - |
|
||||
| `users/import/page.tsx` | - | 缺返回类型 | 原生 table、硬编码颜色 | 文案为中文(正确) |
|
||||
| `school/page.tsx` | - | 缺返回类型 | - | 仅 redirect |
|
||||
| `school/schools/page.tsx` | - | 缺返回类型、英文文案 | - | - |
|
||||
| `school/classes/page.tsx` | - | 缺返回类型、英文文案 | - | - |
|
||||
| `school/grades/page.tsx` | - | 缺返回类型、英文文案 | - | - |
|
||||
| `school/grades/insights/page.tsx` | - | 缺返回类型、英文文案 | 原生 select、任意值、导入顺序、label 未关联 | 问题最多 |
|
||||
| `school/academic-year/page.tsx` | - | 缺返回类型、英文文案 | - | - |
|
||||
| `school/departments/page.tsx` | - | 缺返回类型、英文文案 | - | - |
|
||||
| `audit-logs/page.tsx` | - | 缺返回类型、as 断言、英文文案、getParam 重复 | - | 权限校验正确 |
|
||||
| `audit-logs/login-logs/page.tsx` | - | 缺返回类型、as 断言、英文文案、getParam 重复 | - | 权限校验正确 |
|
||||
| `audit-logs/data-changes/page.tsx` | - | 缺返回类型、as 断言、英文文案、getParam 重复 | - | 权限校验正确 |
|
||||
| `scheduling/auto/page.tsx` | - | 缺返回类型、英文文案 | 从 actions 取数 | - |
|
||||
| `scheduling/changes/page.tsx` | - | 缺返回类型、英文文案、getParam 重复 | 从 actions 取数 | 类型守卫正确 |
|
||||
| `scheduling/rules/page.tsx` | - | 缺返回类型、英文文案 | 从 actions 取数 | - |
|
||||
| `course-plans/page.tsx` | - | 缺返回类型、英文文案、getParam 重复 | - | 类型守卫正确 |
|
||||
| `course-plans/create/page.tsx` | - | 缺返回类型、英文文案 | - | - |
|
||||
| `course-plans/[id]/page.tsx` | - | 缺返回类型 | - | - |
|
||||
| `course-plans/[id]/edit/page.tsx` | - | 缺返回类型、英文文案 | 重复导入 | - |
|
||||
| `elective/page.tsx` | - | 缺返回类型、英文文案、getParam 重复 | - | 类型守卫正确 |
|
||||
| `elective/create/page.tsx` | - | 缺返回类型、英文文案 | - | - |
|
||||
| `elective/[id]/edit/page.tsx` | - | 缺返回类型、英文文案 | - | - |
|
||||
| `attendance/page.tsx` | **缺权限校验** | 缺返回类型、as 断言、英文文案、getParam 重复 | - | 最高优先级 |
|
||||
| `files/page.tsx` | - | 缺返回类型 | - | 权限校验正确、整体合规 |
|
||||
|
||||
---
|
||||
|
||||
> 报告生成完毕。建议按「六、修复优先级」顺序整改,每完成一批次后运行 `npm run lint` 与 `npx tsc --noEmit` 验证,并同步更新架构文档 004 / 005。
|
||||
Reference in New Issue
Block a user