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:
SpecialX
2026-06-19 05:13:09 +08:00
parent 063baffe4c
commit 49291fcc31
114 changed files with 12548 additions and 3395 deletions

551
bugs/parent_bug.md Normal file
View File

@@ -0,0 +1,551 @@
# `src/app/(dashboard)/parent` 前端规范核查报告
> 核查日期2026-06-18
> 核查范围:`src/app/(dashboard)/parent/` 下所有前端文件 + `src/modules/parent/` 配套组件与 data-access
> 依据文档:项目规则、编码规范 `docs/standards/coding-standards.md`、架构影响地图 004、架构数据 005
> 应用技能:`vercel-react-best-practices`、`web-artifacts-builder`、`web-design-guidelines`
---
## 一、核查文件清单
### 1.1 路由页面文件(`src/app/(dashboard)/parent/`
| 文件 | 行数 | 类型 | 用途 |
|------|------|------|------|
| [dashboard/page.tsx](../src/app/(dashboard)/parent/dashboard/page.tsx) | 16 | Server Component | 家长仪表盘入口页 |
| [attendance/page.tsx](../src/app/(dashboard)/parent/attendance/page.tsx) | 61 | Server Component | 子女考勤聚合页 |
| [grades/page.tsx](../src/app/(dashboard)/parent/grades/page.tsx) | 61 | Server Component | 子女成绩聚合页 |
| [children/[studentId]/page.tsx](../src/app/(dashboard)/parent/children/[studentId]/page.tsx) | 71 | Server Component | 单个子女详情页 |
### 1.2 模块组件文件(`src/modules/parent/components/`
| 文件 | 行数 | 类型 | 用途 |
|------|------|------|------|
| [parent-dashboard.tsx](../src/modules/parent/components/parent-dashboard.tsx) | 68 | Server Component | 仪表盘主组件 |
| [child-card.tsx](../src/modules/parent/components/child-card.tsx) | 89 | Server Component | 子女卡片 |
| [child-detail-header.tsx](../src/modules/parent/components/child-detail-header.tsx) | 49 | Server Component | 详情页头部 |
| [child-detail-panel.tsx](../src/modules/parent/components/child-detail-panel.tsx) | 27 | Server Component | 详情页面板 |
| [child-grade-summary.tsx](../src/modules/parent/components/child-grade-summary.tsx) | 163 | Client Component | 成绩趋势图 |
| [child-homework-summary.tsx](../src/modules/parent/components/child-homework-summary.tsx) | 131 | Server Component | 作业摘要 |
| [child-schedule-card.tsx](../src/modules/parent/components/child-schedule-card.tsx) | 67 | Server Component | 今日课表 |
### 1.3 数据访问与类型(`src/modules/parent/`
| 文件 | 行数 | 类型 | 用途 |
|------|------|------|------|
| [data-access.ts](../src/modules/parent/data-access.ts) | 234 | server-only | 家长-子女数据聚合 |
| [types.ts](../src/modules/parent/types.ts) | 57 | 类型定义 | 模块类型 |
---
## 二、违规问题清单
### 2.1 `children/[studentId]/page.tsx` — 严重度:高(架构违规)
#### BUG-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 模块节点。