# `src/app/(dashboard)/teacher` 前端规范核查报告

> 核查日期：2026-06-18
> 核查范围：`src/app/(dashboard)/teacher/` 目录下所有前端文件（page.tsx / loading.tsx）
> 依据文档：项目规则、编码规范 `docs/standards/coding-standards.md`、架构影响地图 004、架构数据 005
> 应用技能：`vercel-react-best-practices`（性能优化）、`web-artifacts-builder`（界面优化）、`web-design-guidelines`（Web 界面规范审查）

---

## 一、核查文件清单

| 文件 | 行数 | 类型 | 用途 |
|------|------|------|------|
| [dashboard/page.tsx](../src/app/(dashboard)/teacher/dashboard/page.tsx) | 37 | 页面 | 教师仪表盘 |
| [attendance/page.tsx](../src/app/(dashboard)/teacher/attendance/page.tsx) | 83 | 页面 | 考勤记录列表 |
| [attendance/sheet/page.tsx](../src/app/(dashboard)/teacher/attendance/sheet/page.tsx) | 49 | 页面 | 考勤登记 |
| [attendance/stats/page.tsx](../src/app/(dashboard)/teacher/attendance/stats/page.tsx) | 120 | 页面 | 考勤统计 |
| [classes/page.tsx](../src/app/(dashboard)/teacher/classes/page.tsx) | 5 | 页面 | 重定向到 my |
| [classes/my/page.tsx](../src/app/(dashboard)/teacher/classes/my/page.tsx) | 18 | 页面 | 我的班级 |
| [classes/my/[id]/page.tsx](../src/app/(dashboard)/teacher/classes/my/[id]/page.tsx) | 109 | 页面 | 班级详情 |
| [classes/my/loading.tsx](../src/app/(dashboard)/teacher/classes/my/loading.tsx) | 31 | 加载态 | 班级列表骨架屏 |
| [classes/schedule/page.tsx](../src/app/(dashboard)/teacher/classes/schedule/page.tsx) | 81 | 页面 | 班级课表 |
| [classes/schedule/loading.tsx](../src/app/(dashboard)/teacher/classes/schedule/loading.tsx) | 28 | 加载态 | 课表骨架屏 |
| [classes/students/page.tsx](../src/app/(dashboard)/teacher/classes/students/page.tsx) | 102 | 页面 | 学生列表 |
| [classes/students/loading.tsx](../src/app/(dashboard)/teacher/classes/students/loading.tsx) | 20 | 加载态 | 学生列表骨架屏 |
| [course-plans/page.tsx](../src/app/(dashboard)/teacher/course-plans/page.tsx) | 49 | 页面 | 课程计划列表 |
| [course-plans/[id]/page.tsx](../src/app/(dashboard)/teacher/course-plans/[id]/page.tsx) | 26 | 页面 | 课程计划详情 |
| [diagnostic/page.tsx](../src/app/(dashboard)/teacher/diagnostic/page.tsx) | 48 | 页面 | 学习诊断报告 |
| [diagnostic/class/[classId]/page.tsx](../src/app/(dashboard)/teacher/diagnostic/class/[classId]/page.tsx) | 45 | 页面 | 班级诊断 |
| [diagnostic/student/[studentId]/page.tsx](../src/app/(dashboard)/teacher/diagnostic/student/[studentId]/page.tsx) | 65 | 页面 | 学生诊断 |
| [elective/page.tsx](../src/app/(dashboard)/teacher/elective/page.tsx) | 50 | 页面 | 选修课程 |
| [exams/page.tsx](../src/app/(dashboard)/teacher/exams/page.tsx) | 5 | 页面 | 重定向到 all |
| [exams/all/page.tsx](../src/app/(dashboard)/teacher/exams/all/page.tsx) | 148 | 页面 | 考试列表 |
| [exams/all/loading.tsx](../src/app/(dashboard)/teacher/exams/all/loading.tsx) | 24 | 加载态 | 考试列表骨架屏 |
| [exams/create/page.tsx](../src/app/(dashboard)/teacher/exams/create/page.tsx) | 10 | 页面 | 创建考试 |
| [exams/create/loading.tsx](../src/app/(dashboard)/teacher/exams/create/loading.tsx) | 16 | 加载态 | 创建考试骨架屏 |
| [exams/[id]/build/page.tsx](../src/app/(dashboard)/teacher/exams/[id]/build/page.tsx) | 120 | 页面 | 组卷 |
| [exams/[id]/proctoring/page.tsx](../src/app/(dashboard)/teacher/exams/[id]/proctoring/page.tsx) | 55 | 页面 | 监考 |
| [exams/grading/page.tsx](../src/app/(dashboard)/teacher/exams/grading/page.tsx) | 5 | 页面 | 重定向 |
| [exams/grading/[submissionId]/page.tsx](../src/app/(dashboard)/teacher/exams/grading/[submissionId]/page.tsx) | 6 | 页面 | 重定向 |
| [exams/grading/loading.tsx](../src/app/(dashboard)/teacher/exams/grading/loading.tsx) | 20 | 加载态 | 批改骨架屏 |
| [grades/page.tsx](../src/app/(dashboard)/teacher/grades/page.tsx) | 101 | 页面 | 成绩管理 |
| [grades/analytics/page.tsx](../src/app/(dashboard)/teacher/grades/analytics/page.tsx) | 259 | 页面 | 成绩分析 |
| [grades/entry/page.tsx](../src/app/(dashboard)/teacher/grades/entry/page.tsx) | 52 | 页面 | 批量录入 |
| [grades/stats/page.tsx](../src/app/(dashboard)/teacher/grades/stats/page.tsx) | 139 | 页面 | 成绩统计 |
| [homework/page.tsx](../src/app/(dashboard)/teacher/homework/page.tsx) | 5 | 页面 | 重定向 |
| [homework/assignments/page.tsx](../src/app/(dashboard)/teacher/homework/assignments/page.tsx) | 119 | 页面 | 作业列表 |
| [homework/assignments/create/page.tsx](../src/app/(dashboard)/teacher/homework/assignments/create/page.tsx) | 43 | 页面 | 创建作业 |
| [homework/assignments/[id]/page.tsx](../src/app/(dashboard)/teacher/homework/assignments/[id]/page.tsx) | 100 | 页面 | 作业详情 |
| [homework/assignments/[id]/submissions/page.tsx](../src/app/(dashboard)/teacher/homework/assignments/[id]/submissions/page.tsx) | 86 | 页面 | 作业提交列表 |
| [homework/submissions/page.tsx](../src/app/(dashboard)/teacher/homework/submissions/page.tsx) | 80 | 页面 | 提交审阅 |
| [homework/submissions/[submissionId]/page.tsx](../src/app/(dashboard)/teacher/homework/submissions/[submissionId]/page.tsx) | 44 | 页面 | 批改详情 |
| [questions/page.tsx](../src/app/(dashboard)/teacher/questions/page.tsx) | 120 | 页面 | 题库 |
| [questions/loading.tsx](../src/app/(dashboard)/teacher/questions/loading.tsx) | 29 | 加载态 | 题库骨架屏 |
| [schedule-changes/page.tsx](../src/app/(dashboard)/teacher/schedule-changes/page.tsx) | 69 | 页面 | 课表变更 |
| [textbooks/page.tsx](../src/app/(dashboard)/teacher/textbooks/page.tsx) | 74 | 页面 | 教材列表 |
| [textbooks/loading.tsx](../src/app/(dashboard)/teacher/textbooks/loading.tsx) | 48 | 加载态 | 教材骨架屏 |
| [textbooks/[id]/page.tsx](../src/app/(dashboard)/teacher/textbooks/[id]/page.tsx) | 63 | 页面 | 教材详情 |
| [textbooks/[id]/loading.tsx](../src/app/(dashboard)/teacher/textbooks/[id]/loading.tsx) | 66 | 加载态 | 教材详情骨架屏 |

