Files
NextEdu/bugs/parent_bug.md
SpecialX 49291fcc31 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.
2026-06-19 05:13:34 +08:00

552 lines
30 KiB
Markdown
Raw Blame History

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