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.
49 KiB
49 KiB
src/app/(dashboard)/{announcements,dashboard,management,messages,profile,settings} 规范核查报告
核查日期:2026-06-18 核查范围:
src/app/(dashboard)/下的 announcements、dashboard、management、messages、profile、settings 子路由及其直接依赖的模块组件 依据文档:
- 项目规则
- 编码规范
- 架构影响地图 004
- 架构数据 005 应用技能:
vercel-react-best-practices、web-design-guidelines(web-artifacts-builder加载失败,界面优化建议已合并至 web-design-guidelines 章节)
一、核查文件清单
二、违规问题清单
2.1 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路由获取公告数据,存在信息泄露风险 - 改进建议:
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 — 严重度:中
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判断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 — 严重度:高
BUG-M01:缺少权限校验
- 位置:
src/app/(dashboard)/management/grade/classes/page.tsx:7-15 - 问题:仅调用
auth()获取 session,未调用requirePermission()校验CLASS_MANAGE权限 - 规范依据:项目规则「Server Action 必须使用
requirePermission()进行权限校验」 - 影响:无
CLASS_MANAGE权限的用户可访问页面并获取教师列表、年级数据 - 改进建议:
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 — 严重度:高
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>元素,与项目其他页面使用的 shadcnSelect组件风格不一致 - 规范依据: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 — 严重度:低
BUG-MSG01:缺少 metadata 导出
- 位置:
src/app/(dashboard)/messages/page.tsx - 问题:未导出
metadata - 改进建议:
export const metadata = { title: "Messages" }
2.6 messages/[id]/page.tsx — 严重度:中
BUG-MSG02:渲染期间执行写操作(标记已读)
- 位置:
src/app/(dashboard)/messages/[id]/page.tsx:20-23 - 问题:在 RSC 渲染期间调用
markMessageAsRead(id, ctx.userId)执行写操作 - 违反规则:React Server Components 规范 — 渲染函数应为纯函数,不应有副作用
- 影响:
- React 18+ 严格模式下渲染函数可能被调用两次,导致重复写入
- 流式渲染时若渲染被中断,写操作可能已执行但 UI 未更新
- 错误边界捕获错误后重试渲染会再次执行写操作
- 改进建议:使用
after()API 延迟执行非阻塞写操作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 — 严重度:低
BUG-MSG03:缺少 metadata 导出
- 改进建议:
export const metadata = { title: "Compose Message" }
2.8 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判断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 () => { ... })()立即执行异步函数,将学生数据加载逻辑内联在组件中 - 影响:
- 函数体过长(60+ 行),难以测试
- 无法被 React
cache()缓存 - 违反单一职责原则
- 改进建议:抽取为
data-access.ts中的getStudentProfileData(userId)函数// 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转换)」 - 改进建议:使用类型守卫
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 — 严重度:中
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 — 严重度:低
BUG-SS01:缺少权限校验
- 位置:
src/app/(dashboard)/settings/security/page.tsx:14-16 - 问题:仅检查
session?.user,未调用requirePermission() - 改进建议:至少调用
requireAuth()确保登录状态
2.11 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 — 严重度:低
BUG-E01:未使用 error.digest 信息
- 位置:
src/app/(dashboard)/error.tsx:7 - 问题:
error参数包含digest字段(用于错误追踪),但未展示给用户或上报 - 改进建议:在描述中包含
digest或提供「复制错误码」按钮
2.13 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 — 严重度:中
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>组件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 — 严重度:低
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包裹组件export const AnnouncementCard = React.memo(function AnnouncementCard({...}) { return <Card>...</Card> })
2.16 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 — 严重度:中
BUG-ML01:使用字符串拼接动态类名
- 位置:
src/modules/messaging/components/message-list.tsx:82,91 - 问题:
className={`transition-colors hover:bg-accent/50 ${unread ? "border-primary/40" : ""}`}使用模板字符串拼接类名 - 规范依据:项目规则「使用
cn()工具函数管理条件类名」 - 改进建议:
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 — 严重度:中
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构建查询字符串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 — 严重度:中
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 — 严重度:中
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 — 严重度:高
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()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>或受控组件重置表单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 — 严重度:高
BUG-PS01:使用 as any 类型断言(严重违规)
- 位置:
src/modules/settings/components/profile-settings-form.tsx:35 - 问题:
resolver: zodResolver(profileFormSchema) as any使用as any - 规范依据:项目规则「禁止
any」「禁止as断言」 - 改进建议:修复
zodResolver类型不匹配问题// 方案 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处理空值age: z.preprocess( (v) => (v === "" || v === null || v === undefined ? undefined : Number(v)), z.number().min(0).optional() )
2.23 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")提交表单<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初始化自preferencesprop,但 prop 变化时状态不更新 - 违反规则:
rerender-derived-state-no-effect— 不应使用 effect 同步派生状态 - 改进建议:使用
keyprop 重置组件,或使用受控组件
BUG-NPF03:中文注释混合英文代码
- 位置:
src/modules/settings/components/notification-preferences-form.tsx:121,161,209 - 问题:
// 本地状态用于即时反馈 Switch 切换、{/* 通知渠道 */}、{/* 通知类别 */}中文注释 - 规范依据:项目代码一致性(其他文件使用英文注释)
- 改进建议:统一为英文注释
2.24 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 — 严重度:高
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 均为英文
- 影响:用户在英文界面中突然看到中文,体验割裂
- 改进建议:统一为英文
<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内部,依赖项仅为[](仅执行一次)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 — 严重度:低
BUG-AS01:Tab 图标语义错误
- 位置:
src/modules/settings/components/admin-settings-view.tsx:50-53 - 问题:
appearanceTab 使用<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 — 严重度:低
BUG-TS01:与 admin-settings-view.tsx 大量重复代码
- 位置:
src/modules/settings/components/teacher-settings-view.tsx - 问题:与
admin-settings-view.tsx、student-settings-view.tsx90% 代码重复,仅「Back to dashboard」链接和「Quick links」不同 - 规范依据:DRY 原则
- 改进建议:抽取为
SettingsLayout共享组件,通过 props 传入backHref和quickLinksexport function SettingsLayout({ title, description, backHref, quickLinks, children }: {...}) { return <div>...</div> }
2.28 student-settings-view.tsx — 严重度:低
BUG-ST01:同 BUG-TS01,代码重复
- 改进建议:同 BUG-TS01
2.29 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.tsxgrade-class-edit-dialog.tsxgrade-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.mdPERF-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 父组件传入
// 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状态来自initialStatusprop 而非 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 - 问题:混合使用受控(
receiverIdstate)和非受控(FormData)模式 - 改进建议:统一使用受控组件或完全使用 FormData
4.4 Content & Copy
UI-09:中英文混合 UI
- 位置:
ai-provider-settings-card.tsx:BUG-AI01grade-classes-view.tsx:BUG-GC03notification-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
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
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(立即修复 — 影响安全与正确性)
- BUG-A01:
announcements/page.tsx增加权限校验 - BUG-M01:
management/grade/classes/page.tsx增加权限校验 - BUG-MI01:
management/grade/insights/page.tsx增加权限校验 - BUG-PC01:
password-change-form.tsx修复动态类名拼接(生产环境进度条无颜色) - BUG-PS01:
profile-settings-form.tsx移除as any - BUG-AI01:
ai-provider-settings-card.tsx统一 UI 语言为英文
P1(本迭代修复 — 影响可维护性与性能)
- BUG-P01、BUG-S01、BUG-D01:使用
roles判断角色,移除权限反推 - BUG-P02:
profile/page.tsx抽取数据加载逻辑到 data-access - BUG-MSG02:
messages/[id]/page.tsx使用after()延迟写操作 - BUG-AL01、BUG-AD01、BUG-MD01、BUG-MC01:替换
<a>为<Link> - BUG-ML01、BUG-NL01:使用
cn()替换字符串拼接 - PERF-01:
usePermission回调 memoize - UI-01:权限相关 UI 改为 RSC prop 传入
P2(下迭代修复 — 增强健壮性)
- BUG-GC01:
grade-classes-view.tsx拆分组件 - BUG-NPF01:
notification-preferences-form.tsx修复 Switch/checkbox 双重切换 - BUG-MI02、BUG-MI03、BUG-MI04:
management/grade/insights改用 shadcn Select - BUG-PC02:
password-change-form.tsx使用useRef替代document.getElementById - BUG-TS01、BUG-ST01:抽取
SettingsLayout共享组件 - BUG-AS01:修复 Tab 图标语义
- UI-10:错误页增加修复步骤
P3(文档同步)
- DOC-01 ~ DOC-04:同步架构文档
八、验证命令
修复完成后应运行以下命令确保零错误:
npm run lint
npx tsc --noEmit
npm run test:unit
针对特定模块的端到端验证:
# 验证权限校验
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技能加载失败,界面优化建议已合并至第四章