共计 **45 个文件**（37 个 page.tsx + 8 个 loading.tsx）。

---

## 二、违规问题清单

### 2.1 架构分层违规 — 严重度：高

#### BUG-T01：app 层直接访问数据库（dashboard/page.tsx）
- **位置**：[dashboard/page.tsx:4-6, 18-21](../src/app/(dashboard)/teacher/dashboard/page.tsx)
- **问题**：页面直接 `import { db } from "@/shared/db"` 并调用 `db.query.users.findFirst()`，违反项目规则「`app/` 只能调用 `modules/` 的 Server Actions 和 data-access，不直接访问 DB」
- **现状**：
  ```typescript
  import { db } from "@/shared/db"
  import { users } from "@/shared/db/schema"
  // ...
  db.query.users.findFirst({
    where: eq(users.id, teacherId),
    columns: { name: true },
  })
  ```
- **改进建议**：通过 `modules/users/data-access.ts` 暴露 `getUserNameById(id)` 函数调用

#### BUG-T02：app 层直接访问数据库（grades/page.tsx）
- **位置**：[grades/page.tsx:5-7, 35](../src/app/(dashboard)/teacher/grades/page.tsx)
- **问题**：直接 `db.query.subjects.findMany()` 查询科目列表，违反三层架构
- **改进建议**：在 `modules/school/data-access.ts` 或 `modules/grades/data-access.ts` 暴露 `getSubjects()` 函数

#### BUG-T03：app 层直接访问数据库（grades/analytics/page.tsx）
- **位置**：[grades/analytics/page.tsx:5-6, 48-50](../src/app/(dashboard)/teacher/grades/analytics/page.tsx)
- **问题**：同 BUG-T02，直接 `db.query.subjects.findMany()`
- **改进建议**：同 BUG-T02

#### BUG-T04：app 层直接访问数据库（grades/entry/page.tsx）
- **位置**：[grades/entry/page.tsx:1-3, 25](../src/app/(dashboard)/teacher/grades/entry/page.tsx)
- **问题**：同 BUG-T02
- **改进建议**：同 BUG-T02

#### BUG-T05：app 层直接访问数据库（grades/stats/page.tsx）
- **位置**：[grades/stats/page.tsx:1-3, 28](../src/app/(dashboard)/teacher/grades/stats/page.tsx)
- **问题**：同 BUG-T02
- **改进建议**：同 BUG-T02

#### BUG-T06：认证上下文获取方式不一致
- **位置**：
  - [course-plans/page.tsx:1, 23](../src/app/(dashboard)/teacher/course-plans/page.tsx)
  - [elective/page.tsx:1, 23](../src/app/(dashboard)/teacher/elective/page.tsx)
- **问题**：使用 `import { auth } from "@/auth"` + `auth()` 获取 session，而其他页面统一使用 `getAuthContext()`（含 DataScope 解析）
- **影响**：无法获得 `dataScope`，无法做数据范围过滤；与项目其他页面不一致
- **改进建议**：统一改为 `const ctx = await getAuthContext(); const teacherId = ctx.userId`

---

### 2.2 Prettier 配置违规 — 严重度：中

项目 `.prettierrc` 配置 `"semi": false`，但以下文件使用分号结尾：

#### BUG-T07：textbooks/page.tsx 使用分号
- **位置**：[textbooks/page.tsx:3, 73](../src/app/(dashboard)/teacher/textbooks/page.tsx)
- **问题**：`import { TextbookCard } from "...";` 等多处使用分号
- **改进建议**：运行 `npx prettier --write` 统一格式

#### BUG-T08：textbooks/[id]/page.tsx 使用分号
- **位置**：[textbooks/[id]/page.tsx](../src/app/(dashboard)/teacher/textbooks/[id]/page.tsx)（全文）
- **问题**：多处语句使用分号结尾
- **改进建议**：同 BUG-T07

