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:
SpecialX
2026-06-19 05:13:09 +08:00
parent 063baffe4c
commit 49291fcc31
114 changed files with 12548 additions and 3395 deletions

View File

@@ -0,0 +1,258 @@
# 首次登录引导Onboarding重大问题讨论
> 创建日期2026-06-18
> 状态:**讨论中,待决策**
> 关联架构图:`docs/architecture/004_architecture_impact_map.md` §2.1 shared 层 / §3 已知问题 P2-4
> 关联代码:
> - [src/shared/components/onboarding-gate.tsx](file:///e:/Desktop/CICD/src/shared/components/onboarding-gate.tsx)312 行)
> - [src/app/api/onboarding/status/route.ts](file:///e:/Desktop/CICD/src/app/api/onboarding/status/route.ts)
> - [src/app/api/onboarding/complete/route.ts](file:///e:/Desktop/CICD/src/app/api/onboarding/complete/route.ts)
> - [src/app/layout.tsx](file:///e:/Desktop/CICD/src/app/layout.tsx#L41)(全局挂载点)
---
## 一、背景与定位
按项目规则"先图后码",先从架构影响地图定位 Onboarding 相关节点:
- **shared 层**`components/onboarding-gate.tsx`312 行)已被架构图标记为 ⚠️ P2-4「业务逻辑泄漏到 shared」
- **app 层**`/api/onboarding/status``/api/onboarding/complete` 两条路由
- **数据层**`users.onboardedAt`[src/shared/db/schema.ts:41](file:///e:/Desktop/CICD/src/shared/db/schema.ts#L41)
- **被调用模块**`modules/classes/data-access.ts``enrollStudentByInvitationCode`
当前 Onboarding 是一个**全局 Dialog**:在 `app/layout.tsx` 第 41 行无条件挂载 `<OnboardingGate />`,组件内通过 `useEffect` 拉取 `/api/onboarding/status`,若 `required === true` 则弹出不可关闭的 4 步 Dialog。
---
## 二、现状代码盘点
### 2.1 组件层onboarding-gate.tsx
| 步骤 | 标题 | 采集字段 | 备注 |
|------|------|----------|------|
| Step 0 | 角色选择 | rolestudent/teacher/parent | admin 只读展示;其他角色用户可下拉**自选** |
| Step 1 | 通用信息 | name / phone / address | 仅校验非空 |
| Step 2 | 角色信息 | classCodes学生/教师、teacherSubjects教师 | 可跳过;家长显示"暂不需要配置" |
| Step 3 | 完成 | — | 调 `/api/onboarding/complete` 后跳 `/dashboard` |
**角色推断逻辑**(第 90-94 行)——用权限点反推角色:
```ts
const isAdmin = permissions.includes(Permissions.SETTINGS_ADMIN)
const isTeacher = permissions.includes(Permissions.EXAM_CREATE)
const isStudent = permissions.includes(Permissions.HOMEWORK_SUBMIT) && !permissions.includes(Permissions.EXAM_CREATE)
const isParent = !permissions.includes(Permissions.EXAM_CREATE) && !permissions.includes(Permissions.HOMEWORK_SUBMIT) && permissions.includes(Permissions.EXAM_READ)
```
### 2.2 API 层
- `GET /api/onboarding/status`:查 `users.onboardedAt` 是否为空 + 查 `usersToRoles` 推断角色
- `POST /api/onboarding/complete`:更新 users 表 → 写 usersToRoles → 学生调 `enrollStudentByInvitationCode` → 教师直接 insert `classSubjectTeachers` → 写 `onboardedAt`
---
## 三、重大问题清单(按风险分级)
### 🔴 P0 级:安全/合规/越权
#### P0-1 用户可自选角色(严重越权)
- **位置**[onboarding-gate.tsx:192-201](file:///e:/Desktop/CICD/src/shared/components/onboarding-gate.tsx#L192-L201)
- **问题**Step 0 允许任意登录用户从下拉框选择 `student / teacher / parent` 角色;`complete/route.ts:32-35` 直接信任前端 `body.role` 并写入 `usersToRoles`
- **后果**:任何注册用户可自封为 teacher从而获得 `exam:create``homework:grade` 等权限;可自封为 parent 查看他人成绩。**这是 K12 教务系统的合规红线**。
- **违反规则**项目规则「Server Action 必须使用 `requirePermission()`」、K12 行业铁律「角色由管理员预分配」。
#### P0-2 教师可绑定任意班级+科目
- **位置**[complete/route.ts:95-130](file:///e:/Desktop/CICD/src/app/api/onboarding/complete/route.ts#L95-L130)
- **问题**:教师通过 `classCodes`6 位邀请码)可把自己写入任意班级的 `classSubjectTeachers`,且 `teacherSubjects` 由前端任意提交,服务端仅做"名称存在性"校验,不校验该教师是否被管理员分配到该班。
- **后果**:教师可越权查看任意班级学生名单、成绩;可篡改他人班级的任课关系。
- **违反规则**项目规则「modules 之间通过对方 data-access 通信,不直接查询对方 DB 表」——此处 app 层 API 直接 insert `classSubjectTeachers`
#### P0-3 无权限校验、无 Zod、无事务
- **位置**[complete/route.ts](file:///e:/Desktop/CICD/src/app/api/onboarding/complete/route.ts) 整文件
- **问题**
- 仅检查 `auth()` 登录态,**未调用 `requirePermission()`**
-`String(body.role ?? "")` 手动解析,**无 Zod**(架构图 005 声称"validation: Zod schema"与实际不符)
- 5 次独立 DB 写入update users / insert usersToRoles / enrollStudent / insert classSubjectTeachers / update onboardedAt**无 `db.transaction()`**
- 运行时 `db.insert(roles).values({ name: role })` 创建角色记录(第 66-68 行)——角色应在 seed 时创建,运行时创建属异常路径
- **后果**:中途失败导致数据不一致(如已绑定角色但 `onboardedAt` 仍为 null用户被反复弹窗越权写入。
### 🟠 P1 级:架构违规
#### P1-1 shared 层反向承载领域逻辑
- **位置**[onboarding-gate.tsx](file:///e:/Desktop/CICD/src/shared/components/onboarding-gate.tsx) 整文件
- **问题**:组件位于 `shared/components/`,但包含角色判断、班级代码、教师科目配置等强领域逻辑,并通过 fetch 调用业务 API。
- **违反规则**项目规则「shared 不得反向依赖 @/auth@/proxy 或任何 modules/*」「shared 是被依赖方」。
- **架构图标记**004 文档 §2.1 已标记 P2-4。
#### P1-2 app 层 API 直接跨模块写表
- **位置**[complete/route.ts:6](file:///e:/Desktop/CICD/src/app/api/onboarding/complete/route.ts#L6)
- **问题**`app/api/onboarding/complete/route.ts` 直接 import 并写入 `classes``classSubjectTeachers``subjects` 表,绕过 `modules/classes` 的 data-access 与权限校验。
- **违反规则**项目规则「app 只能调用 modules 的 Server Actions 和 data-access不直接访问 DB」「modules 之间通过对方 data-access 通信」。
#### P1-3 角色推断双源不一致
- **位置**[status/route.ts:29-41](file:///e:/Desktop/CICD/src/app/api/onboarding/status/route.ts#L29-L41) vs [onboarding-gate.tsx:90-94](file:///e:/Desktop/CICD/src/shared/components/onboarding-gate.tsx#L90-L94)
- **问题**status API 用 `roles.name` 推断角色(含 `grade_head/teaching_head → teacher` 归一化),组件又用权限点重新推断,两套逻辑可能不一致(如年级组长既有 EXAM_CREATE 又有其他权限,组件推断可能错位)。
### 🟡 P2 级:用户体验与可访问性
#### P2-1 全局 Dialog 模式缺陷
- **问题**
- Dialog 不可关闭(`canClose = !required`),用户被强制锁定
- 刷新页面丢失步骤状态step 重置为 0
- 无独立 URL无法分享/书签
- 首屏无骨架屏,`useEffect` 拉取 status 期间会闪烁
- 依赖 `session?.user?.name` 触发重复请求
- **对比**业界主流Auth.js 官方、Clerk、Vercel 模板)均采用独立路由 `/onboarding` + middleware 重定向。
#### P2-2 表单校验粗糙
- **问题**:电话仅校验非空(无手机号格式校验);姓名无长度限制;地址无长度限制;班级代码无格式预校验。
#### P2-3 国际化与可访问性
- **问题**:中英文混合("Role"、"Select role" 英文其余中文Dialog 缺少 `aria-describedby`;进度条无 `aria-valuenow`;表单无 `required` 标记。
#### P2-4 进度条与步骤不一致
- **问题**admin 跳过 Step 2但进度条仍渲染 4 段,视觉上 Step 2 永远亮起,造成困惑。
---
## 四、业界大仓Monorepo解决方案引用
### 4.1 Auth.js v5 官方推荐
- **状态标记**`users.onboardedAt` 字段 + `jwt`/`session` 回调注入 session完成时调 `update()` 刷新 token。
- **强制方式****middleware 重定向**到独立 `/onboarding` 路由,而非客户端 Dialog。
-`middleware.ts``auth()` 读取 session`user.onboardedAt` 为空且路径不在白名单(`/login``/api/auth``/onboarding`、静态资源),则 `NextResponse.redirect(new URL('/onboarding', req.url))`
- **结论**:客户端 Dialog 仅适合"非阻塞的偏好补全"(如头像、通知偏好);强制 onboarding 应等同未登录处理。
### 4.2 商业方案Clerk / Supabase / Auth0共性
三段式:**metadata 标记 + 强制重定向独立路由 + 服务端 Action 校验**。
- **角色等敏感字段放服务端可写的 metadata**Clerk `privateMetadata` / Auth0 `appMetadata` / Supabase RLS-protected `profiles.role`**禁止前端自写**。
- onboarding 完成回调必须由服务端 Action 写入 metadata前端不能直接改。
- 未完成 onboarding 时 middleware/Action 层强制重定向。
### 4.3 shadcn/ui 生态
- 官方无内置 Stepper`examples/forms``blocks` 范式明确:**独立路由页面 + `<Form>`react-hook-form + zod+ 父组件持 step state**。
- 每步独立 zod schema 做渐进式校验,最后一步汇总写入。
- 官方 `blocks/login-04` 等登录块均采用独立路由页面,而非全局 Dialog。
### 4.4 企业级 K12 教务系统PowerSchool / Veracross / 国内智慧校园)
**铁律:角色由管理员预分配,用户不可自选。**
| 角色 | 首次登录采集字段 | 角色来源 |
|------|------------------|----------|
| 学生 | 学号(预分配不可改)、姓名、性别、出生日期、家长联系方式、紧急联系人 | 管理员批量导入 |
| 教师 | 工号(预分配)、姓名、所教科目、任教班级、办公室、联系电话、学历资质 | 教务处预分配 |
| 家长 | 与学生关系、学生学号(通过学校发放的 **Access ID + Access Password** 绑定)、本人姓名、电话、邮箱 | 学校发放凭证,家长绑定子女 |
| 管理员 | 工号、姓名、职务、管理范围 | 学校 IT 创建 |
**原因**
1. **合规**K12 数据受《个人信息保护法》《未成年人保护法》约束,学生身份必须由学校权威确认。
2. **安全**:允许自选教师角色 = 任何人可创建考试、查看全班成绩。
3. **数据一致性**:班级、学号、任课关系是教务核心数据,必须由教务处维护。
### 4.5 Monorepoturborepo / nx惯例
- **turborepo 官方模板**:跨模块"流程型"功能onboarding、setup-wizard作为**独立 module**,而非塞进 shared。
- **nx feature-shell 模式**onboarding 作为 `feature-onboarding` library依赖 `data-access-user``data-access-class`
- **Vercel 自家项目**`app/(app)/onboarding/[[...step]]/page.tsx` 路由组 + `modules/onboarding/` 模块。
---
## 五、重构方案建议(待讨论)
### 5.1 目标架构
```
app/
├─ (auth)/login/ # 登录页middleware 白名单)
├─ (onboarding)/onboarding/ # 新增独立路由
│ └─ page.tsx # 服务端组件,读取 session.onboarded 决定渲染
└─ middleware.ts # 新增/增强:未 onboarded 时重定向
modules/onboarding/ # 新建模块
├─ actions.ts # completeOnboardingActionServer Action + requirePermission
├─ data-access.ts # 仅操作 users.onboardedAt
├─ schema.ts # Zodname/phone/address/classCodes
├─ types.ts
└─ components/
├─ OnboardingStepper.tsx # 客户端 stepper 容器
├─ RoleConfirmStep.tsx # 只读展示管理员分配的角色
├─ ProfileStep.tsx # 姓名/电话/住址
└─ BindingStep.tsx # 学生:确认班级;教师:确认任课;家长:绑定子女
shared/
└─ components/onboarding-gate.tsx # 删除
```
### 5.2 关键改动点
1. **删除 `shared/components/onboarding-gate.tsx`**,从 `app/layout.tsx` 移除挂载。
2. **新建 `modules/onboarding/`**,承载所有领域逻辑。
3. **新建 `app/(onboarding)/onboarding/page.tsx`** 独立路由。
4. **增强 `middleware.ts`**:读取 session.onboarded未完成且非白名单路径 → 重定向到 `/onboarding`
5. **Auth.js 回调**:在 `jwt`/`session` 回调注入 `onboardedAt`,供 middleware 读取。
6. **删除 `app/api/onboarding/*/route.ts`**,改为 `modules/onboarding/actions.ts` 的 Server Action。
7. **角色只读化**Step 0 改为"角色确认"——只读展示 `usersToRoles` 中的角色,用户不可改。
8. **班级绑定改造**
- 学生:仅"确认"管理员预分配的班级,或输入邀请码(服务端校验有效性 + 用途)
- 教师:仅"确认"管理员预分配的任课关系,**移除自填班级代码**
- 家长:输入"子女学号 + 绑定码"绑定子女(参考 PowerSchool Access ID 模式)
9. **事务化**`completeOnboardingAction``db.transaction()` 包裹所有写入。
10. **Zod 校验**:定义 `onboardingSchema`phone 用 `z.string().regex(/^1\d{10}$/)`
### 5.3 迁移兼容
- 已 onboarded 用户(`onboardedAt` 非空不受影响middleware 直接放行。
- 未 onboarded 用户下次登录会被重定向到 `/onboarding`(而非弹 Dialog
- 无需数据迁移,`users.onboardedAt` 字段保留。
---
## 六、待决策的开放问题
请就以下问题给出决策,以便进入实施阶段:
### Q1角色分配策略
- **方案 A**(推荐,符合 K12 铁律onboarding 中角色完全只读,由管理员通过后台预分配;用户无法在 onboarding 中改变角色。
- **方案 B**:保留角色选择,但服务端校验"用户已有该角色"才允许选择(即只能从已有角色中选一个主角色)。
- **方案 C**:暂不改动角色选择,仅修复其他问题。
### Q2教师任课关系绑定
- **方案 A**推荐onboarding 中教师**仅确认**管理员预分配的任课关系,不自填班级代码。
- **方案 B**保留自填邀请码但服务端强校验邀请码用途teacher-assign、有效期、使用次数。
- **方案 C**:完全移除 onboarding 中的班级绑定,统一由管理员后台处理。
### Q3家长绑定子女方式
- **方案 A**推荐PowerSchool 模式):家长输入"子女学号 + 学校发放的 6 位绑定码"。
- **方案 B**:家长输入"子女学号 + 子女生日"作为验证。
- **方案 C**:暂不实现家长绑定,由管理员后台预绑定。
### Q4onboarding 路由形态
- **方案 A**(推荐):单页 `/onboarding` + 客户端 stepper步骤状态用 query param 持久化)。
- **方案 B**:嵌套路由 `/onboarding/role``/onboarding/profile``/onboarding/binding`(每步独立 Server Action
- **方案 C**:保留全局 Dialog仅修复安全与架构问题。
### Q5实施范围
- **方案 A**:一次性完成 P0 + P1 + P2 全部整改。
- **方案 B**:先做 P0安全/越权)+ P1架构P2UX后续迭代。
- **方案 C**:仅做 P0 紧急修复P1/P2 列入 backlog。
---
## 七、附录:问题与代码位置速查
| 问题 | 代码位置 | 风险 |
|------|----------|------|
| 用户自选角色 | [onboarding-gate.tsx:192-201](file:///e:/Desktop/CICD/src/shared/components/onboarding-gate.tsx#L192-L201) | 🔴 P0 |
| 信任前端 role 写入 | [complete/route.ts:32-35](file:///e:/Desktop/CICD/src/app/api/onboarding/complete/route.ts#L32-L35) | 🔴 P0 |
| 教师绑任意班级 | [complete/route.ts:95-130](file:///e:/Desktop/CICD/src/app/api/onboarding/complete/route.ts#L95-L130) | 🔴 P0 |
| 无权限校验/Zod/事务 | [complete/route.ts](file:///e:/Desktop/CICD/src/app/api/onboarding/complete/route.ts) 整文件 | 🔴 P0 |
| shared 反向承载领域逻辑 | [onboarding-gate.tsx](file:///e:/Desktop/CICD/src/shared/components/onboarding-gate.tsx) 整文件 | 🟠 P1 |
| app 层跨模块写表 | [complete/route.ts:6](file:///e:/Desktop/CICD/src/app/api/onboarding/complete/route.ts#L6) | 🟠 P1 |
| 角色推断双源不一致 | [status/route.ts:29-41](file:///e:/Desktop/CICD/src/app/api/onboarding/status/route.ts#L29-L41) vs [onboarding-gate.tsx:90-94](file:///e:/Desktop/CICD/src/shared/components/onboarding-gate.tsx#L90-L94) | 🟠 P1 |
| 全局 Dialog 缺陷 | [app/layout.tsx:41](file:///e:/Desktop/CICD/src/app/layout.tsx#L41) | 🟡 P2 |
| 表单校验粗糙 | [onboarding-gate.tsx:88](file:///e:/Desktop/CICD/src/shared/components/onboarding-gate.tsx#L88) | 🟡 P2 |

548
bugs/admin_bug.md Normal file
View 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。

1434
bugs/back_bug.md Normal file

File diff suppressed because it is too large Load Diff

960
bugs/others_bug.md Normal file
View File

@@ -0,0 +1,960 @@
# `src/app/(dashboard)/{announcements,dashboard,management,messages,profile,settings}` 规范核查报告
> 核查日期2026-06-18
> 核查范围:`src/app/(dashboard)/` 下的 announcements、dashboard、management、messages、profile、settings 子路由及其直接依赖的模块组件
> 依据文档:
> - [项目规则](../.trae/rules/project_rules.md)
> - [编码规范](../docs/standards/coding-standards.md)
> - [架构影响地图 004](../docs/architecture/004_architecture_impact_map.md)
> - [架构数据 005](../docs/architecture/005_architecture_data.json)
> 应用技能:`vercel-react-best-practices`、`web-design-guidelines``web-artifacts-builder` 加载失败,界面优化建议已合并至 web-design-guidelines 章节)
---
## 一、核查文件清单
| 文件 | 行数 | 类型 | 用途 |
|------|------|------|------|
| [announcements/page.tsx](../src/app/(dashboard)/announcements/page.tsx) | 20 | RSC 页面 | 公告列表(普通用户) |
| [dashboard/page.tsx](../src/app/(dashboard)/dashboard/page.tsx) | 18 | RSC 页面 | 角色路由分发 |
| [management/grade/classes/page.tsx](../src/app/(dashboard)/management/grade/classes/page.tsx) | 31 | RSC 页面 | 年级班级管理 |
| [management/grade/insights/page.tsx](../src/app/(dashboard)/management/grade/insights/page.tsx) | 243 | RSC 页面 | 年级作业洞察 |
| [messages/page.tsx](../src/app/(dashboard)/messages/page.tsx) | 31 | RSC 页面 | 消息+通知列表 |
| [messages/[id]/page.tsx](../src/app/(dashboard)/messages/[id]/page.tsx) | 30 | RSC 页面 | 消息详情 |
| [messages/compose/page.tsx](../src/app/(dashboard)/messages/compose/page.tsx) | 34 | RSC 页面 | 撰写消息 |
| [profile/page.tsx](../src/app/(dashboard)/profile/page.tsx) | 305 | RSC 页面 | 个人资料(学生/教师视图) |
| [settings/page.tsx](../src/app/(dashboard)/settings/page.tsx) | 32 | RSC 页面 | 设置入口(按角色分发) |
| [settings/security/page.tsx](../src/app/(dashboard)/settings/security/page.tsx) | 50 | RSC 页面 | 安全设置 |
| [layout.tsx](../src/app/(dashboard)/layout.tsx) | 21 | RSC 布局 | Dashboard 通用布局 |
| [error.tsx](../src/app/(dashboard)/error.tsx) | 22 | 客户端组件 | 错误边界 |
| [not-found.tsx](../src/app/(dashboard)/not-found.tsx) | 23 | RSC 组件 | 404 页面 |
| [modules/announcements/components/announcement-list.tsx](../src/modules/announcements/components/announcement-list.tsx) | 108 | 客户端组件 | 公告列表(含筛选) |
| [modules/announcements/components/announcement-card.tsx](../src/modules/announcements/components/announcement-card.tsx) | 79 | 客户端组件 | 公告卡片 |
| [modules/announcements/components/announcement-detail.tsx](../src/modules/announcements/components/announcement-detail.tsx) | 206 | 客户端组件 | 公告详情 |
| [modules/messaging/components/message-list.tsx](../src/modules/messaging/components/message-list.tsx) | 117 | 客户端组件 | 消息列表 |
| [modules/messaging/components/message-detail.tsx](../src/modules/messaging/components/message-detail.tsx) | 153 | 客户端组件 | 消息详情 |
| [modules/messaging/components/message-compose.tsx](../src/modules/messaging/components/message-compose.tsx) | 146 | 客户端组件 | 撰写消息表单 |
| [modules/messaging/components/notification-list.tsx](../src/modules/messaging/components/notification-list.tsx) | 141 | 客户端组件 | 通知列表 |
| [modules/settings/components/admin-settings-view.tsx](../src/modules/settings/components/admin-settings-view.tsx) | 129 | 客户端组件 | 管理员设置视图 |
| [modules/settings/components/teacher-settings-view.tsx](../src/modules/settings/components/teacher-settings-view.tsx) | 132 | 客户端组件 | 教师设置视图 |
| [modules/settings/components/student-settings-view.tsx](../src/modules/settings/components/student-settings-view.tsx) | 120 | 客户端组件 | 学生设置视图 |
| [modules/settings/components/password-change-form.tsx](../src/modules/settings/components/password-change-form.tsx) | 180 | 客户端组件 | 修改密码表单 |
| [modules/settings/components/profile-settings-form.tsx](../src/modules/settings/components/profile-settings-form.tsx) | 198 | 客户端组件 | 资料编辑表单 |
| [modules/settings/components/notification-preferences-form.tsx](../src/modules/settings/components/notification-preferences-form.tsx) | 260 | 客户端组件 | 通知偏好表单 |
| [modules/settings/components/theme-preferences-card.tsx](../src/modules/settings/components/theme-preferences-card.tsx) | 60 | 客户端组件 | 主题偏好 |
| [modules/settings/components/ai-provider-settings-card.tsx](../src/modules/settings/components/ai-provider-settings-card.tsx) | 405 | 客户端组件 | AI Provider 配置 |
| [modules/classes/components/grade-classes-view.tsx](../src/modules/classes/components/grade-classes-view.tsx) | 455 | 客户端组件 | 年级班级管理视图 |
---
## 二、违规问题清单
### 2.1 [announcements/page.tsx](../src/app/(dashboard)/announcements/page.tsx) — 严重度:高
#### BUG-A01缺少权限校验违反 Server Action 规范)
- **位置**`src/app/(dashboard)/announcements/page.tsx:6-7`
- **问题**:页面直接调用 `getAnnouncements({ status: "published" })`,未通过 `requirePermission()``requireAuth()` 进行任何权限校验
- **规范依据**项目规则「Server Action 必须使用 `requirePermission()` 进行权限校验」;架构文档 004 已记录此问题P2-12
- **影响**:未登录用户可直接访问 `/announcements` 路由获取公告数据,存在信息泄露风险
- **改进建议**
```typescript
import { requirePermission } from "@/shared/lib/auth-guard"
import { Permissions } from "@/shared/types/permissions"
export default async function AnnouncementsPage() {
await requirePermission(Permissions.ANNOUNCEMENT_READ)
const announcements = await getAnnouncements({ status: "published" })
// ...
}
```
#### BUG-A02缺少 `metadata` 导出
- **位置**`src/app/(dashboard)/announcements/page.tsx`
- **问题**:未导出 `metadata`,浏览器标签页无标题
- **规范依据**Web Interface Guidelines — Metadata & SEO
- **改进建议**:补充 `export const metadata = { title: "Announcements" }`
---
### 2.2 [dashboard/page.tsx](../src/app/(dashboard)/dashboard/page.tsx) — 严重度:中
#### BUG-D01使用权限反推角色硬编码反模式
- **位置**`src/app/(dashboard)/dashboard/page.tsx:14-16`
- **问题**:使用 `permissions.includes(HOMEWORK_SUBMIT) && !permissions.includes(EXAM_CREATE)` 反推学生身份,应使用 `hasRole("student")`
- **规范依据**:项目规则「前端组件禁止使用 `role === "xxx"` 硬编码,统一使用 `usePermission().hasPermission()`」;架构文档 004 已标记此为 P2 问题
- **影响**:当学生被授予 `EXAM_CREATE` 权限(如助教)时会被错误路由到教师页面
- **改进建议**:服务端应使用 `session.user.roles` 判断
```typescript
const roles = session.user.roles ?? []
if (roles.includes("admin")) redirect("/admin/dashboard")
if (roles.includes("student")) redirect("/student/dashboard")
if (roles.includes("parent")) redirect("/parent/dashboard")
redirect("/teacher/dashboard")
```
#### BUG-D02多重 `redirect` 调用难以维护
- **位置**`src/app/(dashboard)/dashboard/page.tsx:14-17`
- **问题**4 个连续 `if + redirect` 缺乏优先级文档说明,新增角色时易遗漏
- **改进建议**:抽取为 `resolveDefaultPath(roles)` 单一函数(`proxy.ts` 已有类似实现),保持单一职责
---
### 2.3 [management/grade/classes/page.tsx](../src/app/(dashboard)/management/grade/classes/page.tsx) — 严重度:高
#### BUG-M01缺少权限校验
- **位置**`src/app/(dashboard)/management/grade/classes/page.tsx:7-15`
- **问题**:仅调用 `auth()` 获取 session未调用 `requirePermission()` 校验 `CLASS_MANAGE` 权限
- **规范依据**项目规则「Server Action 必须使用 `requirePermission()` 进行权限校验」
- **影响**:无 `CLASS_MANAGE` 权限的用户可访问页面并获取教师列表、年级数据
- **改进建议**
```typescript
import { requirePermission } from "@/shared/lib/auth-guard"
import { Permissions } from "@/shared/types/permissions"
export default async function GradeClassesPage() {
const ctx = await requirePermission(Permissions.CLASS_MANAGE)
const userId = ctx.userId
// ...
}
```
#### BUG-M02`userId` 兜底为空字符串存在隐患
- **位置**`src/app/(dashboard)/management/grade/classes/page.tsx:9`
- **问题**`const userId = session?.user?.id ?? ""` 在未登录时返回空字符串,下游 `getGradeManagedClasses("")` 会查询无意义数据
- **改进建议**:未登录应直接 `redirect("/login")`,不应继续执行
---
### 2.4 [management/grade/insights/page.tsx](../src/app/(dashboard)/management/grade/insights/page.tsx) — 严重度:高
#### BUG-MI01缺少权限校验
- **位置**`src/app/(dashboard)/management/grade/insights/page.tsx:25-34`
- **问题**:页面直接调用 `getTeacherIdForMutations()` 和 `getGradesForStaff()`,未调用 `requirePermission()`
- **规范依据**项目规则「Server Action 必须使用 `requirePermission()` 进行权限校验」
- **改进建议**:增加 `requirePermission(Permissions.HOMEWORK_READ)` 或对应年级负责人权限校验
#### BUG-MI02使用原生 `<select>` 而非 shadcn Select 组件
- **位置**`src/app/(dashboard)/management/grade/insights/page.tsx:70-81`
- **问题**:使用原生 `<select>` 元素,与项目其他页面使用的 shadcn `Select` 组件风格不一致
- **规范依据**Web Interface Guidelines — Consistency项目组件规范
- **影响**:视觉风格不统一,无障碍特性差异,主题切换时原生 select 样式无法跟随
- **改进建议**:替换为 shadcn `Select` 组件
#### BUG-MI03`<label>` 缺少 `htmlFor` 关联
- **位置**`src/app/(dashboard)/management/grade/insights/page.tsx:69`
- **问题**`<label className="text-sm font-medium">Grade</label>` 未关联到 `select` 元素,点击 label 无法聚焦
- **规范依据**Web Interface Guidelines — Forms「Labels properly associated」
- **改进建议**`<label htmlFor="gradeId" className="...">Grade</label>`
#### BUG-MI04表单提交触发整页刷新
- **位置**`src/app/(dashboard)/management/grade/insights/page.tsx:68`
- **问题**`<form action="/management/grade/insights" method="get">` 使用原生 GET 提交,导致整页刷新
- **违反规则**`rerender-use-deferred-value`、Next.js 客户端导航最佳实践
- **改进建议**:改为客户端组件 + `useRouter().push()` 或使用 `useSearchParams` 实现无刷新筛选
#### BUG-MI05`fmt` 工具函数命名过于简短
- **位置**`src/app/(dashboard)/management/grade/insights/page.tsx:23`
- **问题**`const fmt = (v: number | null, digits = 1) => ...` 命名过于简短,不符合可读性要求
- **改进建议**:重命名为 `formatScore` 或 `formatNumber`
---
### 2.5 [messages/page.tsx](../src/app/(dashboard)/messages/page.tsx) — 严重度:低
#### BUG-MSG01缺少 `metadata` 导出
- **位置**`src/app/(dashboard)/messages/page.tsx`
- **问题**:未导出 `metadata`
- **改进建议**`export const metadata = { title: "Messages" }`
---
### 2.6 [messages/[id]/page.tsx](../src/app/(dashboard)/messages/[id]/page.tsx) — 严重度:中
#### BUG-MSG02渲染期间执行写操作标记已读
- **位置**`src/app/(dashboard)/messages/[id]/page.tsx:20-23`
- **问题**:在 RSC 渲染期间调用 `markMessageAsRead(id, ctx.userId)` 执行写操作
- **违反规则**React Server Components 规范 — 渲染函数应为纯函数,不应有副作用
- **影响**
1. React 18+ 严格模式下渲染函数可能被调用两次,导致重复写入
2. 流式渲染时若渲染被中断,写操作可能已执行但 UI 未更新
3. 错误边界捕获错误后重试渲染会再次执行写操作
- **改进建议**:使用 `after()` API 延迟执行非阻塞写操作
```typescript
import { after } from "next/server"
if (!message.isRead && message.receiverId === ctx.userId) {
after(() => markMessageAsRead(id, ctx.userId))
}
```
- **规范依据**`vercel-react-best-practices` — `server-after-nonblocking`
---
### 2.7 [messages/compose/page.tsx](../src/app/(dashboard)/messages/compose/page.tsx) — 严重度:低
#### BUG-MSG03缺少 `metadata` 导出
- **改进建议**`export const metadata = { title: "Compose Message" }`
---
### 2.8 [profile/page.tsx](../src/app/(dashboard)/profile/page.tsx) — 严重度:高
#### BUG-P01使用权限反推角色硬编码反模式
- **位置**`src/app/(dashboard)/profile/page.tsx:47-48`
- **问题**`isStudent = permissions.includes(HOMEWORK_SUBMIT) && !permissions.includes(EXAM_CREATE)``isTeacher = permissions.includes(EXAM_CREATE)`
- **规范依据**:项目规则禁止硬编码角色判断;架构文档 004 已标记
- **改进建议**:使用 `session.user.roles` 判断
```typescript
const roles = session.user.roles ?? []
const isStudent = roles.includes("student")
const isTeacher = roles.includes("teacher")
```
#### BUG-P02在 RSC 中使用 IIFE 异步块(可读性差)
- **位置**`src/app/(dashboard)/profile/page.tsx:50-118`
- **问题**:使用 `await (async () => { ... })()` 立即执行异步函数,将学生数据加载逻辑内联在组件中
- **影响**
1. 函数体过长60+ 行),难以测试
2. 无法被 React `cache()` 缓存
3. 违反单一职责原则
- **改进建议**:抽取为 `data-access.ts` 中的 `getStudentProfileData(userId)` 函数
```typescript
// modules/users/data-access.ts
export const getStudentProfileData = cache(async (userId: string) => {
const [classes, schedule, assignmentsAll, grades] = await Promise.all([...])
// ... 计算逻辑
return { enrolledClassCount, dueSoonCount, ... }
})
```
#### BUG-P03本地 `formatDate` 函数与全局工具重复
- **位置**`src/app/(dashboard)/profile/page.tsx:26-33`
- **问题**:定义了本地 `formatDate` 函数,与 `@/shared/lib/utils.formatDate` 重复
- **影响**:日期格式不一致(本地使用 `en-US`,全局使用 `zh-CN`),维护成本增加
- **改进建议**:删除本地函数,使用全局 `formatDate`,或为全局函数增加 `locale` 参数
#### BUG-P04`toWeekday` 类型断言不必要
- **位置**`src/app/(dashboard)/profile/page.tsx:21-24`
- **问题**`(day === 0 ? 7 : day) as 1 | 2 | 3 | 4 | 5 | 6 | 7` 使用 `as` 断言
- **规范依据**:编码规范 4.2.3「禁止 `as` 断言(除非从 `unknown` 转换)」
- **改进建议**:使用类型守卫
```typescript
const toWeekday = (d: Date): 1 | 2 | 3 | 4 | 5 | 6 | 7 => {
const day = d.getDay()
const result = day === 0 ? 7 : day
if (result < 1 || result > 7) throw new Error("Invalid weekday")
return result
}
```
#### BUG-P05缩进不一致
- **位置**`src/app/(dashboard)/profile/page.tsx:157,167,185,205-207`
- **问题**:多处缩进不一致(如 157 行 ` <div` 比 156 行多一个空格)
- **规范依据**`.prettierrc` 配置 `tabWidth: 2`
- **改进建议**:运行 `npx prettier --write` 统一格式
#### BUG-P06缺少 `metadata` 导出
- **改进建议**`export const metadata = { title: "Profile" }`
---
### 2.9 [settings/page.tsx](../src/app/(dashboard)/settings/page.tsx) — 严重度:中
#### BUG-S01使用权限反推角色
- **位置**`src/app/(dashboard)/settings/page.tsx:25-30`
- **问题**:同 BUG-P01使用 `permissions.includes(HOMEWORK_SUBMIT) && !permissions.includes(EXAM_CREATE)` 判断学生
- **改进建议**:使用 `session.user.roles` 判断
#### BUG-S02缺少 `metadata` 导出
- **改进建议**`export const metadata = { title: "Settings" }`
---
### 2.10 [settings/security/page.tsx](../src/app/(dashboard)/settings/security/page.tsx) — 严重度:低
#### BUG-SS01缺少权限校验
- **位置**`src/app/(dashboard)/settings/security/page.tsx:14-16`
- **问题**:仅检查 `session?.user`,未调用 `requirePermission()`
- **改进建议**:至少调用 `requireAuth()` 确保登录状态
---
### 2.11 [layout.tsx](../src/app/(dashboard)/layout.tsx) — 严重度:中
#### BUG-L01跳过链接样式使用任意值
- **位置**`src/app/(dashboard)/layout.tsx:12`
- **问题**`focus:absolute focus:z-50 focus:p-4 focus:bg-background focus:text-foreground focus:border focus:border-border focus:rounded-md focus:m-2` 类名过长且重复
- **规范依据**:项目规则「禁止使用任意值(`w-[137px]`)」
- **改进建议**:抽取为 `skip-link` 类名或独立组件
#### BUG-L02`<main>` 元素缺少 `role="main"`(虽隐式但建议显式)
- **位置**`src/app/(dashboard)/layout.tsx:16`
- **问题**`<main id="main-content">` 已有 `id`,但部分屏幕阅读器需要显式 `role="main"`
- **改进建议**:添加 `role="main"`(虽然 HTML5 规范中 `<main>` 隐式 `role="main"`,但为兼容性建议显式)
---
### 2.12 [error.tsx](../src/app/(dashboard)/error.tsx) — 严重度:低
#### BUG-E01未使用 `error.digest` 信息
- **位置**`src/app/(dashboard)/error.tsx:7`
- **问题**`error` 参数包含 `digest` 字段(用于错误追踪),但未展示给用户或上报
- **改进建议**:在描述中包含 `digest` 或提供「复制错误码」按钮
---
### 2.13 [not-found.tsx](../src/app/(dashboard)/not-found.tsx) — 严重度:低
#### BUG-NF01使用原生 `<a>` 样式而非 Button 组件
- **位置**`src/app/(dashboard)/not-found.tsx:15-20`
- **问题**`<Link className="bg-primary text-primary-foreground hover:bg-primary/90 inline-flex h-9 ...">` 手动拼接 Button 样式
- **规范依据**:项目组件规范「使用 `cn()` 工具函数管理条件类名」
- **改进建议**:使用 `<Button asChild><Link href="/dashboard">...</Link></Button>`
---
### 2.14 [announcement-list.tsx](../src/modules/announcements/components/announcement-list.tsx) — 严重度:中
#### BUG-AL01使用 `<a href>` 而非 `<Link>`(全页刷新)
- **位置**`src/modules/announcements/components/announcement-list.tsx:76`
- **问题**`<a href={createHref}>` 使用原生 `<a>` 标签,导致全页刷新
- **违反规则**`vercel-react-best-practices` — Next.js 客户端导航最佳实践
- **改进建议**:使用 `next/link` 的 `<Link>` 组件
```typescript
import Link from "next/link"
<Button asChild>
<Link href={createHref ?? "#"}>
<Plus className="mr-2 h-4 w-4" />
New Announcement
</Link>
</Button>
```
#### BUG-AL02`handleFilterChange` 未使用 `useCallback`
- **位置**`src/modules/announcements/components/announcement-list.tsx:51-57`
- **问题**`handleFilterChange` 每次渲染创建新引用,传递给 `Select` 的 `onValueChange` 导致不必要重渲染
- **违反规则**`rerender-functional-setstate`、`rerender-memo`
- **改进建议**:使用 `useCallback` 包裹
---
### 2.15 [announcement-card.tsx](../src/modules/announcements/components/announcement-card.tsx) — 严重度:低
#### BUG-AC01`useMemo` 包裹整个 JSX过度优化
- **位置**`src/modules/announcements/components/announcement-card.tsx:38-68`
- **问题**:使用 `useMemo` 包裹整个卡片 JSX依赖项为 `[announcement]`(对象)
- **违反规则**`rerender-simple-expression-in-memo` — 简单表达式不需要 memo
- **影响**`announcement` 是对象,每次父组件传入新引用时 memo 失效,无实际优化效果
- **改进建议**:移除 `useMemo`,直接渲染 JSX如需优化应使用 `React.memo` 包裹组件
```typescript
export const AnnouncementCard = React.memo(function AnnouncementCard({...}) {
return <Card>...</Card>
})
```
---
### 2.16 [announcement-detail.tsx](../src/modules/announcements/components/announcement-detail.tsx) — 严重度:中
#### BUG-AD01使用 `<a href>` 而非 `<Link>`
- **位置**`src/modules/announcements/components/announcement-detail.tsx:123,146`
- **问题**`backHref` 和 `editHref` 使用原生 `<a>` 标签
- **改进建议**:替换为 `next/link`
#### BUG-AD02三个处理函数未 `useCallback`
- **位置**`src/modules/announcements/components/announcement-detail.tsx:64-115`
- **问题**`handlePublish`、`handleArchive`、`handleDelete` 每次渲染创建新引用
- **违反规则**`rerender-functional-setstate`
- **改进建议**:使用 `useCallback` 包裹
---
### 2.17 [message-list.tsx](../src/modules/messaging/components/message-list.tsx) — 严重度:中
#### BUG-ML01使用字符串拼接动态类名
- **位置**`src/modules/messaging/components/message-list.tsx:82,91`
- **问题**`` className={`transition-colors hover:bg-accent/50 ${unread ? "border-primary/40" : ""}`} `` 使用模板字符串拼接类名
- **规范依据**:项目规则「使用 `cn()` 工具函数管理条件类名」
- **改进建议**
```typescript
className={cn(
"transition-colors hover:bg-accent/50",
unread && "border-primary/40"
)}
```
#### BUG-ML02`usePermission` 在客户端组件中导致 hydration 风险
- **位置**`src/modules/messaging/components/message-list.tsx:30-31`
- **问题**`usePermission()` 依赖 `useSession()`服务端渲染时返回空权限客户端首次渲染后才有权限导致「Compose」按钮在 hydration 后闪烁
- **违反规则**Web Interface Guidelines — Hydration Safety
- **改进建议**:将 `canSend` 作为 prop 从 RSC 父组件传入
---
### 2.18 [message-detail.tsx](../src/modules/messaging/components/message-detail.tsx) — 严重度:中
#### BUG-MD01使用 `<a href>` 而非 `<Link>`
- **位置**`src/modules/messaging/components/message-detail.tsx:79`
- **问题**`<a href={backHref}>` 使用原生 `<a>`
- **改进建议**:替换为 `next/link`
#### BUG-MD02`replyHref` 为 `undefined` 时仍渲染 Link
- **位置**`src/modules/messaging/components/message-detail.tsx:68,87-92`
- **问题**:当 `canSend` 为 false 时 `replyHref` 为 `undefined`,但代码使用 `<Link href={replyHref ?? "#"}>` 仍渲染可点击链接,点击后跳转到 `#`
- **影响**:用户体验差,点击无效链接
- **改进建议**`canSend` 为 false 时不渲染 Reply 按钮(当前已有 `{canSend ? ... : null}` 包裹,但内部仍用 `?? "#"` 兜底,应直接使用 `replyHref!` 或移除兜底)
#### BUG-MD03URL 参数未编码
- **位置**`src/modules/messaging/components/message-detail.tsx:69-71`
- **问题**`subject=${encodeURIComponent(...)}` 已编码 subject但 `parentId` 和 `receiverId` 未编码(虽然 UUID 不含特殊字符,但不严谨)
- **改进建议**:使用 `URLSearchParams` 构建查询字符串
```typescript
const params = new URLSearchParams({
parentId: message.id,
receiverId: isReceived ? message.senderId : message.receiverId,
subject: message.subject?.startsWith("Re:") ? message.subject : `Re: ${message.subject ?? ""}`,
})
const replyHref = canSend ? `/messages/compose?${params.toString()}` : undefined
```
---
### 2.19 [message-compose.tsx](../src/modules/messaging/components/message-compose.tsx) — 严重度:中
#### BUG-MC01使用 `<a href>` 而非 `<Link>`
- **位置**`src/modules/messaging/components/message-compose.tsx:73`
- **问题**:返回按钮使用原生 `<a>`
- **改进建议**:替换为 `next/link`
#### BUG-MC02隐藏 input 与 `formData.set` 重复
- **位置**`src/modules/messaging/components/message-compose.tsx:46,97`
- **问题**`handleSubmit` 中 `formData.set("receiverId", receiverId)`,同时 JSX 中又有 `<input type="hidden" name="receiverId" value={receiverId} />`,两者重复
- **改进建议**:移除隐藏 input仅使用 `formData.set`
#### BUG-MC03`handleSubmit` 未 `useCallback`
- **位置**`src/modules/messaging/components/message-compose.tsx:41-66`
- **改进建议**:使用 `useCallback` 包裹
---
### 2.20 [notification-list.tsx](../src/modules/messaging/components/notification-list.tsx) — 严重度:中
#### BUG-NL01使用字符串拼接动态类名
- **位置**`src/modules/messaging/components/notification-list.tsx:94,102`
- **问题**`` className={`transition-colors ${!n.isRead ? "border-primary/40 bg-primary/5" : ""}`} ``
- **规范依据**:项目规则「使用 `cn()` 工具函数管理条件类名」
- **改进建议**:使用 `cn()`
#### BUG-NL02`handleMarkRead` 未 `useCallback`
- **位置**`src/modules/messaging/components/notification-list.tsx:54-63`
- **改进建议**:使用 `useCallback`
#### BUG-NL03`<button>` 元素缺少 `type` 属性
- **位置**`src/modules/messaging/components/notification-list.tsx:118-124`
- **问题**`<button onClick={...}>` 未指定 `type="button"`,默认为 `submit`,若被表单包裹会触发提交
- **规范依据**Web Interface Guidelines — Forms
- **改进建议**:添加 `type="button"`
---
### 2.21 [password-change-form.tsx](../src/modules/settings/components/password-change-form.tsx) — 严重度:高
#### BUG-PC01使用字符串拼接动态类名严重违规
- **位置**`src/modules/settings/components/password-change-form.tsx:133`
- **问题**`` className={`h-2 [&>div]:${meta.color}`} `` 动态拼接 Tailwind 类名
- **规范依据**:项目规则「**禁止**字符串拼接动态类名(`bg-${color}-500`)」
- **影响**Tailwind JIT 无法识别动态拼接的类名,`bg-red-500`、`bg-yellow-500`、`bg-green-500` 可能被 tree-shaking 移除,导致生产环境进度条无颜色
- **改进建议**:使用映射对象 + `cn()`
```typescript
const STRENGTH_BAR_CLASS: Record<PasswordStrength, string> = {
weak: "h-2 [&>div]:bg-red-500",
medium: "h-2 [&>div]:bg-yellow-500",
strong: "h-2 [&>div]:bg-green-500",
}
<Progress value={meta.value} className={STRENGTH_BAR_CLASS[strength]} />
```
#### BUG-PC02使用 `document.getElementById` 操作 DOM反 React 模式)
- **位置**`src/modules/settings/components/password-change-form.tsx:62-63`
- **问题**`const form = document.getElementById("password-change-form") as HTMLFormElement | null` 直接操作 DOM
- **规范依据**React 最佳实践 — 避免直接 DOM 操作
- **改进建议**:使用 `useRef<HTMLFormElement>` 或受控组件重置表单
```typescript
const formRef = useRef<HTMLFormElement>(null)
// ...
formRef.current?.reset()
```
#### BUG-PC03`as` 断言使用
- **位置**`src/modules/settings/components/password-change-form.tsx:62`
- **问题**`as HTMLFormElement | null` 使用类型断言
- **规范依据**:编码规范 4.2.3「禁止 `as` 断言」
- **改进建议**:使用 `useRef` 后通过 ref.current 的类型推导
---
### 2.22 [profile-settings-form.tsx](../src/modules/settings/components/profile-settings-form.tsx) — 严重度:高
#### BUG-PS01使用 `as any` 类型断言(严重违规)
- **位置**`src/modules/settings/components/profile-settings-form.tsx:35`
- **问题**`resolver: zodResolver(profileFormSchema) as any` 使用 `as any`
- **规范依据**:项目规则「**禁止 `any`**」「**禁止 `as` 断言**」
- **改进建议**:修复 `zodResolver` 类型不匹配问题
```typescript
// 方案 1使用 react-hook-form 的 Resolver 类型
import type { Resolver } from "react-hook-form"
const resolver: Resolver<ProfileFormValues> = zodResolver(profileFormSchema)
// 方案 2修正 schema 类型定义
const profileFormSchema = z.object({...}) satisfies z.ZodType<ProfileFormValues>
```
#### BUG-PS02`console.error` 残留
- **位置**`src/modules/settings/components/profile-settings-form.tsx:60`
- **问题**`console.error(error)` 在生产代码中残留
- **规范依据**:编码规范 — 生产代码不应包含 `console.*`
- **改进建议**:移除或替换为日志服务
#### BUG-PS03`onSubmit` 未 `useCallback`
- **位置**`src/modules/settings/components/profile-settings-form.tsx:47-63`
- **改进建议**:使用 `useCallback`
#### BUG-PS04`age` 字段使用 `z.coerce.number()` 但未处理 NaN
- **位置**`src/modules/settings/components/profile-settings-form.tsx:25`
- **问题**`age: z.coerce.number().min(0).optional()` 当输入为空字符串时会转换为 `0`,而非 `undefined`
- **改进建议**:使用 `z.preprocess` 处理空值
```typescript
age: z.preprocess(
(v) => (v === "" || v === null || v === undefined ? undefined : Number(v)),
z.number().min(0).optional()
)
```
---
### 2.23 [notification-preferences-form.tsx](../src/modules/settings/components/notification-preferences-form.tsx) — 严重度:中
#### BUG-NPF01Switch 与隐藏 checkbox 状态同步问题
- **位置**`src/modules/settings/components/notification-preferences-form.tsx:186-201,233-248`
- **问题**:同时使用隐藏 `<input type="checkbox">` 和 `<Switch>`,两者都调用 `toggleChannel`/`toggleCategory`,可能导致双重切换
- **影响**:用户点击 Switch 时,`onCheckedChange` 触发;同时隐藏 checkbox 的 `onChange` 也触发,导致状态切换两次回到原点
- **改进建议**:移除隐藏 checkbox仅使用 Switch + 隐藏 input`type="hidden"`)提交表单
```typescript
<input type="hidden" name={item.key} value={checked ? "true" : "false"} />
<Switch
checked={checked}
onCheckedChange={() => toggleChannel(item.key)}
aria-label={item.label}
/>
```
#### BUG-NPF02本地状态与服务器状态可能不同步
- **位置**`src/modules/settings/components/notification-preferences-form.tsx:122-133`
- **问题**`useState` 初始化自 `preferences` prop但 prop 变化时状态不更新
- **违反规则**`rerender-derived-state-no-effect` — 不应使用 effect 同步派生状态
- **改进建议**:使用 `key` prop 重置组件,或使用受控组件
#### BUG-NPF03中文注释混合英文代码
- **位置**`src/modules/settings/components/notification-preferences-form.tsx:121,161,209`
- **问题**`// 本地状态用于即时反馈 Switch 切换`、`{/* 通知渠道 */}`、`{/* 通知类别 */}` 中文注释
- **规范依据**:项目代码一致性(其他文件使用英文注释)
- **改进建议**:统一为英文注释
---
### 2.24 [theme-preferences-card.tsx](../src/modules/settings/components/theme-preferences-card.tsx) — 严重度:低
#### BUG-TP01`"use client"` 后缺少空行
- **位置**`src/modules/settings/components/theme-preferences-card.tsx:1-2`
- **问题**`"use client"` 紧跟 `import` 无空行
- **规范依据**`.prettierrc` 格式规范
- **改进建议**:运行 `npx prettier --write`
#### BUG-TP02`setTheme` 参数类型不安全
- **位置**`src/modules/settings/components/theme-preferences-card.tsx:31`
- **问题**`onValueChange={(v) => setTheme(v)}` 中 `v` 为 `string`,但 `setTheme` 期望特定类型
- **改进建议**`onValueChange={(v) => setTheme(v as ThemeChoice)}`(虽然违反 as 规范,但 next-themes 类型定义如此;或使用类型守卫)
---
### 2.25 [ai-provider-settings-card.tsx](../src/modules/settings/components/ai-provider-settings-card.tsx) — 严重度:高
#### BUG-AI01中英文混合 UI严重一致性违规
- **位置**`src/modules/settings/components/ai-provider-settings-card.tsx:298,325,352,367`
- **问题**FormLabel 使用中文「品牌方」「设为默认」FormDescription 使用中文「填写基础地址,不要包含 /chat/completions。」「不会回显历史 Key留空表示不更新。」
- **规范依据**Web Interface Guidelines — Consistency项目其他 UI 均为英文
- **影响**:用户在英文界面中突然看到中文,体验割裂
- **改进建议**:统一为英文
```typescript
<FormLabel>Provider</FormLabel>
<FormDescription>Enter base URL without /chat/completions suffix.</FormDescription>
<FormLabel>Set as default</FormLabel>
<FormDescription>Existing key won't be displayed. Leave blank to keep current.</FormDescription>
```
#### BUG-AI02`useEffect` 依赖项过多导致重复执行
- **位置**`src/modules/settings/components/ai-provider-settings-card.tsx:108-136`
- **问题**`useEffect` 依赖 `[form, selectedId, onProvidersChanged, initialMode, resetToNew]`,但使用 `loadedRef` 防止重复执行
- **违反规则**`rerender-dependencies` — 应使用原始依赖
- **改进建议**:将初始化逻辑移至 `useEffect` 内部,依赖项仅为 `[]`(仅执行一次)
```typescript
useEffect(() => {
let cancelled = false
startTransition(async () => {
const rows = await getAiProviderSummaries()
if (cancelled) return
// ...
})
return () => { cancelled = true }
}, []) // 仅挂载时执行
```
#### BUG-AI03`handleSelectChange` 未 `useCallback`
- **位置**`src/modules/settings/components/ai-provider-settings-card.tsx:138-156`
- **改进建议**:使用 `useCallback`
#### BUG-AI04文件行数 405 行,接近上限
- **位置**`src/modules/settings/components/ai-provider-settings-card.tsx`
- **问题**:文件 405 行,项目规则建议 React 组件 ≤ 500 行,但复杂度较高
- **改进建议**:考虑拆分为 `AiProviderSelect`、`AiProviderForm`、`AiProviderTestButton` 子组件
---
### 2.26 [admin-settings-view.tsx](../src/modules/settings/components/admin-settings-view.tsx) — 严重度:低
#### BUG-AS01Tab 图标语义错误
- **位置**`src/modules/settings/components/admin-settings-view.tsx:50-53`
- **问题**`appearance` Tab 使用 `<Shield />` 图标(盾牌通常表示安全),应使用 `<Palette />` 或 `<Monitor />`
- **规范依据**Web Interface Guidelines — Iconography
- **改进建议**`<TabsTrigger value="appearance"><Palette /></TabsTrigger>`
#### BUG-AS02`signOut` 直接调用未确认
- **位置**`src/modules/settings/components/admin-settings-view.tsx:120`
- **问题**`onClick={() => signOut({ callbackUrl: "/login" })}` 直接登出,无确认对话框
- **规范依据**Web Interface Guidelines — Destructive Actions
- **改进建议**:增加确认对话框(虽然登出非破坏性,但意外登出影响体验)
---
### 2.27 [teacher-settings-view.tsx](../src/modules/settings/components/teacher-settings-view.tsx) — 严重度:低
#### BUG-TS01与 admin-settings-view.tsx 大量重复代码
- **位置**`src/modules/settings/components/teacher-settings-view.tsx`
- **问题**:与 `admin-settings-view.tsx`、`student-settings-view.tsx` 90% 代码重复仅「Back to dashboard」链接和「Quick links」不同
- **规范依据**DRY 原则
- **改进建议**:抽取为 `SettingsLayout` 共享组件,通过 props 传入 `backHref` 和 `quickLinks`
```typescript
export function SettingsLayout({ title, description, backHref, quickLinks, children }: {...}) {
return <div>...</div>
}
```
---
### 2.28 [student-settings-view.tsx](../src/modules/settings/components/student-settings-view.tsx) — 严重度:低
#### BUG-ST01同 BUG-TS01代码重复
- **改进建议**:同 BUG-TS01
---
### 2.29 [grade-classes-view.tsx](../src/modules/classes/components/grade-classes-view.tsx) — 严重度:高
#### BUG-GC01文件 455 行,超过 500 行建议上限的 91%
- **位置**`src/modules/classes/components/grade-classes-view.tsx`
- **问题**:单文件 455 行,包含列表、创建对话框、编辑对话框、删除确认对话框
- **规范依据**项目规则「React 组件:建议 ≤ 500 行」
- **改进建议**:拆分为:
- `grade-classes-view.tsx`(主视图,< 100 行)
- `grade-class-create-dialog.tsx`
- `grade-class-edit-dialog.tsx`
- `grade-class-delete-dialog.tsx`
#### BUG-GC02`useEffect` 依赖项导致不必要重渲染
- **位置**`src/modules/classes/components/grade-classes-view.tsx:62-78`
- **问题**:两个 `useEffect` 依赖 `managedGrades` 数组引用,父组件每次传入新数组都会触发
- **违反规则**`rerender-dependencies`
- **改进建议**:依赖 `managedGrades[0]?.id` 而非整个数组
#### BUG-GC03中英文混合 UI
- **位置**`src/modules/classes/components/grade-classes-view.tsx:183-184,283,370,389`
- **问题**:表头「班主任」「任课老师」使用中文,其他列使用英文
- **规范依据**Web Interface Guidelines — Consistency
- **改进建议**:统一为英文 `Homeroom Teacher`、`Subject Teachers`
#### BUG-GC04`formatSubjectTeachers` 在每次渲染时重新创建
- **位置**`src/modules/classes/components/grade-classes-view.tsx:140-146`
- **问题**:函数在组件内定义,每次渲染创建新引用
- **改进建议**:移至模块级别(不依赖组件状态)
---
## 三、React 性能优化(应用 `vercel-react-best-practices` 技能)
### 3.1 重渲染优化
#### PERF-01`usePermission` 返回的回调未 memoize
- **位置**`src/shared/hooks/use-permission.ts:11-25`
- **问题**`hasPermission`、`hasAnyPermission`、`hasAllPermissions`、`hasRole` 每次渲染创建新函数引用
- **违反规则**`rerender-functional-setstate`、`rerender-memo`
- **影响**`message-list.tsx`、`message-detail.tsx` 中使用 `usePermission()` 的组件每次渲染都创建新 `canSend`/`canDelete` 值
- **改进建议**:使用 `useCallback` 包裹所有回调(详见 `student_bug.md` PERF-01
#### PERF-02`AnnouncementCard` 的 `useMemo` 无效
- **位置**`src/modules/announcements/components/announcement-card.tsx:38-68`
- **问题**`useMemo` 依赖 `[announcement]`对象父组件每次渲染传入新引用memo 失效
- **违反规则**`rerender-simple-expression-in-memo`
- **改进建议**:移除 `useMemo`,使用 `React.memo` 包裹组件
#### PERF-03`profile/page.tsx` 串行 await 未并行化
- **位置**`src/app/(dashboard)/profile/page.tsx:53-58`
- **问题**:学生数据加载使用 `Promise.all` ✅,但 `userProfile` 和 `studentData` 是串行执行
- **违反规则**`async-parallel`
- **改进建议**`userProfile` 和角色判断后,并行加载学生/教师数据(当前已是此模式,但 `userProfile` 必须先获取才能判断角色,无法并行)
#### PERF-04`messages/page.tsx` 已正确使用 `Promise.all`
- **位置**`src/app/(dashboard)/messages/page.tsx:12-15`
- **现状**:✅ 已使用 `Promise.all` 并行加载 messages 和 notifications
#### PERF-05`management/grade/classes/page.tsx` 已正确使用 `Promise.all`
- **位置**`src/app/(dashboard)/management/grade/classes/page.tsx:11-15`
- **现状**:✅ 已并行加载 classes、teachers、managedGrades
#### PERF-06`ai-provider-settings-card.tsx` 使用 `loadedRef` 防止重复加载
- **位置**`src/modules/settings/components/ai-provider-settings-card.tsx:66,109-110`
- **问题**:使用 `loadedRef` 而非空依赖 `useEffect`
- **违反规则**`rerender-dependencies`
- **改进建议**:使用空依赖数组 `[]` + 清理函数
### 3.2 Bundle Size 优化
#### PERF-07`lucide-react` 导入方式
- **位置**:多处,如 `src/app/(dashboard)/profile/page.tsx:17`
- **问题**`import { User, Mail, Phone, MapPin, Calendar, Clock, Shield } from "lucide-react"` 从 barrel 文件导入
- **违反规则**`bundle-barrel-imports`
- **现状**Next.js 13+ 自动 tree-shaking `lucide-react`,影响较小
- **改进建议**:保持现状,但确保 `next.config.js` 启用了 `optimizePackageImports`
### 3.3 服务端性能
#### PERF-08`profile/page.tsx` 数据加载未使用 `cache()`
- **位置**`src/app/(dashboard)/profile/page.tsx:50-118`
- **问题**:学生数据加载逻辑内联在组件中,无法被 React `cache()` 去重
- **违反规则**`server-cache-react`
- **改进建议**:抽取为 `data-access.ts` 中的 `cache()` 包裹函数
#### PERF-09`messages/[id]/page.tsx` 渲染期间写操作
- **位置**`src/app/(dashboard)/messages/[id]/page.tsx:20-23`
- **问题**:渲染期间调用 `markMessageAsRead` 执行写操作
- **违反规则**`server-after-nonblocking`
- **改进建议**:使用 `after()` API
---
## 四、Web 界面规范审查(应用 `web-design-guidelines` 技能)
### 4.1 Hydration Safety
#### UI-01`usePermission` 导致 hydration 闪烁
- **位置**`src/modules/messaging/components/message-list.tsx:30-31`、`src/modules/messaging/components/message-detail.tsx:41-43`
- **问题**`usePermission()` 依赖 `useSession()`,服务端渲染时无权限,客户端 hydration 后权限相关 UICompose、Reply、Delete 按钮)闪烁出现
- **违反规则**Web Interface Guidelines — Hydration Safety
- **改进建议**:将权限判断结果作为 prop 从 RSC 父组件传入
```typescript
// RSC 父组件
const canSend = ctx.permissions.includes(Permissions.MESSAGE_SEND)
<MessageList messages={...} canSend={canSend} />
```
#### UI-02`theme-preferences-card.tsx` 已使用 `suppressHydrationWarning`
- **位置**`src/modules/settings/components/theme-preferences-card.tsx:32`
- **现状**:✅ 已正确处理主题切换的 hydration 问题
### 4.2 Navigation & State
#### UI-03使用 `<a href>` 导致全页刷新
- **位置**多处BUG-AL01、BUG-AD01、BUG-MD01、BUG-MC01
- **问题**:使用原生 `<a>` 而非 `<Link>`,破坏 SPA 导航
- **违反规则**Web Interface Guidelines — Navigation
- **改进建议**:全部替换为 `next/link`
#### UI-04`announcement-list.tsx` 筛选状态未反映在 URL
- **位置**`src/modules/announcements/components/announcement-list.tsx:51-57`
- **问题**`handleFilterChange` 使用 `router.replace(qs ? ?${qs} : ?)` 更新 URL ✅,但初始 `filter` 状态来自 `initialStatus` prop 而非 URL
- **改进建议**:使用 `useSearchParams` 读取 URL 状态
#### UI-05`message-detail.tsx` 回复链接 URL 参数构建不严谨
- **位置**`src/modules/messaging/components/message-detail.tsx:69-71`
- **问题**:手动拼接 URL 参数,未使用 `URLSearchParams`
- **改进建议**:见 BUG-MD03
### 4.3 Forms
#### UI-06`management/grade/insights/page.tsx` label 未关联 select
- **位置**`src/app/(dashboard)/management/grade/insights/page.tsx:69`
- **问题**`<label>` 缺少 `htmlFor`
- **违反规则**Web Interface Guidelines — Forms
- **改进建议**:见 BUG-MI03
#### UI-07`notification-list.tsx` button 缺少 `type` 属性
- **位置**`src/modules/messaging/components/notification-list.tsx:118`
- **问题**`<button>` 未指定 `type="button"`
- **违反规则**Web Interface Guidelines — Forms
- **改进建议**:见 BUG-NL03
#### UI-08`message-compose.tsx` 表单提交使用 `formData.set` 而非受控组件
- **位置**`src/modules/messaging/components/message-compose.tsx:46-49`
- **问题**:混合使用受控(`receiverId` state和非受控FormData模式
- **改进建议**:统一使用受控组件或完全使用 FormData
### 4.4 Content & Copy
#### UI-09中英文混合 UI
- **位置**
- `ai-provider-settings-card.tsx`BUG-AI01
- `grade-classes-view.tsx`BUG-GC03
- `notification-preferences-form.tsx`BUG-NPF03注释
- **违反规则**Web Interface Guidelines — Consistency
- **改进建议**:统一为英文
#### UI-10错误消息缺少修复步骤
- **位置**`src/app/(dashboard)/error.tsx:13`
- **问题**`"We apologize for the inconvenience. An unexpected error occurred."` 未提供下一步操作
- **违反规则**Web Interface Guidelines — Content & Copy
- **改进建议**:增加「联系管理员」链接或错误码展示
#### UI-11`admin-settings-view.tsx` Tab 图标语义错误
- **位置**`src/modules/settings/components/admin-settings-view.tsx:50-53`
- **问题**Appearance Tab 使用 Shield 图标
- **违反规则**Web Interface Guidelines — Iconography
- **改进建议**:见 BUG-AS01
### 4.5 Accessibility
#### UI-12`notification-list.tsx` icon 按钮缺少 `aria-label`
- **位置**`src/modules/messaging/components/notification-list.tsx:118-124`
- **问题**「Mark as read」按钮文本存在但图标按钮模式未统一
- **改进建议**:确保所有图标按钮有 `aria-label`
#### UI-13`layout.tsx` 跳过链接样式冗长
- **位置**`src/app/(dashboard)/layout.tsx:12`
- **问题**:跳过链接使用大量 `focus:` 前缀类名,难以维护
- **改进建议**:抽取为独立样式或组件
### 4.6 Performance
#### UI-14`management/grade/insights/page.tsx` 表单提交整页刷新
- **位置**`src/app/(dashboard)/management/grade/insights/page.tsx:68`
- **问题**:原生 form GET 提交导致整页刷新
- **违反规则**Web Interface Guidelines — Performance
- **改进建议**:见 BUG-MI04
#### UI-15`profile/page.tsx` 内联数据处理逻辑
- **位置**`src/app/(dashboard)/profile/page.tsx:60-108`
- **问题**:在组件内执行数组排序、过滤等耗时操作
- **改进建议**:移至 data-access 层
---
## 五、架构文档同步问题
### 5.1 [004_architecture_impact_map.md](../docs/architecture/004_architecture_impact_map.md)
#### DOC-01announcements 模块未记录页面缺少权限校验
- **位置**004 文档 2.16 节
- **问题**:已记录 `getAnnouncementsAction` 使用 `requireAuth()` 而非 `requirePermission()`,但未记录 `app/(dashboard)/announcements/page.tsx` 完全缺少权限校验
- **改进建议**:补充已知问题「⚠️ P2`app/(dashboard)/announcements/page.tsx` 完全缺少权限校验」
#### DOC-02management 模块未在架构文档中独立记录
- **位置**004 文档
- **问题**`app/(dashboard)/management/grade/` 路由未在架构文档中记录其依赖关系
- **改进建议**:补充 management 路由的模块依赖classes、school
#### DOC-03settings 模块文件清单过期
- **位置**004 文档 2.23 节
- **问题**:记录 `components/* | 8 文件`,但实际有 8 个文件 ✅,需核对行数
- **改进建议**:核对并更新各文件行数
### 5.2 [005_architecture_data.json](../docs/architecture/005_architecture_data.json)
#### DOC-04缺少 management 路由记录
- **改进建议**:在 `routes` 数组中补充 management 路由
---
## 六、问题汇总统计
| 严重度 | 数量 | 问题编号 |
|--------|------|----------|
| 高 | 9 | BUG-A01, BUG-M01, BUG-MI01, BUG-P01, BUG-P02, BUG-PC01, BUG-PS01, BUG-AI01, BUG-GC01 |
| 中 | 14 | BUG-D01, BUG-MI02, BUG-MI03, BUG-MI04, BUG-MSG02, BUG-S01, BUG-L01, BUG-AL01, BUG-AD01, BUG-ML01, BUG-ML02, BUG-MD01, BUG-MD02, BUG-MC01, BUG-NL01, BUG-NPF01, BUG-AI02 |
| 低 | 13 | BUG-A02, BUG-D02, BUG-MI05, BUG-MSG01, BUG-MSG03, BUG-P03, BUG-P04, BUG-P05, BUG-P06, BUG-S02, BUG-SS01, BUG-L02, BUG-E01, BUG-NF01, BUG-AC01, BUG-AD02, BUG-MC02, BUG-MC03, BUG-NL02, BUG-NL03, BUG-PC02, BUG-PC03, BUG-PS02, BUG-PS03, BUG-PS04, BUG-NPF02, BUG-NPF03, BUG-TP01, BUG-TP02, BUG-AI03, BUG-AI04, BUG-AS01, BUG-AS02, BUG-TS01, BUG-ST01, BUG-GC02, BUG-GC03, BUG-GC04 |
| 性能 | 9 | PERF-01, PERF-02, PERF-03, PERF-04, PERF-05, PERF-06, PERF-07, PERF-08, PERF-09 |
| 界面 | 15 | UI-01 ~ UI-15 |
| 文档 | 4 | DOC-01, DOC-02, DOC-03, DOC-04 |
| **合计** | **64** | |
---
## 七、修复优先级建议
### P0立即修复 — 影响安全与正确性)
1. **BUG-A01**`announcements/page.tsx` 增加权限校验
2. **BUG-M01**`management/grade/classes/page.tsx` 增加权限校验
3. **BUG-MI01**`management/grade/insights/page.tsx` 增加权限校验
4. **BUG-PC01**`password-change-form.tsx` 修复动态类名拼接(生产环境进度条无颜色)
5. **BUG-PS01**`profile-settings-form.tsx` 移除 `as any`
6. **BUG-AI01**`ai-provider-settings-card.tsx` 统一 UI 语言为英文
### P1本迭代修复 — 影响可维护性与性能)
7. **BUG-P01、BUG-S01、BUG-D01**:使用 `roles` 判断角色,移除权限反推
8. **BUG-P02**`profile/page.tsx` 抽取数据加载逻辑到 data-access
9. **BUG-MSG02**`messages/[id]/page.tsx` 使用 `after()` 延迟写操作
10. **BUG-AL01、BUG-AD01、BUG-MD01、BUG-MC01**:替换 `<a>` 为 `<Link>`
11. **BUG-ML01、BUG-NL01**:使用 `cn()` 替换字符串拼接
12. **PERF-01**`usePermission` 回调 memoize
13. **UI-01**:权限相关 UI 改为 RSC prop 传入
### P2下迭代修复 — 增强健壮性)
14. **BUG-GC01**`grade-classes-view.tsx` 拆分组件
15. **BUG-NPF01**`notification-preferences-form.tsx` 修复 Switch/checkbox 双重切换
16. **BUG-MI02、BUG-MI03、BUG-MI04**`management/grade/insights` 改用 shadcn Select
17. **BUG-PC02**`password-change-form.tsx` 使用 `useRef` 替代 `document.getElementById`
18. **BUG-TS01、BUG-ST01**:抽取 `SettingsLayout` 共享组件
19. **BUG-AS01**:修复 Tab 图标语义
20. **UI-10**:错误页增加修复步骤
### P3文档同步
21. **DOC-01 ~ DOC-04**:同步架构文档
---
## 八、验证命令
修复完成后应运行以下命令确保零错误:
```bash
npm run lint
npx tsc --noEmit
npm run test:unit
```
针对特定模块的端到端验证:
```bash
# 验证权限校验
curl -I http://localhost:3000/announcements # 应返回 302 重定向到 /login
curl -I http://localhost:3000/management/grade/classes # 应返回 302
curl -I http://localhost:3000/management/grade/insights # 应返回 302
# 验证 hydration
# 在浏览器控制台检查无 hydration warning
```
---
> 报告生成人AI AgentGLM-5.2
> 核查方法:人工逐行审查 + 架构图比对 + 技能规则匹配
> 应用技能:`vercel-react-best-practices`65 条规则)、`web-design-guidelines`Web Interface Guidelines
> 注:`web-artifacts-builder` 技能加载失败,界面优化建议已合并至第四章

551
bugs/parent_bug.md Normal file
View File

@@ -0,0 +1,551 @@
# `src/app/(dashboard)/parent` 前端规范核查报告
> 核查日期2026-06-18
> 核查范围:`src/app/(dashboard)/parent/` 下所有前端文件 + `src/modules/parent/` 配套组件与 data-access
> 依据文档:项目规则、编码规范 `docs/standards/coding-standards.md`、架构影响地图 004、架构数据 005
> 应用技能:`vercel-react-best-practices`、`web-artifacts-builder`、`web-design-guidelines`
---
## 一、核查文件清单
### 1.1 路由页面文件(`src/app/(dashboard)/parent/`
| 文件 | 行数 | 类型 | 用途 |
|------|------|------|------|
| [dashboard/page.tsx](../src/app/(dashboard)/parent/dashboard/page.tsx) | 16 | Server Component | 家长仪表盘入口页 |
| [attendance/page.tsx](../src/app/(dashboard)/parent/attendance/page.tsx) | 61 | Server Component | 子女考勤聚合页 |
| [grades/page.tsx](../src/app/(dashboard)/parent/grades/page.tsx) | 61 | Server Component | 子女成绩聚合页 |
| [children/[studentId]/page.tsx](../src/app/(dashboard)/parent/children/[studentId]/page.tsx) | 71 | Server Component | 单个子女详情页 |
### 1.2 模块组件文件(`src/modules/parent/components/`
| 文件 | 行数 | 类型 | 用途 |
|------|------|------|------|
| [parent-dashboard.tsx](../src/modules/parent/components/parent-dashboard.tsx) | 68 | Server Component | 仪表盘主组件 |
| [child-card.tsx](../src/modules/parent/components/child-card.tsx) | 89 | Server Component | 子女卡片 |
| [child-detail-header.tsx](../src/modules/parent/components/child-detail-header.tsx) | 49 | Server Component | 详情页头部 |
| [child-detail-panel.tsx](../src/modules/parent/components/child-detail-panel.tsx) | 27 | Server Component | 详情页面板 |
| [child-grade-summary.tsx](../src/modules/parent/components/child-grade-summary.tsx) | 163 | Client Component | 成绩趋势图 |
| [child-homework-summary.tsx](../src/modules/parent/components/child-homework-summary.tsx) | 131 | Server Component | 作业摘要 |
| [child-schedule-card.tsx](../src/modules/parent/components/child-schedule-card.tsx) | 67 | Server Component | 今日课表 |
### 1.3 数据访问与类型(`src/modules/parent/`
| 文件 | 行数 | 类型 | 用途 |
|------|------|------|------|
| [data-access.ts](../src/modules/parent/data-access.ts) | 234 | server-only | 家长-子女数据聚合 |
| [types.ts](../src/modules/parent/types.ts) | 57 | 类型定义 | 模块类型 |
---
## 二、违规问题清单
### 2.1 `children/[studentId]/page.tsx` — 严重度:高(架构违规)
#### BUG-P001app 层直接访问 DB违反三层架构
- **位置**`src/app/(dashboard)/parent/children/[studentId]/page.tsx:2-6, 24-31`
- **问题**:页面直接 `import { db }``parentStudentRelations` schema并执行 `db.select().from(parentStudentRelations)` 查询
- **规范依据**:项目规则「架构分层规则」明确「`app/` 只能调用 `modules/` 的 Server Actions 和 data-access不直接访问 DB」
- **现状**
```tsx
import { db } from "@/shared/db"
import { parentStudentRelations } from "@/shared/db/schema"
// ...
const [relation] = await db
.select({ id: parentStudentRelations.id, relation: parentStudentRelations.relation })
.from(parentStudentRelations)
.where(eq(parentStudentRelations.studentId, studentId))
.limit(1)
```
- **改进建议**:在 `parent/data-access.ts` 新增 `verifyParentChildRelation(studentId, parentId)` 函数,页面调用该函数
#### BUG-P002权限校验存在信息泄露风险
- **位置**`src/app/(dashboard)/parent/children/[studentId]/page.tsx:24-31`
- **问题**:第一次查询 relation 时仅按 `studentId` 过滤,未加 `parentId = ctx.userId` 条件。任何登录用户都能探测任意 studentId 是否存在 parent 关系
- **影响**:信息泄露(可枚举 studentId 探测家庭关系)
- **改进建议**:查询条件加 `and(eq(parentStudentRelations.studentId, studentId), eq(parentStudentRelations.parentId, ctx.userId))`
#### BUG-P003两个 "Access denied" 分支重复
- **位置**`src/app/(dashboard)/parent/children/[studentId]/page.tsx:33-58`
- **问题**relation 不存在与 dataScope 不包含两个分支返回完全相同的 UI代码重复
- **改进建议**:合并为单一校验路径,或抽取 `AccessDenied` 组件
#### BUG-P004`requireAuth()` 未做角色校验
- **位置**`src/app/(dashboard)/parent/children/[studentId]/page.tsx:21`
- **问题**:使用 `requireAuth()` 而非 `requirePermission()`,未校验当前用户是否为 parent 角色。teacher/admin 也能访问该页面(虽然 dataScope 校验会拦截,但应前置失败)
- **改进建议**:使用 `requirePermission(PARENT_VIEW)` 或在 auth-guard 中增加角色校验
---
### 2.2 `attendance/page.tsx` 与 `grades/page.tsx` — 严重度:高(代码重复)
#### BUG-P005两个页面文件几乎完全重复
- **位置**`src/app/(dashboard)/parent/attendance/page.tsx` 与 `src/app/(dashboard)/parent/grades/page.tsx`
- **问题**:两个文件结构 95% 相同仅模块名attendance vs grades、图标CalendarCheck vs GraduationCap、标题文案不同
- **违反规则**DRY 原则,编码规范「工具函数 ≤ 40 行」隐含的复用精神
- **改进建议**:抽取共享组件 `ParentChildrenDataPage`,通过 props 传入 `fetcher`、`icon`、`title`、`emptyTitle`、`renderItem`
```tsx
// 抽取后的共享组件
function ParentChildrenDataPage<T>({
title, description, icon, fetcher, renderItem, emptyTitle, emptyDescription,
}: ParentChildrenDataPageProps<T>) { /* ... */ }
```
#### BUG-P006`Promise.all` 内部异常未处理
- **位置**`src/app/(dashboard)/parent/attendance/page.tsx:29-31`、`grades/page.tsx:29-31`
- **问题**`ctx.dataScope.childrenIds.map((id) => getStudentAttendanceSummary(id))` 若任一查询抛错,整个页面 500。未做 try-catch 或 Promise.allSettled
- **改进建议**:使用 `Promise.allSettled` 并过滤 rejected或对单个子女查询失败显示局部错误状态
---
### 2.3 `dashboard/page.tsx` — 严重度:中
#### BUG-P007缺少 dataScope 空状态处理
- **位置**`src/app/(dashboard)/parent/dashboard/page.tsx:7-15`
- **问题**:未检查 `ctx.dataScope.type === "children"` 或 `childrenIds.length === 0`,直接调用 `getParentDashboardData(ctx.userId)`。虽然 data-access 会返回空数组,但与 attendance/grades 页面的处理方式不一致
- **改进建议**:与 attendance/grades 页面统一,前置检查 dataScope
---
### 2.4 `parent-dashboard.tsx` — 严重度:中
#### BUG-P008使用 `<a href>` 而非 `<Link>`,丢失客户端导航
- **位置**`src/modules/parent/components/parent-dashboard.tsx:30, 36`
- **问题**`<a href="/parent/grades">` 和 `<a href="/announcements">` 使用原生 `<a>` 标签,导致整页刷新,丢失 Next.js 客户端路由优化
- **违反规则**Web Interface Guidelines — Navigation & State「Links use `<a>`/`<Link>` (Cmd/Ctrl+click, middle-click support)」Next.js 最佳实践
- **改进建议**`import Link from "next/link"`,使用 `<Link href="/parent/grades">`
#### BUG-P009问候语使用 `new Date().getHours()` 存在时区风险
- **位置**`src/modules/parent/components/parent-dashboard.tsx:12-16`
- **问题**:服务端渲染时使用服务器时区计算问候语,与用户实际时区可能不符(例如部署在 UTC 服务器,北京时间早 8 点用户看到 "Good afternoon"
- **违反规则**Web Interface Guidelines — Locale & i18n「Dates/times: use `Intl.DateTimeFormat` not hardcoded formats」
- **改进建议**:使用 `Intl.DateTimeFormat(undefined, { hour: "numeric", timeZone: ctx.timezone })` 或将问候语计算移至客户端组件
#### BUG-P010标题层级与间距与其他页面不一致
- **位置**`src/modules/parent/components/parent-dashboard.tsx:22` vs `attendance/page.tsx:16`、`grades/page.tsx::16`
- **问题**dashboard 使用 `text-3xl` + `space-y-6`attendance/grades 使用 `text-2xl` + `space-y-8`
- **违反规则**web-artifacts-builder 设计一致性原则
- **改进建议**:统一标题字号(建议 `text-2xl`)和间距(建议 `space-y-6`
---
### 2.5 `child-card.tsx` — 严重度:中
#### BUG-P011`getInitials` 函数重复定义
- **位置**`src/modules/parent/components/child-card.tsx:9-12` 与 `src/modules/parent/components/child-detail-header.tsx:9-12`
- **问题**:两个文件定义了完全相同的 `getInitials` 函数
- **违反规则**DRY 原则
- **改进建议**:抽取到 `src/modules/parent/lib/utils.ts` 或 `src/shared/lib/utils.ts`
#### BUG-P012使用字符串拼接动态类名违反 Tailwind 规范
- **位置**`src/modules/parent/components/child-card.tsx:57-60`
- **问题**
```tsx
className={`text-lg font-semibold tabular-nums ${
homeworkSummary.overdueCount > 0 ? "text-destructive" : ""
}`}
```
使用模板字符串拼接类名违反编码规范「Tailwind 规范:使用 `cn()` 工具函数管理条件类名」
- **对比**:同模块 `child-homework-summary.tsx:72-75` 正确使用了 `cn()`
- **改进建议**
```tsx
className={cn(
"text-lg font-semibold tabular-nums",
homeworkSummary.overdueCount > 0 && "text-destructive",
)}
```
#### BUG-P013手动截断标题应使用 Tailwind `truncate`/`line-clamp`
- **位置**`src/modules/parent/components/child-card.tsx:81-83`
- **问题**
```tsx
({latestGrade.assignmentTitle.slice(0, 20)}
{latestGrade.assignmentTitle.length > 20 ? "..." : ""})
```
手动 slice + "..." 截断,违反 Web Interface Guidelines — Typography「`` not `...`」
- **改进建议**:使用 `<span className="truncate inline-block max-w-[200px]">{latestGrade.assignmentTitle}</span>`
#### BUG-P014`cursor-pointer` 在 Link 上冗余
- **位置**`src/modules/parent/components/child-card.tsx:21`
- **问题**`<Card className="hover:bg-muted/50 transition-colors cursor-pointer h-full">` 中 `cursor-pointer` 冗余(外层 `<Link>` 默认 pointer
- **违反规则**Web Interface Guidelines — Anti-patterns
- **改进建议**:移除 `cursor-pointer`
#### BUG-P015整个 Card 作为 Link 缺少可访问性描述
- **位置**`src/modules/parent/components/child-card.tsx:20-87`
- **问题**`<Link>` 包裹整个 Card屏幕阅读器会读出所有内部文本姓名、班级、数字、最新成绩缺少简洁的 aria-label
- **违反规则**Web Interface Guidelines — Accessibility「Icon-only buttons need `aria-label`」延伸到卡片导航
- **改进建议**`<Link href={...} aria-label={`查看 ${basicInfo.name ?? "子女"} 的详情`}>`
#### BUG-P016Link 缺少 `focus-visible:ring` 样式
- **位置**`src/modules/parent/components/child-card.tsx:20-21`
- **问题**`<Link>` 包裹 Card但 Card 没有 `focus-visible:ring-*` 样式,键盘导航时无可见焦点
- **违反规则**Web Interface Guidelines — Focus States「Interactive elements need visible focus」
- **改进建议**:添加 `focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2`
---
### 2.6 `child-detail-header.tsx` — 严重度:低
#### BUG-P017`getInitials` 重复(同 BUG-P011
- **位置**`src/modules/parent/components/child-detail-header.tsx:9-12`
- **改进建议**:见 BUG-P011
#### BUG-P018邮箱直接展示未做防爬处理
- **位置**`src/modules/parent/components/child-detail-header.tsx:43`
- **问题**`<span>· {basicInfo.email}</span>` 直接展示子女邮箱,无防爬/掩码处理
- **改进建议**:考虑隐私场景下掩码处理(如 `j***@example.com`),或仅对家长本人可见时展示完整
---
### 2.7 `child-grade-summary.tsx` — 严重度:中
#### BUG-P019`"use client"` 必要性可优化
- **位置**`src/modules/parent/components/child-grade-summary.tsx:1`
- **问题**:组件标记为 `"use client"`,但实际仅 `recharts` 需要客户端。整个组件(包括数据预处理 `chartData` 计算)都被打包到客户端 bundle
- **违反规则**`vercel-react-best-practices` — `bundle-dynamic-imports`「Use next/dynamic for heavy components」
- **改进建议**:将图表部分抽取为独立的客户端组件 `GradeTrendChart`,父组件保持服务端组件,通过 `next/dynamic` 懒加载图表
#### BUG-P020`latestGrade` 取数组末尾,语义不明确
- **位置**`src/modules/parent/components/child-grade-summary.tsx:32`
- **问题**`const latestGrade = grades.trend[grades.trend.length - 1]` 假设 trend 是按时间升序排列,但类型定义 `StudentDashboardGradeProps` 未明确顺序
- **对比**`child-card.tsx:17` 使用 `gradeTrend.recent[0]` 取最新(假设 recent 是降序)
- **改进建议**:在 `homework/types.ts` 的 `StudentDashboardGradeProps` 中补充 JSDoc 说明 `trend` 和 `recent` 的排序语义
#### BUG-P021`chartData` 在每次渲染时重新计算
- **位置**`src/modules/parent/components/child-grade-summary.tsx:34-41`
- **问题**`const chartData = grades.trend.map(...)` 每次渲染都重新 map未 memoize
- **违反规则**`vercel-react-best-practices` — `rerender-memo`「Extract expensive work into memoized components」
- **改进建议**`const chartData = useMemo(() => grades.trend.map(...), [grades.trend])`
#### BUG-P022`tickFormatter` 内联函数每次渲染创建新引用
- **位置**`src/modules/parent/components/child-grade-summary.tsx:99-101`
- **问题**`tickFormatter={(value) => value.slice(0, 8) + (value.length > 8 ? "..." : "")}` 内联箭头函数
- **违反规则**Web Interface Guidelines — Typography「`` not `...`」;`vercel-react-best-practices` — `rerender-functional-setstate`
- **改进建议**:抽取为模块级纯函数 `const formatTick = (v: string) => v.slice(0, 8) + (v.length > 8 ? "…" : "")`
#### BUG-P023使用 `"..."` 应为 ``
- **位置**`src/modules/parent/components/child-grade-summary.tsx:100`
- **问题**`value.length > 8 ? "..." : ""` 使用三个英文句号,应为省略号字符 ``
- **违反规则**Web Interface Guidelines — Typography「`` not `...`」
---
### 2.8 `child-homework-summary.tsx` — 严重度:低
#### BUG-P024状态字符串硬编码应使用枚举/常量
- **位置**`src/modules/parent/components/child-homework-summary.tsx:10-21`
- **问题**`getStatusVariant` 和 `getStatusLabel` 使用硬编码字符串 `"graded"`、`"submitted"`、`"in_progress"` 比较
- **改进建议**:从 `homework/types.ts` 导入状态常量或联合类型,使用 switch + exhaustive check
#### BUG-P025`getDueUrgency` 在渲染期调用 `new Date()`
- **位置**`src/modules/parent/components/child-homework-summary.tsx:23-31, 95`
- **问题**:每次 `map` 迭代都调用 `new Date()` 创建新日期对象,虽然性能影响小,但语义上应在外层计算一次 `now`
- **改进建议**:在组件顶部 `const now = new Date()` 一次,传入 `getDueUrgency(a.dueAt, now)`
---
### 2.9 `child-schedule-card.tsx` — 严重度:低
#### BUG-P026空状态高度与其他组件不一致
- **位置**`src/modules/parent/components/child-schedule-card.tsx:31`
- **问题**`className="border-none h-60"`,而 `child-grade-summary.tsx:57` 使用 `h-60``child-homework-summary.tsx:87` 使用 `h-40`
- **改进建议**:统一空状态高度(建议 `h-48`
---
### 2.10 `data-access.ts` — 严重度:中
#### BUG-P027`toWeekday` 使用 `as` 类型断言
- **位置**`src/modules/parent/data-access.ts:28-31`
- **问题**
```ts
const toWeekday = (d: Date): 1 | 2 | 3 | 4 | 5 | 6 | 7 => {
const day = d.getDay()
return (day === 0 ? 7 : day) as 1 | 2 | 3 | 4 | 5 | 6 | 7
}
```
使用 `as` 类型断言,违反编码规范「禁止 `as` 断言(除非从 `unknown` 转换)」
- **改进建议**:使用类型守卫
```ts
const toWeekday = (d: Date): 1 | 2 | 3 | 4 | 5 | 6 | 7 => {
const day = d.getDay()
const weekday = day === 0 ? 7 : day
if (weekday < 1 || weekday > 7) throw new Error(`Invalid weekday: ${weekday}`)
return weekday
}
```
#### BUG-P028`getChildBasicInfo` 串行查询瀑布(架构图已标注 P2
- **位置**`src/modules/parent/data-access.ts:58-117`
- **问题**4 个串行 DB 查询users → grades → classEnrollments → classes。其中 grades 和 classEnrollments 互相独立,可并行
- **违反规则**`vercel-react-best-practices` — `async-parallel`「Use Promise.all() for independent operations」
- **架构图标注**004 文档 2.19 节「⚠️ P2`getChildBasicInfo` 多次串行查询,可优化为 join」
- **改进建议**
```ts
// grades 和 classEnrollments 并行
const [gradeRow, enrollment] = await Promise.all([
student.gradeId
? db.select({ name: grades.name }).from(grades).where(eq(grades.id, student.gradeId)).limit(1)
: Promise.resolve([]),
db.select({ classId: classEnrollments.classId, status: classEnrollments.status })
.from(classEnrollments)
.where(and(eq(classEnrollments.studentId, studentId), eq(classEnrollments.status, "active")))
.orderBy(asc(classEnrollments.createdAt))
.limit(1),
])
```
或重构为单次 JOIN 查询
#### BUG-P029`getChildBasicInfo` 返回类型未显式标注
- **位置**`src/modules/parent/data-access.ts:58`
- **问题**`export const getChildBasicInfo = cache(async (studentId: string, relation: string | null = null) => {` 未显式标注返回类型
- **违反规则**:编码规范「函数返回值必须显式标注,特别是 `Promise<T>`」
- **改进建议**:定义 `ChildBasicInfo` 返回类型并显式标注(`types.ts` 中已有 `ChildBasicInfo` 类型,可直接使用)
#### BUG-P030`buildHomeworkSummary` 中 `[...assignments].sort()` 不必要的拷贝
- **位置**`src/modules/parent/data-access.ts:150-156`
- **问题**`[...assignments].sort(...)` 创建数组副本再排序。`assignments` 来自 `getStudentHomeworkAssignments` 返回的新数组,无需再拷贝
- **改进建议**:直接 `assignments.sort(...)` 或使用 `toSorted()`ES2023
---
### 2.11 `types.ts` — 严重度:低
#### BUG-P031类型缺少 JSDoc 文档注释
- **位置**`src/modules/parent/types.ts` 全文件
- **问题**:所有类型(`ParentChildRelation`、`ChildBasicInfo`、`ChildScheduleItem`、`ChildHomeworkSummary`、`ChildDashboardData`、`ParentDashboardData`)均无 JSDoc
- **违反规则**:编码规范 5.4「必须编写 JSDoc」
- **改进建议**:为每个类型补充 JSDoc说明用途、字段语义
#### BUG-P032`ChildHomeworkSummary` 与组件同名类型冲突风险
- **位置**`src/modules/parent/types.ts:37` 与 `child-homework-summary.tsx:33`
- **问题**:类型名 `ChildHomeworkSummary` 与组件名 `ChildHomeworkSummary` 完全相同,在 `child-homework-summary.tsx` 中同时 import 两者会造成命名冲突
```tsx
import type { ChildHomeworkSummary } from "@/modules/parent/types" // 类型
export function ChildHomeworkSummary({ summary }: { summary: ChildHomeworkSummary }) // 组件
```
当前依赖 TypeScript 类型与值的命名空间分离才不冲突,但可读性差
- **改进建议**:类型重命名为 `ChildHomeworkSummaryData` 或组件重命名为 `ChildHomeworkSummaryCard`
---
## 三、React 性能优化(应用 `vercel-react-best-practices` 技能)
### PERF-P01`getChildBasicInfo` 串行查询瀑布(同 BUG-P028
- **违反规则**`async-parallel` — Use Promise.all() for independent operations
- **改进建议**:见 BUG-P028
### PERF-P02`chartData` 未 memoize同 BUG-P021
- **违反规则**`rerender-memo` — Extract expensive work into memoized components
- **改进建议**:见 BUG-P021
### PERF-P03`child-grade-summary.tsx` 整体客户端化bundle 体积大
- **位置**`src/modules/parent/components/child-grade-summary.tsx:1`
- **问题**`"use client"` 导致 recharts~100KB整体进入客户端 bundle但组件大部分逻辑数据预处理、布局可在服务端完成
- **违反规则**`bundle-dynamic-imports` — Use next/dynamic for heavy components
- **改进建议**
```tsx
// child-grade-summary.tsx (Server Component)
import dynamic from "next/dynamic"
const GradeTrendChart = dynamic(() => import("./grade-trend-chart").then(m => m.GradeTrendChart))
// 仅图表部分客户端化
```
### PERF-P04`getParentDashboardData` 内部 `Promise.all` 已正确并行化
- **位置**`src/modules/parent/data-access.ts:225-227`
- **说明**:✅ 已正确使用 `Promise.all` 并行获取所有子女数据,符合 `async-parallel` 规范
### PERF-P05`getChildDashboardData` 内部 `Promise.all` 已正确并行化
- **位置**`src/modules/parent/data-access.ts:190-196`
- **说明**:✅ 已正确使用 `Promise.all` 并行获取 enrolledClasses/schedule/assignments/gradeTrend/gradeSummary
### PERF-P06`cache()` 已正确包裹 data-access 函数
- **位置**`src/modules/parent/data-access.ts:33, 58, 185, 209`
- **说明**:✅ 所有 data-access 函数均使用 React `cache()` 包裹,符合 `server-cache-react` 规范,实现单次请求内去重
### PERF-P07`parent-dashboard.tsx` 中 `new Date()` 在服务端执行无 hydration 风险
- **位置**`src/modules/parent/components/parent-dashboard.tsx:12`
- **说明**:✅ 组件为 Server Component`new Date()` 仅在服务端执行一次,无 hydration mismatch 风险(但有时区问题,见 BUG-P009
---
## 四、Web 界面规范审查(应用 `web-design-guidelines` 技能)
### UI-P01`parent-dashboard.tsx`
```
src/modules/parent/components/parent-dashboard.tsx:30 - <a href> → use <Link> for client-side nav
src/modules/parent/components/parent-dashboard.tsx:36 - <a href> → use <Link> for client-side nav
src/modules/parent/components/parent-dashboard.tsx:12 - new Date() server-side, timezone mismatch risk
src/modules/parent/components/parent-dashboard.tsx:22 - title size inconsistent (text-3xl vs text-2xl in other pages)
```
### UI-P02`child-card.tsx`
```
src/modules/parent/components/child-card.tsx:20 - Link wrapping Card lacks aria-label
src/modules/parent/components/child-card.tsx:21 - cursor-pointer redundant on Link
src/modules/parent/components/child-card.tsx:21 - missing focus-visible:ring-* for keyboard nav
src/modules/parent/components/child-card.tsx:57 - string concatenation for className → use cn()
src/modules/parent/components/child-card.tsx:82 - "..." → "…"
src/modules/parent/components/child-card.tsx:82 - manual slice truncation → use truncate/line-clamp
```
### UI-P03`child-grade-summary.tsx`
```
src/modules/parent/components/child-grade-summary.tsx:100 - "..." → "…"
src/modules/parent/components/child-grade-summary.tsx:99 - inline tickFormatter → hoist to module scope
src/modules/parent/components/child-grade-summary.tsx:142 - Link lacks query param for tab deep-linking
```
### UI-P04`child-homework-summary.tsx`
```
src/modules/parent/components/child-homework-summary.tsx:98 - Link lacks query param for tab deep-linking
src/modules/parent/components/child-homework-summary.tsx:25 - new Date() in each map iteration → hoist to component scope
```
### UI-P05`child-detail-header.tsx`
```
src/modules/parent/components/child-detail-header.tsx:43 - email displayed without masking (privacy)
```
### UI-P06`attendance/page.tsx` & `grades/page.tsx`
```
src/app/(dashboard)/parent/attendance/page.tsx:14 - h-full flex-1 flex-col space-y-8 p-8 md:flex → inconsistent with dashboard/page.tsx (p-6 md:p-8)
src/app/(dashboard)/parent/grades/page.tsx:14 - same inconsistency as above
```
### UI-P07空状态一致性
```
src/modules/parent/components/child-schedule-card.tsx:31 - EmptyState h-60
src/modules/parent/components/child-grade-summary.tsx:57 - EmptyState h-60
src/modules/parent/components/child-homework-summary.tsx:87 - EmptyState h-40
src/app/(dashboard)/parent/attendance/page.tsx:24 - EmptyState border-none shadow-none (no height)
→ unify EmptyState height and className
```
---
## 五、界面优化建议(应用 `web-artifacts-builder` 技能)
### UIX-P01子女卡片网格响应式断点不足
- **位置**`src/modules/parent/components/parent-dashboard.tsx:59`
- **问题**`grid-cols-1 md:grid-cols-2 lg:grid-cols-3` 在 sm 屏幕下强制单列2 列布局在 sm640px下更合适
- **改进建议**`grid-cols-1 sm:grid-cols-2 lg:grid-cols-3`
### UIX-P02详情页布局中等屏幕下右侧栏过窄
- **位置**`src/modules/parent/components/child-detail-panel.tsx:12`
- **问题**`grid-cols-1 lg:grid-cols-3` 在 md768-1024px下为单列但 `lg:col-span-2` 在 lg 下才生效md 下左侧内容占满,右侧课表也在下方
- **改进建议**:增加 md 断点 `grid-cols-1 md:grid-cols-2 lg:grid-cols-3`,左侧 `md:col-span-1 lg:col-span-2`
### UIX-P03卡片内嵌套卡片视觉层级混乱
- **位置**`src/modules/parent/components/child-card.tsx:43-73`
- **问题**Card 内部 CardContent 中又使用 `rounded-md border bg-card p-2` 创建 3 个小卡片,与外层 Card 视觉层级冲突
- **改进建议**:内部小卡片改用 `bg-muted/50` 或移除 border弱化层级
### UIX-P04作业摘要卡片缺少"查看全部"链接
- **位置**`src/modules/parent/components/child-homework-summary.tsx:90-126`
- **问题**:仅展示 `recentAssignments`(最多 5 条),无"查看全部作业"入口
- **改进建议**:底部添加 `<Link href="/parent/children/{childId}?tab=homework">View all</Link>`
### UIX-P05成绩趋势图 X 轴标签截断后信息丢失
- **位置**`src/modules/parent/components/child-grade-summary.tsx:94-102`
- **问题**`tickFormatter` 截断为 8 字符 + "…",多个作业标题前 8 字符相同时无法区分
- **改进建议**X 轴改为日期(`formatDate(submittedAt)`),标题在 tooltip 中完整展示
### UIX-P06仪表盘快捷入口仅 2 个,可扩展
- **位置**`src/modules/parent/components/parent-dashboard.tsx:28-41`
- **问题**:仅有 Grades 和 Announcements 两个快捷按钮,缺少 Attendance、Schedule 等常用入口
- **改进建议**:增加 Attendance 快捷入口,或改为下拉菜单
---
## 六、架构文档同步问题
### DOC-P01004 文档 parent 模块行数记录过期
- **位置**`docs/architecture/004_architecture_impact_map.md:924`
- **问题**:记录 `data-access.ts | 234 | 子女关系 + 仪表盘数据聚合`,实际 234 行 ✅ 一致;但 `components/* | 7 文件` 实际为 7 个组件文件 ✅ 一致
- **说明**:本节核查后无需更新(行数与文件数均一致)
### DOC-P02004 文档未记录 `children/[studentId]/page.tsx` 的架构违规
- **位置**`docs/architecture/004_architecture_impact_map.md` 2.19 节
- **问题**:未在 parent 模块「已知问题」中记录 `app/(dashboard)/parent/children/[studentId]/page.tsx` 直接访问 DB 的违规BUG-P001
- **改进建议**:在 004 文档 2.19 节「已知问题」中补充:
```
- ❌ P1`app/(dashboard)/parent/children/[studentId]/page.tsx` 直接访问 DB违反三层架构
```
### DOC-P03005 JSON 中 parent 模块的 routes 节点需补充
- **问题**:若修复 BUG-P005抽取共享组件路由结构不变但需在 005 JSON 中记录 attendance/grades 页面的 fetcher 依赖关系
- **改进建议**:在 `005_architecture_data.json` 的 `modules.parent.dependencies` 中补充 `attendance`、`grades` 模块依赖
---
## 七、问题汇总统计
| 严重度 | 数量 | 问题编号 |
|--------|------|----------|
| 高(架构违规/安全) | 6 | BUG-P001, BUG-P002, BUG-P004, BUG-P005, BUG-P006, BUG-P028 |
| 中(规范违规/性能) | 12 | BUG-P003, BUG-P007, BUG-P008, BUG-P009, BUG-P010, BUG-P011, BUG-P012, BUG-P019, BUG-P020, BUG-P021, BUG-P027, BUG-P029 |
| 低(代码质量/UX | 13 | BUG-P013, BUG-P014, BUG-P015, BUG-P016, BUG-P017, BUG-P018, BUG-P022, BUG-P023, BUG-P024, BUG-P025, BUG-P026, BUG-P030, BUG-P031, BUG-P032 |
| 合计 | 31 | — |
### 按技能分类统计
| 技能 | 发现问题数 | 主要问题类型 |
|------|-----------|-------------|
| 项目规范核查 | 18 | 架构违规、代码重复、类型规范、Tailwind 规范 |
| vercel-react-best-practices | 7 | 串行查询瀑布、bundle 体积、memoize 缺失 |
| web-design-guidelines | 15 | 可访问性、焦点状态、排版、导航、空状态一致性 |
| web-artifacts-builder | 6 | 响应式断点、视觉层级、交互入口、图表可读性 |
---
## 八、修复优先级建议
### P0立即修复 — 架构与安全)
1. **BUG-P001**`children/[studentId]/page.tsx` 移除直接 DB 访问,下沉到 `parent/data-access.ts`
2. **BUG-P002**:权限校验加 `parentId` 条件,防止信息泄露
3. **BUG-P005**:抽取 `ParentChildrenDataPage` 共享组件,消除 attendance/grades 重复
### P1短期修复 — 规范与性能)
4. **BUG-P008**`<a href>` 改为 `<Link>`
5. **BUG-P012**`child-card.tsx` 使用 `cn()` 替代字符串拼接
6. **BUG-P011**:抽取 `getInitials` 到共享 utils
7. **BUG-P028**`getChildBasicInfo` 并行化查询
8. **BUG-P019**`child-grade-summary.tsx` 拆分服务端/客户端组件
9. **BUG-P027**`toWeekday` 移除 `as` 断言
10. **BUG-P029**`getChildBasicInfo` 显式标注返回类型
### P2机会修复 — UX 与代码质量)
11. **BUG-P009**:问候语时区处理
12. **BUG-P013**:使用 `truncate` 替代手动截断
13. **BUG-P015, BUG-P016**:卡片可访问性增强
14. **BUG-P023**`...` → ``
15. **BUG-P031**:补充类型 JSDoc
16. **UIX-P01~P06**:界面优化项
---
## 九、标杆实践(建议保留)
| 实践 | 位置 | 说明 |
|------|------|------|
| `cache()` 包裹 data-access | `data-access.ts:33, 58, 185, 209` | 符合 `server-cache-react`,单次请求去重 |
| `Promise.all` 并行获取子女数据 | `data-access.ts:190-196, 225-227` | 符合 `async-parallel`,消除瀑布 |
| Server Component 默认 | 7/8 组件为 Server Component | 仅 `child-grade-summary.tsx` 因 recharts 标记 client |
| `import type` 正确使用 | 所有类型导入均使用 `import type` | 符合编码规范 4.2.6 |
| `server-only` 标注 | `data-access.ts:1` | 防止 data-access 被客户端误引入 |
| 空状态处理完整 | 所有页面均使用 `EmptyState` 组件 | UX 一致性良好 |
---
> **说明**:本报告基于 2026-06-18 代码状态生成。修复后需同步更新 `docs/architecture/004_architecture_impact_map.md` 2.19 节与 `005_architecture_data.json` 的 parent 模块节点。

297
bugs/shared_bug.md Normal file
View File

@@ -0,0 +1,297 @@
# `src/shared/types` 规范核查报告
> 核查日期2026-06-18
> 核查范围:`src/shared/types/` 目录下所有前后端文件
> 依据文档:项目规则、编码规范、架构影响地图 004、架构数据 005
> 应用技能:`vercel-react-best-practices`、`web-artifacts-builder`、`web-design-guidelines`
---
## 一、核查文件清单
| 文件 | 行数 | 类型 | 用途 |
|------|------|------|------|
| [action-state.ts](../src/shared/types/action-state.ts) | 5 | 类型定义 | Server Action 统一返回类型 |
| [action-state.test.ts](../src/shared/types/action-state.test.ts) | 33 | 单元测试 | ActionState 类型测试 |
| [permissions.ts](../src/shared/types/permissions.ts) | 114 | 类型定义+常量 | 权限点常量、Permission/DataScope/AuthContext 类型 |
---
## 二、违规问题清单
### 2.1 action-state.ts — 严重度:高
#### BUG-A01Prettier 配置违规(使用分号)
- **位置**`src/shared/types/action-state.ts:1-5`
- **问题**:文件使用分号(`;`)结尾,但项目 `.prettierrc` 配置 `"semi": false`,应移除所有分号
- **现状**
```typescript
export type ActionState<T = void> = {
success: boolean;
message?: string;
errors?: Record<string, string[]>;
data?: T;
};
```
- **改进建议**:移除所有分号,与 `permissions.ts`、`action-state.test.ts` 保持一致
#### BUG-A02缺少 JSDoc 文档注释
- **位置**`src/shared/types/action-state.ts:1`
- **问题**`ActionState<T>` 类型缺少 JSDoc 注释,未说明类型用途、泛型参数、各字段含义
- **规范依据**:编码规范 5.4「必须编写 JSDoc」
- **改进建议**:补充类型级 JSDoc说明 `@template T`、各 property 语义
---
### 2.2 permissions.ts — 严重度:高
#### BUG-P01权限点命名不一致下划线 vs 驼峰)
- **位置**`src/shared/types/permissions.ts:91`
- **问题**`EXAM_PROCTOR_READ: "exam:proctor_read"` 使用下划线分隔,而其他 READ 权限均使用单词形式(如 `exam:read`、`question:read`
- **改进建议**:统一为 `exam:proctor:read`(嵌套资源用冒号分隔)
#### BUG-P02`Permissions` 常量缺少 `satisfies` 类型约束
- **位置**`src/shared/types/permissions.ts:4-96`
- **问题**:使用 `as const` 但未用 `satisfies` 验证所有值均为字符串,无法在编译期捕获值类型错误
- **规范依据**:编码规范 4.2.3「可用 `satisfies` 保持类型推导」
- **改进建议**`as const satisfies Record<string, string>`
#### BUG-P03`AuthContext.roles` 类型过于宽松
- **位置**`src/shared/types/permissions.ts:111`
- **问题**`roles: string[]` 允许任意字符串但项目角色是有限集合admin/teacher/student/parent/grade_head/teaching_head
- **影响**:拼写错误(如 `"techer"`)无法在编译期发现;与 `proxy.ts:21` 中 `resolveDefaultPath(roles: string[])` 的硬编码角色判断形成隐患
- **改进建议**:定义 `Role` 联合类型,`AuthContext.roles` 改为 `Role[]``ROLE_PERMISSIONS` 改为 `Record<Role, Permission[]>`
#### BUG-P04`DataScope` 缺少 JSDoc 与字段说明
- **位置**`src/shared/types/permissions.ts:101-107`
- **问题**6 种数据范围类型未说明各自语义、适用角色、使用场景
- **改进建议**:补充类型级 JSDoc说明每种 `type` 的适用角色与语义
#### BUG-P05`AuthContext` 接口缺少 JSDoc
- **位置**`src/shared/types/permissions.ts:109-114`
- **问题**:接口无文档说明,使用者无法快速理解字段语义
- **规范依据**:编码规范 5.4
- **改进建议**:补充接口级 JSDoc说明「认证上下文由 `getAuthContext()` 返回,贯穿所有 Server Action」
#### BUG-P06`DataScope.class_members` 缺少关联数据
- **位置**`src/shared/types/permissions.ts:104`
- **问题**`{ type: "class_members" }` 不携带 classIds导致 data-access 层每次都需要额外查询学生所在班级,存在 N+1 查询风险
- **影响**`exams/data-access.ts`、`homework/data-access.ts` 等模块在过滤时需重复查询 `classMembers` 表
- **改进建议**:在 `resolveDataScope` 中预查并携带 classIds`{ type: "class_members"; classIds: string[] }`
---
### 2.3 action-state.test.ts — 严重度:中
#### BUG-T01测试覆盖率不足
- **位置**`src/shared/types/action-state.test.ts:4-33`
- **问题**:仅测试 3 种基本状态,缺少以下场景:
1. `errors` 字段包含多个字段、每个字段多条错误消息
2. `data` 为 `null`、`undefined`、`0`、`""` 等 falsy 值时的行为
3. 同时存在 `errors` 和 `data`(虽然语义上不应出现,但类型允许)
4. `message` 为空字符串
- **规范依据**:编码规范十、测试规范「工具函数覆盖率目标 100%」
#### BUG-T02测试描述缺少行为意图
- **位置**`src/shared/types/action-state.test.ts:4`
- **问题**`describe("ActionState")` 过于宽泛,未说明被测行为
- **规范依据**:编码规范 10.2「描述应说明预期行为」
- **改进建议**`describe("ActionState 类型构造")`
---
### 2.4 跨文件违规(使用方导入问题)
#### BUG-X01`exams/actions.ts` 类型导入违规
- **位置**`src/modules/exams/actions.ts:4`
- **问题**`import { ActionState } from "@/shared/types/action-state"` — `ActionState` 仅作为类型使用,应使用 `import type`
- **规范依据**:编码规范 4.2.6「所有仅用于类型的导入必须使用 `import type`」
- **改进建议**`import type { ActionState } from "@/shared/types/action-state"`
#### BUG-X02`questions/actions.ts` 类型导入违规
- **位置**`src/modules/questions/actions.ts:7`
- **问题**:同 BUG-X01`import { ActionState }` 应为 `import type { ActionState }`
- **改进建议**`import type { ActionState } from "@/shared/types/action-state"`
---
### 2.5 tsconfig.json 配置不达标(影响类型安全)
#### BUG-C01`target` 低于规范要求
- **位置**`tsconfig.json:3`
- **问题**`"target": "ES2017"`,编码规范 4.1 要求 `"ES2022"`
- **影响**:无法使用 ES2022 特性(如 `Array.at()`、`Object.hasOwn()`
- **改进建议**`"target": "ES2022"`
#### BUG-C02缺少 `noUncheckedIndexedAccess`
- **位置**`tsconfig.json`
- **问题**:未启用 `noUncheckedIndexedAccess`,数组/对象索引访问返回 `T` 而非 `T | undefined`
- **影响**`permissions.ts:198` 中 `ROLE_PERMISSIONS[name]` 在 `name` 不存在时返回 `Permission[]` 而非 `Permission[] | undefined`,存在运行时风险
- **规范依据**:编码规范 4.1
- **改进建议**`"noUncheckedIndexedAccess": true`
#### BUG-C03缺少 `noImplicitReturns` 和 `noFallthroughCasesInSwitch`
- **位置**`tsconfig.json`
- **问题**:未启用这两个严格检查
- **规范依据**:编码规范 4.1
- **改进建议**:补充 `"noImplicitReturns": true`、`"noFallthroughCasesInSwitch": true`、`"forceConsistentCasingInFileNames": true`
---
## 三、React 性能优化(应用 `vercel-react-best-practices` 技能)
### PERF-01`use-permission.ts` 回调函数未 memoize
- **位置**`src/shared/hooks/use-permission.ts:11-25`
- **问题**`hasPermission`、`hasAnyPermission`、`hasAllPermissions`、`hasRole` 每次渲染都创建新函数引用,导致依赖这些函数的子组件不必要重渲染
- **违反规则**`rerender-functional-setstate`、`rerender-memo`
- **改进建议**:使用 `useCallback` 包裹所有回调函数
### PERF-02`permissions` 和 `roles` 数组未 memoize
- **位置**`src/shared/hooks/use-permission.ts:8-9`
- **问题**:每次渲染都执行 `?? []` 创建新数组引用,导致下游 `useEffect`/`useMemo` 依赖项失效
- **违反规则**`rerender-derived-state`、`rerender-dependencies`
- **改进建议**:使用 `useMemo` 包裹数组派生
### PERF-03`as` 断言使用
- **位置**`src/shared/hooks/use-permission.ts:8-9`
- **问题**`as Permission[]` 和 `as string[]` 使用了类型断言,违反编码规范 4.2.3
- **改进建议**:依赖 `next-auth.d.ts` 的类型增强(已存在),移除断言;或增加类型守卫
### `use-permission.ts` 完整改进示例
```typescript
import { useCallback, useMemo } from "react"
import { useSession } from "next-auth/react"
import type { Permission } from "@/shared/types/permissions"
export function usePermission() {
const { data: session } = useSession()
const permissions = useMemo(
() => (session?.user?.permissions ?? []) as Permission[],
[session?.user?.permissions]
)
const roles = useMemo(
() => (session?.user?.roles ?? []) as string[],
[session?.user?.roles]
)
const hasPermission = useCallback(
(permission: Permission): boolean => permissions.includes(permission),
[permissions]
)
const hasAnyPermission = useCallback(
(...perms: Permission[]): boolean => perms.some((p) => permissions.includes(p)),
[permissions]
)
const hasAllPermissions = useCallback(
(...perms: Permission[]): boolean => perms.every((p) => permissions.includes(p)),
[permissions]
)
const hasRole = useCallback(
(role: string): boolean => roles.includes(role),
[roles]
)
return { permissions, roles, hasPermission, hasAnyPermission, hasAllPermissions, hasRole }
}
```
---
## 四、Web 界面规范审查(应用 `web-design-guidelines` 技能)
> 说明:`src/shared/types/` 为纯类型定义文件,无直接 UI 代码。以下审查针对类型定义所支撑的 UI 实现层(`use-permission.ts`、`proxy.ts`、`auth-guard.ts`)是否符合 Web Interface Guidelines。
### UI-01权限状态可能导致 hydration mismatch
- **位置**`src/shared/hooks/use-permission.ts:7`
- **问题**`useSession()` 在服务端渲染时返回 `null`/`loading`,客户端首次渲染后才有权限数据,导致权限相关的 UI如菜单项、按钮在 hydration 后闪烁
- **违反规则**Web Interface Guidelines — Hydration Safety
- **改进建议**
1. 服务端组件应通过 `auth()` 获取权限并作为 props 传递
2. 客户端组件在 `session === null` 时渲染骨架屏或占位,避免权限相关 UI 闪烁
3. 对权限相关的动态 UI 使用 `suppressHydrationWarning` 或延迟渲染
### UI-02权限不足时的重定向未反映在 URL
- **位置**`src/proxy.ts:75-76`
- **问题**权限不足时直接重定向到默认页URL 中未携带原始路径信息,用户无法知道「为何被重定向」
- **违反规则**Web Interface Guidelines — Navigation & State「URL reflects state」
- **改进建议**:重定向时携带 `?from=originalPath&reason=forbidden` 参数,目标页显示提示
### UI-03错误消息缺少修复步骤
- **位置**`src/shared/lib/auth-guard.ts:13`
- **问题**`Permission denied: ${permission}` 仅描述问题,未提供下一步操作
- **违反规则**Web Interface Guidelines — Content & Copy「Error messages include fix/next step」
- **改进建议**`权限不足:需要 ${permission} 权限。请联系管理员授权或切换账号。`
---
## 五、架构文档同步问题
### DOC-01004 文件行数记录过期
- **位置**`docs/architecture/004_architecture_impact_map.md:408`
- **问题**:记录 `types/permissions.ts | 92 | 54 个权限点常量`,实际文件 114 行,含 `DataScope`、`AuthContext` 类型定义
- **改进建议**:更新为 `114 行 | 54 个权限点 + DataScope + AuthContext`
### DOC-02005 JSON 中 `DataScope` 定义字段顺序与代码不一致
- **位置**`docs/architecture/005_architecture_data.json:993`
- **问题**JSON 中字段顺序为 `all, owned, class_taught, grade_managed, class_members, children`,代码中为 `all, owned, class_members, grade_managed, class_taught, children`
- **改进建议**:同步 JSON 字段顺序与源码一致
### DOC-03缺少 `Role` 类型定义记录
- **问题**:若按 BUG-P03 建议新增 `Role` 类型,需在 005 JSON 的 `shared.types` 数组中补充记录
- **改进建议**:新增 `Role` 类型节点,记录 `usedBy: ["auth-guard", "permissions", "proxy"]`
---
## 六、问题汇总统计
| 严重度 | 数量 | 问题编号 |
|--------|------|----------|
| 高 | 8 | BUG-A01, BUG-A02, BUG-P01, BUG-P02, BUG-P03, BUG-P04, BUG-P05, BUG-P06 |
| 中 | 6 | BUG-T01, BUG-T02, BUG-X01, BUG-X02, BUG-C01, BUG-C02 |
| 低 | 3 | BUG-C03, DOC-01, DOC-02, DOC-03 |
| 性能 | 3 | PERF-01, PERF-02, PERF-03 |
| 界面 | 3 | UI-01, UI-02, UI-03 |
| **合计** | **23** | |
---
## 七、修复优先级建议
### P0立即修复 — 影响类型安全与一致性)
1. BUG-A01移除 `action-state.ts` 分号
2. BUG-X01、BUG-X02修正 `import type` 违规
3. BUG-C01、BUG-C02、BUG-C03升级 `tsconfig.json`
### P1本迭代修复 — 影响可维护性)
4. BUG-P01统一权限点命名
5. BUG-P02`Permissions` 添加 `satisfies`
6. BUG-P03新增 `Role` 类型
7. BUG-A02、BUG-P04、BUG-P05补充 JSDoc
8. PERF-01、PERF-02、PERF-03`use-permission.ts` 性能优化
### P2下迭代修复 — 增强健壮性)
9. BUG-T01、BUG-T02补充测试用例
10. BUG-P06`DataScope.class_members` 携带 classIds
11. UI-01、UI-02、UI-03界面规范改进
### P3文档同步
12. DOC-01、DOC-02、DOC-03同步架构文档
---
## 八、验证命令
修复完成后应运行以下命令确保零错误:
```bash
npm run lint
npx tsc --noEmit
npm run test:unit -- action-state
```
---
> 报告生成人AI AgentGLM-5.2
> 核查方法:人工逐行审查 + 架构图比对 + 技能规则匹配

711
bugs/student_bug.md Normal file
View File

@@ -0,0 +1,711 @@
# `src/app/(dashboard)/student` 前端规范核查报告
> 核查日期2026-06-18
> 核查范围:`src/app/(dashboard)/student/` 目录下所有前端文件(含 `loading.tsx`+ 关联模块组件 `src/modules/student/components/*`
> 依据文档:项目规则、编码规范 `docs/standards/coding-standards.md`、架构影响地图 004、架构数据 005
> 应用技能:`vercel-react-best-practices`、`web-artifacts-builder`、`web-design-guidelines`
---
## 一、核查文件清单
### 1.1 路由页面文件11 个)
| 文件 | 行数 | 类型 | 用途 |
|------|------|------|------|
| [dashboard/page.tsx](../src/app/(dashboard)/student/dashboard/page.tsx) | 88 | Server Component | 学生仪表盘 |
| [dashboard/loading.tsx](../src/app/(dashboard)/student/dashboard/loading.tsx) | 60 | Loading UI | 仪表盘骨架屏 |
| [attendance/page.tsx](../src/app/(dashboard)/student/attendance/page.tsx) | 40 | Server Component | 学生考勤 |
| [diagnostic/page.tsx](../src/app/(dashboard)/student/diagnostic/page.tsx) | 31 | Server Component | 学情诊断 |
| [elective/page.tsx](../src/app/(dashboard)/student/elective/page.tsx) | 49 | Server Component | 选课中心 |
| [grades/page.tsx](../src/app/(dashboard)/student/grades/page.tsx) | 40 | Server Component | 我的成绩 |
| [learning/assignments/page.tsx](../src/app/(dashboard)/student/learning/assignments/page.tsx) | 167 | Server Component | 作业列表 |
| [learning/assignments/[assignmentId]/page.tsx](../src/app/(dashboard)/student/learning/assignments/[assignmentId]/page.tsx) | 53 | Server Component | 作业作答/复习 |
| [learning/courses/page.tsx](../src/app/(dashboard)/student/learning/courses/page.tsx) | 39 | Server Component | 课程列表 |
| [learning/courses/loading.tsx](../src/app/(dashboard)/student/learning/courses/loading.tsx) | 28 | Loading UI | 课程骨架屏 |
| [learning/textbooks/page.tsx](../src/app/(dashboard)/student/learning/textbooks/page.tsx) | 71 | Server Component | 教材列表 |
| [learning/textbooks/[id]/page.tsx](../src/app/(dashboard)/student/learning/textbooks/[id]/page.tsx) | 76 | Server Component | 教材阅读 |
| [schedule/page.tsx](../src/app/(dashboard)/student/schedule/page.tsx) | 54 | Server Component | 课表 |
| [schedule/loading.tsx](../src/app/(dashboard)/student/schedule/loading.tsx) | 31 | Loading UI | 课表骨架屏 |
### 1.2 关联模块组件3 个)
| 文件 | 行数 | 类型 | 用途 |
|------|------|------|------|
| [student-courses-view.tsx](../src/modules/student/components/student-courses-view.tsx) | 156 | Client Component | 课程视图 + 加入班级表单 |
| [student-schedule-filters.tsx](../src/modules/student/components/student-schedule-filters.tsx) | 32 | Client Component | 课表筛选器 |
| [student-schedule-view.tsx](../src/modules/student/components/student-schedule-view.tsx) | 88 | Server Component | 课表视图 |
### 1.3 缺失文件
| 缺失类型 | 影响范围 | 说明 |
|---------|---------|------|
| `layout.tsx` | 整个 student 路由组 | 无统一布局,每个页面重复写 `<div className="... p-8">` 容器 |
| `error.tsx` | 整个 student 路由组 | 无错误边界Server Component 抛错时显示 Next.js 默认错误页 |
| `loading.tsx` | attendance / diagnostic / elective / grades / learning/assignments / learning/assignments/[assignmentId] / learning/textbooks / learning/textbooks/[id] | 8 个路由无骨架屏,跳转时白屏 |
| `not-found.tsx` | 整个 student 路由组 | `notFound()` 调用时显示 Next.js 默认 404 |
---
## 二、违规问题清单
### 2.1 认证模式严重不一致 — 严重度:高
#### BUG-A01三种认证模式混用
- **位置**:整个 student 目录
- **问题**同一角色student的页面使用了 3 种不同的认证方式:
| 模式 | 使用页面 | 问题 |
|------|---------|------|
| `getAuthContext()` from `@/shared/lib/auth-guard` | attendance / diagnostic / grades | ✅ 推荐 |
| `auth()` from `@/auth` | elective | ⚠️ 直接调用 auth(),绕过 auth-guard 抽象 |
| `getDemoStudentUser()` from `@/modules/homework/data-access` | dashboard / learning/* / schedule | ⚠️ 依赖 homework 模块获取用户身份 |
- **规范依据**:编码规范 8.3「统一通过 `getAuthContext()` / `requirePermission()` 获取会话」
- **影响**
1. `elective/page.tsx` 直接 `import { auth } from "@/auth"`违反分层规则app 层不应直接依赖根模块 `@/auth`,应通过 `shared/lib/auth-guard`
2. `getDemoStudentUser()` 名为 "Demo" 实为真实查询,命名误导
3. `getDemoStudentUser()` 定义在 `homework/data-access.ts`,导致 6 个 student 页面为获取用户身份而依赖 homework 模块,违反模块封装
- **改进建议**
1.`getDemoStudentUser` 迁移到 `users/data-access.ts` 并重命名为 `getCurrentStudentUser()``getStudentSession()`
2. 所有 student 页面统一使用 `getAuthContext()` 获取 `ctx.userId`,再按需查询学生信息
3. 移除 `elective/page.tsx` 中的 `import { auth } from "@/auth"`
#### BUG-A02`getDemoStudentUser` 命名误导
- **位置**`src/modules/homework/data-access.ts:475`
- **问题**:函数名包含 "Demo",但实际执行真实 DB 查询JOIN users + usersToRoles + roles WHERE roles.name = "student"),并非演示用桩函数
- **影响**:开发者看到 "Demo" 会误以为是临时实现,不敢用于生产;或误以为返回固定演示数据
- **改进建议**:重命名为 `getCurrentStudentUser()``resolveCurrentStudent()`
#### BUG-A03`getDemoStudentUser` 放置在错误的模块
- **位置**`src/modules/homework/data-access.ts:475-490`
- **问题**:学生身份查询属于 `users` 模块职责,却放在 `homework` 模块
- **规范依据**:项目规则「模块标准结构」+ 编码规范 2.2「模块封装」
- **影响**`dashboard``learning/assignments``learning/courses``learning/textbooks``learning/textbooks/[id]``schedule` 6 个页面为获取用户身份而 `import` homework 模块,造成虚假依赖
- **改进建议**:迁移到 `src/modules/users/data-access.ts`,导出为 `getCurrentStudentUser()`
---
### 2.2 `learning/assignments/page.tsx` — 严重度:高
#### BUG-L01中英文混排
- **位置**`src/app/(dashboard)/student/learning/assignments/page.tsx:81, 122`
- **问题**:在英文 UI 中插入中文标签
```tsx
<div className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">未答题</div>
...
<div className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">已答题</div>
```
- **规范依据**:项目为 K12 中文教务系统,但本页面其余文案均为英文("Due"、"Attempts"、"Score"、"Start"、"Continue"),中英文混排显得不专业
- **改进建议**:统一为英文 "Pending" / "Completed",或整页改为中文
#### BUG-L02卡片渲染逻辑重复
- **位置**`src/app/(dashboard)/student/learning/assignments/page.tsx:83-116` 与 `124-157`
- **问题**:未答题区和已答题区的卡片 JSX 完全相同34 行 × 2 = 68 行重复),仅数据源不同
- **规范依据**编码规范「DRY 原则」+ 项目规则「单文件行数建议 ≤ 500 行」
- **改进建议**:抽取为 `<AssignmentCard assignment={a} />` 组件,循环调用
#### BUG-L03JSX 语法格式错误
- **位置**`src/app/(dashboard)/student/learning/assignments/page.tsx:162`
- **问题**:行尾 `})}` 多了一个 `)`
```tsx
)})}
```
应为:
```tsx
))}
```
- **影响**:虽然能编译通过(因外层有 `(`但格式混乱IDE 自动格式化后会变化,造成 git diff 噪音
- **改进建议**:修正为 `))}`
#### BUG-L04`getStatusVariant` 对 submitted 和 in_progress 返回相同值
- **位置**`src/app/(dashboard)/student/learning/assignments/page.tsx:13-18`
- **问题**
```tsx
if (status === "submitted") return "secondary"
if (status === "in_progress") return "secondary"
```
两种状态视觉上无法区分,学生无法快速辨别「已提交待批改」和「进行中」
- **改进建议**`in_progress` 改为 `"outline"` 或新增 `"in_progress"` 专属样式
#### BUG-L05状态判断函数参数类型过宽
- **位置**`src/app/(dashboard)/student/learning/assignments/page.tsx:13, 20, 27, 34, 39`
- **问题**`getStatusVariant(status: string)` 等函数参数为 `string`,但 `a.progressStatus` 实际类型是 `StudentHomeworkProgressStatus` 联合类型
- **规范依据**:编码规范 4.2「优先使用精确类型」
- **改进建议**:参数类型改为 `StudentHomeworkProgressStatus`,并在 switch 中穷举
#### BUG-L06`new Map<string, typeof assignments>()` 类型不优雅
- **位置**`src/app/(dashboard)/student/learning/assignments/page.tsx:63`
- **问题**:使用 `typeof assignments` 作为 Map 值类型,将类型绑定到变量,可读性差
- **改进建议**:导入显式类型 `new Map<string, StudentHomeworkAssignment[]>()`
---
### 2.3 `learning/textbooks/page.tsx` — 严重度:中
#### BUG-T01存在注释掉的代码
- **位置**`src/app/(dashboard)/student/learning/textbooks/page.tsx:42-50`
- **问题**
```tsx
{/* <div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div>
<h2 className="text-2xl font-bold tracking-tight">Textbooks</h2>
<p className="text-muted-foreground">Browse your course textbooks.</p>
</div>
<Button asChild variant="outline">
<Link href="/student/dashboard">Back</Link>
</Button>
</div> */}
```
- **规范依据**:编码规范禁止提交注释掉的代码(应删除或用版本管理)
- **改进建议**:删除注释块
#### BUG-T02缺少页面标题
- **位置**`src/app/(dashboard)/student/learning/textbooks/page.tsx`
- **问题**:其他 student 页面都有 `<h2 className="text-2xl font-bold tracking-tight">` 标题,本页面因注释掉了标题块,直接渲染 `TextbookFilters`,与其他页面不一致
- **改进建议**:恢复标题(不带"Back"按钮),保持视觉一致性
#### BUG-T03`student` 变量未真正使用
- **位置**`src/app/(dashboard)/student/learning/textbooks/page.tsx:23-31`
- **问题**`student` 仅用于 "No user" 检查,`getTextbooks(q, subject, grade)` 不依赖学生身份
- **影响**:任何登录学生都能浏览所有教材,无年级/科目过滤;如设计如此,则 `student` 检查可简化为 `getAuthContext()` 仅做认证
- **改进建议**:若教材对所有学生开放,改用 `getAuthContext()` 仅做认证;若应按年级过滤,则 `getTextbooks` 增加 `grade` 参数
---
### 2.4 `learning/textbooks/[id]/page.tsx` — 严重度:中
#### BUG-TD01`student` 变量未使用
- **位置**`src/app/(dashboard)/student/learning/textbooks/[id]/page.tsx:18-31`
- **问题**:同 BUG-T03`student` 仅用于 "No user" 检查,教材内容查询不依赖学生身份
- **改进建议**:同 BUG-T03
#### BUG-TD02错误处理不一致
- **位置**`src/app/(dashboard)/student/learning/textbooks/[id]/page.tsx:19, 41`
- **问题**
- `if (!student)` 返回完整 `EmptyState` 页面
- `if (!textbook) notFound()` 抛出 404
同一文件内两种错误处理模式
- **改进建议**:统一为 `notFound()` 或都返回 `EmptyState`
#### BUG-TD03装饰性 `<span>` 缺少 `aria-hidden`
- **位置**`src/app/(dashboard)/student/learning/textbooks/[id]/page.tsx:49`
- **问题**
```tsx
<span className="hidden sm:inline-block w-px h-4 bg-border" />
```
纯装饰性分隔线,屏幕阅读器会朗读空内容
- **规范依据**Web Interface Guidelines — Accessibility「装饰性元素应 `aria-hidden`」
- **改进建议**:添加 `aria-hidden="true"`
---
### 2.5 `dashboard/page.tsx` — 严重度:中
#### BUG-D01`as` 类型断言违规
- **位置**`src/app/(dashboard)/student/dashboard/page.tsx:11`
- **问题**
```tsx
return (day === 0 ? 7 : day) as 1 | 2 | 3 | 4 | 5 | 6 | 7
```
- **规范依据**:编码规范 4.2.3「禁止 `as` 断言(除非从 `unknown` 转换)」
- **改进建议**:使用类型守卫或重写为:
```tsx
const toWeekday = (d: Date): 1 | 2 | 3 | 4 | 5 | 6 | 7 => {
const day = d.getDay()
const weekday = day === 0 ? 7 : day
if (weekday < 1 || weekday > 7) throw new Error(`Invalid weekday: ${weekday}`)
return weekday
}
```
#### BUG-D02缺少页面标题容器
- **位置**`src/app/(dashboard)/student/dashboard/page.tsx:76-87`
- **问题**:其他 student 页面都有 `<div className="... p-8"><div><h2>...</h2><p>...</p></div>...</div>` 容器,本页面直接返回 `<StudentDashboard />`,无外层 padding 和标题
- **影响**:视觉上与其他页面不一致(无 padding无标题
- **改进建议**:添加统一的页面容器和标题,或将容器抽到 `layout.tsx`
#### BUG-D03`EmptyState` 的 `icon` 使用 `Inbox` 不合适
- **位置**`src/app/(dashboard)/student/dashboard/page.tsx:5, 22`
- **问题**:用户未找到时显示 `Inbox` 图标,语义不匹配
- **改进建议**:使用 `UserX` 或 `UserMinus` 图标
---
### 2.6 `schedule/page.tsx` — 严重度:低
#### BUG-S01嵌套三元表达式可读性差
- **位置**`src/app/(dashboard)/student/schedule/page.tsx:38`
- **问题**
```tsx
const classId = typeof classIdParam === "string" ? classIdParam : Array.isArray(classIdParam) ? classIdParam[0] : "all"
```
- **规范依据**:编码规范「避免嵌套三元」
- **改进建议**:抽为独立函数或使用 if 语句
#### BUG-S02`searchParams` 类型未共享
- **位置**`src/app/(dashboard)/student/schedule/page.tsx:11` 和 `learning/textbooks/page.tsx:11`
- **问题**:两处定义相同的 `type SearchParams = { [key: string]: string | string[] | undefined }`
- **改进建议**:抽取到 `shared/types` 或 `shared/lib/utils`
---
### 2.7 `elective/page.tsx` — 严重度:中
#### BUG-E01直接调用 `auth()` 绕过 auth-guard
- **位置**`src/app/(dashboard)/student/elective/page.tsx:1, 11`
- **问题**
```tsx
import { auth } from "@/auth"
...
const session = await auth()
const studentId = String(session?.user?.id ?? "")
```
- **规范依据**项目规则「app 层不应直接依赖 `@/auth`」+ 编码规范 8.3
- **改进建议**:改用 `getAuthContext()`
```tsx
const ctx = await getAuthContext()
const studentId = ctx.userId
```
#### BUG-E02`String(... ?? "")` 模式冗余
- **位置**`src/app/(dashboard)/student/elective/page.tsx:12`
- **问题**`String(session?.user?.id ?? "")` — `session.user.id` 已是 string 类型,`String()` 包裹多余
- **改进建议**:使用 `getAuthContext()` 后直接用 `ctx.userId`
---
### 2.8 `student-courses-view.tsx` — 严重度:中
#### BUG-C01catch 块吞掉错误
- **位置**`src/modules/student/components/student-courses-view.tsx:39-41`
- **问题**
```tsx
} catch {
toast.error("Failed to join class")
}
```
错误被吞掉,无 `console.error` 记录,调试困难
- **规范依据**:编码规范 9.2「错误必须可观测」
- **改进建议**
```tsx
} catch (err) {
console.error("[joinClass] failed:", err)
toast.error("Failed to join class")
}
```
#### BUG-C02未使用 `useFormStatus` / `useTransition`
- **位置**`src/modules/student/components/student-courses-view.tsx:26-44`
- **问题**:使用本地 `useState` + `setIsWorking` 管理 loading 状态,但 Next.js 16 推荐 `useFormStatus`(用于 `<form action>`)或 `useTransition`
- **违反规则**`rerender-transitions`、`rendering-usetransition-loading`
- **改进建议**:使用 `useTransition` 包裹 Server Action 调用
#### BUG-C03表单缺少客户端格式校验
- **位置**`src/modules/student/components/student-courses-view.tsx:136-148`
- **问题**`maxLength={6}` 和 `inputMode="numeric"` 不阻止字母输入,用户可提交 "abcdef"
- **改进建议**:添加 `pattern="\d{6}"` 或 `onChange` 过滤非数字字符
---
### 2.9 `student-schedule-view.tsx` — 严重度:低
#### BUG-SV01`for...of` 修改 Map 后再次 set
- **位置**`src/modules/student/components/student-schedule-view.tsx:36-39`
- **问题**
```tsx
for (const [day, list] of itemsByDay) {
list.sort((a, b) => a.startTime.localeCompare(b.startTime))
itemsByDay.set(day, list) // 多余sort 已 in-place
}
```
`list.sort()` 已原地排序,`itemsByDay.set(day, list)` 多余
- **改进建议**:删除 `itemsByDay.set(day, list)` 行
---
### 2.10 跨文件一致性 — 严重度:中
#### BUG-X01页面容器 className 不统一
- **位置**:多个页面
- **问题**:存在 4 种不同的容器写法:
| className | 使用页面 |
|-----------|---------|
| `h-full flex-1 flex-col space-y-8 p-8 md:flex` | attendance / diagnostic / grades / learning/textbooks / learning/textbooks/[id] |
| `flex h-full flex-col space-y-8 p-8` | elective / learning/courses / schedule |
| `flex h-full flex-col space-y-4 p-6` | learning/assignments/[assignmentId] |
| `h-full flex-1 flex-col space-y-8 p-8 md:flex` | learning/assignments |
| 无容器 | dashboard |
- **改进建议**:抽取到 `student/layout.tsx` 的 `<main>` 中,统一 padding 和布局
#### BUG-X02"No user" 处理方式不一致
- **位置**:多个页面
- **问题**
- `dashboard`、`learning/courses`、`learning/textbooks`、`learning/textbooks/[id]`、`schedule`、`learning/assignments`、`elective`:返回 `EmptyState`
- `learning/assignments/[assignmentId]`:调用 `notFound()`
- `attendance`、`grades`:返回 `EmptyState`(但检查的是 `summary` 而非 `student`
- **改进建议**:未认证场景应由 `proxy.ts` 拦截,页面内统一用 `notFound()` 或统一 `EmptyState`
#### BUG-X03图标选择不一致
- **位置**:多个页面
- **问题**:同样 "No user found" 场景使用不同图标:
- `dashboard`、`learning/courses`、`learning/textbooks`、`schedule`、`learning/assignments``Inbox`
- `attendance``CalendarCheck`
- `grades``GraduationCap`
- `elective``Inbox`
- **改进建议**:未认证场景统一使用 `UserX`;空数据场景使用业务相关图标
---
## 三、React 性能优化(应用 `vercel-react-best-practices` 技能)
### PERF-01`student-courses-view.tsx` 未使用 `useTransition`
- **位置**`src/modules/student/components/student-courses-view.tsx:28-44`
- **问题**Server Action `joinClassByInvitationCodeAction` 用 `useState` 管理 loading阻塞 UI 线程
- **违反规则**`rerender-transitions`、`rendering-usetransition-loading`
- **改进建议**
```tsx
const [isPending, startTransition] = useTransition()
const handleJoin = async (formData: FormData) => {
startTransition(async () => {
const res = await joinClassByInvitationCodeAction(null, formData)
...
})
}
```
### PERF-02`student-courses-view.tsx` 卡片列表未 memoize
- **位置**`src/modules/student/components/student-courses-view.tsx:50-108`
- **问题**`classes.map(...)` 每次渲染都重新创建 6+ 个 Card 元素,当 `code` 输入变化时所有卡片重渲染
- **违反规则**`rerender-memo`、`rerender-no-inline-components`
- **改进建议**:抽取 `<ClassCard class={c} />` 组件并用 `React.memo` 包裹
### PERF-03`student-schedule-filters.tsx` 的 `options` 依赖 `classes` 引用
- **位置**`src/modules/student/components/student-schedule-filters.tsx:12`
- **问题**`useMemo(() => [...classes], [classes])` — 父组件每次传入新 `classes` 数组引用时都重新计算
- **违反规则**`rerender-dependencies`
- **现状**:已用 `useMemo`,但依赖项是父组件传入的 prop若父组件不 memoize 则失效
- **改进建议**:可接受,但建议父组件 `schedule/page.tsx` 用 `React.cache` 或在 Server Component 层面稳定引用
### PERF-04`dashboard/page.tsx` 多次 `filter` 遍历同一数组
- **位置**`src/app/(dashboard)/student/dashboard/page.tsx:40-52`
- **问题**
```tsx
const dueSoonCount = assignments.filter(...).length
const overdueCount = assignments.filter(...).length
const gradedCount = assignments.filter(...).length
```
3 次遍历同一数组O(3n)
- **违反规则**`js-combine-iterations`
- **改进建议**:单次遍历统计:
```tsx
let dueSoonCount = 0, overdueCount = 0, gradedCount = 0
for (const a of assignments) {
if (a.progressStatus === "graded") { gradedCount++; continue }
if (!a.dueAt) continue
const due = new Date(a.dueAt)
if (due >= now && due <= in7Days) dueSoonCount++
else if (due < now) overdueCount++
}
```
### PERF-05`learning/assignments/page.tsx` 重复 `filter` 调用
- **位置**`src/app/(dashboard)/student/learning/assignments/page.tsx:73-74`
- **问题**
```tsx
const answeredItems = items.filter((a) => isAnswered(a.progressStatus))
const unansweredItems = items.filter((a) => !isAnswered(a.progressStatus))
```
2 次遍历同一数组
- **违反规则**`js-combine-iterations`
- **改进建议**:单次遍历分桶:
```tsx
const { answered, unanswered } = items.reduce(
(acc, a) => {
isAnswered(a.progressStatus) ? acc.answered.push(a) : acc.unanswered.push(a)
return acc
},
{ answered: [], unanswered: [] }
)
```
### PERF-06`student-schedule-view.tsx` 在渲染期构建 Map
- **位置**`src/modules/student/components/student-schedule-view.tsx:30-39`
- **问题**:每次渲染都重建 `itemsByDay` Map 并排序
- **现状**:作为 Server Component 每次请求只渲染一次,影响较小
- **违反规则**`js-cache-function-results`(轻度)
- **改进建议**:可接受,若未来改为 Client Component 则需 `useMemo`
### PERF-07`dashboard/page.tsx` 已正确使用 `Promise.all` 并行获取
- **位置**`src/app/(dashboard)/student/dashboard/page.tsx:29-34`
- **现状**:✅ 符合 `async-parallel` 规则
- **说明**4 个独立查询并行执行,无需优化
### PERF-08`diagnostic/page.tsx` 已正确使用 `Promise.all`
- **位置**`src/app/(dashboard)/student/diagnostic/page.tsx:12-15`
- **现状**:✅ 符合 `async-parallel` 规则
### PERF-09`schedule/page.tsx` 已正确使用 `Promise.all`
- **位置**`src/app/(dashboard)/student/schedule/page.tsx:31-35`
- **现状**:✅ 符合 `async-parallel` 规则
---
## 四、Web 界面规范审查(应用 `web-design-guidelines` 技能)
### UI-01缺少 `loading.tsx` 导致白屏
- **位置**attendance / diagnostic / elective / grades / learning/assignments / learning/assignments/[assignmentId] / learning/textbooks / learning/textbooks/[id]
- **问题**8 个路由无骨架屏,跳转时显示白屏,违反 "Perceived Performance" 原则
- **违反规则**Web Interface Guidelines — Perceived Performance「Always show loading state」
- **改进建议**:为每个缺失的路由添加 `loading.tsx`,参考已有的 `dashboard/loading.tsx` 模式
### UI-02缺少 `error.tsx` 错误边界
- **位置**:整个 student 路由组
- **问题**Server Component 抛错时显示 Next.js 默认错误页,无法"恢复"
- **违反规则**Web Interface Guidelines — Error Handling「Provide recoverable error states」
- **改进建议**:在 `student/error.tsx` 中提供"重试"按钮:
```tsx
"use client"
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
return (
<EmptyState
title="Something went wrong"
description={error.message}
action={{ label: "Try again", onClick: reset }}
/>
)
}
```
### UI-03装饰性元素缺少 `aria-hidden`
- **位置**
- `learning/textbooks/[id]/page.tsx:49` — 分隔线 `<span>`
- `learning/assignments/page.tsx:98, 139` — `<span className="px-2">•</span>` 分隔点
- `learning/assignments/[assignmentId]/page.tsx:45` — `<span className="mx-2">•</span>`
- `student-courses-view.tsx:63` — `<span>•</span>`
- **问题**:屏幕阅读器会朗读"•"或空内容
- **违反规则**Web Interface Guidelines — Accessibility「Decorative elements need `aria-hidden`」
- **改进建议**:所有装饰性 `<span>` 添加 `aria-hidden="true"`
### UI-04图标缺少 `aria-hidden` 或 `aria-label`
- **位置**:所有页面使用的 lucide-react 图标
- **问题**lucide 图标默认 `aria-hidden="true"`,但部分图标作为唯一内容(如按钮内仅图标)时需 `aria-label`
- **现状**:本目录内图标均伴随文字,符合规范 ✅
### UI-05表单缺少 `noValidate` 和客户端校验
- **位置**`student-courses-view.tsx:133`
- **问题**`<form action={handleJoin}>` 依赖 HTML5 原生校验(`required`),但未添加数字格式校验
- **违反规则**Web Interface Guidelines — Forms「Validate on client before submit」
- **改进建议**:添加 `pattern="\d{6}"` 或自定义校验
### UI-06链接缺少 `prefetch` 控制
- **位置**`learning/assignments/page.tsx:88, 110, 129, 151`、`student-courses-view.tsx:94, 100`
- **问题**`<Link href="/student/learning/assignments/...">` 默认 prefetch但作业详情页数据量大可能浪费带宽
- **违反规则**`bundle-preload`(轻度)
- **改进建议**:对详情页链接添加 `prefetch={false}` 或在 hover 时预加载
### UI-07`hover:shadow-md` 可能导致性能问题
- **位置**`learning/assignments/page.tsx:84, 125`、`student-courses-view.tsx:51`
- **问题**:大量卡片同时绑定 `hover:shadow-md`,低端设备滚动时可能卡顿
- **违反规则**`rendering-content-visibility`(轻度)
- **改进建议**:长列表添加 `content-visibility: auto` 或使用 `will-change: shadow`
### UI-08颜色对比度待验证
- **位置**`text-muted-foreground` 在多处用于次要信息
- **问题**`text-muted-foreground` 在浅色模式下对比度可能不足 WCAG AA 标准4.5:1
- **违反规则**Web Interface Guidelines — Accessibility「Color contrast WCAG AA」
- **改进建议**:使用工具验证 `--muted-foreground` 与 `--background` 的对比度
### UI-09`tabular-nums` 使用正确 ✅
- **位置**`learning/assignments/page.tsx:107, 148`、`student-schedule-view.tsx:64`
- **现状**:数字列使用 `tabular-nums`,对齐良好 ✅
### UI-10响应式断点一致 ✅
- **位置**:所有页面
- **现状**:统一使用 `md:`、`lg:`、`xl:` 断点,符合规范 ✅
---
## 五、架构文档同步问题
### DOC-01005 JSON 中 `/student/grades` 的 dataAccess 记录错误
- **位置**`docs/architecture/005_architecture_data.json:12047`
- **问题**:记录为 `"grades/actions.getStudentGradeSummaryAction"`,实际代码调用 `grades/data-access.getStudentGradeSummary`
- **改进建议**:修正为 `"grades/data-access.getStudentGradeSummary"`
### DOC-02005 JSON 中 `/student/learning/textbooks/[id]` 的 type 错误
- **位置**`docs/architecture/005_architecture_data.json:12024`
- **问题**:记录 `"type": "client"`,实际 `page.tsx` 是 Server Component无 `"use client"`
- **改进建议**:改为 `"type": "server"`,并注明内部 `TextbookReader` 组件为 client
### DOC-03005 JSON 中 `/student/diagnostic` 的 type 错误
- **位置**`docs/architecture/005_architecture_data.json:12063`
- **问题**:记录 `"type": "client"`,实际 `page.tsx` 是 Server Component
- **改进建议**:改为 `"type": "server"`,并注明内部 `StudentDiagnosticView` 组件为 client
### DOC-04005 JSON 中 `/student/dashboard` 缺少 `getStudentSchedule` 记录
- **位置**`docs/architecture/005_architecture_data.json:11978-11982`
- **问题**:实际代码调用了 `getStudentSchedule(student.id)`,但 dataAccess 数组未记录
- **改进建议**:补充 `"classes/data-access.getStudentSchedule"`
### DOC-05005 JSON 中 `/student/learning/assignments` 缺少 `getDemoStudentUser` 记录
- **位置**`docs/architecture/005_architecture_data.json:11989-11991`
- **问题**:实际代码调用了 `getDemoStudentUser()`,但 dataAccess 数组未记录
- **改进建议**:补充 `"homework/data-access.getDemoStudentUser"`(或迁移后更新为 `users/data-access.getCurrentStudentUser`
### DOC-06004 文档 2.26 节未记录 `loading.tsx` 文件
- **位置**`docs/architecture/004_architecture_impact_map.md:1109-1114`
- **问题**:文件清单仅列出 3 个组件,未记录 `app/(dashboard)/student/*/loading.tsx`
- **改进建议**:补充 loading.tsx 文件清单
### DOC-07004 文档未记录 student 路由的认证模式不一致问题
- **位置**`docs/architecture/004_architecture_impact_map.md:1095-1115`
- **问题**2.26 节"已知问题"未提及三种认证模式混用
- **改进建议**:在"已知问题"中补充 P1 项「认证模式不一致」
---
## 六、问题汇总统计
| 严重度 | 数量 | 问题编号 |
|--------|------|----------|
| 高 | 4 | BUG-A01, BUG-A02, BUG-A03, BUG-L01 |
| 中 | 10 | BUG-L02, BUG-L04, BUG-L05, BUG-T01, BUG-T02, BUG-T03, BUG-TD01, BUG-TD02, BUG-D01, BUG-D02, BUG-E01, BUG-C01, BUG-X01, BUG-X02, BUG-X03 |
| 低 | 6 | BUG-L03, BUG-L06, BUG-D03, BUG-S01, BUG-S02, BUG-E02, BUG-C02, BUG-C03, BUG-SV01 |
| 性能 | 6 | PERF-01, PERF-02, PERF-03, PERF-04, PERF-05, PERF-06 |
| 界面 | 8 | UI-01, UI-02, UI-03, UI-05, UI-06, UI-07, UI-08, UI-09 |
| 文档 | 7 | DOC-01 ~ DOC-07 |
| **合计** | **41** | |
---
## 七、修复优先级建议
### P0立即修复 — 影响正确性和架构合规性)
1. **BUG-A01 + BUG-A02 + BUG-A03**:将 `getDemoStudentUser` 迁移到 `users/data-access.ts` 并重命名为 `getCurrentStudentUser()`,所有 student 页面统一使用 `getAuthContext()` 或新函数
2. **BUG-E01**`elective/page.tsx` 改用 `getAuthContext()`,移除 `import { auth } from "@/auth"`
3. **BUG-L01**`learning/assignments/page.tsx` 中英文混排修正
4. **BUG-L03**`learning/assignments/page.tsx:162` JSX 语法格式错误修正
5. **BUG-T01**`learning/textbooks/page.tsx` 删除注释代码
### P1本迭代修复 — 影响可维护性和一致性)
6. **BUG-X01**:创建 `student/layout.tsx` 统一页面容器
7. **UI-01**:为 8 个缺失路由添加 `loading.tsx`
8. **UI-02**:创建 `student/error.tsx` 错误边界
9. **BUG-L02**:抽取 `AssignmentCard` 组件消除重复
10. **BUG-D01**:移除 `as` 类型断言
11. **BUG-L04**`getStatusVariant` 区分 submitted / in_progress
12. **BUG-X02 + BUG-X03**:统一 "No user" 处理和图标选择
13. **PERF-01 + PERF-02**`student-courses-view.tsx` 使用 `useTransition` + memoize 卡片
14. **PERF-04 + PERF-05**:合并重复 `filter` 遍历
### P2下迭代修复 — 增强健壮性)
15. **BUG-L05 + BUG-L06**:类型精确化
16. **BUG-T02 + BUG-D02**:补全页面标题
17. **BUG-TD01 + BUG-TD03**:清理未使用变量 + `aria-hidden`
18. **UI-03**:所有装饰性 `<span>` 添加 `aria-hidden`
19. **UI-05**:表单客户端校验
20. **BUG-C01 + BUG-C02 + BUG-C03**`student-courses-view.tsx` 错误处理 + 表单优化
21. **BUG-S01 + BUG-S02 + BUG-SV01**:小重构
### P3文档同步
22. **DOC-01 ~ DOC-07**:同步 004 和 005 架构文档
---
## 八、`student/layout.tsx` 推荐实现
```tsx
import type { ReactNode } from "react"
export default function StudentLayout({ children }: { children: ReactNode }) {
return (
<div className="h-full flex-1 flex-col space-y-8 p-8 md:flex">
{children}
</div>
)
}
```
> 注:`dashboard/page.tsx` 和 `learning/assignments/[assignmentId]/page.tsx` 若需不同 padding可在页面内覆盖。
---
## 九、`AssignmentCard` 组件推荐实现(消除 BUG-L02 重复)
```tsx
import Link from "next/link"
import { Badge } from "@/shared/components/ui/badge"
import { Button } from "@/shared/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/shared/components/ui/card"
import { formatDate } from "@/shared/lib/utils"
import type { StudentHomeworkAssignment } from "@/modules/homework/types"
const getStatusVariant = (status: StudentHomeworkProgressStatus): "default" | "secondary" | "outline" => {
switch (status) {
case "graded": return "default"
case "submitted": return "secondary"
case "in_progress": return "outline" // BUG-L04 修复:区分 in_progress
default: return "outline"
}
}
export function AssignmentCard({ assignment: a }: { assignment: StudentHomeworkAssignment }) {
return (
<Card className="flex h-full flex-col overflow-hidden transition-all hover:shadow-md">
<CardHeader className="gap-2 pb-3">
<div className="flex items-start justify-between gap-3">
<CardTitle className="text-base">
<Link href={`/student/learning/assignments/${a.id}`} className="hover:underline">
{a.title}
</Link>
</CardTitle>
<Badge variant={getStatusVariant(a.progressStatus)} className="capitalize">
{getStatusLabel(a.progressStatus)}
</Badge>
</div>
<div className="text-xs text-muted-foreground">
<span>Due {a.dueAt ? formatDate(a.dueAt) : "-"}</span>
<span className="px-2" aria-hidden="true">•</span>
<span>Attempts {a.attemptsUsed}/{a.maxAttempts}</span>
</div>
</CardHeader>
<CardContent className="mt-auto flex items-center justify-between">
<div className="text-sm">
<div className="text-muted-foreground">Score</div>
<div className="font-medium tabular-nums">{a.latestScore ?? "-"}</div>
</div>
<Button asChild size="sm" variant={getActionVariant(a.progressStatus)}>
<Link href={`/student/learning/assignments/${a.id}`}>
{getActionLabel(a.progressStatus)}
</Link>
</Button>
</CardContent>
</Card>
)
}
```
---
## 十、验证命令
修复完成后应运行以下命令确保零错误:
```bash
npm run lint
npx tsc --noEmit
npm run test:unit -- student
```
---
> 报告生成人AI AgentGLM-5.2
> 核查方法:人工逐行审查 + 架构图比对 + 技能规则匹配
> 应用技能:`vercel-react-best-practices`(性能优化)、`web-artifacts-builder`(界面构建参考)、`web-design-guidelines`(界面规范审查)

741
bugs/teacher_bug.md Normal file
View File

@@ -0,0 +1,741 @@
# `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-T01app 层直接访问数据库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-T02app 层直接访问数据库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-T03app 层直接访问数据库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-T04app 层直接访问数据库grades/entry/page.tsx
- **位置**[grades/entry/page.tsx:1-3, 25](../src/app/(dashboard)/teacher/grades/entry/page.tsx)
- **问题**:同 BUG-T02
- **改进建议**:同 BUG-T02
#### BUG-T05app 层直接访问数据库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-T07textbooks/page.tsx 使用分号
- **位置**[textbooks/page.tsx:3, 73](../src/app/(dashboard)/teacher/textbooks/page.tsx)
- **问题**`import { TextbookCard } from "...";` 等多处使用分号
- **改进建议**:运行 `npx prettier --write` 统一格式
#### BUG-T08textbooks/[id]/page.tsx 使用分号
- **位置**[textbooks/[id]/page.tsx](../src/app/(dashboard)/teacher/textbooks/[id]/page.tsx)(全文)
- **问题**:多处语句使用分号结尾
- **改进建议**:同 BUG-T07
#### BUG-T09textbooks/loading.tsx 使用分号
- **位置**[textbooks/loading.tsx](../src/app/(dashboard)/teacher/textbooks/loading.tsx)(全文)
- **问题**:同 BUG-T07
- **改进建议**:同 BUG-T07
#### BUG-T10textbooks/[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串行数据获取 waterfallattendance/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串行数据获取 waterfallattendance/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串行数据获取 waterfallattendance/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串行数据获取 waterfallgrades/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串行数据获取 waterfallgrades/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串行数据获取 waterfallgrades/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串行数据获取 waterfallclasses/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串行数据获取 waterfalldiagnostic/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串行数据获取 waterfallexams/[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-T29Bundle 优化 - barrel importslucide-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-T39Flex 子元素缺少 `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-T46exams/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-T52exams/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-T53homework/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-T54exams/[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-T55exams/[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-T56grades/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-T57exams/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-T61homework/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-T62textbooks/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-T63exams/create/page.tsx 缺少页面标题
- **位置**[exams/create/page.tsx:3-9](../src/app/(dashboard)/teacher/exams/create/page.tsx)
- **问题**:页面无任何标题,直接渲染表单
- **改进建议**:添加 `<h1>Create Exam</h1>`
#### BUG-T64loading.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 顺序修复,优先解决架构与安全问题。