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:
258
bugs/001_first_login_onboarding.md
Normal file
258
bugs/001_first_login_onboarding.md
Normal 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 | 角色选择 | role(student/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 Monorepo(turborepo / 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 # completeOnboardingAction(Server Action + requirePermission)
|
||||
├─ data-access.ts # 仅操作 users.onboardedAt
|
||||
├─ schema.ts # Zod:name/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**:暂不实现家长绑定,由管理员后台预绑定。
|
||||
|
||||
### Q4:onboarding 路由形态
|
||||
- **方案 A**(推荐):单页 `/onboarding` + 客户端 stepper(步骤状态用 query param 持久化)。
|
||||
- **方案 B**:嵌套路由 `/onboarding/role`、`/onboarding/profile`、`/onboarding/binding`(每步独立 Server Action)。
|
||||
- **方案 C**:保留全局 Dialog,仅修复安全与架构问题。
|
||||
|
||||
### Q5:实施范围
|
||||
- **方案 A**:一次性完成 P0 + P1 + P2 全部整改。
|
||||
- **方案 B**:先做 P0(安全/越权)+ P1(架构),P2(UX)后续迭代。
|
||||
- **方案 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
548
bugs/admin_bug.md
Normal file
@@ -0,0 +1,548 @@
|
||||
# Admin 前端文件规范核查报告
|
||||
|
||||
> 核查范围:`src/app/(dashboard)/admin/` 下全部 26 个 `page.tsx` 文件
|
||||
> 核查依据:
|
||||
> - `.trae/rules/project_rules.md`(项目规则)
|
||||
> - `docs/standards/coding-standards.md`(编码规范 v1.0)
|
||||
> - `docs/architecture/004_architecture_impact_map.md`(架构影响地图)
|
||||
> - React / Next.js 16 最佳实践
|
||||
> - Web 界面设计规范(WCAG 2.2 AA)
|
||||
> 核查日期:2026-06-18
|
||||
|
||||
---
|
||||
|
||||
## 一、核查概览
|
||||
|
||||
| 维度 | 文件数 | 通过 | 待改进 |
|
||||
|------|--------|------|--------|
|
||||
| 架构分层 | 26 | 24 | 2 |
|
||||
| TypeScript 规范 | 26 | 4 | 22 |
|
||||
| 安全与权限 | 26 | 3 | 23 |
|
||||
| UI 一致性与设计令牌 | 26 | 18 | 8 |
|
||||
| 错误与加载边界 | 26 | 0 | 26 |
|
||||
| 代码复用(DRY) | 26 | 0 | 26 |
|
||||
|
||||
**结论**:整体架构清晰、服务端组件使用规范、并行数据获取到位,但在**返回类型标注、权限校验一致性、加载/错误边界、代码复用、UI 文案一致性**方面存在系统性问题,需统一整改。
|
||||
|
||||
---
|
||||
|
||||
## 二、问题清单(按严重程度排序)
|
||||
|
||||
### P0 严重问题(必须立即修复)
|
||||
|
||||
#### P0-1 全部 26 个页面缺少 `error.tsx` 与 `loading.tsx`
|
||||
|
||||
**违反规范**:
|
||||
- 编码规范 §2.3:「每个路由段应提供 `loading.tsx`(骨架屏)和 `error.tsx`(错误边界)」
|
||||
- 编码规范 §2.4:「每个路由段都必须提供 `error.tsx`,不得出现未捕获异常导致白屏」
|
||||
- 编码规范 §2.4:「`loading.tsx` 必须提供骨架屏或最小可感知的加载状态,不得使用全局 spin 遮罩」
|
||||
|
||||
**现状**:`src/app/(dashboard)/admin/` 及其所有子路由(`dashboard/`、`announcements/`、`school/*`、`audit-logs/*`、`scheduling/*`、`course-plans/*`、`elective/*`、`attendance/`、`files/`、`users/import/`)均**未提供** `loading.tsx` 和 `error.tsx`。
|
||||
|
||||
对比:`teacher/`、`student/` 路由组在关键页面已提供 `loading.tsx`(如 `teacher/exams/all/loading.tsx`、`student/dashboard/loading.tsx`),admin 路由组完全缺失。
|
||||
|
||||
**影响**:
|
||||
- 数据获取失败时整页白屏,用户体验差
|
||||
- 无加载态感知,用户误以为页面卡死
|
||||
- 不符合 Next.js 16 App Router 最佳实践(Suspense 流式渲染)
|
||||
|
||||
**修复建议**:
|
||||
1. 在 `src/app/(dashboard)/admin/` 根目录新增 `error.tsx`(具名导出,客户端组件,含重试按钮)
|
||||
2. 在 `src/app/(dashboard)/admin/` 根目录新增 `loading.tsx`(骨架屏,匹配各页面布局)
|
||||
3. 对数据量大的页面(`audit-logs/*`、`school/grades/insights`、`attendance`)单独提供 `loading.tsx`
|
||||
4. 对动态路由(`[id]/page.tsx`)单独提供 `error.tsx` 处理 `notFound` 以外的异常
|
||||
|
||||
---
|
||||
|
||||
#### P0-2 `attendance/page.tsx` 缺少权限校验
|
||||
|
||||
**文件**:[src/app/(dashboard)/admin/attendance/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/attendance/page.tsx)
|
||||
|
||||
**违反规范**:
|
||||
- 项目规则:「Server Action 必须使用 `requirePermission()` 进行权限校验」
|
||||
- 编码规范 §8.3:「权限校验:Server Action 必须使用 `requirePermission()`」
|
||||
|
||||
**现状**:第 26 行仅调用 `getAuthContext()` 获取上下文,**未调用 `requirePermission()`** 验证用户是否有考勤查看权限。
|
||||
|
||||
```tsx
|
||||
// 当前代码(第 26 行)
|
||||
const ctx = await getAuthContext()
|
||||
```
|
||||
|
||||
**对比**:同类 admin 页面均做了权限校验:
|
||||
- `audit-logs/page.tsx` 第 22 行:`await requirePermission(Permissions.AUDIT_LOG_READ)`
|
||||
- `audit-logs/login-logs/page.tsx` 第 22 行:`await requirePermission(Permissions.AUDIT_LOG_READ)`
|
||||
- `audit-logs/data-changes/page.tsx` 第 26 行:`await requirePermission(Permissions.AUDIT_LOG_READ)`
|
||||
- `files/page.tsx` 第 12 行:`await requirePermission(Permissions.FILE_READ)`
|
||||
|
||||
**影响**:越权风险——无考勤查看权限的用户可直接访问 `/admin/attendance` 查看全校考勤数据。
|
||||
|
||||
**修复建议**:在 `getAuthContext()` 前增加权限校验:
|
||||
```tsx
|
||||
await requirePermission(Permissions.ATTENDANCE_READ)
|
||||
const ctx = await getAuthContext()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### P1 重要问题(应尽快修复)
|
||||
|
||||
#### P1-1 全部 26 个页面组件缺少返回类型标注
|
||||
|
||||
**违反规范**:
|
||||
- 编码规范 §4.2:「函数返回值必须显式标注,特别是 `Promise<T>`」
|
||||
- 项目规则:「函数返回值必须显式标注」
|
||||
|
||||
**现状**:所有 `page.tsx` 的默认导出函数均未标注返回类型,例如:
|
||||
|
||||
```tsx
|
||||
// dashboard/page.tsx
|
||||
export default async function AdminDashboardPage() { // ❌ 缺少 : Promise<JSX.Element>
|
||||
const data = await getAdminDashboardData()
|
||||
return <AdminDashboardView data={data} />
|
||||
}
|
||||
```
|
||||
|
||||
**影响**:26 个文件全部不合规,类型推导依赖 TS 隐式推断,不利于代码审查与维护。
|
||||
|
||||
**修复建议**:统一补充返回类型:
|
||||
```tsx
|
||||
export default async function AdminDashboardPage(): Promise<JSX.Element> {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
涉及文件:admin 目录下全部 26 个 `page.tsx`。
|
||||
|
||||
---
|
||||
|
||||
#### P1-2 `getParam` 工具函数在 27 个文件中重复定义
|
||||
|
||||
**违反规范**:
|
||||
- 编码规范 §一:「单一职责」「工具函数 ≤ 40 行」
|
||||
- DRY 原则
|
||||
|
||||
**现状**:以下 admin 文件各自重复定义了相同的 `getParam` / `SearchParams` 类型与函数:
|
||||
|
||||
| 文件 | 行号 |
|
||||
|------|------|
|
||||
| `announcements/page.tsx` | 8-13 |
|
||||
| `audit-logs/page.tsx` | 10-15 |
|
||||
| `audit-logs/login-logs/page.tsx` | 10-15 |
|
||||
| `audit-logs/data-changes/page.tsx` | 14-19 |
|
||||
| `scheduling/changes/page.tsx` | 16-21 |
|
||||
| `course-plans/page.tsx` | 7-12 |
|
||||
| `elective/page.tsx` | 7-12 |
|
||||
| `attendance/page.tsx` | 13-18 |
|
||||
| `school/grades/insights/page.tsx` | 15-22 |
|
||||
|
||||
全项目共 27 个文件重复(含 teacher / student / management 路由组)。
|
||||
|
||||
**影响**:维护成本高,任何一处逻辑变更需同步修改 27 处。
|
||||
|
||||
**修复建议**:
|
||||
1. 在 `src/shared/lib/utils.ts` 新增共享工具:
|
||||
```tsx
|
||||
export type SearchParams = { [key: string]: string | string[] | undefined }
|
||||
|
||||
export function getSearchParam(params: SearchParams, key: string): string | undefined {
|
||||
const v = params[key]
|
||||
return Array.isArray(v) ? v[0] : v
|
||||
}
|
||||
```
|
||||
2. 全部页面改为 `import { getSearchParam, type SearchParams } from "@/shared/lib/utils"`
|
||||
3. 同步更新架构文档 004 / 005
|
||||
|
||||
---
|
||||
|
||||
#### P1-3 多个页面使用 `as` 类型断言违反 TypeScript 规范
|
||||
|
||||
**违反规范**:
|
||||
- 编码规范 §4.2:「不使用 `as` 断言,除非从 `unknown` 强制转换或在测试中(需注释原因)」
|
||||
- 项目规则:「禁止 `as` 断言(除非从 `unknown` 转换或测试中,需注释原因)」
|
||||
|
||||
**现状**:以下文件使用 `as` 进行类型断言而非类型守卫:
|
||||
|
||||
| 文件 | 行号 | 问题代码 |
|
||||
|------|------|---------|
|
||||
| `audit-logs/page.tsx` | 28 | `(getParam(params, "status") as AuditLogStatus \| undefined)` |
|
||||
| `audit-logs/login-logs/page.tsx` | 26-27 | `as LoginLogAction \| undefined`、`as LoginLogStatus \| undefined` |
|
||||
| `audit-logs/data-changes/page.tsx` | 31 | `as DataChangeAction \| undefined` |
|
||||
| `attendance/page.tsx` | 39 | `as "present" \| "absent" \| "late" \| "early_leave" \| "excused"` |
|
||||
|
||||
**对比(正确示例)**:以下文件已使用类型守卫,应作为模板推广:
|
||||
- `announcements/page.tsx` 第 15-16 行:`isValidStatus` 类型守卫
|
||||
- `scheduling/changes/page.tsx` 第 23-24 行:`isValidStatus` 类型守卫
|
||||
- `course-plans/page.tsx` 第 14-15 行:`isValidStatus` 类型守卫
|
||||
- `elective/page.tsx` 第 14-15 行:`isValidStatus` 类型守卫
|
||||
|
||||
**影响**:运行时无法捕获非法枚举值,类型安全被绕过。
|
||||
|
||||
**修复建议**:为每个枚举类型补充类型守卫,替换 `as` 断言:
|
||||
```tsx
|
||||
const isValidAuditLogStatus = (v?: string): v is AuditLogStatus =>
|
||||
v === "success" || v === "failure" || v === "pending"
|
||||
|
||||
const status = isValidAuditLogStatus(statusParam) ? statusParam : undefined
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### P1-4 UI 文案语言不统一(中英文混用)
|
||||
|
||||
**违反规范**:
|
||||
- 编码规范 §一:「可读性优先」
|
||||
- 项目定位为「Next_Edu K12 智慧教务系统」(中文用户)
|
||||
|
||||
**现状**:
|
||||
|
||||
| 文件 | 文案语言 |
|
||||
|------|---------|
|
||||
| `users/import/page.tsx` | 中文("批量导入用户"、"返回") |
|
||||
| `announcements/[id]/page.tsx` | 英文("Edit Announcement") |
|
||||
| `school/schools/page.tsx` | 英文("Schools"、"Manage schools...") |
|
||||
| `school/classes/page.tsx` | 英文("Classes"、"Manage classes...") |
|
||||
| `school/grades/page.tsx` | 英文("Grades"、"Manage grades...") |
|
||||
| `school/grades/insights/page.tsx` | 英文("Grade Insights"、"Filters") |
|
||||
| `school/academic-year/page.tsx` | 英文("Academic Year") |
|
||||
| `school/departments/page.tsx` | 英文("Departments") |
|
||||
| `audit-logs/page.tsx` | 英文("Audit Logs") |
|
||||
| `audit-logs/login-logs/page.tsx` | 英文("Login Logs") |
|
||||
| `audit-logs/data-changes/page.tsx` | 英文("Data Change Logs") |
|
||||
| `scheduling/auto/page.tsx` | 英文("Auto Schedule") |
|
||||
| `scheduling/changes/page.tsx` | 英文("Schedule Change Requests") |
|
||||
| `scheduling/rules/page.tsx` | 英文("Scheduling Rules") |
|
||||
| `course-plans/page.tsx` | 英文("Course Plans") |
|
||||
| `course-plans/create/page.tsx` | 英文("New Course Plan") |
|
||||
| `course-plans/[id]/edit/page.tsx` | 英文("Edit Course Plan") |
|
||||
| `elective/page.tsx` | 英文("Elective Courses") |
|
||||
| `elective/create/page.tsx` | 英文("New Elective Course") |
|
||||
| `elective/[id]/edit/page.tsx` | 英文("Edit Elective Course") |
|
||||
| `attendance/page.tsx` | 英文("Attendance Overview") |
|
||||
|
||||
**影响**:用户体验割裂,admin 区仅 `users/import` 为中文,其余全英文,与系统定位不符。
|
||||
|
||||
**修复建议**:统一为中文(与 `users/import/page.tsx` 保持一致),或引入 i18n 方案统一管理。建议优先统一为中文。
|
||||
|
||||
---
|
||||
|
||||
### P2 一般问题(建议修复)
|
||||
|
||||
#### P2-1 `school/grades/insights/page.tsx` 使用原生 `<select>` 而非共享组件
|
||||
|
||||
**文件**:[src/app/(dashboard)/admin/school/grades/insights/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/school/grades/insights/page.tsx#L57-L68)
|
||||
|
||||
**现状**:第 57-68 行使用原生 `<select>` 元素,而项目已提供 `@/shared/components/ui/select.tsx`(shadcn Select)。
|
||||
|
||||
```tsx
|
||||
<select
|
||||
name="gradeId"
|
||||
defaultValue={selected || "all"}
|
||||
className="h-10 w-full rounded-md border bg-background px-3 text-sm md:w-[360px]"
|
||||
>
|
||||
```
|
||||
|
||||
**影响**:
|
||||
- UI 风格与其他页面不一致(其他页面使用 shadcn Select)
|
||||
- 原生 `<select>` 样式难以跨浏览器统一
|
||||
- 可访问性较弱(缺少 ARIA 属性)
|
||||
|
||||
**修复建议**:替换为 `@/shared/components/ui/select.tsx` 的 `Select` / `SelectTrigger` / `SelectContent` / `SelectItem` 组合。
|
||||
|
||||
---
|
||||
|
||||
#### P2-2 `users/import/page.tsx` 使用原生 `<table>` 而非共享组件
|
||||
|
||||
**文件**:[src/app/(dashboard)/admin/users/import/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/users/import/page.tsx#L93-L128)
|
||||
|
||||
**现状**:第 93-128 行使用原生 `<table>` 元素手写表格,而项目已提供 `@/shared/components/ui/table.tsx`(shadcn Table)。
|
||||
|
||||
**影响**:与 `school/grades/insights/page.tsx` 等使用 shadcn Table 的页面风格不一致。
|
||||
|
||||
**修复建议**:替换为 `Table` / `TableHeader` / `TableBody` / `TableRow` / `TableHead` / `TableCell` 组合。
|
||||
|
||||
---
|
||||
|
||||
#### P2-3 Tailwind 任意值违规
|
||||
|
||||
**违反规范**:
|
||||
- 编码规范 §6.2:「禁止使用任意值(`w-[137px]`),除非有充分理由并注释说明」
|
||||
- 项目规则:「禁止使用任意值(`w-[137px]`),除非有充分理由并注释」
|
||||
|
||||
**现状**:
|
||||
|
||||
| 文件 | 行号 | 问题类名 |
|
||||
|------|------|---------|
|
||||
| `school/grades/insights/page.tsx` | 60 | `md:w-[360px]` |
|
||||
| `school/grades/insights/page.tsx` | 82, 89, 96 | `h-[360px]` |
|
||||
| `users/import/page.tsx` | 16 | `h-full flex-1 flex-col`(`flex-1` 合理,但整体布局类应复用) |
|
||||
|
||||
**修复建议**:
|
||||
- `md:w-[360px]` → 使用设计令牌宽度类(如 `md:w-72` 或 `md:w-80`)或在 globals.css 定义 `--filter-width` 变量
|
||||
- `h-[360px]` → 使用 `h-80`(320px)或 `h-96`(384px)等标准档位
|
||||
|
||||
---
|
||||
|
||||
#### P2-4 `users/import/page.tsx` 使用硬编码颜色
|
||||
|
||||
**违反规范**:
|
||||
- 编码规范 §6.3:「所有视觉设计决策(颜色、字号、间距)必须体现在设计令牌中,组件中不使用硬编码值」
|
||||
|
||||
**文件**:[src/app/(dashboard)/admin/users/import/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/users/import/page.tsx#L67)
|
||||
|
||||
**现状**:第 67 行使用 `text-amber-500` 硬编码颜色:
|
||||
```tsx
|
||||
<Info className="h-5 w-5 text-amber-500" />
|
||||
```
|
||||
|
||||
**修复建议**:使用设计令牌颜色,如 `text-warning`(若存在)或在 globals.css 定义 `--warning` 变量。如暂无 warning 令牌,可使用 `text-primary` 或 `text-muted-foreground` 保持一致。
|
||||
|
||||
---
|
||||
|
||||
#### P2-5 `school/grades/insights/page.tsx` 导入顺序违规
|
||||
|
||||
**违反规范**:
|
||||
- 编码规范 §4.3:「导入顺序:React → 第三方 → 内部绝对路径 → 相对路径 → 类型导入」
|
||||
- 项目规则引用的 ESLint `import/order` 规则
|
||||
|
||||
**文件**:[src/app/(dashboard)/admin/school/grades/insights/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/school/grades/insights/page.tsx#L1-L11)
|
||||
|
||||
**现状**:第 1-11 行导入顺序混乱,`lucide-react`(第三方库)被放在所有 `@/` 内部导入之后:
|
||||
```tsx
|
||||
import Link from "next/link" // next(外部)
|
||||
import { getGrades } from "@/modules/school/data-access" // 内部
|
||||
import { getGradeHomeworkInsights } from "@/modules/classes/data-access"
|
||||
import { EmptyState } from "@/shared/components/ui/empty-state"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/shared/components/ui/card"
|
||||
import { Badge } from "@/shared/components/ui/badge"
|
||||
import { Button } from "@/shared/components/ui/button"
|
||||
import { Table, TableBody, ... } from "@/shared/components/ui/table"
|
||||
import { formatDate } from "@/shared/lib/utils"
|
||||
import { BarChart3 } from "lucide-react" // ❌ 第三方应在前
|
||||
```
|
||||
|
||||
**修复建议**:调整为 `next` → `lucide-react` → `@/` 内部导入,分组间空一行:
|
||||
```tsx
|
||||
import Link from "next/link"
|
||||
|
||||
import { BarChart3 } from "lucide-react"
|
||||
|
||||
import { getGrades } from "@/modules/school/data-access"
|
||||
// ...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### P2-6 `course-plans/[id]/edit/page.tsx` 同模块重复导入
|
||||
|
||||
**文件**:[src/app/(dashboard)/admin/course-plans/[id]/edit/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/course-plans/[id]/edit/page.tsx#L3-L4)
|
||||
|
||||
**现状**:第 3-4 行从同一模块 `@/modules/course-plans/data-access` 分两行导入:
|
||||
```tsx
|
||||
import { getCoursePlanById } from "@/modules/course-plans/data-access"
|
||||
import { getSubjectOptions } from "@/modules/course-plans/data-access"
|
||||
```
|
||||
|
||||
**修复建议**:合并为单行:
|
||||
```tsx
|
||||
import { getCoursePlanById, getSubjectOptions } from "@/modules/course-plans/data-access"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### P2-7 `scheduling/*` 页面从 `actions` 而非 `data-access` 获取数据
|
||||
|
||||
**违反规范**:
|
||||
- 编码规范 §7.1:「服务端数据获取通过模块的 `data-access.ts` 函数」
|
||||
- 架构影响地图:「actions.ts(编排层:权限 + 调用 data-access + revalidate)」
|
||||
|
||||
**现状**:以下页面从 `@/modules/scheduling/actions` 导入数据查询函数:
|
||||
|
||||
| 文件 | 导入函数 |
|
||||
|------|---------|
|
||||
| `scheduling/auto/page.tsx` | `getAdminClassesForScheduling` |
|
||||
| `scheduling/changes/page.tsx` | `getAdminClassesForScheduling`、`getScheduleChanges` |
|
||||
| `scheduling/rules/page.tsx` | `getAdminClassesForScheduling`、`getSchedulingRules` |
|
||||
|
||||
**说明**:规范允许 `app/` 调用 Server Actions,但 Server Actions 的职责是「编排:权限 + 调用 data-access + revalidate」,主要用于**变更操作**。纯读取操作应通过 `data-access.ts` 暴露,避免在 Server Component 中触发不必要的 `revalidate` 逻辑。
|
||||
|
||||
**修复建议**:将 `getAdminClassesForScheduling`、`getScheduleChanges`、`getSchedulingRules` 等纯查询函数迁移到 `scheduling/data-access.ts`,或在 actions 中明确标注其为只读封装。需同步更新架构文档 004 / 005。
|
||||
|
||||
---
|
||||
|
||||
## 三、React 性能优化建议(基于最佳实践)
|
||||
|
||||
### R1 利用 Suspense 流式渲染提升首屏感知性能
|
||||
|
||||
**现状**:所有页面使用 `export const dynamic = "force-dynamic"` 整页动态渲染,数据获取完成前无任何内容呈现。
|
||||
|
||||
**建议**:对数据量大的页面(`audit-logs/*`、`school/grades/insights`、`attendance`)拆分为多个 Suspense 边界,优先渲染页面骨架,慢查询部分流式注入:
|
||||
|
||||
```tsx
|
||||
import { Suspense } from "react"
|
||||
|
||||
export default async function AuditLogsPage(): Promise<JSX.Element> {
|
||||
return (
|
||||
<div className="flex h-full flex-col space-y-8 p-8">
|
||||
<Header />
|
||||
<Suspense fallback={<FilterSkeleton />}>
|
||||
<Filters />
|
||||
</Suspense>
|
||||
<Suspense fallback={<TableSkeleton />}>
|
||||
<AuditTable />
|
||||
</Suspense>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### R2 `school/grades/insights/page.tsx` 串行查询可优化为并行
|
||||
|
||||
**文件**:[src/app/(dashboard)/admin/school/grades/insights/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/school/grades/insights/page.tsx#L30-L33)
|
||||
|
||||
**现状**:第 30-33 行先 `await getGrades()` 再条件 `await getGradeHomeworkInsights()`,两次串行查询:
|
||||
```tsx
|
||||
const grades = await getGrades()
|
||||
const selected = gradeId && gradeId !== "all" ? gradeId : ""
|
||||
const insights = selected ? await getGradeHomeworkInsights({ gradeId: selected, limit: 50 }) : null
|
||||
```
|
||||
|
||||
**说明**:`insights` 依赖 `selected`(来自 URL 参数,非 `grades` 结果),两者无数据依赖,可并行:
|
||||
```tsx
|
||||
const selected = gradeId && gradeId !== "all" ? gradeId : ""
|
||||
const [grades, insights] = await Promise.all([
|
||||
getGrades(),
|
||||
selected ? getGradeHomeworkInsights({ gradeId: selected, limit: 50 }) : Promise.resolve(null),
|
||||
])
|
||||
```
|
||||
|
||||
### R3 列表页 `classOptions` 映射可下沉至 data-access
|
||||
|
||||
**现状**:`scheduling/auto`、`scheduling/changes`、`scheduling/rules`、`attendance`、`course-plans/create`、`course-plans/[id]/edit`、`elective/create`、`elective/[id]/edit` 等页面均在组件内 `.map()` 转换数据形状:
|
||||
|
||||
```tsx
|
||||
const classOptions = classes.map((c) => ({ id: c.id, name: c.name, grade: c.grade }))
|
||||
```
|
||||
|
||||
**建议**:在对应 `data-access.ts` 提供 `getClassOptions()`、`getStaffOptions()` 等轻量查询函数,仅返回 `{ id, name }` 形状,减少传输数据量与组件层转换逻辑。
|
||||
|
||||
---
|
||||
|
||||
## 四、Web 界面设计规范建议(基于 WCAG 2.2 AA)
|
||||
|
||||
### W1 表单 `<label>` 与控件关联不规范
|
||||
|
||||
**文件**:[src/app/(dashboard)/admin/school/grades/insights/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/school/grades/insights/page.tsx#L56-L57)
|
||||
|
||||
**现状**:第 56-57 行 `<label>` 与 `<select>` 未通过 `htmlFor` / `id` 关联:
|
||||
```tsx
|
||||
<label className="text-sm font-medium">Grade</label>
|
||||
<select name="gradeId" ...>
|
||||
```
|
||||
|
||||
**违反**:WCAG 2.2 SC 1.3.1(信息与关系)、SC 3.3.2(标签或指令)。
|
||||
|
||||
**修复建议**:
|
||||
```tsx
|
||||
<label htmlFor="grade-filter" className="text-sm font-medium">Grade</label>
|
||||
<select id="grade-filter" name="gradeId" ...>
|
||||
```
|
||||
|
||||
### W2 `users/import/page.tsx` 表格缺少 `<caption>` 与语义化标注
|
||||
|
||||
**文件**:[src/app/(dashboard)/admin/users/import/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/users/import/page.tsx#L93-L128)
|
||||
|
||||
**现状**:原生 `<table>` 缺少 `<caption>` 描述表格用途,屏幕阅读器无法快速理解表格主题。
|
||||
|
||||
**修复建议**:增加 `<caption className="sr-only">模板字段说明</caption>`,或替换为 shadcn Table 后通过 `aria-label` 补充。
|
||||
|
||||
### W3 页面标题层级不统一
|
||||
|
||||
**现状**:
|
||||
- 部分页面使用 `<h2>` 作为页面主标题(如 `school/schools`、`audit-logs`)
|
||||
- `users/import/page.tsx` 也使用 `<h2>`
|
||||
- 但页面布局中未见统一的 `<h1>` 主标题层级
|
||||
|
||||
**建议**:确认 `(dashboard)/layout.tsx` 是否提供 `<h1>` 或页面 `<main>` 的 accessible name,若无,建议各页面统一使用 `<h1>` 作为页面主标题,`<h2>` 用于区块标题,保持标题层级连贯。
|
||||
|
||||
### W4 交互式筛选器缺少 `aria-live` 反馈
|
||||
|
||||
**文件**:`school/grades/insights/page.tsx`、`attendance/page.tsx`、`audit-logs/*`
|
||||
|
||||
**现状**:筛选器提交后表格数据刷新,但屏幕阅读器用户无法感知数据已更新。
|
||||
|
||||
**违反**:WCAG 2.2 SC 4.1.3(状态消息)。
|
||||
|
||||
**建议**:在表格容器添加 `aria-live="polite"` 或使用项目已有的 `useAriaLive` Hook 通知「已加载 N 条记录」。
|
||||
|
||||
### W5 `EmptyState` 组件使用一致但图标语义可优化
|
||||
|
||||
**现状**:`scheduling/*`、`attendance`、`school/grades/insights` 均使用 `EmptyState` 组件,图标统一使用 `ClipboardList` / `BarChart3`,体验一致(优点)。
|
||||
|
||||
**建议**:`BarChart3` 用于「无数据」与「选择年级」两种语义略显混淆,建议「等待操作」类空状态使用 `MousePointerClick` 或 `Filter` 图标区分。
|
||||
|
||||
---
|
||||
|
||||
## 五、优秀实践(已符合规范,应保持)
|
||||
|
||||
1. **服务端组件默认化**:全部 26 个页面均为 async 服务端组件,未滥用 `"use client"`,符合 §5.2。
|
||||
2. **并行数据获取**:`announcements/page.tsx`、`audit-logs/*`、`course-plans/create`、`course-plans/[id]/edit`、`elective/create`、`elective/[id]/edit`、`school/grades`、`scheduling/rules` 等均使用 `Promise.all` 并行查询,性能良好。
|
||||
3. **类型守卫正确使用**:`announcements/page.tsx`、`scheduling/changes/page.tsx`、`course-plans/page.tsx`、`elective/page.tsx` 使用 `isValidStatus` 类型守卫,是 `as` 断言的正确替代方案。
|
||||
4. **404 处理**:`announcements/[id]/page.tsx`、`course-plans/[id]/page.tsx`、`course-plans/[id]/edit/page.tsx`、`elective/[id]/edit/page.tsx` 使用 `notFound()` 处理资源不存在场景。
|
||||
5. **权限校验到位**:`audit-logs/*`(3 个文件)、`files/page.tsx` 正确调用 `requirePermission()`。
|
||||
6. **模块化组合**:页面仅负责数据获取与组合,UI 逻辑下沉至 `modules/*/components/`,符合三层架构。
|
||||
7. **`force-dynamic` 标注**:需要实时数据的页面均显式声明 `export const dynamic = "force-dynamic"`。
|
||||
8. **`metadata` 导出**:`users/import/page.tsx` 正确导出 `metadata` 用于 SEO(建议其他页面补充)。
|
||||
|
||||
---
|
||||
|
||||
## 六、修复优先级与建议执行顺序
|
||||
|
||||
| 优先级 | 问题编号 | 建议执行顺序 | 影响范围 |
|
||||
|--------|---------|-------------|---------|
|
||||
| P0 | P0-2 | 立即修复 attendance 权限 | 1 文件 |
|
||||
| P0 | P0-1 | 补充 error.tsx / loading.tsx | 新增 ~6 文件 |
|
||||
| P1 | P1-1 | 补充返回类型标注 | 26 文件 |
|
||||
| P1 | P1-2 | 抽取共享 getSearchParam | 27 文件 |
|
||||
| P1 | P1-3 | 替换 as 断言为类型守卫 | 4 文件 |
|
||||
| P1 | P1-4 | 统一 UI 文案语言 | ~20 文件 |
|
||||
| P2 | P2-1 ~ P2-7 | 逐步整改 | 单文件级 |
|
||||
| R1 ~ R3 | 性能优化 | 迭代优化 | 关键页面 |
|
||||
| W1 ~ W5 | 可访问性 | 迭代优化 | 关键页面 |
|
||||
|
||||
---
|
||||
|
||||
## 七、附:文件清单与合规状态
|
||||
|
||||
| 文件 | P0 | P1 | P2 | 备注 |
|
||||
|------|----|----|----|----|
|
||||
| `dashboard/page.tsx` | - | 缺返回类型 | - | 整体合规 |
|
||||
| `announcements/page.tsx` | - | 缺返回类型、getParam 重复 | - | 类型守卫正确 |
|
||||
| `announcements/[id]/page.tsx` | - | 缺返回类型、英文文案 | - | - |
|
||||
| `users/import/page.tsx` | - | 缺返回类型 | 原生 table、硬编码颜色 | 文案为中文(正确) |
|
||||
| `school/page.tsx` | - | 缺返回类型 | - | 仅 redirect |
|
||||
| `school/schools/page.tsx` | - | 缺返回类型、英文文案 | - | - |
|
||||
| `school/classes/page.tsx` | - | 缺返回类型、英文文案 | - | - |
|
||||
| `school/grades/page.tsx` | - | 缺返回类型、英文文案 | - | - |
|
||||
| `school/grades/insights/page.tsx` | - | 缺返回类型、英文文案 | 原生 select、任意值、导入顺序、label 未关联 | 问题最多 |
|
||||
| `school/academic-year/page.tsx` | - | 缺返回类型、英文文案 | - | - |
|
||||
| `school/departments/page.tsx` | - | 缺返回类型、英文文案 | - | - |
|
||||
| `audit-logs/page.tsx` | - | 缺返回类型、as 断言、英文文案、getParam 重复 | - | 权限校验正确 |
|
||||
| `audit-logs/login-logs/page.tsx` | - | 缺返回类型、as 断言、英文文案、getParam 重复 | - | 权限校验正确 |
|
||||
| `audit-logs/data-changes/page.tsx` | - | 缺返回类型、as 断言、英文文案、getParam 重复 | - | 权限校验正确 |
|
||||
| `scheduling/auto/page.tsx` | - | 缺返回类型、英文文案 | 从 actions 取数 | - |
|
||||
| `scheduling/changes/page.tsx` | - | 缺返回类型、英文文案、getParam 重复 | 从 actions 取数 | 类型守卫正确 |
|
||||
| `scheduling/rules/page.tsx` | - | 缺返回类型、英文文案 | 从 actions 取数 | - |
|
||||
| `course-plans/page.tsx` | - | 缺返回类型、英文文案、getParam 重复 | - | 类型守卫正确 |
|
||||
| `course-plans/create/page.tsx` | - | 缺返回类型、英文文案 | - | - |
|
||||
| `course-plans/[id]/page.tsx` | - | 缺返回类型 | - | - |
|
||||
| `course-plans/[id]/edit/page.tsx` | - | 缺返回类型、英文文案 | 重复导入 | - |
|
||||
| `elective/page.tsx` | - | 缺返回类型、英文文案、getParam 重复 | - | 类型守卫正确 |
|
||||
| `elective/create/page.tsx` | - | 缺返回类型、英文文案 | - | - |
|
||||
| `elective/[id]/edit/page.tsx` | - | 缺返回类型、英文文案 | - | - |
|
||||
| `attendance/page.tsx` | **缺权限校验** | 缺返回类型、as 断言、英文文案、getParam 重复 | - | 最高优先级 |
|
||||
| `files/page.tsx` | - | 缺返回类型 | - | 权限校验正确、整体合规 |
|
||||
|
||||
---
|
||||
|
||||
> 报告生成完毕。建议按「六、修复优先级」顺序整改,每完成一批次后运行 `npm run lint` 与 `npx tsc --noEmit` 验证,并同步更新架构文档 004 / 005。
|
||||
1434
bugs/back_bug.md
Normal file
1434
bugs/back_bug.md
Normal file
File diff suppressed because it is too large
Load Diff
960
bugs/others_bug.md
Normal file
960
bugs/others_bug.md
Normal 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-MD03:URL 参数未编码
|
||||
- **位置**:`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-NPF01:Switch 与隐藏 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-AS01:Tab 图标语义错误
|
||||
- **位置**:`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 后权限相关 UI(Compose、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-01:announcements 模块未记录页面缺少权限校验
|
||||
- **位置**:004 文档 2.16 节
|
||||
- **问题**:已记录 `getAnnouncementsAction` 使用 `requireAuth()` 而非 `requirePermission()`,但未记录 `app/(dashboard)/announcements/page.tsx` 完全缺少权限校验
|
||||
- **改进建议**:补充已知问题「⚠️ P2:`app/(dashboard)/announcements/page.tsx` 完全缺少权限校验」
|
||||
|
||||
#### DOC-02:management 模块未在架构文档中独立记录
|
||||
- **位置**:004 文档
|
||||
- **问题**:`app/(dashboard)/management/grade/` 路由未在架构文档中记录其依赖关系
|
||||
- **改进建议**:补充 management 路由的模块依赖(classes、school)
|
||||
|
||||
#### DOC-03:settings 模块文件清单过期
|
||||
- **位置**: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 Agent(GLM-5.2)
|
||||
> 核查方法:人工逐行审查 + 架构图比对 + 技能规则匹配
|
||||
> 应用技能:`vercel-react-best-practices`(65 条规则)、`web-design-guidelines`(Web Interface Guidelines)
|
||||
> 注:`web-artifacts-builder` 技能加载失败,界面优化建议已合并至第四章
|
||||
551
bugs/parent_bug.md
Normal file
551
bugs/parent_bug.md
Normal 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-P001:app 层直接访问 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-P016:Link 缺少 `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 列布局在 sm(640px)下更合适
|
||||
- **改进建议**:`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` 在 md(768-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-P01:004 文档 parent 模块行数记录过期
|
||||
- **位置**:`docs/architecture/004_architecture_impact_map.md:924`
|
||||
- **问题**:记录 `data-access.ts | 234 | 子女关系 + 仪表盘数据聚合`,实际 234 行 ✅ 一致;但 `components/* | 7 文件` 实际为 7 个组件文件 ✅ 一致
|
||||
- **说明**:本节核查后无需更新(行数与文件数均一致)
|
||||
|
||||
### DOC-P02:004 文档未记录 `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-P03:005 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
297
bugs/shared_bug.md
Normal 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-A01:Prettier 配置违规(使用分号)
|
||||
- **位置**:`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-01:004 文件行数记录过期
|
||||
- **位置**:`docs/architecture/004_architecture_impact_map.md:408`
|
||||
- **问题**:记录 `types/permissions.ts | 92 | 54 个权限点常量`,实际文件 114 行,含 `DataScope`、`AuthContext` 类型定义
|
||||
- **改进建议**:更新为 `114 行 | 54 个权限点 + DataScope + AuthContext`
|
||||
|
||||
### DOC-02:005 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 Agent(GLM-5.2)
|
||||
> 核查方法:人工逐行审查 + 架构图比对 + 技能规则匹配
|
||||
711
bugs/student_bug.md
Normal file
711
bugs/student_bug.md
Normal 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-L03:JSX 语法格式错误
|
||||
- **位置**:`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-C01:catch 块吞掉错误
|
||||
- **位置**:`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-01:005 JSON 中 `/student/grades` 的 dataAccess 记录错误
|
||||
- **位置**:`docs/architecture/005_architecture_data.json:12047`
|
||||
- **问题**:记录为 `"grades/actions.getStudentGradeSummaryAction"`,实际代码调用 `grades/data-access.getStudentGradeSummary`
|
||||
- **改进建议**:修正为 `"grades/data-access.getStudentGradeSummary"`
|
||||
|
||||
### DOC-02:005 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-03:005 JSON 中 `/student/diagnostic` 的 type 错误
|
||||
- **位置**:`docs/architecture/005_architecture_data.json:12063`
|
||||
- **问题**:记录 `"type": "client"`,实际 `page.tsx` 是 Server Component
|
||||
- **改进建议**:改为 `"type": "server"`,并注明内部 `StudentDiagnosticView` 组件为 client
|
||||
|
||||
### DOC-04:005 JSON 中 `/student/dashboard` 缺少 `getStudentSchedule` 记录
|
||||
- **位置**:`docs/architecture/005_architecture_data.json:11978-11982`
|
||||
- **问题**:实际代码调用了 `getStudentSchedule(student.id)`,但 dataAccess 数组未记录
|
||||
- **改进建议**:补充 `"classes/data-access.getStudentSchedule"`
|
||||
|
||||
### DOC-05:005 JSON 中 `/student/learning/assignments` 缺少 `getDemoStudentUser` 记录
|
||||
- **位置**:`docs/architecture/005_architecture_data.json:11989-11991`
|
||||
- **问题**:实际代码调用了 `getDemoStudentUser()`,但 dataAccess 数组未记录
|
||||
- **改进建议**:补充 `"homework/data-access.getDemoStudentUser"`(或迁移后更新为 `users/data-access.getCurrentStudentUser`)
|
||||
|
||||
### DOC-06:004 文档 2.26 节未记录 `loading.tsx` 文件
|
||||
- **位置**:`docs/architecture/004_architecture_impact_map.md:1109-1114`
|
||||
- **问题**:文件清单仅列出 3 个组件,未记录 `app/(dashboard)/student/*/loading.tsx`
|
||||
- **改进建议**:补充 loading.tsx 文件清单
|
||||
|
||||
### DOC-07:004 文档未记录 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 Agent(GLM-5.2)
|
||||
> 核查方法:人工逐行审查 + 架构图比对 + 技能规则匹配
|
||||
> 应用技能:`vercel-react-best-practices`(性能优化)、`web-artifacts-builder`(界面构建参考)、`web-design-guidelines`(界面规范审查)
|
||||
741
bugs/teacher_bug.md
Normal file
741
bugs/teacher_bug.md
Normal 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-T01:app 层直接访问数据库(dashboard/page.tsx)
|
||||
- **位置**:[dashboard/page.tsx:4-6, 18-21](../src/app/(dashboard)/teacher/dashboard/page.tsx)
|
||||
- **问题**:页面直接 `import { db } from "@/shared/db"` 并调用 `db.query.users.findFirst()`,违反项目规则「`app/` 只能调用 `modules/` 的 Server Actions 和 data-access,不直接访问 DB」
|
||||
- **现状**:
|
||||
```typescript
|
||||
import { db } from "@/shared/db"
|
||||
import { users } from "@/shared/db/schema"
|
||||
// ...
|
||||
db.query.users.findFirst({
|
||||
where: eq(users.id, teacherId),
|
||||
columns: { name: true },
|
||||
})
|
||||
```
|
||||
- **改进建议**:通过 `modules/users/data-access.ts` 暴露 `getUserNameById(id)` 函数调用
|
||||
|
||||
#### BUG-T02:app 层直接访问数据库(grades/page.tsx)
|
||||
- **位置**:[grades/page.tsx:5-7, 35](../src/app/(dashboard)/teacher/grades/page.tsx)
|
||||
- **问题**:直接 `db.query.subjects.findMany()` 查询科目列表,违反三层架构
|
||||
- **改进建议**:在 `modules/school/data-access.ts` 或 `modules/grades/data-access.ts` 暴露 `getSubjects()` 函数
|
||||
|
||||
#### BUG-T03:app 层直接访问数据库(grades/analytics/page.tsx)
|
||||
- **位置**:[grades/analytics/page.tsx:5-6, 48-50](../src/app/(dashboard)/teacher/grades/analytics/page.tsx)
|
||||
- **问题**:同 BUG-T02,直接 `db.query.subjects.findMany()`
|
||||
- **改进建议**:同 BUG-T02
|
||||
|
||||
#### BUG-T04:app 层直接访问数据库(grades/entry/page.tsx)
|
||||
- **位置**:[grades/entry/page.tsx:1-3, 25](../src/app/(dashboard)/teacher/grades/entry/page.tsx)
|
||||
- **问题**:同 BUG-T02
|
||||
- **改进建议**:同 BUG-T02
|
||||
|
||||
#### BUG-T05:app 层直接访问数据库(grades/stats/page.tsx)
|
||||
- **位置**:[grades/stats/page.tsx:1-3, 28](../src/app/(dashboard)/teacher/grades/stats/page.tsx)
|
||||
- **问题**:同 BUG-T02
|
||||
- **改进建议**:同 BUG-T02
|
||||
|
||||
#### BUG-T06:认证上下文获取方式不一致
|
||||
- **位置**:
|
||||
- [course-plans/page.tsx:1, 23](../src/app/(dashboard)/teacher/course-plans/page.tsx)
|
||||
- [elective/page.tsx:1, 23](../src/app/(dashboard)/teacher/elective/page.tsx)
|
||||
- **问题**:使用 `import { auth } from "@/auth"` + `auth()` 获取 session,而其他页面统一使用 `getAuthContext()`(含 DataScope 解析)
|
||||
- **影响**:无法获得 `dataScope`,无法做数据范围过滤;与项目其他页面不一致
|
||||
- **改进建议**:统一改为 `const ctx = await getAuthContext(); const teacherId = ctx.userId`
|
||||
|
||||
---
|
||||
|
||||
### 2.2 Prettier 配置违规 — 严重度:中
|
||||
|
||||
项目 `.prettierrc` 配置 `"semi": false`,但以下文件使用分号结尾:
|
||||
|
||||
#### BUG-T07:textbooks/page.tsx 使用分号
|
||||
- **位置**:[textbooks/page.tsx:3, 73](../src/app/(dashboard)/teacher/textbooks/page.tsx)
|
||||
- **问题**:`import { TextbookCard } from "...";` 等多处使用分号
|
||||
- **改进建议**:运行 `npx prettier --write` 统一格式
|
||||
|
||||
#### BUG-T08:textbooks/[id]/page.tsx 使用分号
|
||||
- **位置**:[textbooks/[id]/page.tsx](../src/app/(dashboard)/teacher/textbooks/[id]/page.tsx)(全文)
|
||||
- **问题**:多处语句使用分号结尾
|
||||
- **改进建议**:同 BUG-T07
|
||||
|
||||
#### BUG-T09:textbooks/loading.tsx 使用分号
|
||||
- **位置**:[textbooks/loading.tsx](../src/app/(dashboard)/teacher/textbooks/loading.tsx)(全文)
|
||||
- **问题**:同 BUG-T07
|
||||
- **改进建议**:同 BUG-T07
|
||||
|
||||
#### BUG-T10:textbooks/[id]/loading.tsx 使用分号
|
||||
- **位置**:[textbooks/[id]/loading.tsx](../src/app/(dashboard)/teacher/textbooks/[id]/loading.tsx)(全文)
|
||||
- **问题**:同 BUG-T07
|
||||
- **改进建议**:同 BUG-T07
|
||||
|
||||
---
|
||||
|
||||
### 2.3 TypeScript 规范违规 — 严重度:高
|
||||
|
||||
#### BUG-T11:使用 `as` 类型断言(exams/[id]/build/page.tsx)
|
||||
- **位置**:[exams/[id]/build/page.tsx:32-34](../src/app/(dashboard)/teacher/exams/[id]/build/page.tsx)
|
||||
- **问题**:使用 `as` 断言转换类型,违反编码规范「禁止 `as` 断言(除非从 `unknown` 转换)」
|
||||
- **现状**:
|
||||
```typescript
|
||||
content: q.content as Question["content"],
|
||||
type: q.type as Question["type"],
|
||||
```
|
||||
- **改进建议**:在 data-access 层返回正确类型,或使用类型守卫函数
|
||||
|
||||
#### BUG-T12:使用 `as` 类型断言(attendance/page.tsx)
|
||||
- **位置**:[attendance/page.tsx:39](../src/app/(dashboard)/teacher/attendance/page.tsx)
|
||||
- **问题**:`status as "present" | "absent" | "late" | "early_leave" | "excused"` 直接断言
|
||||
- **改进建议**:使用类型守卫函数 `isAttendanceStatus(value): value is AttendanceStatus`
|
||||
|
||||
#### BUG-T13:使用 `as` 类型断言(grades/page.tsx)
|
||||
- **位置**:[grades/page.tsx:43-44](../src/app/(dashboard)/teacher/grades/page.tsx)
|
||||
- **问题**:`type as "exam" | "quiz" | "homework" | "other"` 和 `semester as "1" | "2"` 直接断言
|
||||
- **改进建议**:使用类型守卫
|
||||
|
||||
#### BUG-T14:使用 `as` 类型断言(grades/analytics/page.tsx)
|
||||
- **位置**:[grades/analytics/page.tsx](../src/app/(dashboard)/teacher/grades/analytics/page.tsx)(多处)
|
||||
- **问题**:同上模式
|
||||
- **改进建议**:同上
|
||||
|
||||
#### BUG-T15:使用 `as` 类型断言(diagnostic/page.tsx)
|
||||
- **位置**:[diagnostic/page.tsx:27-28](../src/app/(dashboard)/teacher/diagnostic/page.tsx)
|
||||
- **问题**:`reportType as DiagnosticReportType` 和 `status as DiagnosticReportStatus`
|
||||
- **改进建议**:使用类型守卫
|
||||
|
||||
#### BUG-T16:函数返回值未显式标注(getParam 工具函数)
|
||||
- **位置**:以下 15 个文件中的 `getParam` 函数均未标注返回类型
|
||||
- attendance/page.tsx:15
|
||||
- attendance/sheet/page.tsx:9
|
||||
- attendance/stats/page.tsx:12
|
||||
- classes/schedule/page.tsx:14
|
||||
- classes/students/page.tsx:14
|
||||
- course-plans/page.tsx:10
|
||||
- diagnostic/page.tsx:10
|
||||
- elective/page.tsx:10
|
||||
- exams/all/page.tsx:16
|
||||
- grades/page.tsx:19
|
||||
- grades/analytics/page.tsx:28
|
||||
- grades/entry/page.tsx:12
|
||||
- grades/stats/page.tsx:15
|
||||
- homework/assignments/page.tsx:23
|
||||
- questions/page.tsx:15
|
||||
- textbooks/page.tsx:13
|
||||
- **问题**:违反编码规范「函数返回值必须显式标注,特别是 `Promise<T>`」
|
||||
- **现状**:`const getParam = (params: SearchParams, key: string) => { ... }`
|
||||
- **改进建议**:`const getParam = (params: SearchParams, key: string): string | undefined => { ... }`
|
||||
|
||||
#### BUG-T17:页面默认导出函数未标注返回类型
|
||||
- **位置**:所有 page.tsx 文件的 `export default async function XxxPage()`
|
||||
- **问题**:未标注 `Promise<JSX.Element>` 或 `Promise<React.ReactNode>`
|
||||
- **规范依据**:编码规范 5.2 示例 `export default async function UsersPage(): Promise<JSX.Element>`
|
||||
- **改进建议**:统一补充返回类型标注
|
||||
|
||||
---
|
||||
|
||||
### 2.4 DRY 违规(重复代码) — 严重度:中
|
||||
|
||||
#### BUG-T18:`getParam` 工具函数在 16 个文件中重复定义
|
||||
- **位置**:见 BUG-T16 列表
|
||||
- **问题**:完全相同的工具函数 `getParam` 和类型 `SearchParams` 在 16 个页面文件中复制粘贴
|
||||
- **改进建议**:提取到 `shared/lib/search-params.ts`:
|
||||
```typescript
|
||||
export type SearchParams = { [key: string]: string | string[] | undefined }
|
||||
export function getParam(params: SearchParams, key: string): string | undefined {
|
||||
const v = params[key]
|
||||
return Array.isArray(v) ? v[0] : v
|
||||
}
|
||||
```
|
||||
|
||||
#### BUG-T19:`StatsClassSelector` 模式重复
|
||||
- **位置**:
|
||||
- [attendance/stats/page.tsx:91-119](../src/app/(dashboard)/teacher/attendance/stats/page.tsx)
|
||||
- [grades/stats/page.tsx:86-138](../src/app/(dashboard)/teacher/grades/stats/page.tsx)
|
||||
- [grades/analytics/page.tsx:150-258](../src/app/(dashboard)/teacher/grades/analytics/page.tsx)
|
||||
- **问题**:三处文件都定义了「类筛选按钮组」组件,结构几乎相同(`<a>` 标签 + 条件 className)
|
||||
- **改进建议**:提取为共享组件 `shared/components/ui/filter-chips.tsx`
|
||||
|
||||
---
|
||||
|
||||
### 2.5 性能问题(vercel-react-best-practices) — 严重度:高
|
||||
|
||||
#### BUG-T20:串行数据获取 waterfall(attendance/page.tsx)
|
||||
- **位置**:[attendance/page.tsx:32-41](../src/app/(dashboard)/teacher/attendance/page.tsx)
|
||||
- **问题**:`getTeacherClasses()` 与 `getAttendanceRecords()` 串行执行,但二者无依赖关系
|
||||
- **违反规则**:`async-parallel` - 独立操作应使用 `Promise.all()`
|
||||
- **改进建议**:
|
||||
```typescript
|
||||
const [classes, result] = await Promise.all([
|
||||
getTeacherClasses(),
|
||||
getAttendanceRecords({ ... }),
|
||||
])
|
||||
```
|
||||
|
||||
#### BUG-T21:串行数据获取 waterfall(attendance/sheet/page.tsx)
|
||||
- **位置**:[attendance/sheet/page.tsx:24-29](../src/app/(dashboard)/teacher/attendance/sheet/page.tsx)
|
||||
- **问题**:`getTeacherClasses()` 与 `getClassStudentsForAttendance()` 串行,但 students 依赖 defaultClassId(来自 searchParams),可与 classes 并行
|
||||
- **改进建议**:使用 `Promise.all` 并行
|
||||
|
||||
#### BUG-T22:串行数据获取 waterfall(attendance/stats/page.tsx)
|
||||
- **位置**:[attendance/stats/page.tsx:28-53](../src/app/(dashboard)/teacher/attendance/stats/page.tsx)
|
||||
- **问题**:`getTeacherClasses()` → `getClassAttendanceStats()` 串行,但 stats 依赖 classId(可从 classes[0] 取默认),可优化
|
||||
- **改进建议**:先并行获取 classes,再取 targetClassId 后获取 stats(当前逻辑合理但可考虑预取)
|
||||
|
||||
#### BUG-T23:串行数据获取 waterfall(grades/page.tsx)
|
||||
- **位置**:[grades/page.tsx:33-45](../src/app/(dashboard)/teacher/grades/page.tsx)
|
||||
- **问题**:`Promise.all([getTeacherClasses, db.query])` 之后串行 `getGradeRecords`,但 `getGradeRecords` 不依赖前两者结果
|
||||
- **改进建议**:三个查询全部 `Promise.all`
|
||||
|
||||
#### BUG-T24:串行数据获取 waterfall(grades/entry/page.tsx)
|
||||
- **位置**:[grades/entry/page.tsx:23-34](../src/app/(dashboard)/teacher/grades/entry/page.tsx)
|
||||
- **问题**:`Promise.all([getTeacherClasses, db.query])` 后串行 `getClassStudentsForEntry`,但 students 依赖 defaultClassId(来自 searchParams),可并行
|
||||
- **改进建议**:`Promise.all` 三个查询
|
||||
|
||||
#### BUG-T25:串行数据获取 waterfall(grades/stats/page.tsx)
|
||||
- **位置**:[grades/stats/page.tsx:26-54](../src/app/(dashboard)/teacher/grades/stats/page.tsx)
|
||||
- **问题**:`Promise.all([getTeacherClasses, db.query])` → `Promise.all([stats, ranking])` 两段串行
|
||||
- **改进建议**:合并为单个 `Promise.all`
|
||||
|
||||
#### BUG-T26:串行数据获取 waterfall(classes/my/[id]/page.tsx)
|
||||
- **位置**:[classes/my/[id]/page.tsx:21-30](../src/app/(dashboard)/teacher/classes/my/[id]/page.tsx)
|
||||
- **问题**:`Promise.all([insights, students, schedule])` 后串行 `getClassStudentSubjectScoresV2`
|
||||
- **改进建议**:将 `getClassStudentSubjectScoresV2` 加入第一个 `Promise.all`
|
||||
|
||||
#### BUG-T27:串行数据获取 waterfall(diagnostic/student/[studentId]/page.tsx)
|
||||
- **位置**:[diagnostic/student/[studentId]/page.tsx:30-45](../src/app/(dashboard)/teacher/diagnostic/student/[studentId]/page.tsx)
|
||||
- **问题**:`Promise.all([summary, reports])` 后串行 `getKnowledgePointStats()`
|
||||
- **改进建议**:合并为单个 `Promise.all`
|
||||
|
||||
#### BUG-T28:串行数据获取 waterfall(exams/[id]/build/page.tsx)
|
||||
- **位置**:[exams/[id]/build/page.tsx:12-26](../src/app/(dashboard)/teacher/exams/[id]/build/page.tsx)
|
||||
- **问题**:`getExamById` → `getQuestions` → `getQuestions(ids)` 三段串行
|
||||
- **改进建议**:前两个可并行;第三个依赖 exam.questions 的 ID 列表,需串行但可优化
|
||||
|
||||
#### BUG-T29:Bundle 优化 - barrel imports(lucide-react)
|
||||
- **位置**:几乎所有页面文件
|
||||
- **问题**:`import { PlusCircle, BarChart3, ClipboardList } from "lucide-react"` 使用 barrel 文件导入,违反 `bundle-barrel-imports` 规则
|
||||
- **改进建议**:lucide-react 已支持 tree-shaking,但可考虑使用 `lucide-react/icons` 直接导入路径
|
||||
|
||||
#### BUG-T30:缺少 `export const dynamic = "force-dynamic"` 声明
|
||||
- **位置**:
|
||||
- [exams/all/page.tsx](../src/app/(dashboard)/teacher/exams/all/page.tsx)(使用 Suspense,可省略)
|
||||
- [exams/create/page.tsx](../src/app/(dashboard)/teacher/exams/create/page.tsx)
|
||||
- [exams/[id]/build/page.tsx](../src/app/(dashboard)/teacher/exams/[id]/build/page.tsx)
|
||||
- [questions/page.tsx](../src/app/(dashboard)/teacher/questions/page.tsx)(使用 Suspense)
|
||||
- [textbooks/page.tsx](../src/app/(dashboard)/teacher/textbooks/page.tsx)(使用 Suspense)
|
||||
- **问题**:动态数据页面未声明 `force-dynamic`,可能导致静态生成尝试失败
|
||||
- **改进建议**:所有含动态数据的页面统一添加 `export const dynamic = "force-dynamic"`
|
||||
|
||||
---
|
||||
|
||||
### 2.6 Web 界面规范违规(web-design-guidelines) — 严重度:中
|
||||
|
||||
#### BUG-T31:`<a>` 标签缺少 focus-visible 焦点样式
|
||||
- **位置**:
|
||||
- [attendance/stats/page.tsx:106-117](../src/app/(dashboard)/teacher/attendance/stats/page.tsx)
|
||||
- [grades/analytics/page.tsx:192-253](../src/app/(dashboard)/teacher/grades/analytics/page.tsx)
|
||||
- [grades/stats/page.tsx:100-135](../src/app/(dashboard)/teacher/grades/stats/page.tsx)
|
||||
- **问题**:筛选按钮使用 `<a>` 标签但仅有 `hover:bg-accent`,缺少 `focus-visible:ring-*` 或 `focus-visible:outline` 焦点样式
|
||||
- **违反规则**:Focus States - Interactive elements need visible focus
|
||||
- **改进建议**:添加 `focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2`
|
||||
|
||||
#### BUG-T32:`<a>` 标签作为筛选按钮语义不当
|
||||
- **位置**:同 BUG-T31
|
||||
- **问题**:筛选操作使用 `<a>` 标签导航到带 query 的 URL,虽然支持 Cmd/Ctrl+click,但视觉上是按钮形态,应使用 `<button>` 或添加 `role="button"`
|
||||
- **违反规则**:`<button>` for actions, `<a>`/`<Link>` for navigation
|
||||
- **改进建议**:使用 Next.js `<Link>` 并补充焦点样式,或改为 `<button>` + `useRouter` + `useSearchParams`
|
||||
|
||||
#### BUG-T33:标题层级缺失(exams/[id]/build/page.tsx)
|
||||
- **位置**:[exams/[id]/build/page.tsx:104-118](../src/app/(dashboard)/teacher/exams/[id]/build/page.tsx)
|
||||
- **问题**:页面无 `<h1>` 标题,直接渲染 `<ExamAssembly>` 组件,违反「Headings hierarchical `<h1>`–`<h6>`」
|
||||
- **改进建议**:在页面顶部添加 `<h1>` 标题(如「Build Exam」)
|
||||
|
||||
#### BUG-T34:标题层级缺失(exams/[id]/proctoring/page.tsx)
|
||||
- **位置**:[exams/[id]/proctoring/page.tsx:50-54](../src/app/(dashboard)/teacher/exams/[id]/proctoring/page.tsx)
|
||||
- **问题**:同 BUG-T33,无 `<h1>`
|
||||
- **改进建议**:同 BUG-T33
|
||||
|
||||
#### BUG-T35:标题层级缺失(classes/my/[id]/page.tsx)
|
||||
- **位置**:[classes/my/[id]/page.tsx:65-108](../src/app/(dashboard)/teacher/classes/my/[id]/page.tsx)
|
||||
- **问题**:页面无 `<h1>`,依赖 `<ClassHeader>` 组件渲染标题,需确认组件内是否有 h1
|
||||
- **改进建议**:确认 `ClassHeader` 包含 `<h1>`
|
||||
|
||||
#### BUG-T36:长文本未截断(homework/assignments/page.tsx)
|
||||
- **位置**:[homework/assignments/page.tsx:99-101](../src/app/(dashboard)/teacher/homework/assignments/page.tsx)
|
||||
- **问题**:作业标题 `<Link>{a.title}</Link>` 未限制长度,长标题会破坏表格布局
|
||||
- **违反规则**:Content Handling - Text containers handle long content
|
||||
- **改进建议**:添加 `line-clamp-2` 或 `truncate max-w-[200px]`
|
||||
|
||||
#### BUG-T37:长文本未截断(homework/submissions/page.tsx)
|
||||
- **位置**:[homework/submissions/page.tsx:58-60](../src/app/(dashboard)/teacher/homework/submissions/page.tsx)
|
||||
- **问题**:同 BUG-T36
|
||||
- **改进建议**:同 BUG-T36
|
||||
|
||||
#### BUG-T38:长文本未截断(homework/assignments/[id]/submissions/page.tsx)
|
||||
- **位置**:[homework/assignments/[id]/submissions/page.tsx:65](../src/app/(dashboard)/teacher/homework/assignments/[id]/submissions/page.tsx)
|
||||
- **问题**:学生姓名单元格未限制长度
|
||||
- **改进建议**:添加 `truncate max-w-[160px]`
|
||||
|
||||
#### BUG-T39:Flex 子元素缺少 `min-w-0`
|
||||
- **位置**:
|
||||
- [homework/assignments/[id]/page.tsx:26-42](../src/app/(dashboard)/teacher/homework/assignments/[id]/page.tsx)
|
||||
- [classes/my/[id]/page.tsx:86-104](../src/app/(dashboard)/teacher/classes/my/[id]/page.tsx)
|
||||
- **问题**:flex 容器内的文本子元素未设置 `min-w-0`,长内容无法正确截断
|
||||
- **违反规则**:Flex children need `min-w-0` to allow text truncation
|
||||
- **改进建议**:在 flex 子元素添加 `min-w-0`
|
||||
|
||||
#### BUG-T40:使用 `transition: all` 或 `transition-colors` 未列明属性
|
||||
- **位置**:
|
||||
- [attendance/stats/page.tsx:109](../src/app/(dashboard)/teacher/attendance/stats/page.tsx) - `transition-colors`(可接受)
|
||||
- [grades/analytics/page.tsx:195](../src/app/(dashboard)/teacher/grades/analytics/page.tsx) - `transition-colors`(可接受)
|
||||
- **问题**:`transition-colors` 实际上列明了属性,符合规范;但需检查是否有 `transition: all` 使用
|
||||
- **现状**:未发现 `transition: all`,此项通过
|
||||
|
||||
#### BUG-T41:硬编码日期/数字格式
|
||||
- **位置**:所有使用 `formatDate` 的文件
|
||||
- **问题**:需确认 `formatDate` 内部是否使用 `Intl.DateTimeFormat`,若使用硬编码格式则违规
|
||||
- **违反规则**:Locale & i18n - Dates/times: use `Intl.DateTimeFormat`
|
||||
- **改进建议**:检查 `shared/lib/utils.ts` 的 `formatDate` 实现
|
||||
|
||||
#### BUG-T42:数字列未使用 `tabular-nums`
|
||||
- **位置**:
|
||||
- [exams/all/page.tsx:54-60](../src/app/(dashboard)/teacher/exams/all/page.tsx) - 考试计数
|
||||
- [homework/submissions/page.tsx:69-71](../src/app/(dashboard)/teacher/homework/submissions/page.tsx) - 计数列
|
||||
- [homework/assignments/[id]/submissions/page.tsx:73](../src/app/(dashboard)/teacher/homework/assignments/[id]/submissions/page.tsx) - 分数
|
||||
- **问题**:数字列未使用 `font-variant-numeric: tabular-nums`,对齐不整齐
|
||||
- **违反规则**:Typography - `font-variant-numeric: tabular-nums` for number columns
|
||||
- **改进建议**:数字单元格添加 `tabular-nums` 类
|
||||
|
||||
#### BUG-T43:大列表未虚拟化
|
||||
- **位置**:
|
||||
- [questions/page.tsx:42](../src/app/(dashboard)/teacher/questions/page.tsx) - `pageSize: 200`
|
||||
- [exams/all/page.tsx](../src/app/(dashboard)/teacher/exams/all/page.tsx) - ExamDataTable
|
||||
- **问题**:题库页面一次加载 200 条题目,若渲染全部 DOM 节点会卡顿
|
||||
- **违反规则**:Performance - Large lists (>50 items): virtualize
|
||||
- **改进建议**:使用 `virtua` 或 `content-visibility: auto` 虚拟化长列表
|
||||
|
||||
---
|
||||
|
||||
### 2.7 组件规范违规 — 严重度:中
|
||||
|
||||
#### BUG-T44:不必要的包装组件(classes/my/page.tsx)
|
||||
- **位置**:[classes/my/page.tsx:6-17](../src/app/(dashboard)/teacher/classes/my/page.tsx)
|
||||
- **问题**:默认导出 `MyClassesPage` 仅调用 `MyClassesPageImpl`,多此一举
|
||||
- **现状**:
|
||||
```typescript
|
||||
export default function MyClassesPage() {
|
||||
return <MyClassesPageImpl />
|
||||
}
|
||||
|
||||
async function MyClassesPageImpl() {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
- **改进建议**:直接默认导出 async 函数:
|
||||
```typescript
|
||||
export default async function MyClassesPage() {
|
||||
const [classes, subjectOptions] = await Promise.all([...])
|
||||
return <MyClassesGrid classes={classes} subjectOptions={subjectOptions} />
|
||||
}
|
||||
```
|
||||
|
||||
#### BUG-T45:非导出组件定义在 page.tsx 中
|
||||
- **位置**:
|
||||
- [attendance/stats/page.tsx:91-119](../src/app/(dashboard)/teacher/attendance/stats/page.tsx) - `StatsClassSelector`
|
||||
- [grades/analytics/page.tsx:150-258](../src/app/(dashboard)/teacher/grades/analytics/page.tsx) - `AnalyticsFilters`
|
||||
- [grades/stats/page.tsx:86-138](../src/app/(dashboard)/teacher/grades/stats/page.tsx) - `StatsClassSelector`
|
||||
- [classes/schedule/page.tsx:45-63](../src/app/(dashboard)/teacher/classes/schedule/page.tsx) - `ScheduleResultsFallback`
|
||||
- [classes/students/page.tsx:68-81](../src/app/(dashboard)/teacher/classes/students/page.tsx) - `StudentsResultsFallback`
|
||||
- [exams/all/page.tsx:101-128](../src/app/(dashboard)/teacher/exams/all/page.tsx) - `ExamsResultsFallback`
|
||||
- [questions/page.tsx:75-88](../src/app/(dashboard)/teacher/questions/page.tsx) - `QuestionBankResultsFallback`
|
||||
- **问题**:辅助组件定义在 page.tsx 中,违反「其余所有组件使用具名导出」规范,且无法复用
|
||||
- **改进建议**:提取到 `components/` 目录或 `shared/components/ui/`
|
||||
|
||||
#### BUG-T46:exams/create/page.tsx 顶部多余空行
|
||||
- **位置**:[exams/create/page.tsx:5](../src/app/(dashboard)/teacher/exams/create/page.tsx)
|
||||
- **问题**:JSX 开始标签前有多余空行
|
||||
- **现状**:
|
||||
```typescript
|
||||
return (
|
||||
|
||||
<div className="...">
|
||||
```
|
||||
- **改进建议**:删除空行
|
||||
|
||||
---
|
||||
|
||||
### 2.8 安全与权限违规 — 严重度:高
|
||||
|
||||
#### BUG-T47:缺少权限校验(course-plans/page.tsx)
|
||||
- **位置**:[course-plans/page.tsx](../src/app/(dashboard)/teacher/course-plans/page.tsx)
|
||||
- **问题**:仅通过 `auth()` 获取 session,未调用 `requirePermission()` 或 `getAuthContext()` 进行权限校验
|
||||
- **改进建议**:使用 `getAuthContext()` 替代 `auth()`,并在 data-access 层做 DataScope 过滤
|
||||
|
||||
#### BUG-T48:缺少权限校验(elective/page.tsx)
|
||||
- **位置**:[elective/page.tsx](../src/app/(dashboard)/teacher/elective/page.tsx)
|
||||
- **问题**:同 BUG-T47
|
||||
- **改进建议**:同 BUG-T47
|
||||
|
||||
#### BUG-T49:缺少权限校验(dashboard/page.tsx)
|
||||
- **位置**:[dashboard/page.tsx](../src/app/(dashboard)/teacher/dashboard/page.tsx)
|
||||
- **问题**:依赖路由层代理(proxy.ts)做角色路由,但页面本身未做二次权限校验
|
||||
- **改进建议**:添加 `getAuthContext()` 确认教师身份
|
||||
|
||||
#### BUG-T50:权限校验方式不一致
|
||||
- **位置**:
|
||||
- [exams/[id]/proctoring/page.tsx:21](../src/app/(dashboard)/teacher/exams/[id]/proctoring/page.tsx) - 使用 `requirePermission(Permissions.EXAM_PROCTOR)`
|
||||
- [diagnostic/class/[classId]/page.tsx:15-23](../src/app/(dashboard)/teacher/diagnostic/class/[classId]/page.tsx) - 使用 `getAuthContext()` + DataScope 校验
|
||||
- [grades/page.tsx:26](../src/app/(dashboard)/teacher/grades/page.tsx) - 使用 `getAuthContext()`
|
||||
- **问题**:权限校验方式不统一,部分用 `requirePermission`,部分用 `getAuthContext`,部分无校验
|
||||
- **改进建议**:统一权限校验策略,页面入口用 `getAuthContext()`,写操作用 `requirePermission()`
|
||||
|
||||
---
|
||||
|
||||
### 2.9 加载态缺失 — 严重度:低
|
||||
|
||||
#### BUG-T51:缺少 loading.tsx 的目录
|
||||
- **位置**:
|
||||
- `attendance/`(含 sheet/、stats/)
|
||||
- `course-plans/`(含 [id]/)
|
||||
- `diagnostic/`(含 class/、student/)
|
||||
- `elective/`
|
||||
- `exams/[id]/`(含 build/、proctoring/)
|
||||
- `grades/`(含 analytics/、entry/、stats/)
|
||||
- `homework/`(含 assignments/、submissions/)
|
||||
- `schedule-changes/`
|
||||
- **问题**:以上目录无 `loading.tsx`,导航时无骨架屏反馈
|
||||
- **改进建议**:为每个动态页面目录添加 `loading.tsx`,参考 `classes/my/loading.tsx` 模式
|
||||
|
||||
#### BUG-T52:exams/grading/loading.tsx 实际无用
|
||||
- **位置**:[exams/grading/loading.tsx](../src/app/(dashboard)/teacher/exams/grading/loading.tsx)
|
||||
- **问题**:`exams/grading/page.tsx` 仅做 `redirect()`,loading.tsx 永远不会显示
|
||||
- **改进建议**:删除该 loading.tsx
|
||||
|
||||
---
|
||||
|
||||
### 2.10 逻辑与代码质量问题 — 严重度:中
|
||||
|
||||
#### BUG-T53:homework/assignments/page.tsx 条件取数逻辑反直觉
|
||||
- **位置**:[homework/assignments/page.tsx:33-36](../src/app/(dashboard)/teacher/homework/assignments/page.tsx)
|
||||
- **问题**:`classId && classId !== "all" ? getTeacherClasses() : Promise.resolve([])` 仅在有 classId 时才获取班级列表,逻辑反直觉(通常应始终获取班级列表用于筛选下拉)
|
||||
- **现状**:classes 仅用于查找 className 显示,逻辑正确但可读性差
|
||||
- **改进建议**:始终获取 classes,或添加注释说明「仅在过滤时需要 className」
|
||||
|
||||
#### BUG-T54:exams/[id]/build/page.tsx `normalizeStructure` 函数过长
|
||||
- **位置**:[exams/[id]/build/page.tsx:52-91](../src/app/(dashboard)/teacher/exams/[id]/build/page.tsx)
|
||||
- **问题**:40 行的 `normalizeStructure` 函数定义在组件内部,包含嵌套递归逻辑,可读性差
|
||||
- **改进建议**:提取到 `modules/exams/utils/normalize-structure.ts`,并添加单元测试
|
||||
|
||||
#### BUG-T55:exams/[id]/build/page.tsx 使用 `satisfies` 但混合 `as`
|
||||
- **位置**:[exams/[id]/build/page.tsx:74, 84, 86](../src/app/(dashboard)/teacher/exams/[id]/build/page.tsx)
|
||||
- **问题**:同时使用 `satisfies ExamNode`(好)和 `as ExamNode[]`(违规),类型处理不一致
|
||||
- **改进建议**:移除 `as ExamNode[]`,改用类型守卫或 `Array.from()` 配合 filter
|
||||
|
||||
#### BUG-T56:grades/analytics/page.tsx 文件过长
|
||||
- **位置**:[grades/analytics/page.tsx](../src/app/(dashboard)/teacher/grades/analytics/page.tsx) - 259 行
|
||||
- **问题**:单文件 259 行,接近 React 组件 500 行建议上限的 50%,包含页面 + `AnalyticsFilters` 组件
|
||||
- **改进建议**:将 `AnalyticsFilters` 提取到 `modules/grades/components/analytics-filters.tsx`
|
||||
|
||||
#### BUG-T57:exams/all/page.tsx 缺少 `export const dynamic`
|
||||
- **位置**:[exams/all/page.tsx](../src/app/(dashboard)/teacher/exams/all/page.tsx)
|
||||
- **问题**:使用 Suspense 但未声明 `force-dynamic`,可能导致构建时尝试静态生成
|
||||
- **改进建议**:添加 `export const dynamic = "force-dynamic"`
|
||||
|
||||
---
|
||||
|
||||
### 2.11 可访问性问题 — 严重度:中
|
||||
|
||||
#### BUG-T58:图标按钮缺少 aria-label
|
||||
- **位置**:
|
||||
- [textbooks/[id]/page.tsx:33-36](../src/app/(dashboard)/teacher/textbooks/[id]/page.tsx) - 返回按钮
|
||||
- [homework/assignments/[id]/page.tsx:28-31](../src/app/(dashboard)/teacher/homework/assignments/[id]/page.tsx) - 面包屑链接(有文本,OK)
|
||||
- **问题**:`textbooks/[id]/page.tsx` 的返回按钮仅含图标,无 `aria-label`
|
||||
- **违反规则**:Accessibility - Icon-only buttons need `aria-label`
|
||||
- **改进建议**:添加 `aria-label="Back to textbooks"`
|
||||
|
||||
#### BUG-T59:装饰性图标未标记 aria-hidden
|
||||
- **位置**:几乎所有页面中的 lucide 图标
|
||||
- **问题**:如 `<BarChart3 className="mr-2 h-4 w-4" />` 等装饰性图标未添加 `aria-hidden="true"`
|
||||
- **违反规则**:Accessibility - Decorative icons need `aria-hidden="true"`
|
||||
- **改进建议**:装饰性图标添加 `aria-hidden="true"`
|
||||
|
||||
#### BUG-T60:缺少 skip link
|
||||
- **位置**:所有页面
|
||||
- **问题**:页面无「跳到主内容」的 skip link,键盘用户需 Tab 遍历整个侧边栏
|
||||
- **违反规则**:Accessibility - include skip link for main content
|
||||
- **改进建议**:在 dashboard layout 添加 skip link(应在 layout 层处理)
|
||||
|
||||
---
|
||||
|
||||
### 2.12 其他问题
|
||||
|
||||
#### BUG-T61:homework/assignments/[id]/page.tsx 使用 h1 但其他页面用 h2
|
||||
- **位置**:
|
||||
- [homework/assignments/[id]/page.tsx:36](../src/app/(dashboard)/teacher/homework/assignments/[id]/page.tsx) - `<h1>`
|
||||
- [attendance/page.tsx:47](../src/app/(dashboard)/teacher/attendance/page.tsx) - `<h2>`
|
||||
- [grades/page.tsx:54](../src/app/(dashboard)/teacher/grades/page.tsx) - `<h2>`
|
||||
- **问题**:页面主标题层级不统一,部分用 h1,部分用 h2
|
||||
- **改进建议**:统一使用 h1 作为页面主标题(layout 可能已有 h1,需确认)
|
||||
|
||||
#### BUG-T62:textbooks/page.tsx 使用 h1,其他页面用 h2
|
||||
- **位置**:
|
||||
- [textbooks/page.tsx:57](../src/app/(dashboard)/teacher/textbooks/page.tsx) - `<h1>`
|
||||
- [textbooks/[id]/page.tsx:45](../src/app/(dashboard)/teacher/textbooks/[id]/page.tsx) - `<h1>`
|
||||
- **问题**:同 BUG-T61,标题层级不统一
|
||||
- **改进建议**:统一标题层级策略
|
||||
|
||||
#### BUG-T63:exams/create/page.tsx 缺少页面标题
|
||||
- **位置**:[exams/create/page.tsx:3-9](../src/app/(dashboard)/teacher/exams/create/page.tsx)
|
||||
- **问题**:页面无任何标题,直接渲染表单
|
||||
- **改进建议**:添加 `<h1>Create Exam</h1>`
|
||||
|
||||
#### BUG-T64:loading.tsx 文件命名风格不一致
|
||||
- **位置**:
|
||||
- [textbooks/loading.tsx](../src/app/(dashboard)/teacher/textbooks/loading.tsx) - 使用 Card 组件
|
||||
- [classes/my/loading.tsx](../src/app/(dashboard)/teacher/classes/my/loading.tsx) - 使用纯 div
|
||||
- **问题**:骨架屏风格不统一,部分用 Card 组件,部分用纯 div
|
||||
- **改进建议**:统一骨架屏风格,提取共享骨架屏组件
|
||||
|
||||
---
|
||||
|
||||
## 三、改进优先级汇总
|
||||
|
||||
### P0 - 立即修复(架构与安全)
|
||||
|
||||
| BUG ID | 问题 | 影响 |
|
||||
|--------|------|------|
|
||||
| T01-T05 | app 层直接访问 DB | 破坏三层架构,模块封装失效 |
|
||||
| T06 | 认证方式不一致 | 数据范围过滤缺失 |
|
||||
| T47-T50 | 权限校验缺失/不一致 | 越权访问风险 |
|
||||
|
||||
### P1 - 高优先级(TypeScript 与性能)
|
||||
|
||||
| BUG ID | 问题 | 影响 |
|
||||
|--------|------|------|
|
||||
| T11-T15 | 使用 `as` 类型断言 | 类型安全受损 |
|
||||
| T16-T17 | 函数返回值未标注 | 类型推导不显式 |
|
||||
| T20-T28 | 串行数据获取 waterfall | 页面加载性能差 |
|
||||
| T43 | 大列表未虚拟化 | 题库页面卡顿 |
|
||||
|
||||
### P2 - 中优先级(规范与可访问性)
|
||||
|
||||
| BUG ID | 问题 | 影响 |
|
||||
|--------|------|------|
|
||||
| T07-T10 | Prettier 分号违规 | 代码风格不一致 |
|
||||
| T18-T19 | DRY 违规 | 维护成本高 |
|
||||
| T31-T32 | 筛选按钮焦点样式/语义 | 键盘可访问性差 |
|
||||
| T36-T39 | 长文本未截断 | 布局破坏风险 |
|
||||
| T42 | 数字列未用 tabular-nums | 数字对齐不整齐 |
|
||||
| T58-T60 | 可访问性缺失 | 屏幕阅读器体验差 |
|
||||
|
||||
### P3 - 低优先级(代码质量)
|
||||
|
||||
| BUG ID | 问题 | 影响 |
|
||||
|--------|------|------|
|
||||
| T44-T46 | 组件定义问题 | 可读性差 |
|
||||
| T51-T52 | loading.tsx 缺失/冗余 | 用户体验不一致 |
|
||||
| T53-T57 | 逻辑与长度问题 | 可维护性 |
|
||||
| T61-T64 | 标题层级与风格 | 一致性 |
|
||||
|
||||
---
|
||||
|
||||
## 四、推荐改进方案
|
||||
|
||||
### 4.1 提取共享工具(解决 T16, T18)
|
||||
|
||||
新建 `src/shared/lib/search-params.ts`:
|
||||
|
||||
```typescript
|
||||
export type SearchParams = { [key: string]: string | string[] | undefined }
|
||||
|
||||
export function getParam(params: SearchParams, key: string): string | undefined {
|
||||
const v = params[key]
|
||||
return Array.isArray(v) ? v[0] : v
|
||||
}
|
||||
```
|
||||
|
||||
所有页面统一 `import { getParam, type SearchParams } from "@/shared/lib/search-params"`。
|
||||
|
||||
### 4.2 提取共享筛选组件(解决 T19, T31, T32)
|
||||
|
||||
新建 `src/shared/components/ui/filter-chips.tsx`:
|
||||
|
||||
```tsx
|
||||
import Link from "next/link"
|
||||
import { cn } from "@/shared/lib/utils"
|
||||
|
||||
interface FilterChip {
|
||||
id: string
|
||||
label: string
|
||||
href: string
|
||||
active: boolean
|
||||
}
|
||||
|
||||
export function FilterChips({ chips }: { chips: FilterChip[] }) {
|
||||
return (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{chips.map((c) => (
|
||||
<Link
|
||||
key={c.id}
|
||||
href={c.href}
|
||||
className={cn(
|
||||
"rounded-md border px-3 py-1.5 text-sm transition-colors",
|
||||
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
||||
c.active
|
||||
? "border-primary bg-primary text-primary-foreground"
|
||||
: "bg-card hover:bg-accent"
|
||||
)}
|
||||
>
|
||||
{c.label}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 统一权限校验模式(解决 T47-T50)
|
||||
|
||||
所有教师页面入口统一:
|
||||
|
||||
```typescript
|
||||
import { getAuthContext } from "@/shared/lib/auth-guard"
|
||||
|
||||
export default async function XxxPage() {
|
||||
const ctx = await getAuthContext()
|
||||
// 使用 ctx.userId、ctx.dataScope 进行数据过滤
|
||||
}
|
||||
```
|
||||
|
||||
### 4.4 并行数据获取优化(解决 T20-T28)
|
||||
|
||||
将串行 `await` 改为 `Promise.all`:
|
||||
|
||||
```typescript
|
||||
// 优化前
|
||||
const classes = await getTeacherClasses()
|
||||
const records = await getGradeRecords({ ... })
|
||||
|
||||
// 优化后
|
||||
const [classes, records] = await Promise.all([
|
||||
getTeacherClasses(),
|
||||
getGradeRecords({ ... }),
|
||||
])
|
||||
```
|
||||
|
||||
### 4.5 DB 访问下沉到 data-access(解决 T01-T05)
|
||||
|
||||
在 `modules/school/data-access.ts` 添加:
|
||||
|
||||
```typescript
|
||||
import "server-only"
|
||||
import { db } from "@/shared/db"
|
||||
import { subjects } from "@/shared/db/schema"
|
||||
import { asc } from "drizzle-orm"
|
||||
|
||||
export async function getSubjectsOrdered(): Promise<Subject[]> {
|
||||
return db.query.subjects.findMany({
|
||||
orderBy: [asc(subjects.order), asc(subjects.name)],
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
页面改为 `import { getSubjectsOrdered } from "@/modules/school/data-access"`。
|
||||
|
||||
---
|
||||
|
||||
## 五、架构图同步建议
|
||||
|
||||
本次核查未修改源码,无需同步架构图。但建议在后续修复时:
|
||||
|
||||
1. 若新增 `shared/lib/search-params.ts`,需在 005_architecture_data.json 的 `shared.lib.exports` 中添加
|
||||
2. 若新增 `shared/components/ui/filter-chips.tsx`,需在 005 的 `shared.components.exports` 中添加
|
||||
3. 若 `modules/school/data-access.ts` 新增 `getSubjectsOrdered`,需在 005 的 `modules.school.dataAccess` 中添加
|
||||
|
||||
---
|
||||
|
||||
## 六、总结
|
||||
|
||||
本次核查覆盖 `src/app/(dashboard)/teacher/` 下全部 45 个前端文件,共发现 **64 个问题**,分布如下:
|
||||
|
||||
| 严重度 | 数量 | 类别 |
|
||||
|--------|------|------|
|
||||
| P0 | 9 | 架构违规、权限缺失 |
|
||||
| P1 | 16 | TypeScript、性能 |
|
||||
| P2 | 18 | 规范、可访问性 |
|
||||
| P3 | 21 | 代码质量 |
|
||||
|
||||
**核心问题**:
|
||||
1. **架构层违规严重**:5 处 app 层直接访问 DB,破坏三层架构
|
||||
2. **权限校验不一致**:部分页面无校验,部分用 `auth()`,部分用 `getAuthContext()`
|
||||
3. **性能 waterfall 普遍**:9 处串行数据获取,应改为并行
|
||||
4. **DRY 违规突出**:`getParam` 函数在 16 个文件中重复
|
||||
5. **可访问性缺失**:焦点样式、aria-label、skip link 普遍缺失
|
||||
|
||||
建议按 P0 → P1 → P2 → P3 顺序修复,优先解决架构与安全问题。
|
||||
Reference in New Issue
Block a user