#### BUG-T09：textbooks/loading.tsx 使用分号
- **位置**：[textbooks/loading.tsx](../src/app/(dashboard)/teacher/textbooks/loading.tsx)（全文）
- **问题**：同 BUG-T07
- **改进建议**：同 BUG-T07

#### BUG-T10：textbooks/[id]/loading.tsx 使用分号
- **位置**：[textbooks/[id]/loading.tsx](../src/app/(dashboard)/teacher/textbooks/[id]/loading.tsx)（全文）
- **问题**：同 BUG-T07
- **改进建议**：同 BUG-T07

---

### 2.3 TypeScript 规范违规 — 严重度：高

#### BUG-T11：使用 `as` 类型断言（exams/[id]/build/page.tsx）
- **位置**：[exams/[id]/build/page.tsx:32-34](../src/app/(dashboard)/teacher/exams/[id]/build/page.tsx)
- **问题**：使用 `as` 断言转换类型，违反编码规范「禁止 `as` 断言（除非从 `unknown` 转换）」
- **现状**：
  ```typescript
  content: q.content as Question["content"],
  type: q.type as Question["type"],
  ```
- **改进建议**：在 data-access 层返回正确类型，或使用类型守卫函数

#### BUG-T12：使用 `as` 类型断言（attendance/page.tsx）
- **位置**：[attendance/page.tsx:39](../src/app/(dashboard)/teacher/attendance/page.tsx)
- **问题**：`status as "present" | "absent" | "late" | "early_leave" | "excused"` 直接断言
- **改进建议**：使用类型守卫函数 `isAttendanceStatus(value): value is AttendanceStatus`

#### BUG-T13：使用 `as` 类型断言（grades/page.tsx）
- **位置**：[grades/page.tsx:43-44](../src/app/(dashboard)/teacher/grades/page.tsx)
- **问题**：`type as "exam" | "quiz" | "homework" | "other"` 和 `semester as "1" | "2"` 直接断言
- **改进建议**：使用类型守卫

#### BUG-T14：使用 `as` 类型断言（grades/analytics/page.tsx）
- **位置**：[grades/analytics/page.tsx](../src/app/(dashboard)/teacher/grades/analytics/page.tsx)（多处）
- **问题**：同上模式
- **改进建议**：同上

#### BUG-T15：使用 `as` 类型断言（diagnostic/page.tsx）
- **位置**：[diagnostic/page.tsx:27-28](../src/app/(dashboard)/teacher/diagnostic/page.tsx)
- **问题**：`reportType as DiagnosticReportType` 和 `status as DiagnosticReportStatus`
- **改进建议**：使用类型守卫

#### BUG-T16：函数返回值未显式标注（getParam 工具函数）
- **位置**：以下 15 个文件中的 `getParam` 函数均未标注返回类型
  - attendance/page.tsx:15
  - attendance/sheet/page.tsx:9
  - attendance/stats/page.tsx:12
  - classes/schedule/page.tsx:14
  - classes/students/page.tsx:14
  - course-plans/page.tsx:10
  - diagnostic/page.tsx:10
  - elective/page.tsx:10
  - exams/all/page.tsx:16
  - grades/page.tsx:19
  - grades/analytics/page.tsx:28
  - grades/entry/page.tsx:12
  - grades/stats/page.tsx:15
  - homework/assignments/page.tsx:23
  - questions/page.tsx:15
  - textbooks/page.tsx:13
- **问题**：违反编码规范「函数返回值必须显式标注，特别是 `Promise<T>`」
- **现状**：`const getParam = (params: SearchParams, key: string) => { ... }`
- **改进建议**：`const getParam = (params: SearchParams, key: string): string | undefined => { ... }`

#### BUG-T17：页面默认导出函数未标注返回类型
- **位置**：所有 page.tsx 文件的 `export default async function XxxPage()`
- **问题**：未标注 `Promise<JSX.Element>` 或 `Promise<React.ReactNode>`
- **规范依据**：编码规范 5.2 示例 `export default async function UsersPage(): Promise<JSX.Element>`
- **改进建议**：统一补充返回类型标注

---

### 2.4 DRY 违规（重复代码） — 严重度：中

#### BUG-T18：`getParam` 工具函数在 16 个文件中重复定义
- **位置**：见 BUG-T16 列表
- **问题**：完全相同的工具函数 `getParam` 和类型 `SearchParams` 在 16 个页面文件中复制粘贴
- **改进建议**：提取到 `shared/lib/search-params.ts`：
  ```typescript
  export type SearchParams = { [key: string]: string | string[] | undefined }
  export function getParam(params: SearchParams, key: string): string | undefined {
    const v = params[key]
    return Array.isArray(v) ? v[0] : v
  }
  ```

#### BUG-T19：`StatsClassSelector` 模式重复
- **位置**：
  - [attendance/stats/page.tsx:91-119](../src/app/(dashboard)/teacher/attendance/stats/page.tsx)
  - [grades/stats/page.tsx:86-138](../src/app/(dashboard)/teacher/grades/stats/page.tsx)
  - [grades/analytics/page.tsx:150-258](../src/app/(dashboard)/teacher/grades/analytics/page.tsx)
- **问题**：三处文件都定义了「类筛选按钮组」组件，结构几乎相同（`<a>` 标签 + 条件 className）
- **改进建议**：提取为共享组件 `shared/components/ui/filter-chips.tsx`

---

### 2.5 性能问题（vercel-react-best-practices） — 严重度：高

#### BUG-T20：串行数据获取 waterfall（attendance/page.tsx）
- **位置**：[attendance/page.tsx:32-41](../src/app/(dashboard)/teacher/attendance/page.tsx)
- **问题**：`getTeacherClasses()` 与 `getAttendanceRecords()` 串行执行，但二者无依赖关系
- **违反规则**：`async-parallel` - 独立操作应使用 `Promise.all()`
- **改进建议**：
  ```typescript
  const [classes, result] = await Promise.all([
    getTeacherClasses(),
    getAttendanceRecords({ ... }),
  ])
  ```

