feat(attendance,elective): 实现所有 P2 长期改进项
P2 修复(来自审计报告): - 2.4.4: Server Action 错误消息 i18n 化(attendance/elective 全部 Action) - 2.5.3: 抽取 AttendancePageLayout 组件复用(admin/teacher 页面) - 2.5.4: 抽取 ElectivePageLayout 组件复用(admin/teacher 列表页) - 2.6.3: 考勤月历键盘导航(tabIndex + 方向键 + Home/End + role=grid) - 2.8.2: getStudentAttendanceSummary 分页优化(SQL 聚合统计 + LIMIT 分页) - 2.8.3: resolveCourseDisplayNames 缓存优化(React cache 去重) - 2.1.4: elective data-access 跨模块依赖接口抽象(resolvers.ts 可注入) P2 建议项: - 选课时间冲突检测(parseSchedule + isScheduleConflict 纯函数 + checkScheduleConflict) - 学分上限校验(MAX_CREDIT_PER_TERM + checkCreditLimit) - 考勤/选课数据导出 Excel(export.ts + API 路由扩展) 新增文件: - src/modules/attendance/components/attendance-page-layout.tsx - src/modules/elective/components/elective-page-layout.tsx - src/modules/elective/resolvers.ts - src/modules/attendance/export.ts - src/modules/elective/export.ts 校验: - npm run lint 通过(exit 0) - npx tsc --noEmit attendance/elective/parent 相关零错误
This commit is contained in:
@@ -451,7 +451,7 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
|
||||
| **表单字段** | `TextareaField` | `components/form-fields/textarea-field.tsx` | 通用多行文本字段(FormField + Textarea 包装) | 1 个(P1-2: create-question-dialog) |
|
||||
| **图表组件** | `ChartCardShell` | `components/charts/chart-card-shell.tsx` | 图表卡片外壳(Card+Header+EmptyState+Content 统一结构) | 8 个(P3-c) |
|
||||
| **图表组件** | `TrendLineChart` | `components/charts/trend-line-chart.tsx` | 趋势折线图(LineChart 统一配置,支持单/多系列) | 8 个(P3-c: grade-trend-chart 等) |
|
||||
| **图表组件** | `SimpleBarChart` | `components/charts/simple-bar-chart.tsx` | 柱状图(BarChart 统一配置,支持单/多 Bar + Cell 分桶着色) | 8 个(P3-c: grade-distribution-chart 等) |
|
||||
| **图表组件** | `SimpleBarChart` | `components/charts/simple-bar-chart.tsx` | 柱状图(BarChart 统一配置,支持单/多 Bar + Cell 分桶着色 + defs 自定义 SVG 图案) | 8 个(P3-c: grade-distribution-chart 等) |
|
||||
| **图表组件** | `ComparisonRadarChart` | `components/charts/comparison-radar-chart.tsx` | 对比雷达图(RadarChart 统一配置,支持双 Radar 对比) | 8 个(P3-c: subject-comparison-chart, mastery-radar-chart 等) |
|
||||
| **课表组件** | `ScheduleList` / `ScheduleListItem` | `components/schedule/schedule-list.tsx` | 课表列表+列表项(课程+时间+地点+班级徽章,separator/card 两种变体) | 3 个(P3-a: student-today-schedule-card, child-schedule-card, student-schedule-view) |
|
||||
| **题库组件** | `QuestionBankFilters` | `components/question/question-bank-filters.tsx` | 题库筛选栏(搜索+题型+难度,default/compact 两种布局) | 2 个(P3-d: exam-assembly, question-bank-picker) |
|
||||
@@ -766,6 +766,7 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
|
||||
- ✅ v3-P2 改进(2026-06-23):`getGradeTrend`/`getGradeDistribution`/`getSubjectComparison`/`getClassComparison` 均新增 `semester` 和 `examId` 可选参数,支持按学期和考试筛选分析
|
||||
- ✅ v3-P2 改进(2026-06-23):新增 `getSchoolWideGradeSummary` data-access 函数 + `SchoolWideSummaryCard` 组件 + `SchoolWideGradeSummary`/`SchoolWideGradeSummaryItem` 类型,管理员全校成绩汇总视图(按年级聚合平均分/及格率/优秀率/学生数/班级数,加权平均计算全校汇总);admin/school/grades/insights/page.tsx 顶部新增 SchoolWideSummaryCard
|
||||
- ✅ v3-P2 改进(2026-06-23):parent/grades/page.tsx 为每个子女并行查询 `getClassAverageTrend`,渲染 GradeTrendCard
|
||||
- ✅ v3-P3-4 改进(2026-06-23):GradeTrendCard 新增日期范围选择器(全部/近7天/近30天/近90天),通过 nuqs `trendRange` URL 参数持久化,useEffect 中计算截止时间戳避免渲染阶段调用 Date.now()
|
||||
- ✅ P3 修复(2026-06-23):~~`lib/grade-utils.ts` 72 行超 40 行工具函数上限~~ P3-26 将 `buildScopeClassFilter` 迁移至 `lib/scope-filter.ts`,grade-utils.ts 仅保留 toNumber/normalize(20 行)
|
||||
- ✅ P3 修复(2026-06-23):~~`stats-service.ts` createDefaultBuckets 不必要导出~~ P3-10 移除 export 关键字,改为内部函数
|
||||
- ✅ P3 修复(2026-06-23):~~`stats-service.ts` buildGradeTrendPoints 使用 as 断言~~ P3-24 新增 isGradeTrendType 类型守卫函数替代 as 断言
|
||||
@@ -1111,7 +1112,7 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
|
||||
- ✅ V2-P1-2 已修复:~~MessageList 客户端过滤冗余~~ 客户端过滤仅在初始数据(type=all)时执行,搜索结果已由服务端按 tab 过滤
|
||||
- ✅ V2-P1-3 已修复:~~消息详情页分散编排~~ 新增 `getMessageDetailPageData` 编排函数,替代 page.tsx 中 `after()` + `getMessageById` + `markMessageAsRead` 的分散编排
|
||||
- ✅ V2-P1-4 已修复:~~表单无服务端校验错误展示~~ `message-compose.tsx` 新增 `fieldErrors` 状态 + `aria-invalid` 字段级错误展示(receiverId/subject/content)
|
||||
- ✅ V2-P2-1 已修复:~~轮询间隔魔法数字~~ `unread-message-badge.tsx` 轮询间隔提取为 `POLL_INTERVAL_MS` 常量(60_000ms)
|
||||
- ✅ V2-P2-1 已修复:~~轮询间隔魔法数字~~ `unread-message-badge.tsx` 轮询间隔提取为 `POLL_INTERVAL_MS` 常量(~~60_000ms~~ → 30_000ms,✅ V2-P3 与通知组件保持一致)
|
||||
|
||||
**文件清单**:
|
||||
| 文件 | 行数 | 职责 |
|
||||
@@ -1128,7 +1129,7 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
|
||||
| `components/message-list.tsx` | 消息列表(✅ P1-7:使用 `useMessageSearch` hook + 客户端分页 UI,PAGE_SIZE=20;✅ V2-P1-2:客户端过滤仅在初始数据时执行) |
|
||||
| `components/message-detail.tsx` | 消息详情(含回复) |
|
||||
| `components/message-compose.tsx` | 撰写新消息(✅ V2-P1-4:fieldErrors + aria-invalid 字段级错误展示) |
|
||||
| `components/unread-message-badge.tsx` | 未读消息计数徽章(侧边栏,每 60 秒轮询 `getUnreadMessageCountAction`;✅ V2-P2-1:POLL_INTERVAL_MS 常量) |
|
||||
| `components/unread-message-badge.tsx` | 未读消息计数徽章(侧边栏,每 30 秒轮询 `getUnreadMessageCountAction`;✅ V2-P2-1:POLL_INTERVAL_MS 常量;✅ V2-P3:间隔从 60s 缩短为 30s 与通知一致) |
|
||||
|
||||
**客户端行为**:
|
||||
- `message-list.tsx`:客户端调用 `getMessagesAction` 搜索消息(useMessageSearch hook,400ms 防抖,请求竞态取消);V2-P1-2 优化:客户端过滤仅在初始数据(type=all)时执行
|
||||
@@ -1147,6 +1148,7 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
|
||||
- Preferences:`getNotificationPreferences` / `upsertNotificationPreferences`(✅ P0-4 / P1-5 修复后从 messaging 迁移)
|
||||
- Channels:`InAppChannelSender` / `SmsChannelSender` / `EmailChannelSender` / `WeChatChannelSender`
|
||||
- Components:`NotificationList` / `NotificationDropdown`(✅ P1-4 新增:从 messaging/components 迁移)
|
||||
- Hooks:`useNotificationStream`(✅ V2-P3 新增:SSE 实时推送 + 轮询降级 Hook)
|
||||
|
||||
**依赖关系**:
|
||||
- 依赖:`shared/*`、`@/auth`、`classes`(✅ P1-1 已修复:通过 classes data-access.getClassExists/getStudentIdsByClassId)
|
||||
@@ -1160,6 +1162,7 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
|
||||
- ✅ P2-11 已修复:~~通知标记已读无埋点~~ `markNotificationAsReadAction` / `markAllNotificationsAsReadAction` 新增 `trackEvent` 埋点(notification.marked_read / notification.marked_all_read)
|
||||
- ✅ V2-P0-1 已修复:~~通知 i18n 键混在 messages.json 中~~ 新增独立的 `notifications.json` 命名空间(zh-CN/en),通知组件 `useTranslations` 从 `"messages"` 切换到 `"notifications"`;`src/i18n/request.ts` 新增 notifications 命名空间加载
|
||||
- ✅ V2-P2-1 已修复:~~轮询间隔魔法数字~~ `notification-dropdown.tsx` 轮询间隔提取为 `POLL_INTERVAL_MS` 常量(30_000ms)
|
||||
- ✅ V2-P3 已优化:~~30 秒轮询~~ `notification-dropdown.tsx` 改为 SSE 实时推送(`/api/notifications/stream`),SSE 不可用时自动降级为轮询(调用 Server Actions,间隔 30 秒)
|
||||
- ⚠️ P1:发送日志仅 console,无 `notification_logs` 表
|
||||
- ✅ 渠道抽象优秀(接口 + 工厂 + Mock 实现)
|
||||
|
||||
@@ -1174,16 +1177,18 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
|
||||
| `index.ts` | ~75 | 对外导出入口(✅ P1-4:新增组件和 CRUD Action 导出) |
|
||||
| `channels/*` | 5 文件 | 4 个渠道实现 |
|
||||
| `components/notification-list.tsx` | ~140 | ✅ P1-4 新增(从 messaging 迁移):通知列表组件 |
|
||||
| `components/notification-dropdown.tsx` | ~180 | ✅ P1-4 新增(从 messaging 迁移):通知下拉菜单组件 |
|
||||
| `components/notification-dropdown.tsx` | ~150 | ✅ P1-4 新增(从 messaging 迁移):通知下拉菜单组件;✅ V2-P3:改用 SSE 实时推送 + 轮询降级 |
|
||||
| `hooks/use-notification-stream.ts` | ~195 | ✅ V2-P3 新增:SSE 实时推送 Hook(EventSource + 轮询降级) |
|
||||
|
||||
**组件清单**:
|
||||
| 组件 | 职责 |
|
||||
|------|------|
|
||||
| `components/notification-list.tsx` | 通知列表(消息页底部,展示所有通知,支持标记已读;✅ V2-P0-1:useTranslations 命名空间从 "messages" 切换到 "notifications") |
|
||||
| `components/notification-dropdown.tsx` | 通知下拉菜单(站点头部,每 30 秒轮询 `getNotificationsAction` + `getUnreadNotificationCountAction`;✅ V2-P0-1:useTranslations 命名空间切换;✅ V2-P2-1:POLL_INTERVAL_MS 常量) |
|
||||
| `components/notification-dropdown.tsx` | 通知下拉菜单(站点头部,✅ V2-P3:改用 SSE 实时推送 `/api/notifications/stream` + 轮询降级;✅ V2-P0-1:useTranslations 命名空间切换;✅ V2-P2-1:POLL_INTERVAL_MS 常量) |
|
||||
|
||||
**客户端行为**:
|
||||
- `notification-dropdown.tsx`:每 `POLL_INTERVAL_MS`(30_000ms)轮询 `getNotificationsAction`(pageSize=10)和 `getUnreadNotificationCountAction` 刷新通知和未读计数
|
||||
- `notification-dropdown.tsx`:✅ V2-P3 改用 `useNotificationStream` Hook 消费 SSE 实时推送(`/api/notifications/stream`),SSE 不可用时自动降级为轮询(调用 `getNotificationsAction` + `getUnreadNotificationCountAction`,间隔 30 秒)
|
||||
- `hooks/use-notification-stream.ts`:✅ V2-P3 新增,管理 SSE 连接生命周期(EventSource onopen/onmessage/onerror),降级时通过 `pollFnRef` 调用 Server Actions 轮询
|
||||
|
||||
---
|
||||
|
||||
@@ -1509,13 +1514,13 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
|
||||
|
||||
**导出函数**:
|
||||
- Actions:`generateStudentReportAction` / `generateClassReportAction` / `publishReportAction` / `deleteReportAction`(v2-P2-3 修复:删除死代码 `getDiagnosticReportsAction` / `getDiagnosticReportByIdAction`,页面直接调用 data-access 并自行权限校验)
|
||||
- Data-access:`updateMasteryFromSubmission`(v2-P1-8 修复:累积模式;v2-P2-5 修复:db.transaction 包裹)/ `getStudentMastery` / `getStudentMasterySummary` / `getClassMasterySummary`(v2-P2-4 修复:totalStudents 语义 + 班级平均掌握度按学生平均)/ `getKnowledgePointStats`(v2-P1-7 修复:页面先查班级再传参)
|
||||
- Data-access-reports:`generateDiagnosticReport` / `generateClassDiagnosticReport`(v2-P2-6 修复:校验掌握度数据)/ `getDiagnosticReports` / `getDiagnosticReportById` / `publishDiagnosticReport` / `deleteDiagnosticReport`(✅ P2 已修复:使用 `React.cache()` 包装实现请求级 memoization)
|
||||
- Stats-service(✅ v2-P1-6 新增):`serializeMasteryWithKp` / `computeAverageMastery` / `classifyStrengthsWeaknesses` / `buildStudentMasterySummary` / `aggregateClassMastery` / `computeKpStats` / `computeClassAverageMastery` / `buildStudentsNeedingAttention` / `buildClassMasterySummary` / `buildStudentReportContent` / `buildClassReportContent` / `computeMasteryLevel` / `serializeMastery`(从 data-access / data-access-reports 抽取的纯统计函数)
|
||||
- Data-access:`updateMasteryFromSubmission`(v2-P1-8 修复:累积模式;v2-P2-5 修复:db.transaction 包裹)/ `getStudentMastery`(P3-19 修复:移除 export,改为模块内部函数)/ `getStudentMasterySummary`(P3-18 修复:getUserNamesByIds 与 getStudentMastery 并行查询)/ `getClassMasterySummary`(v2-P2-4 修复:totalStudents 语义 + 班级平均掌握度按学生平均)/ `getKnowledgePointStats`(v2-P1-7 修复:页面先查班级再传参)
|
||||
- Data-access-reports:`generateDiagnosticReport` / `generateClassDiagnosticReport`(v2-P2-6 修复:校验掌握度数据;P3-27 修复:使用 DiagnosticReportError 结构化错误码)/ `getDiagnosticReports`(P3-15 修复:支持分页 limit/offset,返回 { reports, total } 结构)/ `getDiagnosticReportById` / `publishDiagnosticReport` / `deleteDiagnosticReport`(✅ P2 已修复:使用 `React.cache()` 包装实现请求级 memoization;P3-1 修复:toNumber 从 grades 模块导入)/ `DiagnosticReportError`(P3-27 新增:结构化错误码类)
|
||||
- Stats-service(✅ v2-P1-6 新增):`serializeMasteryWithKp` / `computeAverageMastery` / `classifyStrengthsWeaknesses`(P3-16 修复:弱项阈值从 <60 改为 <80,消除 60-79 盲区)/ `buildStudentMasterySummary` / `aggregateClassMastery` / `computeKpStats` / `computeClassAverageMastery` / `buildStudentsNeedingAttention` / `buildClassMasterySummary` / `buildStudentReportContent` / `buildClassReportContent` / `computeMasteryLevel` / `serializeMastery`(从 data-access / data-access-reports 抽取的纯统计函数)
|
||||
- Schema:`GenerateStudentReportSchema` / `GenerateClassReportSchema` / `PublishReportSchema` / `DeleteReportSchema`(v2-P2-3 修复:删除死代码 `GetDiagnosticReportsSchema` / `GetDiagnosticReportByIdSchema`)
|
||||
|
||||
**依赖关系**:
|
||||
- 依赖:`shared/*`、`@/auth`、`exams`(✅ P1-1 已修复:通过 exams data-access.getExamSubmissionWithAnswers)、`questions`(✅ P1-1 已修复:通过 questions data-access.getKnowledgePointsForQuestions)、`classes`(✅ P1-1 已修复:通过 classes data-access.getClassExists/getClassNameById/getActiveStudentIdsByClassId;v2-P1-7 新增 getStudentActiveClassId)、`users`(✅ P1-1 已修复:通过 users data-access.getUserNamesByIds/getUserIdsByGradeId)
|
||||
- 依赖:`shared/*`、`@/auth`、`exams`(✅ P1-1 已修复:通过 exams data-access.getExamSubmissionWithAnswers)、`questions`(✅ P1-1 已修复:通过 questions data-access.getKnowledgePointsForQuestions)、`classes`(✅ P1-1 已修复:通过 classes data-access.getClassExists/getClassNameById/getActiveStudentIdsByClassId;v2-P1-7 新增 getStudentActiveClassId)、`users`(✅ P1-1 已修复:通过 users data-access.getUserNamesByIds/getUserIdsByGradeId)、`grades`(P3-1 修复:通过 grades/lib/grade-utils.toNumber 复用工具函数)
|
||||
- 被依赖:无
|
||||
|
||||
**已知问题**:
|
||||
@@ -1539,6 +1544,13 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
|
||||
- ✅ v2-P2-7 已修复:~~`report-list.tsx` 过滤器 Label 缺少 `htmlFor`~~ 添加 `htmlFor` 和 `id`
|
||||
- ✅ 与 grades 模块无职责重叠
|
||||
- ✅ v3-P2 改进(2026-06-23):`StudentDiagnosticView` 新增 `practiceHrefBase` prop(string | null),null 时隐藏练习按钮;teacher/diagnostic/student/[studentId] 传入 `practiceHrefBase="/teacher/questions"`,parent/diagnostic 传入 `practiceHrefBase={null}` 隐藏练习按钮
|
||||
- ✅ P3-1 已修复(2026-06-23):~~`data-access-reports.ts` 本地定义 `toNumber` 与 `grades/lib/grade-utils.ts` 重复~~ 改为从 grades 模块导入
|
||||
- ✅ P3-15 已修复(2026-06-23):~~`getDiagnosticReports` 无分页,可能返回大量数据~~ 添加 limit/offset 分页支持,返回 `{ reports, total }` 结构,Promise.all 并行查询总数和数据
|
||||
- ✅ P3-16 已修复(2026-06-23):~~强弱项分类存在 60-79 盲区~~ 弱项阈值从 <60 改为 <80,确保所有知识点都被分类
|
||||
- ✅ P3-17 已修复:~~班级报告 strengths 无数量上限~~ `buildClassReportContent` 中 strengths 已限制为前 5 个(按掌握度降序)
|
||||
- ✅ P3-18 已修复(2026-06-23):~~`getStudentMasterySummary` 串行查询用户名和掌握度~~ 改为 Promise.all 并行查询
|
||||
- ✅ P3-19 已修复(2026-06-23):~~`getStudentMastery` 使用 export 但仅内部使用~~ 移除 export,改为模块内部函数
|
||||
- ✅ P3-27 已修复(2026-06-23):~~`generateDiagnosticReport` / `generateClassDiagnosticReport` 中 `throw new Error("...")` 直接暴露给用户~~ 改为使用 `DiagnosticReportError` 结构化错误码(继承 BusinessError)
|
||||
|
||||
**文件清单**:
|
||||
| 文件 | 行数 | 职责 |
|
||||
@@ -1952,6 +1964,7 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
|
||||
| **Server Actions** | `recommendStudyPathAction` | `modules/ai/actions.ts` | 学习路径推荐(权限:AI_CHAT)— V2 新增 |
|
||||
| **Server Actions** | `getAiUsageStatsAction` | `modules/ai/actions.ts` | AI 使用统计(权限:AI_CONFIGURE)— V2 新增 |
|
||||
| **SSE Route** | `POST /api/ai/chat/stream` | `app/api/ai/chat/stream/route.ts` | 流式 AI 对话(SSE)— V2 新增 |
|
||||
| **SSE Route** | `GET /api/notifications/stream` | `app/api/notifications/stream/route.ts` | 通知实时推送(SSE,15s 心跳 + 5min 超时)— V2-P3 新增 |
|
||||
| **Service** | `AiService` | `modules/ai/types.ts` | 服务端 AI 服务接口(含 8 个方法) |
|
||||
| **Service** | `AiClientService` | `modules/ai/types.ts` | 客户端 AI 服务接口(Server Action 引用集合) |
|
||||
| **Provider** | `AiClientProvider` | `modules/ai/context/ai-client-provider.tsx` | React Context Provider,注入 AiClientService |
|
||||
@@ -2036,6 +2049,7 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
|
||||
| `modules/ai/hooks/use-ai-chat.ts` | ~57 | 非流式 AI 对话 Hook |
|
||||
| `modules/ai/hooks/use-ai-suggestion.ts` | ~72 | AI 建议 Hook |
|
||||
| `app/api/ai/chat/stream/route.ts` | ~160 | SSE 流式端点 — V2 新增 |
|
||||
| `app/api/notifications/stream/route.ts` | ~120 | 通知实时推送 SSE 端点 — V2-P3 新增 |
|
||||
|
||||
**i18n**:
|
||||
- 翻译文件:`shared/i18n/messages/{locale}/ai.json`
|
||||
|
||||
Reference in New Issue
Block a user