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:
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` 技能加载失败,界面优化建议已合并至第四章
|
||||
Reference in New Issue
Block a user