#### BUG-T21：串行数据获取 waterfall（attendance/sheet/page.tsx）
- **位置**：[attendance/sheet/page.tsx:24-29](../src/app/(dashboard)/teacher/attendance/sheet/page.tsx)
- **问题**：`getTeacherClasses()` 与 `getClassStudentsForAttendance()` 串行，但 students 依赖 defaultClassId（来自 searchParams），可与 classes 并行
- **改进建议**：使用 `Promise.all` 并行

#### BUG-T22：串行数据获取 waterfall（attendance/stats/page.tsx）
- **位置**：[attendance/stats/page.tsx:28-53](../src/app/(dashboard)/teacher/attendance/stats/page.tsx)
- **问题**：`getTeacherClasses()` → `getClassAttendanceStats()` 串行，但 stats 依赖 classId（可从 classes[0] 取默认），可优化
- **改进建议**：先并行获取 classes，再取 targetClassId 后获取 stats（当前逻辑合理但可考虑预取）

#### BUG-T23：串行数据获取 waterfall（grades/page.tsx）
- **位置**：[grades/page.tsx:33-45](../src/app/(dashboard)/teacher/grades/page.tsx)
- **问题**：`Promise.all([getTeacherClasses, db.query])` 之后串行 `getGradeRecords`，但 `getGradeRecords` 不依赖前两者结果
- **改进建议**：三个查询全部 `Promise.all`

#### BUG-T24：串行数据获取 waterfall（grades/entry/page.tsx）
- **位置**：[grades/entry/page.tsx:23-34](../src/app/(dashboard)/teacher/grades/entry/page.tsx)
- **问题**：`Promise.all([getTeacherClasses, db.query])` 后串行 `getClassStudentsForEntry`，但 students 依赖 defaultClassId（来自 searchParams），可并行
- **改进建议**：`Promise.all` 三个查询

#### BUG-T25：串行数据获取 waterfall（grades/stats/page.tsx）
- **位置**：[grades/stats/page.tsx:26-54](../src/app/(dashboard)/teacher/grades/stats/page.tsx)
- **问题**：`Promise.all([getTeacherClasses, db.query])` → `Promise.all([stats, ranking])` 两段串行
- **改进建议**：合并为单个 `Promise.all`

#### BUG-T26：串行数据获取 waterfall（classes/my/[id]/page.tsx）
- **位置**：[classes/my/[id]/page.tsx:21-30](../src/app/(dashboard)/teacher/classes/my/[id]/page.tsx)
- **问题**：`Promise.all([insights, students, schedule])` 后串行 `getClassStudentSubjectScoresV2`
- **改进建议**：将 `getClassStudentSubjectScoresV2` 加入第一个 `Promise.all`

#### BUG-T27：串行数据获取 waterfall（diagnostic/student/[studentId]/page.tsx）
- **位置**：[diagnostic/student/[studentId]/page.tsx:30-45](../src/app/(dashboard)/teacher/diagnostic/student/[studentId]/page.tsx)
- **问题**：`Promise.all([summary, reports])` 后串行 `getKnowledgePointStats()`
- **改进建议**：合并为单个 `Promise.all`

#### BUG-T28：串行数据获取 waterfall（exams/[id]/build/page.tsx）
- **位置**：[exams/[id]/build/page.tsx:12-26](../src/app/(dashboard)/teacher/exams/[id]/build/page.tsx)
- **问题**：`getExamById` → `getQuestions` → `getQuestions(ids)` 三段串行
- **改进建议**：前两个可并行；第三个依赖 exam.questions 的 ID 列表，需串行但可优化

#### BUG-T29：Bundle 优化 - barrel imports（lucide-react）
- **位置**：几乎所有页面文件
- **问题**：`import { PlusCircle, BarChart3, ClipboardList } from "lucide-react"` 使用 barrel 文件导入，违反 `bundle-barrel-imports` 规则
- **改进建议**：lucide-react 已支持 tree-shaking，但可考虑使用 `lucide-react/icons` 直接导入路径

#### BUG-T30：缺少 `export const dynamic = "force-dynamic"` 声明
- **位置**：
  - [exams/all/page.tsx](../src/app/(dashboard)/teacher/exams/all/page.tsx)（使用 Suspense，可省略）
  - [exams/create/page.tsx](../src/app/(dashboard)/teacher/exams/create/page.tsx)
  - [exams/[id]/build/page.tsx](../src/app/(dashboard)/teacher/exams/[id]/build/page.tsx)
  - [questions/page.tsx](../src/app/(dashboard)/teacher/questions/page.tsx)（使用 Suspense）
  - [textbooks/page.tsx](../src/app/(dashboard)/teacher/textbooks/page.tsx)（使用 Suspense）
- **问题**：动态数据页面未声明 `force-dynamic`，可能导致静态生成尝试失败
- **改进建议**：所有含动态数据的页面统一添加 `export const dynamic = "force-dynamic"`

---

### 2.6 Web 界面规范违规（web-design-guidelines） — 严重度：中

#### BUG-T31：`<a>` 标签缺少 focus-visible 焦点样式
- **位置**：
  - [attendance/stats/page.tsx:106-117](../src/app/(dashboard)/teacher/attendance/stats/page.tsx)
  - [grades/analytics/page.tsx:192-253](../src/app/(dashboard)/teacher/grades/analytics/page.tsx)
  - [grades/stats/page.tsx:100-135](../src/app/(dashboard)/teacher/grades/stats/page.tsx)
- **问题**：筛选按钮使用 `<a>` 标签但仅有 `hover:bg-accent`，缺少 `focus-visible:ring-*` 或 `focus-visible:outline` 焦点样式
- **违反规则**：Focus States - Interactive elements need visible focus
- **改进建议**：添加 `focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2`

