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.
39 KiB
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 界面规范审查)
一、核查文件清单
共计 45 个文件(37 个 page.tsx + 8 个 loading.tsx)。
二、违规问题清单
2.1 架构分层违规 — 严重度:高
BUG-T01:app 层直接访问数据库(dashboard/page.tsx)
- 位置:dashboard/page.tsx:4-6, 18-21
- 问题:页面直接
import { db } from "@/shared/db"并调用db.query.users.findFirst(),违反项目规则「app/只能调用modules/的 Server Actions 和 data-access,不直接访问 DB」 - 现状:
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
- 问题:直接
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
- 问题:同 BUG-T02,直接
db.query.subjects.findMany() - 改进建议:同 BUG-T02
BUG-T04:app 层直接访问数据库(grades/entry/page.tsx)
- 位置:grades/entry/page.tsx:1-3, 25
- 问题:同 BUG-T02
- 改进建议:同 BUG-T02
BUG-T05:app 层直接访问数据库(grades/stats/page.tsx)
- 位置:grades/stats/page.tsx:1-3, 28
- 问题:同 BUG-T02
- 改进建议:同 BUG-T02
BUG-T06:认证上下文获取方式不一致
- 位置:
- 问题:使用
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
- 问题:
import { TextbookCard } from "...";等多处使用分号 - 改进建议:运行
npx prettier --write统一格式
BUG-T08:textbooks/[id]/page.tsx 使用分号
- 位置:textbooks/[id]/page.tsx(全文)
- 问题:多处语句使用分号结尾
- 改进建议:同 BUG-T07
BUG-T09:textbooks/loading.tsx 使用分号
- 位置:textbooks/loading.tsx(全文)
- 问题:同 BUG-T07
- 改进建议:同 BUG-T07
BUG-T10:textbooks/[id]/loading.tsx 使用分号
- 位置: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
- 问题:使用
as断言转换类型,违反编码规范「禁止as断言(除非从unknown转换)」 - 现状:
content: q.content as Question["content"], type: q.type as Question["type"], - 改进建议:在 data-access 层返回正确类型,或使用类型守卫函数
BUG-T12:使用 as 类型断言(attendance/page.tsx)
- 位置:attendance/page.tsx:39
- 问题:
status as "present" | "absent" | "late" | "early_leave" | "excused"直接断言 - 改进建议:使用类型守卫函数
isAttendanceStatus(value): value is AttendanceStatus
BUG-T13:使用 as 类型断言(grades/page.tsx)
- 位置:grades/page.tsx:43-44
- 问题:
type as "exam" | "quiz" | "homework" | "other"和semester as "1" | "2"直接断言 - 改进建议:使用类型守卫
BUG-T14:使用 as 类型断言(grades/analytics/page.tsx)
- 位置:grades/analytics/page.tsx(多处)
- 问题:同上模式
- 改进建议:同上
BUG-T15:使用 as 类型断言(diagnostic/page.tsx)
- 位置:diagnostic/page.tsx:27-28
- 问题:
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: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 模式重复
- 位置:
- 问题:三处文件都定义了「类筛选按钮组」组件,结构几乎相同(
<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
- 问题:
getTeacherClasses()与getAttendanceRecords()串行执行,但二者无依赖关系 - 违反规则:
async-parallel- 独立操作应使用Promise.all() - 改进建议:
const [classes, result] = await Promise.all([ getTeacherClasses(), getAttendanceRecords({ ... }), ])
BUG-T21:串行数据获取 waterfall(attendance/sheet/page.tsx)
- 位置:attendance/sheet/page.tsx:24-29
- 问题:
getTeacherClasses()与getClassStudentsForAttendance()串行,但 students 依赖 defaultClassId(来自 searchParams),可与 classes 并行 - 改进建议:使用
Promise.all并行
BUG-T22:串行数据获取 waterfall(attendance/stats/page.tsx)
- 位置:attendance/stats/page.tsx:28-53
- 问题:
getTeacherClasses()→getClassAttendanceStats()串行,但 stats 依赖 classId(可从 classes[0] 取默认),可优化 - 改进建议:先并行获取 classes,再取 targetClassId 后获取 stats(当前逻辑合理但可考虑预取)
BUG-T23:串行数据获取 waterfall(grades/page.tsx)
- 位置:grades/page.tsx:33-45
- 问题:
Promise.all([getTeacherClasses, db.query])之后串行getGradeRecords,但getGradeRecords不依赖前两者结果 - 改进建议:三个查询全部
Promise.all
BUG-T24:串行数据获取 waterfall(grades/entry/page.tsx)
- 位置:grades/entry/page.tsx:23-34
- 问题:
Promise.all([getTeacherClasses, db.query])后串行getClassStudentsForEntry,但 students 依赖 defaultClassId(来自 searchParams),可并行 - 改进建议:
Promise.all三个查询
BUG-T25:串行数据获取 waterfall(grades/stats/page.tsx)
- 位置:grades/stats/page.tsx:26-54
- 问题:
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
- 问题:
Promise.all([insights, students, schedule])后串行getClassStudentSubjectScoresV2 - 改进建议:将
getClassStudentSubjectScoresV2加入第一个Promise.all
BUG-T27:串行数据获取 waterfall(diagnostic/student/[studentId]/page.tsx)
- 位置:diagnostic/student/[studentId]/page.tsx:30-45
- 问题:
Promise.all([summary, reports])后串行getKnowledgePointStats() - 改进建议:合并为单个
Promise.all
BUG-T28:串行数据获取 waterfall(exams/[id]/build/page.tsx)
- 位置:exams/[id]/build/page.tsx:12-26
- 问题:
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(使用 Suspense,可省略)
- exams/create/page.tsx
- exams/[id]/build/page.tsx
- questions/page.tsx(使用 Suspense)
- textbooks/page.tsx(使用 Suspense)
- 问题:动态数据页面未声明
force-dynamic,可能导致静态生成尝试失败 - 改进建议:所有含动态数据的页面统一添加
export const dynamic = "force-dynamic"
2.6 Web 界面规范违规(web-design-guidelines) — 严重度:中
BUG-T31:<a> 标签缺少 focus-visible 焦点样式
- 位置:
- 问题:筛选按钮使用
<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
- 问题:页面无
<h1>标题,直接渲染<ExamAssembly>组件,违反「Headings hierarchical<h1>–<h6>」 - 改进建议:在页面顶部添加
<h1>标题(如「Build Exam」)
BUG-T34:标题层级缺失(exams/[id]/proctoring/page.tsx)
- 位置:exams/[id]/proctoring/page.tsx:50-54
- 问题:同 BUG-T33,无
<h1> - 改进建议:同 BUG-T33
BUG-T35:标题层级缺失(classes/my/[id]/page.tsx)
- 位置:classes/my/[id]/page.tsx:65-108
- 问题:页面无
<h1>,依赖<ClassHeader>组件渲染标题,需确认组件内是否有 h1 - 改进建议:确认
ClassHeader包含<h1>
BUG-T36:长文本未截断(homework/assignments/page.tsx)
- 位置:homework/assignments/page.tsx:99-101
- 问题:作业标题
<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
- 问题:同 BUG-T36
- 改进建议:同 BUG-T36
BUG-T38:长文本未截断(homework/assignments/[id]/submissions/page.tsx)
- 位置:homework/assignments/[id]/submissions/page.tsx:65
- 问题:学生姓名单元格未限制长度
- 改进建议:添加
truncate max-w-[160px]
BUG-T39:Flex 子元素缺少 min-w-0
- 位置:
- 问题:flex 容器内的文本子元素未设置
min-w-0,长内容无法正确截断 - 违反规则:Flex children need
min-w-0to allow text truncation - 改进建议:在 flex 子元素添加
min-w-0
BUG-T40:使用 transition: all 或 transition-colors 未列明属性
- 位置:
- attendance/stats/page.tsx:109 -
transition-colors(可接受) - grades/analytics/page.tsx:195 -
transition-colors(可接受)
- attendance/stats/page.tsx:109 -
- 问题:
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
- 位置:
- 问题:数字列未使用
font-variant-numeric: tabular-nums,对齐不整齐 - 违反规则:Typography -
font-variant-numeric: tabular-numsfor number columns - 改进建议:数字单元格添加
tabular-nums类
BUG-T43:大列表未虚拟化
- 位置:
- questions/page.tsx:42 -
pageSize: 200 - exams/all/page.tsx - ExamDataTable
- questions/page.tsx:42 -
- 问题:题库页面一次加载 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
- 问题:默认导出
MyClassesPage仅调用MyClassesPageImpl,多此一举 - 现状:
export default function MyClassesPage() { return <MyClassesPageImpl /> } async function MyClassesPageImpl() { // ... } - 改进建议:直接默认导出 async 函数:
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 -
StatsClassSelector - grades/analytics/page.tsx:150-258 -
AnalyticsFilters - grades/stats/page.tsx:86-138 -
StatsClassSelector - classes/schedule/page.tsx:45-63 -
ScheduleResultsFallback - classes/students/page.tsx:68-81 -
StudentsResultsFallback - exams/all/page.tsx:101-128 -
ExamsResultsFallback - questions/page.tsx:75-88 -
QuestionBankResultsFallback
- attendance/stats/page.tsx:91-119 -
- 问题:辅助组件定义在 page.tsx 中,违反「其余所有组件使用具名导出」规范,且无法复用
- 改进建议:提取到
components/目录或shared/components/ui/
BUG-T46:exams/create/page.tsx 顶部多余空行
- 位置:exams/create/page.tsx:5
- 问题:JSX 开始标签前有多余空行
- 现状:
return ( <div className="..."> - 改进建议:删除空行
2.8 安全与权限违规 — 严重度:高
BUG-T47:缺少权限校验(course-plans/page.tsx)
- 位置:course-plans/page.tsx
- 问题:仅通过
auth()获取 session,未调用requirePermission()或getAuthContext()进行权限校验 - 改进建议:使用
getAuthContext()替代auth(),并在 data-access 层做 DataScope 过滤
BUG-T48:缺少权限校验(elective/page.tsx)
- 位置:elective/page.tsx
- 问题:同 BUG-T47
- 改进建议:同 BUG-T47
BUG-T49:缺少权限校验(dashboard/page.tsx)
- 位置:dashboard/page.tsx
- 问题:依赖路由层代理(proxy.ts)做角色路由,但页面本身未做二次权限校验
- 改进建议:添加
getAuthContext()确认教师身份
BUG-T50:权限校验方式不一致
- 位置:
- exams/[id]/proctoring/page.tsx:21 - 使用
requirePermission(Permissions.EXAM_PROCTOR) - diagnostic/class/[classId]/page.tsx:15-23 - 使用
getAuthContext()+ DataScope 校验 - grades/page.tsx:26 - 使用
getAuthContext()
- exams/[id]/proctoring/page.tsx:21 - 使用
- 问题:权限校验方式不统一,部分用
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
- 问题:
exams/grading/page.tsx仅做redirect(),loading.tsx 永远不会显示 - 改进建议:删除该 loading.tsx
2.10 逻辑与代码质量问题 — 严重度:中
BUG-T53:homework/assignments/page.tsx 条件取数逻辑反直觉
- 位置:homework/assignments/page.tsx:33-36
- 问题:
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
- 问题: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
- 问题:同时使用
satisfies ExamNode(好)和as ExamNode[](违规),类型处理不一致 - 改进建议:移除
as ExamNode[],改用类型守卫或Array.from()配合 filter
BUG-T56:grades/analytics/page.tsx 文件过长
- 位置: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
- 问题:使用 Suspense 但未声明
force-dynamic,可能导致构建时尝试静态生成 - 改进建议:添加
export const dynamic = "force-dynamic"
2.11 可访问性问题 — 严重度:中
BUG-T58:图标按钮缺少 aria-label
- 位置:
- textbooks/[id]/page.tsx:33-36 - 返回按钮
- homework/assignments/[id]/page.tsx:28-31 - 面包屑链接(有文本,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 -
<h1> - attendance/page.tsx:47 -
<h2> - grades/page.tsx:54 -
<h2>
- homework/assignments/[id]/page.tsx:36 -
- 问题:页面主标题层级不统一,部分用 h1,部分用 h2
- 改进建议:统一使用 h1 作为页面主标题(layout 可能已有 h1,需确认)
BUG-T62:textbooks/page.tsx 使用 h1,其他页面用 h2
- 位置:
- textbooks/page.tsx:57 -
<h1> - textbooks/[id]/page.tsx:45 -
<h1>
- textbooks/page.tsx:57 -
- 问题:同 BUG-T61,标题层级不统一
- 改进建议:统一标题层级策略
BUG-T63:exams/create/page.tsx 缺少页面标题
- 位置:exams/create/page.tsx:3-9
- 问题:页面无任何标题,直接渲染表单
- 改进建议:添加
<h1>Create Exam</h1>
BUG-T64:loading.tsx 文件命名风格不一致
- 位置:
- textbooks/loading.tsx - 使用 Card 组件
- 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:
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:
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)
所有教师页面入口统一:
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:
// 优化前
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 添加:
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"。
五、架构图同步建议
本次核查未修改源码,无需同步架构图。但建议在后续修复时:
- 若新增
shared/lib/search-params.ts,需在 005_architecture_data.json 的shared.lib.exports中添加 - 若新增
shared/components/ui/filter-chips.tsx,需在 005 的shared.components.exports中添加 - 若
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 | 代码质量 |
核心问题:
- 架构层违规严重:5 处 app 层直接访问 DB,破坏三层架构
- 权限校验不一致:部分页面无校验,部分用
auth(),部分用getAuthContext() - 性能 waterfall 普遍:9 处串行数据获取,应改为并行
- DRY 违规突出:
getParam函数在 16 个文件中重复 - 可访问性缺失:焦点样式、aria-label、skip link 普遍缺失
建议按 P0 → P1 → P2 → P3 顺序修复,优先解决架构与安全问题。