# `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-P001:app 层直接访问 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({ title, description, icon, fetcher, renderItem, emptyTitle, emptyDescription, }: ParentChildrenDataPageProps) { /* ... */ } ``` #### 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:使用 `` 而非 ``,丢失客户端导航 - **位置**:`src/modules/parent/components/parent-dashboard.tsx:30, 36` - **问题**:`` 和 `` 使用原生 `` 标签,导致整页刷新,丢失 Next.js 客户端路由优化 - **违反规则**:Web Interface Guidelines — Navigation & State「Links use ``/`` (Cmd/Ctrl+click, middle-click support)」;Next.js 最佳实践 - **改进建议**:`import Link from "next/link"`,使用 `` #### 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 `...`」 - **改进建议**:使用 `{latestGrade.assignmentTitle}` #### BUG-P014:`cursor-pointer` 在 Link 上冗余 - **位置**:`src/modules/parent/components/child-card.tsx:21` - **问题**:`` 中 `cursor-pointer` 冗余(外层 `` 默认 pointer) - **违反规则**:Web Interface Guidelines — Anti-patterns - **改进建议**:移除 `cursor-pointer` #### BUG-P015:整个 Card 作为 Link 缺少可访问性描述 - **位置**:`src/modules/parent/components/child-card.tsx:20-87` - **问题**:`` 包裹整个 Card,屏幕阅读器会读出所有内部文本(姓名、班级、数字、最新成绩),缺少简洁的 aria-label - **违反规则**:Web Interface Guidelines — Accessibility「Icon-only buttons need `aria-label`」延伸到卡片导航 - **改进建议**:`` #### BUG-P016:Link 缺少 `focus-visible:ring` 样式 - **位置**:`src/modules/parent/components/child-card.tsx:20-21` - **问题**:`` 包裹 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` - **问题**:`· {basicInfo.email}` 直接展示子女邮箱,无防爬/掩码处理 - **改进建议**:考虑隐私场景下掩码处理(如 `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`」 - **改进建议**:定义 `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 - → use for client-side nav src/modules/parent/components/parent-dashboard.tsx:36 - → use 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 列布局在 sm(640px)下更合适 - **改进建议**:`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` 在 md(768-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 条),无"查看全部作业"入口 - **改进建议**:底部添加 `View all` ### 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-P01:004 文档 parent 模块行数记录过期 - **位置**:`docs/architecture/004_architecture_impact_map.md:924` - **问题**:记录 `data-access.ts | 234 | 子女关系 + 仪表盘数据聚合`,实际 234 行 ✅ 一致;但 `components/* | 7 文件` 实际为 7 个组件文件 ✅ 一致 - **说明**:本节核查后无需更新(行数与文件数均一致) ### DOC-P02:004 文档未记录 `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-P03:005 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**:`` 改为 `` 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 模块节点。