#### BUG-T32：`<a>` 标签作为筛选按钮语义不当
- **位置**：同 BUG-T31
- **问题**：筛选操作使用 `<a>` 标签导航到带 query 的 URL，虽然支持 Cmd/Ctrl+click，但视觉上是按钮形态，应使用 `<button>` 或添加 `role="button"`
- **违反规则**：`<button>` for actions, `<a>`/`<Link>` for navigation
- **改进建议**：使用 Next.js `<Link>` 并补充焦点样式，或改为 `<button>` + `useRouter` + `useSearchParams`

#### BUG-T33：标题层级缺失（exams/[id]/build/page.tsx）
- **位置**：[exams/[id]/build/page.tsx:104-118](../src/app/(dashboard)/teacher/exams/[id]/build/page.tsx)
- **问题**：页面无 `<h1>` 标题，直接渲染 `<ExamAssembly>` 组件，违反「Headings hierarchical `<h1>`–`<h6>`」
- **改进建议**：在页面顶部添加 `<h1>` 标题（如「Build Exam」）

#### BUG-T34：标题层级缺失（exams/[id]/proctoring/page.tsx）
- **位置**：[exams/[id]/proctoring/page.tsx:50-54](../src/app/(dashboard)/teacher/exams/[id]/proctoring/page.tsx)
- **问题**：同 BUG-T33，无 `<h1>`
- **改进建议**：同 BUG-T33

#### BUG-T35：标题层级缺失（classes/my/[id]/page.tsx）
- **位置**：[classes/my/[id]/page.tsx:65-108](../src/app/(dashboard)/teacher/classes/my/[id]/page.tsx)
- **问题**：页面无 `<h1>`，依赖 `<ClassHeader>` 组件渲染标题，需确认组件内是否有 h1
- **改进建议**：确认 `ClassHeader` 包含 `<h1>`

#### BUG-T36：长文本未截断（homework/assignments/page.tsx）
- **位置**：[homework/assignments/page.tsx:99-101](../src/app/(dashboard)/teacher/homework/assignments/page.tsx)
- **问题**：作业标题 `<Link>{a.title}</Link>` 未限制长度，长标题会破坏表格布局
- **违反规则**：Content Handling - Text containers handle long content
- **改进建议**：添加 `line-clamp-2` 或 `truncate max-w-[200px]`

#### BUG-T37：长文本未截断（homework/submissions/page.tsx）
- **位置**：[homework/submissions/page.tsx:58-60](../src/app/(dashboard)/teacher/homework/submissions/page.tsx)
- **问题**：同 BUG-T36
- **改进建议**：同 BUG-T36

#### BUG-T38：长文本未截断（homework/assignments/[id]/submissions/page.tsx）
- **位置**：[homework/assignments/[id]/submissions/page.tsx:65](../src/app/(dashboard)/teacher/homework/assignments/[id]/submissions/page.tsx)
- **问题**：学生姓名单元格未限制长度
- **改进建议**：添加 `truncate max-w-[160px]`

#### BUG-T39：Flex 子元素缺少 `min-w-0`
- **位置**：
  - [homework/assignments/[id]/page.tsx:26-42](../src/app/(dashboard)/teacher/homework/assignments/[id]/page.tsx)
  - [classes/my/[id]/page.tsx:86-104](../src/app/(dashboard)/teacher/classes/my/[id]/page.tsx)
- **问题**：flex 容器内的文本子元素未设置 `min-w-0`，长内容无法正确截断
- **违反规则**：Flex children need `min-w-0` to allow text truncation
- **改进建议**：在 flex 子元素添加 `min-w-0`

#### BUG-T40：使用 `transition: all` 或 `transition-colors` 未列明属性
- **位置**：
  - [attendance/stats/page.tsx:109](../src/app/(dashboard)/teacher/attendance/stats/page.tsx) - `transition-colors`（可接受）
  - [grades/analytics/page.tsx:195](../src/app/(dashboard)/teacher/grades/analytics/page.tsx) - `transition-colors`（可接受）
- **问题**：`transition-colors` 实际上列明了属性，符合规范；但需检查是否有 `transition: all` 使用
- **现状**：未发现 `transition: all`，此项通过

#### BUG-T41：硬编码日期/数字格式
- **位置**：所有使用 `formatDate` 的文件
- **问题**：需确认 `formatDate` 内部是否使用 `Intl.DateTimeFormat`，若使用硬编码格式则违规
- **违反规则**：Locale & i18n - Dates/times: use `Intl.DateTimeFormat`
- **改进建议**：检查 `shared/lib/utils.ts` 的 `formatDate` 实现

#### BUG-T42：数字列未使用 `tabular-nums`
- **位置**：
  - [exams/all/page.tsx:54-60](../src/app/(dashboard)/teacher/exams/all/page.tsx) - 考试计数
  - [homework/submissions/page.tsx:69-71](../src/app/(dashboard)/teacher/homework/submissions/page.tsx) - 计数列
  - [homework/assignments/[id]/submissions/page.tsx:73](../src/app/(dashboard)/teacher/homework/assignments/[id]/submissions/page.tsx) - 分数
- **问题**：数字列未使用 `font-variant-numeric: tabular-nums`，对齐不整齐
- **违反规则**：Typography - `font-variant-numeric: tabular-nums` for number columns
- **改进建议**：数字单元格添加 `tabular-nums` 类

#### BUG-T43：大列表未虚拟化
- **位置**：
  - [questions/page.tsx:42](../src/app/(dashboard)/teacher/questions/page.tsx) - `pageSize: 200`
  - [exams/all/page.tsx](../src/app/(dashboard)/teacher/exams/all/page.tsx) - ExamDataTable
- **问题**：题库页面一次加载 200 条题目，若渲染全部 DOM 节点会卡顿
- **违反规则**：Performance - Large lists (>50 items): virtualize
- **改进建议**：使用 `virtua` 或 `content-visibility: auto` 虚拟化长列表

---

### 2.7 组件规范违规 — 严重度：中

#### BUG-T44：不必要的包装组件（classes/my/page.tsx）
- **位置**：[classes/my/page.tsx:6-17](../src/app/(dashboard)/teacher/classes/my/page.tsx)
- **问题**：默认导出 `MyClassesPage` 仅调用 `MyClassesPageImpl`，多此一举
- **现状**：
  ```typescript
  export default function MyClassesPage() {
    return <MyClassesPageImpl />
  }

  async function MyClassesPageImpl() {
    // ...
  }
  ```
- **改进建议**：直接默认导出 async 函数：
  ```typescript
  export default async function MyClassesPage() {
    const [classes, subjectOptions] = await Promise.all([...])
    return <MyClassesGrid classes={classes} subjectOptions={subjectOptions} />
  }
  ```

#### BUG-T45：非导出组件定义在 page.tsx 中
- **位置**：
  - [attendance/stats/page.tsx:91-119](../src/app/(dashboard)/teacher/attendance/stats/page.tsx) - `StatsClassSelector`
  - [grades/analytics/page.tsx:150-258](../src/app/(dashboard)/teacher/grades/analytics/page.tsx) - `AnalyticsFilters`
  - [grades/stats/page.tsx:86-138](../src/app/(dashboard)/teacher/grades/stats/page.tsx) - `StatsClassSelector`
  - [classes/schedule/page.tsx:45-63](../src/app/(dashboard)/teacher/classes/schedule/page.tsx) - `ScheduleResultsFallback`
  - [classes/students/page.tsx:68-81](../src/app/(dashboard)/teacher/classes/students/page.tsx) - `StudentsResultsFallback`
  - [exams/all/page.tsx:101-128](../src/app/(dashboard)/teacher/exams/all/page.tsx) - `ExamsResultsFallback`
  - [questions/page.tsx:75-88](../src/app/(dashboard)/teacher/questions/page.tsx) - `QuestionBankResultsFallback`
- **问题**：辅助组件定义在 page.tsx 中，违反「其余所有组件使用具名导出」规范，且无法复用
- **改进建议**：提取到 `components/` 目录或 `shared/components/ui/`

#### BUG-T46：exams/create/page.tsx 顶部多余空行
- **位置**：[exams/create/page.tsx:5](../src/app/(dashboard)/teacher/exams/create/page.tsx)
- **问题**：JSX 开始标签前有多余空行
- **现状**：
  ```typescript
  return (

    <div className="...">
  ```
- **改进建议**：删除空行

---

### 2.8 安全与权限违规 — 严重度：高

#### BUG-T47：缺少权限校验（course-plans/page.tsx）
- **位置**：[course-plans/page.tsx](../src/app/(dashboard)/teacher/course-plans/page.tsx)
- **问题**：仅通过 `auth()` 获取 session，未调用 `requirePermission()` 或 `getAuthContext()` 进行权限校验
- **改进建议**：使用 `getAuthContext()` 替代 `auth()`，并在 data-access 层做 DataScope 过滤

#### BUG-T48：缺少权限校验（elective/page.tsx）
- **位置**：[elective/page.tsx](../src/app/(dashboard)/teacher/elective/page.tsx)
- **问题**：同 BUG-T47
- **改进建议**：同 BUG-T47

#### BUG-T49：缺少权限校验（dashboard/page.tsx）
- **位置**：[dashboard/page.tsx](../src/app/(dashboard)/teacher/dashboard/page.tsx)
- **问题**：依赖路由层代理（proxy.ts）做角色路由，但页面本身未做二次权限校验
- **改进建议**：添加 `getAuthContext()` 确认教师身份

#### BUG-T50：权限校验方式不一致
- **位置**：
  - [exams/[id]/proctoring/page.tsx:21](../src/app/(dashboard)/teacher/exams/[id]/proctoring/page.tsx) - 使用 `requirePermission(Permissions.EXAM_PROCTOR)`
  - [diagnostic/class/[classId]/page.tsx:15-23](../src/app/(dashboard)/teacher/diagnostic/class/[classId]/page.tsx) - 使用 `getAuthContext()` + DataScope 校验
  - [grades/page.tsx:26](../src/app/(dashboard)/teacher/grades/page.tsx) - 使用 `getAuthContext()`
- **问题**：权限校验方式不统一，部分用 `requirePermission`，部分用 `getAuthContext`，部分无校验
- **改进建议**：统一权限校验策略，页面入口用 `getAuthContext()`，写操作用 `requirePermission()`

---

### 2.9 加载态缺失 — 严重度：低

#### BUG-T51：缺少 loading.tsx 的目录
- **位置**：
  - `attendance/`（含 sheet/、stats/）
  - `course-plans/`（含 [id]/）
  - `diagnostic/`（含 class/、student/）
  - `elective/`
  - `exams/[id]/`（含 build/、proctoring/）
  - `grades/`（含 analytics/、entry/、stats/）
  - `homework/`（含 assignments/、submissions/）
  - `schedule-changes/`
- **问题**：以上目录无 `loading.tsx`，导航时无骨架屏反馈
- **改进建议**：为每个动态页面目录添加 `loading.tsx`，参考 `classes/my/loading.tsx` 模式

#### BUG-T52：exams/grading/loading.tsx 实际无用
- **位置**：[exams/grading/loading.tsx](../src/app/(dashboard)/teacher/exams/grading/loading.tsx)
- **问题**：`exams/grading/page.tsx` 仅做 `redirect()`，loading.tsx 永远不会显示
- **改进建议**：删除该 loading.tsx

---

### 2.10 逻辑与代码质量问题 — 严重度：中

#### BUG-T53：homework/assignments/page.tsx 条件取数逻辑反直觉
- **位置**：[homework/assignments/page.tsx:33-36](../src/app/(dashboard)/teacher/homework/assignments/page.tsx)
- **问题**：`classId && classId !== "all" ? getTeacherClasses() : Promise.resolve([])` 仅在有 classId 时才获取班级列表，逻辑反直觉（通常应始终获取班级列表用于筛选下拉）
- **现状**：classes 仅用于查找 className 显示，逻辑正确但可读性差
- **改进建议**：始终获取 classes，或添加注释说明「仅在过滤时需要 className」

#### BUG-T54：exams/[id]/build/page.tsx `normalizeStructure` 函数过长
- **位置**：[exams/[id]/build/page.tsx:52-91](../src/app/(dashboard)/teacher/exams/[id]/build/page.tsx)
- **问题**：40 行的 `normalizeStructure` 函数定义在组件内部，包含嵌套递归逻辑，可读性差
- **改进建议**：提取到 `modules/exams/utils/normalize-structure.ts`，并添加单元测试

#### BUG-T55：exams/[id]/build/page.tsx 使用 `satisfies` 但混合 `as`
- **位置**：[exams/[id]/build/page.tsx:74, 84, 86](../src/app/(dashboard)/teacher/exams/[id]/build/page.tsx)
- **问题**：同时使用 `satisfies ExamNode`（好）和 `as ExamNode[]`（违规），类型处理不一致
- **改进建议**：移除 `as ExamNode[]`，改用类型守卫或 `Array.from()` 配合 filter

#### BUG-T56：grades/analytics/page.tsx 文件过长
- **位置**：[grades/analytics/page.tsx](../src/app/(dashboard)/teacher/grades/analytics/page.tsx) - 259 行
- **问题**：单文件 259 行，接近 React 组件 500 行建议上限的 50%，包含页面 + `AnalyticsFilters` 组件
- **改进建议**：将 `AnalyticsFilters` 提取到 `modules/grades/components/analytics-filters.tsx`

#### BUG-T57：exams/all/page.tsx 缺少 `export const dynamic`
- **位置**：[exams/all/page.tsx](../src/app/(dashboard)/teacher/exams/all/page.tsx)
- **问题**：使用 Suspense 但未声明 `force-dynamic`，可能导致构建时尝试静态生成
- **改进建议**：添加 `export const dynamic = "force-dynamic"`

---

### 2.11 可访问性问题 — 严重度：中

#### BUG-T58：图标按钮缺少 aria-label
- **位置**：
  - [textbooks/[id]/page.tsx:33-36](../src/app/(dashboard)/teacher/textbooks/[id]/page.tsx) - 返回按钮
  - [homework/assignments/[id]/page.tsx:28-31](../src/app/(dashboard)/teacher/homework/assignments/[id]/page.tsx) - 面包屑链接（有文本，OK）
- **问题**：`textbooks/[id]/page.tsx` 的返回按钮仅含图标，无 `aria-label`
- **违反规则**：Accessibility - Icon-only buttons need `aria-label`
- **改进建议**：添加 `aria-label="Back to textbooks"`

#### BUG-T59：装饰性图标未标记 aria-hidden
- **位置**：几乎所有页面中的 lucide 图标
- **问题**：如 `<BarChart3 className="mr-2 h-4 w-4" />` 等装饰性图标未添加 `aria-hidden="true"`
- **违反规则**：Accessibility - Decorative icons need `aria-hidden="true"`
- **改进建议**：装饰性图标添加 `aria-hidden="true"`

#### BUG-T60：缺少 skip link
- **位置**：所有页面
- **问题**：页面无「跳到主内容」的 skip link，键盘用户需 Tab 遍历整个侧边栏
- **违反规则**：Accessibility - include skip link for main content
- **改进建议**：在 dashboard layout 添加 skip link（应在 layout 层处理）

---

### 2.12 其他问题

#### BUG-T61：homework/assignments/[id]/page.tsx 使用 h1 但其他页面用 h2
- **位置**：
  - [homework/assignments/[id]/page.tsx:36](../src/app/(dashboard)/teacher/homework/assignments/[id]/page.tsx) - `<h1>`
  - [attendance/page.tsx:47](../src/app/(dashboard)/teacher/attendance/page.tsx) - `<h2>`
  - [grades/page.tsx:54](../src/app/(dashboard)/teacher/grades/page.tsx) - `<h2>`
- **问题**：页面主标题层级不统一，部分用 h1，部分用 h2
- **改进建议**：统一使用 h1 作为页面主标题（layout 可能已有 h1，需确认）

#### BUG-T62：textbooks/page.tsx 使用 h1，其他页面用 h2
- **位置**：
  - [textbooks/page.tsx:57](../src/app/(dashboard)/teacher/textbooks/page.tsx) - `<h1>`
  - [textbooks/[id]/page.tsx:45](../src/app/(dashboard)/teacher/textbooks/[id]/page.tsx) - `<h1>`
- **问题**：同 BUG-T61，标题层级不统一
- **改进建议**：统一标题层级策略

#### BUG-T63：exams/create/page.tsx 缺少页面标题
- **位置**：[exams/create/page.tsx:3-9](../src/app/(dashboard)/teacher/exams/create/page.tsx)
- **问题**：页面无任何标题，直接渲染表单
- **改进建议**：添加 `<h1>Create Exam</h1>`

#### BUG-T64：loading.tsx 文件命名风格不一致
- **位置**：
  - [textbooks/loading.tsx](../src/app/(dashboard)/teacher/textbooks/loading.tsx) - 使用 Card 组件
  - [classes/my/loading.tsx](../src/app/(dashboard)/teacher/classes/my/loading.tsx) - 使用纯 div
- **问题**：骨架屏风格不统一，部分用 Card 组件，部分用纯 div
- **改进建议**：统一骨架屏风格，提取共享骨架屏组件

---

## 三、改进优先级汇总

### P0 - 立即修复（架构与安全）

| BUG ID | 问题 | 影响 |
|--------|------|------|
| T01-T05 | app 层直接访问 DB | 破坏三层架构，模块封装失效 |
| T06 | 认证方式不一致 | 数据范围过滤缺失 |
| T47-T50 | 权限校验缺失/不一致 | 越权访问风险 |

### P1 - 高优先级（TypeScript 与性能）

| BUG ID | 问题 | 影响 |
|--------|------|------|
| T11-T15 | 使用 `as` 类型断言 | 类型安全受损 |
| T16-T17 | 函数返回值未标注 | 类型推导不显式 |
| T20-T28 | 串行数据获取 waterfall | 页面加载性能差 |
| T43 | 大列表未虚拟化 | 题库页面卡顿 |

### P2 - 中优先级（规范与可访问性）

| BUG ID | 问题 | 影响 |
|--------|------|------|
| T07-T10 | Prettier 分号违规 | 代码风格不一致 |
| T18-T19 | DRY 违规 | 维护成本高 |
| T31-T32 | 筛选按钮焦点样式/语义 | 键盘可访问性差 |
| T36-T39 | 长文本未截断 | 布局破坏风险 |
| T42 | 数字列未用 tabular-nums | 数字对齐不整齐 |
| T58-T60 | 可访问性缺失 | 屏幕阅读器体验差 |

### P3 - 低优先级（代码质量）

| BUG ID | 问题 | 影响 |
|--------|------|------|
| T44-T46 | 组件定义问题 | 可读性差 |
| T51-T52 | loading.tsx 缺失/冗余 | 用户体验不一致 |
| T53-T57 | 逻辑与长度问题 | 可维护性 |
| T61-T64 | 标题层级与风格 | 一致性 |

---

## 四、推荐改进方案

### 4.1 提取共享工具（解决 T16, T18）

新建 `src/shared/lib/search-params.ts`：

```typescript
export type SearchParams = { [key: string]: string | string[] | undefined }

export function getParam(params: SearchParams, key: string): string | undefined {
  const v = params[key]
  return Array.isArray(v) ? v[0] : v
}
```

所有页面统一 `import { getParam, type SearchParams } from "@/shared/lib/search-params"`。

### 4.2 提取共享筛选组件（解决 T19, T31, T32）

新建 `src/shared/components/ui/filter-chips.tsx`：

```tsx
import Link from "next/link"
import { cn } from "@/shared/lib/utils"

interface FilterChip {
  id: string
  label: string
  href: string
  active: boolean
}

export function FilterChips({ chips }: { chips: FilterChip[] }) {
  return (
    <div className="flex flex-wrap gap-2">
      {chips.map((c) => (
        <Link
          key={c.id}
          href={c.href}
          className={cn(
            "rounded-md border px-3 py-1.5 text-sm transition-colors",
            "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
            c.active
              ? "border-primary bg-primary text-primary-foreground"
              : "bg-card hover:bg-accent"
          )}
        >
          {c.label}
        </Link>
      ))}
    </div>
  )
}
```

### 4.3 统一权限校验模式（解决 T47-T50）

所有教师页面入口统一：

```typescript
import { getAuthContext } from "@/shared/lib/auth-guard"

export default async function XxxPage() {
  const ctx = await getAuthContext()
  // 使用 ctx.userId、ctx.dataScope 进行数据过滤
}
```

### 4.4 并行数据获取优化（解决 T20-T28）

将串行 `await` 改为 `Promise.all`：

```typescript
// 优化前
const classes = await getTeacherClasses()
const records = await getGradeRecords({ ... })

// 优化后
const [classes, records] = await Promise.all([
  getTeacherClasses(),
  getGradeRecords({ ... }),
])
```

### 4.5 DB 访问下沉到 data-access（解决 T01-T05）

在 `modules/school/data-access.ts` 添加：

```typescript
import "server-only"
import { db } from "@/shared/db"
import { subjects } from "@/shared/db/schema"
import { asc } from "drizzle-orm"

export async function getSubjectsOrdered(): Promise<Subject[]> {
  return db.query.subjects.findMany({
    orderBy: [asc(subjects.order), asc(subjects.name)],
  })
}
```

页面改为 `import { getSubjectsOrdered } from "@/modules/school/data-access"`。

---

## 五、架构图同步建议

本次核查未修改源码，无需同步架构图。但建议在后续修复时：

1. 若新增 `shared/lib/search-params.ts`，需在 005_architecture_data.json 的 `shared.lib.exports` 中添加
2. 若新增 `shared/components/ui/filter-chips.tsx`，需在 005 的 `shared.components.exports` 中添加
3. 若 `modules/school/data-access.ts` 新增 `getSubjectsOrdered`，需在 005 的 `modules.school.dataAccess` 中添加

---

## 六、总结

本次核查覆盖 `src/app/(dashboard)/teacher/` 下全部 45 个前端文件，共发现 **64 个问题**，分布如下：

| 严重度 | 数量 | 类别 |
|--------|------|------|
| P0 | 9 | 架构违规、权限缺失 |
| P1 | 16 | TypeScript、性能 |
| P2 | 18 | 规范、可访问性 |
| P3 | 21 | 代码质量 |

**核心问题**：
1. **架构层违规严重**：5 处 app 层直接访问 DB，破坏三层架构
2. **权限校验不一致**：部分页面无校验，部分用 `auth()`，部分用 `getAuthContext()`
3. **性能 waterfall 普遍**：9 处串行数据获取，应改为并行
4. **DRY 违规突出**：`getParam` 函数在 16 个文件中重复
5. **可访问性缺失**：焦点样式、aria-label、skip link 普遍缺失

建议按 P0 → P1 → P2 → P3 顺序修复，优先解决架构与安全问题。
