From 7e320d78c10c1c0253d3bab20693087bcb0e9f16 Mon Sep 17 00:00:00 2001 From: SpecialX <47072643+wangxiner55@users.noreply.github.com> Date: Tue, 23 Jun 2026 19:33:28 +0800 Subject: [PATCH] =?UTF-8?q?feat(ai):=20=E7=BB=9F=E4=B8=80=20AI=20=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E5=85=A5=E5=8F=A3=E5=88=B0=20/admin/ai-settings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 新增 - 创建 /admin/ai-settings 统一配置页(AiProviderSettingsCard + AiUsageDashboard) - admin 侧边栏新增"AI 配置"菜单项(权限 AI_CONFIGURE,图标 Sparkles) - 新增 deleteAiProvider 数据访问层(事务删除 + 自动转移默认) - 新增 deleteAiProviderAction Server Action(Zod 校验 + 权限校验) - AiProviderSettingsCard 新增删除按钮(AlertDialog 确认 + destructive 变体) - 新增 i18n 翻译键(delete/deleteConfirm/deleteSuccess 等,zh-CN + en) ## 移除 - 从 /settings 移除 AI 标签页(原 VALID_TABS 含 "ai",现仅 4 标签页) - 从考试页面移除 AI 配置弹窗(Dialog + AiProviderSettingsCard 内嵌) - 从 ai-provider-selector.tsx 移除配置弹窗(managePanel/manageOpen props) - 移除 settings-view.tsx 中 canConfigureAi 逻辑和未使用 import ## 变更 - 考试页面"管理"按钮改为 Link 跳转到 /admin/ai-settings - ai-provider-selector.tsx"管理"按钮改为 Link 跳转到 /admin/ai-settings - exam-form.tsx 移除 providerDialogOpen/providerDialogKey 状态 - 修正架构文档 004 中 Action 命名(getAiProvidersAction → getAiProviderSummaries 等) ## 架构文档同步 - 004 更新 settings 模块章节(V3 标记/修正 Action 名称/新增 deleteAiProvider) - 005 新增 deleteAiProviderAction 节点 + /admin/ai-settings 路由 --- .../004_architecture_impact_map.md | 81 +++-- docs/architecture/005_architecture_data.json | 321 ++++++++++++++++-- .../(dashboard)/admin/ai-settings/page.tsx | 43 +++ .../ai/components/ai-provider-selector.tsx | 47 +-- .../exams/components/exam-ai-generator.tsx | 63 +--- src/modules/exams/components/exam-form.tsx | 6 - src/modules/layout/config/navigation.ts | 28 ++ src/modules/settings/actions.ts | 29 ++ .../components/ai-provider-settings-card.tsx | 129 +++++-- .../settings/components/settings-view.tsx | 28 +- src/modules/settings/data-access.ts | 36 ++ src/shared/i18n/messages/en/settings.json | 10 +- src/shared/i18n/messages/zh-CN/settings.json | 10 +- 13 files changed, 618 insertions(+), 213 deletions(-) create mode 100644 src/app/(dashboard)/admin/ai-settings/page.tsx diff --git a/docs/architecture/004_architecture_impact_map.md b/docs/architecture/004_architecture_impact_map.md index fe4f2a2..e7e83ea 100644 --- a/docs/architecture/004_architecture_impact_map.md +++ b/docs/architecture/004_architecture_impact_map.md @@ -540,8 +540,8 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions" **职责**:考试全生命周期管理(创建/编辑/预览/发布/删除/复制)+ AI 辅助出题。 **导出函数**: -- Actions:`createExamAction` / `createAiExamAction` / `previewAiExamAction` / `regenerateAiQuestionAction` / `updateExamAction` / `deleteExamAction` / `duplicateExamAction` / `getExamPreviewAction` / `getSubjectsAction` / `getGradesAction`(✅ P1-2 已修复:actions 层不再直接访问 DB,全部下沉到 data-access) -- Data-access:`getExams` / `getExamById` / `persistExamDraft` / `persistAiGeneratedExamDraft` / `buildExamDescription` / `resolveSubjectGradeNames` / `getExamCreatorId` / `updateExamWithQuestions` / `deleteExamById` / `duplicateExam` / `getExamPreview` / `getExamSubjects` / `getExamGrades`(后 7 个为 P1-2 新增) +- Actions:`createExamAction` / `createAiExamAction` / `previewAiExamAction` / `regenerateAiQuestionAction` / `updateExamAction` / `deleteExamAction` / `duplicateExamAction` / `getExamPreviewAction` / `getSubjectsAction` / `getGradesAction` / `getExamsByGradeIdAction`(✅ v4-P2-7 新增:年级仪表盘维度3,按 gradeId 查询年级下所有考试 + 提交统计,EXAM_READ 权限)(✅ P1-2 已修复:actions 层不再直接访问 DB,全部下沉到 data-access) +- Data-access:`getExams` / `getExamById` / `persistExamDraft` / `persistAiGeneratedExamDraft` / `buildExamDescription` / `resolveSubjectGradeNames` / `getExamCreatorId` / `updateExamWithQuestions` / `deleteExamById` / `duplicateExam` / `getExamPreview` / `getExamSubjects` / `getExamGrades` / `getExamsByGradeId`(✅ v4-P2-7 新增:年级仪表盘维度3,exams 表有直接 gradeId 字段,配合 examSubmissions 聚合提交数/已评分数/平均分,支持 scope 行级过滤)(后 8 个为 P1-2 新增) - AI Pipeline:`generateAiCreateDraftFromSource` / `generateAiPreviewData` / `regenerateAiQuestionByInstruction` - Utils:`normalizeStructure`(v3 新增:将持久化的 `exam.structure` unknown JSON 运行时校验并归一化为类型安全的 `ExamNode[]`,类型守卫模式无 `as` 断言,从 `teacher/exams/[id]/build/page.tsx` 提取) - Stats-service(V3-8 新增):`getExamAnalytics`(cache 包装,聚合考试所有作业的已批改提交,计算平均分/及格率/分数段分布/逐题错误率与难度等级,对标智学网考试分析)+ `ExamAnalyticsSummary` 类型 @@ -725,10 +725,10 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions" **职责**:成绩分析(录入/查询/统计/导出/趋势对比分析)。 **导出函数**: -- Actions:`getGradeRecordsAction` / `createGradeRecordAction`(v4-P1-6 增强:成绩录入后通知学生和家长,调用 `notifyGradeEntered`)/ `updateGradeRecordAction` / `deleteGradeRecordAction` / `exportGradesAction`(v4-P1-12 增强:新增可选 `studentId` 参数,支持按学生导出,家长视角调用 `exportStudentGradeRecordsToExcel`,校验 studentId 属于家长子女)/ `getGradeTrendAction` / `getClassComparisonAction` / `getSubjectComparisonAction` / `getGradeDistributionAction` / `getClassRankingAction` / `getRankingTrendAction` / `getGradeRecordByIdAction` / `getClassGradeStatsAction` / `getStudentGradeSummaryAction` / `batchCreateGradeRecordsAction`(v4-P1-6 增强:批量成绩录入后通知学生和家长)/ `assertClassInScope`(✅ P3 新增导出:班级 scope 校验工具,供 actions-analytics 复用)/ `saveGradeDraftAction` / `getGradeDraftAction` / `deleteGradeDraftAction`(✅ v3-P2 新增:成绩录入草稿 Server Actions,分别使用 GRADE_RECORD_MANAGE/GRADE_RECORD_READ/GRADE_RECORD_MANAGE 权限) -- Data-access:`getGradeRecords` / `getStudentGradeSummary` / `getClassRanking` / `getClassStudentsForEntry` / `getClassGradeStats` / `getClassGradeStatsWithMeta` / `getGradeTrend` / `getClassComparison` / `getSubjectComparison` / `getGradeDistribution` / `getRankingTrend` / `PaginatedGradeRecords`(✅ P3 新增:分页结果接口 `{ records, total }`)/ `saveGradeDraft` / `getGradeDraft` / `deleteGradeDraft`(✅ v3-P2 新增:成绩录入草稿 CRUD,upsert + 24 小时过期)/ `getExamOptionsForGrades` / `getSchoolWideGradeSummary`(✅ v3-P2 新增:考试选项查询 + 全校各年级成绩汇总,管理员视图按年级聚合平均分/及格率/优秀率/学生数/班级数,加权平均计算全校汇总) -- Types(✅ v3-P2 新增):`SchoolWideGradeSummaryItem`(全校汇总按年级聚合项:gradeId/gradeName/schoolName/classCount/studentCount/averageScore/passRate/excellentRate/recordCount)/ `SchoolWideGradeSummary`(全校汇总:grades 数组 + totals 汇总对象)/ `GradeDraftData`(草稿数据接口:{ scores: Record, timestamp: number },位于 data-access.ts) -- Lib(✅ P1-2 新增,✅ P3 更新签名,✅ P3-26 拆分):`toNumber` / `normalize`(位于 `lib/grade-utils.ts`);`buildScopeClassFilter(scope, currentUserId?)`(P3-26 从 grade-utils.ts 迁移至 `lib/scope-filter.ts`,P3 修复:`class_members` scope 内置 studentId 过滤,需传入 currentUserId 参数) +- Actions:`getGradeRecordsAction` / `createGradeRecordAction`(v4-P1-6 增强:成绩录入后通知学生和家长,调用 `notifyGradeEntered`)/ `updateGradeRecordAction` / `deleteGradeRecordAction` / `exportGradesAction`(v4-P1-12 增强:新增可选 `studentId` 参数,支持按学生导出,家长视角调用 `exportStudentGradeRecordsToExcel`,校验 studentId 属于家长子女)/ `getGradeTrendAction` / `getClassComparisonAction` / `getSubjectComparisonAction` / `getGradeDistributionAction` / `getGradeDistributionByGradeIdAction`(✅ v4-P2-7 新增:年级仪表盘维度1,按 gradeId 查询年级整体 + 按班级拆分的成绩分布,GRADE_RECORD_READ 权限)/ `getClassRankingAction` / `getRankingTrendAction` / `getGradeRecordByIdAction` / `getClassGradeStatsAction` / `getStudentGradeSummaryAction` / `batchCreateGradeRecordsAction`(v4-P1-6 增强:批量成绩录入后通知学生和家长)/ `saveGradeDraftAction` / `getGradeDraftAction` / `deleteGradeDraftAction`(✅ v3-P2 新增:成绩录入草稿 Server Actions,分别使用 GRADE_RECORD_MANAGE/GRADE_RECORD_READ/GRADE_RECORD_MANAGE 权限)。注:`assertClassInScope` 原位于 actions.ts(✅ P3 新增导出:班级 scope 校验工具,供 actions-analytics 复用),✅ v4-P2-6 修复:因 "use server" 文件要求所有 export 为 async,而 `assertClassInScope` 是同步函数,已迁移至独立文件 `lib/scope-check.ts`,actions.ts 与 actions-analytics.ts 均从 `./lib/scope-check` 导入 +- Data-access:`getGradeRecords` / `getStudentGradeSummary` / `getClassRanking` / `getClassStudentsForEntry` / `getClassGradeStats` / `getClassGradeStatsWithMeta` / `getGradeTrend` / `getClassComparison` / `getSubjectComparison` / `getGradeDistribution` / `getGradeDistributionByGradeId`(✅ v4-P2-7 新增:年级仪表盘维度1,通过 getClassesByGradeId 获取年级下所有班级,inArray 查询成绩记录,复用 computeGradeDistribution/computeGradeStats 纯函数,返回整体分布 + 按班级拆分)/ `getRankingTrend` / `PaginatedGradeRecords`(✅ P3 新增:分页结果接口 `{ records, total }`)/ `saveGradeDraft` / `getGradeDraft` / `deleteGradeDraft`(✅ v3-P2 新增:成绩录入草稿 CRUD,upsert + 24 小时过期)/ `getExamOptionsForGrades` / `getSchoolWideGradeSummary`(✅ v3-P2 新增:考试选项查询 + 全校各年级成绩汇总,管理员视图按年级聚合平均分/及格率/优秀率/学生数/班级数,加权平均计算全校汇总) +- Types(✅ v3-P2 新增,✅ v4-P2-7 新增年级分布类型):`SchoolWideGradeSummaryItem` / `SchoolWideGradeSummary` / `GradeDraftData`(草稿数据接口)/ `GradeDistributionByGradeResult`(✅ v4-P2-7 新增:年级维度成绩分布结果,含 overall 整体分布 + stats 统计 + byClass 按班级拆分数组)/ `GradeDistributionByGradeClassItem`(✅ v4-P2-7 新增:按班级拆分的分布项:classId/className/distribution/stats) +- Lib(✅ P1-2 新增,✅ P3 更新签名,✅ P3-26 拆分,✅ v4-P2-6 新增 scope-check):`toNumber` / `normalize`(位于 `lib/grade-utils.ts`);`buildScopeClassFilter(scope, currentUserId?)`(P3-26 从 grade-utils.ts 迁移至 `lib/scope-filter.ts`,P3 修复:`class_members` scope 内置 studentId 过滤,需传入 currentUserId 参数);`assertClassInScope(scope: DataScope, classId: string): string | null`(✅ v4-P2-6 从 actions.ts 迁移至 `lib/scope-check.ts`:校验 classId 是否在 scope 允许范围内,供 actions.ts 与 actions-analytics.ts 复用。迁移原因:actions.ts 是 "use server" 文件要求所有 export 为 async,而 assertClassInScope 是同步函数) - Stats-service(✅ P1-1 新增):`computeGradeStats` / `computeAverageScore` / `buildGradeTrendPoints` / `computeTrendAverage` / `computeClassComparisonStats` / `computeSubjectComparisonStats` / `computeGradeDistribution` / `buildRankingTrendPoints`(从 3 个 data-access 文件抽取的纯函数,使数据层专注 DB I/O,统计逻辑可独立测试) - Export(✅ v4-P1-12 新增):`exportGradeRecordsToExcel` / `exportClassGradeReportToExcel` / `exportStudentGradeRecordsToExcel`(v4-P1-12 新增:导出单个学生成绩单家长视角,仅含成绩明细 + 个人统计,不含班级数据,scope 为 children 自动按 studentId 过滤)/ `formatDateForFile`(已迁移至 shared/lib/utils) - Components(✅ P1-5 新增):`WidgetBoundary`(Error Boundary + Suspense + Skeleton 组合,含 a11y 属性)/ `SchoolWideSummaryCard`(✅ v3-P2 新增:管理员全校成绩汇总卡片,4 个统计卡片 + 各年级对比表格)/ `ScoreCell`(✅ v4-P1-7 新增:成绩单元格组件,根据得分率着色——红<60%/黄60-84%/绿≥85%,使用语义化 Tailwind 类名避免动态拼接,fullScore<=0 时不着色) @@ -783,12 +783,13 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions" - ✅ v3-P3-1 改进(2026-06-23):`batch-grade-entry.tsx` 新增"下载模板"按钮,客户端生成 CSV 模板(含学生姓名/分数/备注列头 + BOM 支持 Excel UTF-8),教师可下载填好后粘贴到录入表格 - ✅ v3-P3-2 改进(2026-06-23):`grade-record-list.tsx` 新增多选复选框(全选/单选)+ 批量删除工具栏 + 批量删除确认对话框;新增 `bulkDeleteGradeRecords` data-access 函数(使用 inArray 一次性删除避免 N+1)+ `bulkDeleteGradeRecordsAction` Server Action(限制单次最多 500 条) - ✅ v4-P3-2 改进(2026-06-23):`batch-grade-entry.tsx` 顶部新增可折叠新手引导提示框(4 步使用说明),使用 localStorage 记住用户关闭状态避免重复显示 +- ✅ v4-P2-6 修复(2026-06-23):~~`assertClassInScope` 是同步函数但位于 "use server" 文件 actions.ts 中~~ Next.js 要求 "use server" 文件中所有 export 必须为 async,同步 export 会导致构建错误。修复:将 `assertClassInScope` 迁移至独立文件 `lib/scope-check.ts`(含 `import "server-only"`),actions.ts 与 actions-analytics.ts 均从 `./lib/scope-check` 导入 **文件清单**: | 文件 | 行数 | 职责 | |------|------|------| -| `actions.ts` | 670+ | 19 个 Server Action(含 Zod 校验,含 v2-P1-5 安全修复:assertClassInScope + 行级 scope 校验;P3 修复:handleActionError + safeJsonParse + scope 传递 + DB 层分页;v3-P2 新增:saveGradeDraftAction/getGradeDraftAction/deleteGradeDraftAction;v4-P1-6:createGradeRecordAction/batchCreateGradeRecordsAction 新增通知;v4-P1-12:exportGradesAction 新增 studentId 参数;v3-P3-2 新增:bulkDeleteGradeRecordsAction 批量删除) | -| `actions-analytics.ts` | 170 | 5 个分析 Action(含 Zod 校验,P3 修复:handleActionError + assertClassInScope 校验) | +| `actions.ts` | 670+ | 19 个 Server Action(含 Zod 校验,含 v2-P1-5 安全修复:assertClassInScope + 行级 scope 校验;P3 修复:handleActionError + safeJsonParse + scope 传递 + DB 层分页;v3-P2 新增:saveGradeDraftAction/getGradeDraftAction/deleteGradeDraftAction;v4-P1-6:createGradeRecordAction/batchCreateGradeRecordsAction 新增通知;v4-P1-12:exportGradesAction 新增 studentId 参数;v3-P3-2 新增:bulkDeleteGradeRecordsAction 批量删除;v4-P2-6:assertClassInScope 迁移至 lib/scope-check.ts) | +| `actions-analytics.ts` | 170 | 5 个分析 Action(含 Zod 校验,P3 修复:handleActionError + assertClassInScope 校验;v4-P2-6:assertClassInScope 改从 ./lib/scope-check 导入) | | `data-access.ts` | 450+ | 成绩 CRUD + 统计 + 草稿(含 v2-P2-9 修复:recorderName 批量查询;P3 修复:PaginatedGradeRecords 接口 + DB 层分页 + 事务 + 存在性检查 + scope 过滤 + 并列排名;v3-P2 新增:saveGradeDraft/getGradeDraft/deleteGradeDraft + GradeDraftData 接口;v3-P3-2 新增:bulkDeleteGradeRecords 使用 inArray 批量删除) | | `data-access-analytics.ts` | 200+ | 趋势/对比分析(P3 修复:getClassComparison 应用 buildScopeClassFilter;v3-P2 新增:getExamOptionsForGrades/getSchoolWideGradeSummary;getGradeTrend/getClassComparison/getSubjectComparison/getGradeDistribution 新增 semester/examId 可选参数) | | `data-access-ranking.ts` | 83 | 排名查询(P3 修复:getRankingTrend 接受 scope 参数 + class_taught 校验) | @@ -797,6 +798,7 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions" | `schema.ts` | 113+ | Zod 校验(含 12 个查询 schema;P3 修复:score .max(1000) + records .max(500) + 补全查询字段;v3-P2 新增:grade_drafts 表定义第 1444-1469 行) | | `lib/grade-utils.ts` | 20 | 公共工具函数(toNumber/normalize;P3-26:buildScopeClassFilter 迁移至 scope-filter.ts) | | `lib/scope-filter.ts` | 56 | DB 行级权限过滤(buildScopeClassFilter;P3-26 从 grade-utils.ts 迁移;v2-P2-2 修复:改用 classes data-access 子查询;P3 修复:新增 currentUserId 参数) | +| `lib/scope-check.ts` | 34 | v4-P2-6 新增:班级 scope 校验工具(assertClassInScope 同步函数,从 actions.ts 迁移至此独立文件以避开 "use server" 文件要求 export 必须为 async 的限制;含 `import "server-only"`) | | `types.ts` | 168+ | 类型定义(v3-P2 新增:SchoolWideGradeSummaryItem/SchoolWideGradeSummary) | | `components/widget-boundary.tsx` | 136 | Widget 边界组件(P1-5 新增,v2-P1-1 已在 3 个页面应用) | | `components/school-wide-summary-card.tsx` | - | v3-P2 新增:管理员全校成绩汇总卡片(4 个统计卡片 + 各年级对比表格) | @@ -870,7 +872,9 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions" **导出函数**: - Actions:`createSchoolAction` / `updateSchoolAction` / `deleteSchoolAction` / `createAcademicYearAction` / `updateAcademicYearAction` / `deleteAcademicYearAction` / `createDepartmentAction` / `updateDepartmentAction` / `deleteDepartmentAction` / `createGradeAction` / `updateGradeAction` / `deleteGradeAction` / `promoteGradesAction`(编排层:权限校验 + Zod 校验 + 调用 data-access + revalidatePath + after(logAudit);`promoteGradesAction` 年级升级,审计日志 `grade.promote`) -- Data-access:只读查询(`getSchools` / `getGrades` / `getDepartments` / `getAcademicYears` / `getStaffOptions` / `getGradesForStaff` / `getOrgTree` / `getSubjectOptions` / `getGradeOptions` / `getSubjectNameMapByIds`(P1-1 新增:批量科目名称映射,供 homework/data-access-classes 调用))+ 写操作(`create/update/delete` × `Department/School/Grade/AcademicYear`)+ `promoteGrades(schoolId)` 年级升级(order +1 + 名称升级,辅助函数 `promoteGradeName`) +- Data-access:只读查询(`getSchools` / `getGrades` / `getDepartments` / `getAcademicYears` / `getStaffOptions` / `getGradesForStaff` / `getOrgTree` / `getSubjectOptions` / `getGradeOptions` / `getSubjectNameMapByIds`(P1-1 新增:批量科目名称映射,供 homework/data-access-classes 调用)/ `getGradeOverviewStats`(✅ v4-P2-6 新增:年级概览统计,返回 `GradeOverviewStats[]`,每个年级的 classCount/studentCount/teacherCount,供年级管理卡片视图使用,动态导入 classes/data-access 避免循环依赖))+ 写操作(`create/update/delete` × `Department/School/Grade/AcademicYear`)+ `promoteGrades(schoolId)` 年级升级(order +1 + 名称升级,辅助函数 `promoteGradeName`) +- Types(✅ v4-P2-6 新增):`GradeOverviewStats`(年级概览统计接口:{ gradeId, classCount, studentCount, teacherCount },位于 data-access.ts) +- Components:`SchoolsClient` / `SchoolFormDialog` / `SchoolDeleteDialog` / `SchoolListToolbar` / `SchoolErrorBoundary` / `SchoolListSkeleton` / `SchoolCardSkeleton` / `OrgTreeNav` / `GradesClient`(✅ v4-P2-6 更新:新增 `gradeStats: GradeOverviewStats[]` prop,渲染年级概览卡片视图——每个年级卡片展示班级/学生/教师数 + 年级主任/教学主任 + 快捷操作入口)/ `GradeInsightsFilters`(✅ v4-P2-6 新增:年级洞察筛选器,使用 ChipNav 替代原生 form get,点击 chip 即时通过 URL 参数切换,无整页刷新)/ `GradeDistributionPanel` / `GradeHomeworkPanel` / `GradeExamsPanel` / `GradeProgressPanel`(✅ v4-P2-7 新增:年级仪表盘 4 个维度面板组件,位于 `components/grade-dashboard/` 子目录,服务端组件直接渲染数据,无客户端交互) **依赖关系**: - 依赖:`shared/*`、`@/auth`、`users`(⚠️ `getStaffOptions` 直查 users/roles,可接受) @@ -892,6 +896,8 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions" - ✅ P2-3 修复(2026-06-23):新增 `promoteGradesAction` + `promoteGrades(schoolId)` + `promoteGradeName(name)` 辅助函数(中文数字 一→二…十二、阿拉伯数字 1→2…12 识别),按 order 降序逐条 +1 避免唯一约束冲突;含 `after(() => logAudit({ action: "grade.promote" }))` 审计日志 - ✅ P2-4 修复(2026-06-23):新增 `bulkEnrollStudentsAction`(CSV 批量导入学生,复用 enrollStudentByEmail)+ `bulkAssignSubjectTeachersAction`(CSV 批量分配教师,简化实现含 TODO 待完善查找逻辑);classes/actions.ts barrel 导出 - ✅ P2-5 修复(2026-06-23):为 department/academicYear/grade 的 9 个 CRUD Action 补充 `after(() => logAudit(...))` 审计日志(action: department.create/update/delete、academicYear.create/update/delete、grade.create/update/delete),与 school 实体审计日志策略一致 +- ✅ v4-P2-6 改进(2026-06-23):年级管理体验增强——新增 `getGradeOverviewStats()` data-access 函数(返回 `GradeOverviewStats[]`:每个年级的 classCount/studentCount/teacherCount,动态导入 classes/data-access 避免循环依赖);`GradesClient` 新增 `gradeStats` prop,渲染年级概览卡片视图(班级/学生/教师数 + 年级主任/教学主任 + 快捷操作入口);admin/school/grades/page.tsx 新增 `getGradeOverviewStats()` 查询并传入 GradesClient +- ✅ v4-P2-6 改进(2026-06-23):新增 `GradeInsightsFilters` 组件(使用 ChipNav 替代原生 form get,点击 chip 即时通过 URL 参数切换,无整页刷新);admin/school/grades/insights/page.tsx 与 management/grade/insights/page.tsx 重写,使用 `GradeInsightsFilters` 替代原生 form get,添加 i18n,表格添加 `overflow-x-auto` 水平滚动 **文件清单**: | 文件 | 行数 | 职责 | @@ -907,6 +913,8 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions" | components/school-error-boundary.tsx | 72 | 共享 Error Boundary(P1-3 修复) | | components/school-skeleton.tsx | 69 | 共享骨架屏(P1-3 修复) | | components/org-tree-nav.tsx | 134 | 学校→年级→班级三级树形导航(P2-2 修复:搜索过滤 + 选中高亮 + 展开折叠 + 节点类型图标) | +| components/grades-view.tsx | 920+ | 年级管理客户端(v4-P2-6 更新:新增 gradeStats prop + 年级概览卡片视图,展示班级/学生/教师数 + 年级主任/教学主任 + 快捷操作入口) | +| components/grade-insights-filters.tsx | 48 | v4-P2-6 新增:年级洞察筛选器(ChipNav 替代原生 form get,点击 chip 即时通过 URL 参数切换,无整页刷新) | | hooks/use-school-data.ts | 40 | 学校数据管理 hook(P2-1 修复) | --- @@ -1327,8 +1335,9 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions" **职责**:课程计划 CRUD + 周计划项 CRUD + 排序。 **导出函数**: -- Actions:`getCoursePlansAction` / `getCoursePlanByIdAction` / `createCoursePlanAction` / `updateCoursePlanAction` / `deleteCoursePlanAction` / `createCoursePlanItemAction` / `updateCoursePlanItemAction` / `deleteCoursePlanItemAction` / `toggleCoursePlanItemCompletedAction` -- Data-access:与 actions 对应 +- Actions:`getCoursePlansAction` / `getCoursePlanByIdAction` / `createCoursePlanAction` / `updateCoursePlanAction` / `deleteCoursePlanAction` / `createCoursePlanItemAction` / `updateCoursePlanItemAction` / `deleteCoursePlanItemAction` / `toggleCoursePlanItemCompletedAction` / `getGradeCoursePlanProgressAction`(✅ v4-P2-7 新增:年级仪表盘维度4,按 gradeId 查询年级下所有班级的教学计划进度,COURSE_PLAN_READ 权限) +- Data-access:与 actions 对应 + `getGradeCoursePlanProgress`(✅ v4-P2-7 新增:通过 getClassesByGradeId 获取年级下所有班级,inArray 查询 course_plans + course_plan_items,返回整体进度汇总 + 按班级/科目拆分的进度矩阵) +- Types(✅ v4-P2-7 新增):`GradeCoursePlanProgressItem`(年级进度项:planId/classId/className/subjectId/subjectName/teacherName/semester/totalHours/completedHours/progressRate/status/itemCount/completedItemCount)/ `GradeCoursePlanProgressResult`(年级进度结果:gradeId + overall 汇总 + items 数组) **依赖关系**: - 依赖:`shared/*`、`@/auth`、`classes`(合理,getAdminClasses/getStaffOptions)、`school`(合理,getAcademicYears) @@ -1621,20 +1630,20 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions" --- -## 2.23 settings(设置模块) +## 2.23 settings(设置模块)— V3 AI 配置统一入口 -**职责**:系统设置(学校信息/安全策略/文件上传/通知配置)+ AI Provider 管理 + 密码修改 + 个人资料 + 主题偏好 + 通知偏好 + 个人信息页(学生/教师概览)。 +**职责**:系统设置(学校信息/安全策略/文件上传/通知配置)+ AI Provider 管理 + 密码修改 + 个人资料 + 主题偏好 + 通知偏好 + 个人信息页(学生/教师概览)。V3 将 AI Provider 配置统一到 `/admin/ai-settings` 独立页面,移除 `/settings?tab=ai` 标签页和考试页面内嵌弹窗。 **导出函数**: -- Actions:`getAiProvidersAction` / `createAiProviderAction` / `updateAiProviderAction` / `deleteAiProviderAction` / `testAiProviderAction` +- Actions:`getAiProviderSummaries` / `upsertAiProviderAction` / `testAiProviderAction` / `deleteAiProviderAction`(V3 新增删除能力) - Actions-password:`changePasswordAction`(✅ P1 已修复:使用 `requirePermission(USER_PROFILE_UPDATE)` + Zod 校验 + DB 操作下沉到 data-access) - Actions-avatar:`updateUserAvatarAction` / `removeUserAvatarAction`(✅ P2-8 新增:头像上传/删除,复用 `/api/upload` 路由) - Actions-notifications:`sendTestNotificationAction`(✅ P2-10 新增:发送测试通知,占位实现待接入真实通知服务) - Actions-system-settings:`getAdminSystemSettingsAction` / `saveAdminSystemSettingsAction`(✅ P0-3 新增:管理员系统设置 CRUD,4 分类 Zod 校验) - Actions-security:`getSecurityCenterAction` / `toggleTwoFactorAction`(✅ P2-9 新增:2FA 状态查询/切换 + 最近登录历史) -- Data-access:`getAiProviderSummaries` / `countDefaultAiProviders` / `getAiProviderForUpdate` / `updateAiProvider` / `createAiProvider` / `getUserPasswordHash` / `getPasswordSecurityByUserId` / `updateUserPassword` / `upsertPasswordSecurityOnPasswordChange`(P1 新增,从 actions 下沉) +- Data-access:`getAiProviderSummaries` / `countDefaultAiProviders` / `getAiProviderForUpdate` / `updateAiProvider` / `createAiProvider` / `deleteAiProvider`(V3 新增:事务删除 + 自动转移默认)/ `getUserPasswordHash` / `getPasswordSecurityByUserId` / `updateUserPassword` / `upsertPasswordSecurityOnPasswordChange`(P1 新增,从 actions 下沉) - Data-access-system-settings:`getSystemSettingsByCategory` / `getAllSystemSettings` / `getSystemSetting` / `upsertSystemSetting` / `upsertSystemSettings`(✅ P0-3 新增:system_settings 表 CRUD,键值对存储模式) -- Components:`SettingsView`(统一设置页布局,5 标签页 General/Notifications/Appearance/Security/AI;角色差异通过 `resolveRoleSettingsConfig` 配置驱动 + `generalExtra` props 注入;Tab URL 持久化;每个 TabsContent 包裹 `SettingsSectionErrorBoundary` + `Suspense` 骨架屏;AI 标签页条件渲染需 `AI_CONFIGURE` 权限)、`SettingsServiceProvider` / `useSettingsService`(Context 注入 `SettingsService` 接口,解耦组件对 users/messaging actions 的直接依赖)、`SettingsSectionErrorBoundary`(分区 Error Boundary,局部失败不影响整页)、`QuickLinksCard`(快捷链接卡片,i18n 键驱动)、`ProfileStudentOverview` / `ProfileStudentOverviewSkeleton`(学生概览异步 Server Component + 骨架屏)、`ProfileTeacherOverview` / `ProfileTeacherOverviewSkeleton`(教师概览异步 Server Component + 骨架屏)、`AdminSettingsView`(✅ P0-3 已修复:从 mock 改为真实数据层,通过 Server Actions 加载/保存到 system_settings 表)、`AvatarUpload`(✅ P2-8 新增:头像上传/预览/删除客户端组件,文件验证 + i18n)、`SecurityCenterCard`(✅ P2-9 新增:2FA 开关 + 最近登录历史卡片)、`ThemePreferencesCard`(✅ P2-11 已增强:集成 `LocaleSwitcher` 语言切换) +- Components:`SettingsView`(统一设置页布局,V3 移除 AI 标签页后为 4 标签页 General/Notifications/Appearance/Security;角色差异通过 `resolveRoleSettingsConfig` 配置驱动 + `generalExtra` props 注入;Tab URL 持久化;每个 TabsContent 包裹 `SettingsSectionErrorBoundary` + `Suspense` 骨架屏)、`SettingsServiceProvider` / `useSettingsService`(Context 注入 `SettingsService` 接口,解耦组件对 users/messaging actions 的直接依赖)、`SettingsSectionErrorBoundary`(分区 Error Boundary,局部失败不影响整页)、`QuickLinksCard`(快捷链接卡片,i18n 键驱动)、`ProfileStudentOverview` / `ProfileStudentOverviewSkeleton`(学生概览异步 Server Component + 骨架屏)、`ProfileTeacherOverview` / `ProfileTeacherOverviewSkeleton`(教师概览异步 Server Component + 骨架屏)、`AdminSettingsView`(✅ P0-3 已修复:从 mock 改为真实数据层,通过 Server Actions 加载/保存到 system_settings 表)、`AvatarUpload`(✅ P2-8 新增:头像上传/预览/删除客户端组件,文件验证 + i18n)、`SecurityCenterCard`(✅ P2-9 新增:2FA 开关 + 最近登录历史卡片)、`ThemePreferencesCard`(✅ P2-11 已增强:集成 `LocaleSwitcher` 语言切换)、`AiProviderSettingsCard`(V3 增强:新增删除按钮 + AlertDialog 确认,统一在 `/admin/ai-settings` 页面渲染) - Config:`ROLE_SETTINGS_CONFIG` / `resolveRoleSettingsConfig`(配置驱动角色 → 设置视图映射,新增角色只需添加条目) - Lib:`buildStudentOverviewData` / `computeStudentStats` / `sortUpcomingAssignments` / `filterTodaySchedule` / `toWeekday`(纯数据计算函数,与 UI 分离,便于单元测试) - Types:`AiProviderSummary` / `AiProviderName` / `AiProviderExisting` / `SettingsService` / `ProfileService` / `NotificationPreferenceService`(服务接口定义,用于依赖注入解耦) @@ -1812,17 +1821,17 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions" - `FlowEdge`:流程连线(教学节点 → 教学节点) **导出函数**: -- Data-access(`data-access.ts`):`getLessonPlans` / `getLessonPlanById` / `createLessonPlan` / `updateLessonPlanContent` / `softDeleteLessonPlan` / `duplicateLessonPlan` / `getTemplateById` / `buildInitialContent` / `migrateV1ToV2` / `normalizeDocument`(v3 规范化,兼容 v1/v2 旧数据)/ `buildDefaultSkeleton`(v3 默认 10 节点骨架)/ `getTextbooksForPicker` / `getChaptersForPicker` / `findChapterById` +- Data-access(`data-access.ts`):`getLessonPlans` / `getLessonPlanById` / `createLessonPlan` / `updateLessonPlanContent` / `softDeleteLessonPlan` / `duplicateLessonPlan` / `getTemplateById` / `buildInitialContent` / `migrateV1ToV2` / `normalizeDocument`(v3 规范化,兼容 v1/v2 旧数据)/ `buildDefaultSkeleton`(v3 默认 10 节点骨架)/ `getTextbooksForPicker` / `getChaptersForPicker` / `findChapterById` / `publishLessonPlan`(V3 新增,设置 status=published)/ `unpublishLessonPlan`(V3 新增,设置 status=draft,仅 published 课案) - Lib(`lib/document-migration.ts`):`defaultDataForType` / `migrateV1ToV2` / `migrateV2ToV3` / `normalizeDocument` / `buildInitialContent` / `buildDefaultSkeleton` / `isTextbookContentNode` / `isAnchorEdge` / `getAnchorsForNode` / `getActiveAnchorIds` / `getAnchorEdges` - Lib(`lib/anchor-injector.ts`):`markdownToPlainText` / `injectPlaceholders` / `parseAnchoredText` / `toCircledNumber` / `getNextPointIndex` / `relocateAnchors` / `getAnchorColor` - Lib(`lib/node-summary.ts`):`getNodeSummary` / `getTextbookContentSummary` / `getNodeColor` / `NODE_COLORS` -- Lib(`lib/rf-mappers.ts`):`toRfNodes`(支持 textbook_content 节点)/ `toRfEdges`(区分 anchor/flow 边透明度)/ `fromRfEdges` +- Lib(`lib/rf-mappers.ts`):`toRfNodes`(支持 textbook_content 节点;V3:ctx 新增 `anchorableNodes` 和 `onCreateNewNode` 字段)/ `toRfEdges`(区分 anchor/flow 边透明度;V3:锚点边颜色使用 `getNodeColor(anchor.nodeId)` 替代硬编码,anchorId 存入 edge.data)/ `fromRfEdges`(V3:从 `e.data.anchorId` 读取 anchorId,回退到 className 判断) - Data-access-versions(`data-access-versions.ts`):`getLessonPlanVersions` / `createLessonPlanVersion` / `getVersionContent` / `revertToVersion` / `pruneAutoVersions` - Data-access-templates(`data-access-templates.ts`):`getLessonPlanTemplates` / `saveAsTemplate` / `deletePersonalTemplate` - Data-access-knowledge(`data-access-knowledge.ts`):`getLessonPlansByKnowledgePoint` / `getLessonPlansByQuestion` - Publish-service(`publish-service.ts`):`publishLessonPlanHomework` - AI-suggest(`ai-suggest.ts`):`suggestKnowledgePoints` -- Actions:`getLessonPlansAction` / `getLessonPlanByIdAction` / `createLessonPlanAction` / `updateLessonPlanAction` / `saveLessonPlanVersionAction` / `getLessonPlanVersionsAction` / `revertLessonPlanVersionAction` / `deleteLessonPlanAction` / `duplicateLessonPlanAction` / `getLessonPlanTemplatesAction` / `saveAsTemplateAction` / `deleteTemplateAction` / `suggestKnowledgePointsAction` / `publishLessonPlanHomeworkAction` / `getKnowledgePointOptionsAction` / `getTextbooksForPickerAction` / `getChaptersForPickerAction` +- Actions:`getLessonPlansAction` / `getLessonPlanByIdAction` / `createLessonPlanAction` / `updateLessonPlanAction` / `saveLessonPlanVersionAction` / `getLessonPlanVersionsAction` / `revertLessonPlanVersionAction` / `deleteLessonPlanAction` / `duplicateLessonPlanAction` / `getLessonPlanTemplatesAction` / `saveAsTemplateAction` / `deleteTemplateAction` / `suggestKnowledgePointsAction` / `publishLessonPlanHomeworkAction` / `getKnowledgePointOptionsAction` / `getTextbooksForPickerAction` / `getChaptersForPickerAction` / `publishLessonPlanAction`(V3 新增,requirePermission(LESSON_PLAN_PUBLISH))/ `unpublishLessonPlanAction`(V3 新增,requirePermission(LESSON_PLAN_PUBLISH)) **依赖关系**: - 依赖:`shared/*`、`@/auth`、`shared/lib/ai`、`@xyflow/react`(节点图编辑器)、`textbooks`(只读章节/知识点树)、`questions`(创建/查询题目)、`exams`(创建 exam 草稿)、`homework`(创建作业下发)、`classes`(查询教师班级)、`files`(附件) @@ -1855,6 +1864,18 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions" > - **统一错误处理**:所有 Server Action catch 块改用 `handleActionError`;`JSON.parse` 改用 `safeJsonParse` > - **block-renderer 拖拽 BUG 修复**:修复拖拽时节点位置计算错误 +> 架构变更(2026-06-23,V3 多角色课案查看): +> - **V3-1 课案发布/撤回**:新增 `publishLessonPlan(planId, userId)` / `unpublishLessonPlan(planId, userId)` data-access 函数 + `publishLessonPlanAction(planId)` / `unpublishLessonPlanAction(planId)` Server Actions(均 requirePermission(LESSON_PLAN_PUBLISH)),支持课案 status=published 供学生/家长/教研组长只读查看 +> - **V3-2 只读画布组件**:新增 `LessonPlanReadonlyView`,复用 React Flow(nodesDraggable=false, nodesConnectable=false),供学生/家长/管理员/教研组长查看已发布课案 +> - **V3-3 多角色视图**:`LessonPlanCard` 新增 viewMode prop(teacher/student/parent/admin/gradeHead),动态跳转链接 + 发布/撤回按钮;`LessonPlanEditor` 新增 initialStatus prop + 发布/撤回按钮(AlertDialog 确认);`LessonPlanList` 新增 viewMode prop +> - **V3-4 锚点选择器重写**:`TextbookContentNode` 新增 props: `anchorableNodes`, `onCreateNewNode`;AnchorNodeSelector 重写为节点列表+创建新节点选项;`NodeEditPanel` 选中 textbook_content 时显示操作提示 + 锚点列表(含删除功能) +> - **V3-5 模板分区显示**:`TemplatePicker` 加载并显示个人模板(调用 getLessonPlanTemplatesAction),分区显示系统/个人模板 +> - **V3-6 rf-mappers 增强**:`toRfNodes` ctx 新增 `anchorableNodes` 和 `onCreateNewNode` 字段;`toRfEdges` 锚点边颜色使用 `getNodeColor(anchor.nodeId)` 替代硬编码,anchorId 存入 edge.data;`fromRfEdges` 从 `e.data.anchorId` 读取 anchorId,回退到 className 判断 +> - **V3-7 权限扩展**:student/parent/grade_head/teaching_head 角色新增 `LESSON_PLAN_READ` 权限,可查看已发布课案 +> - **V3-8 DataScope 扩展**:`class_members` 和 `children` 新增可选 `gradeIds?: string[]` 字段;auth-guard `resolveDataScope` 中 student 通过 `classEnrollments.innerJoin(classes)` 预解析 gradeIds,parent 通过孩子的 classEnrollments.innerJoin(classes) 预解析 gradeIds +> - **V3-9 新增路由**:`/student/lesson-plans`、`/student/lesson-plans/[planId]/view`、`/parent/lesson-plans`、`/parent/lesson-plans/[planId]/view`、`/admin/lesson-plans`、`/admin/lesson-plans/[planId]/view` +> - **V3-10 导航变更**:admin 导航新增「课案管理」(/admin/lesson-plans);student 导航新增「我的课案」(/student/lesson-plans);parent 导航新增「孩子课案」(/parent/lesson-plans) + **文件清单**: | 文件 | 职责 | |------|------| @@ -1864,34 +1885,35 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions" | `lib/document-migration.ts` | **纯函数**:v1→v2(migrateV1ToV2)/ v2→v3(migrateV2ToV3)/ 规范化(normalizeDocument,兼容 v1/v2/v3)/ 初始内容(buildInitialContent)/ 默认骨架(buildDefaultSkeleton,10 节点 + 正文节点)/ defaultDataForType / 工具函数(isTextbookContentNode/isAnchorEdge/getAnchorsForNode/getActiveAnchorIds/getAnchorEdges) | | `lib/anchor-injector.ts` | **纯函数**:锚点注入算法(markdownToPlainText/injectPlaceholders/parseAnchoredText/toCircledNumber/getNextPointIndex/relocateAnchors/getAnchorColor) | | `lib/node-summary.ts` | **纯函数**:getNodeSummary(支持 11 种节点类型)+ getTextbookContentSummary + NODE_COLORS + getNodeColor | -| `lib/rf-mappers.ts` | **纯函数**:toRfNodes(支持 textbook_content 节点 + 锚点回调)/ toRfEdges(区分 anchor/flow 边透明度)/ fromRfEdges | +| `lib/rf-mappers.ts` | **纯函数**:toRfNodes(支持 textbook_content 节点 + 锚点回调;V3:ctx 新增 anchorableNodes/onCreateNewNode 字段)/ toRfEdges(区分 anchor/flow 边透明度;V3:锚点边颜色使用 getNodeColor(anchor.nodeId) 替代硬编码,anchorId 存入 edge.data)/ fromRfEdges(V3:从 e.data.anchorId 读取 anchorId,回退到 className 判断) | | `config/block-registry.tsx` | **配置驱动**:BLOCK_REGISTRY 注册表 + BlockRenderer(switch 渲染 11 种定制节点 + textbook_content) | | `providers/lesson-plan-provider.tsx` | **Provider + Context(P1-5/P1-7/P2-4/V2-6)**:LessonPlanProvider 注入数据服务/角色配置/埋点;定义 LessonPlanDataService 接口、4 个角色配置(TEACHER/ADMIN/STUDENT/PARENT)、ROLE_CONFIGS 注册表、LessonPlanTracker 接口 + noopTracker;hooks:useLessonPlanContextSafe(返回 null 不抛错)/useLessonPlanContext/useRoleConfig/useLessonPlanService/useLessonPlanTracker/useLessonPlanTrackerSafe(V2-6 新增,返回 noopTracker 不抛错) | | `services/default-data-service.ts` | **默认数据服务实现**:createDefaultDataService() 包装 Server Actions 为 LessonPlanDataService 实现,测试可替换为 mock | -| `data-access.ts` | 课案 CRUD + 模板查询(migrateV1ToV2/normalizeDocument/buildInitialContent 从 lib/ 导入并 re-export 保持向后兼容;buildScopeCondition 按 scope 类型精确过滤 P0-3;V2-1:抛出 `LessonPlanDataError` 错误码;V2-3:mapRowToLessonPlan/mapRowToListItem/mapRowToTemplate 显式映射 + isLessonPlanStatus/isTemplateType/isTemplateScope 类型守卫) | +| `data-access.ts` | 课案 CRUD + 模板查询(migrateV1ToV2/normalizeDocument/buildInitialContent 从 lib/ 导入并 re-export 保持向后兼容;buildScopeCondition 按 scope 类型精确过滤 P0-3;V2-1:抛出 `LessonPlanDataError` 错误码;V2-3:mapRowToLessonPlan/mapRowToListItem/mapRowToTemplate 显式映射 + isLessonPlanStatus/isTemplateType/isTemplateScope 类型守卫;V3:新增 publishLessonPlan/unpublishLessonPlan 函数) | | `data-access-versions.ts` | 版本管理(创建/查询/回滚/清理;V2-3:mapRowToVersion 显式映射) | | `data-access-templates.ts` | 个人模板 CRUD(V2-3:mapRowToTemplate 显式映射 + 类型守卫) | | `data-access-knowledge.ts` | 按知识点/题目反查课案(V2-3:显式字段映射替代 `as unknown as`) | | `actions.ts` | 课案 CRUD/版本/模板 Server Actions(V2-1:getTranslations i18n + 错误码捕获;V2-2:createLessonPlanAction 传入 translateTitle 翻译 SYSTEM_TEMPLATES) | -| `actions-publish.ts` | 发布作业 Server Action(V2-1:getTranslations i18n + PUBLISH_ERROR_KEY_MAP 错误码映射) | +| `actions-publish.ts` | 发布作业 Server Action(V2-1:getTranslations i18n + PUBLISH_ERROR_KEY_MAP 错误码映射;V3:新增 publishLessonPlanAction/unpublishLessonPlanAction,requirePermission(LESSON_PLAN_PUBLISH)) | | `actions-ai.ts` | AI 知识点建议 Server Action(V2-1:i18n + 错误码) | | `actions-kp.ts` | 知识点选项 Server Action(V2-1:i18n + 错误码) | | `publish-service.ts` | 发布作业服务(编排 homework/exams/classes,通过对方 data-access 调用,无直查跨模块表;V2-1:抛出 `PublishServiceError` 错误码;V2-3:显式字段映射替代 `as unknown as`) | | `ai-suggest.ts` | AI 知识点建议服务 | | `seed-templates.ts` | 模板种子数据 | | `hooks/use-lesson-plan-editor.ts` | 课案编辑器 Hook(基于 zustand,支持 nodes/edges/anchors 操作:addNode/updateNode/updateNodePosition/removeNode/connect/disconnect/setEdges/selectNode + 锚点操作 addAnchor/removeAnchor/updateAnchor + 正文节点操作 updateTextbookContent/getTextbookContentNode;实时拖动) | -| `components/lesson-plan-list.tsx` | 课案列表(i18n 已接入) | -| `components/lesson-plan-card.tsx` | 课案卡片(i18n 已接入;V2-6:duplicate/archive 调用 tracker.track) | +| `components/lesson-plan-list.tsx` | 课案列表(i18n 已接入;V3:新增 viewMode prop,支持 teacher/student/parent/admin/gradeHead 多角色视图) | +| `components/lesson-plan-card.tsx` | 课案卡片(i18n 已接入;V2-6:duplicate/archive 调用 tracker.track;V3:新增 viewMode prop(teacher/student/parent/admin/gradeHead),动态跳转链接 + 发布/撤回按钮) | | `components/lesson-plan-filters.tsx` | 课案筛选器(i18n 已接入;V2-5:3 个表单元素 label htmlFor 关联) | -| `components/lesson-plan-editor.tsx` | 课案编辑器(编排 NodeEditor + NodeEditPanel,i18n 已接入;V2-6:handleManualSave 调用 tracker.track;V3:顶部工具栏显示教材/章节标题指示器) | +| `components/lesson-plan-editor.tsx` | 课案编辑器(编排 NodeEditor + NodeEditPanel,i18n 已接入;V2-6:handleManualSave 调用 tracker.track;V3:顶部工具栏显示教材/章节标题指示器;V3:新增 initialStatus prop + 发布/撤回按钮(AlertDialog 确认)) | +| `components/lesson-plan-readonly-view.tsx` | **只读画布组件**(V3 新增):复用 React Flow(nodesDraggable=false, nodesConnectable=false),供学生/家长/管理员/教研组长查看已发布课案 | | `components/node-editor.tsx` | **节点图画布**(React Flow,使用 lib/rf-mappers + lib/node-summary 纯函数,i18n 已接入;V2-4:MiniMap 复用 getNodeColor;V2-5:role=application + 键盘导航配置;V3:注册 textbook_content 节点类型 + 锚点回调 + 实时拖动) | -| `components/node-edit-panel.tsx` | **侧边内容编辑面板**(配置驱动渲染 Block,通过 BlockRenderer + LessonPlanErrorBoundary 包裹,i18n 已接入;V3:处理 textbook_content 节点,教学节点类型收窄) | +| `components/node-edit-panel.tsx` | **侧边内容编辑面板**(配置驱动渲染 Block,通过 BlockRenderer + LessonPlanErrorBoundary 包裹,i18n 已接入;V3:处理 textbook_content 节点,教学节点类型收窄;V3:选中 textbook_content 时显示操作提示 + 锚点列表(含删除功能)) | | `components/nodes/lesson-node.tsx` | **自定义教学节点组件**(使用 lib/node-summary 的 getNodeSummary/getNodeColor,i18n 已接入) | -| `components/nodes/textbook-content-node.tsx` | **正文节点组件**(V3 新增):ReactMarkdown 渲染正文 + 锚点注入 + 文本选择(range 锚定)+ 点击位置(point 锚定)+ 缩放控制 + 锚点浮动菜单 | +| `components/nodes/textbook-content-node.tsx` | **正文节点组件**(V3 新增):ReactMarkdown 渲染正文 + 锚点注入 + 文本选择(range 锚定)+ 点击位置(point 锚定)+ 缩放控制 + 锚点浮动菜单;V3:新增 props `anchorableNodes`, `onCreateNewNode`;AnchorNodeSelector 重写为节点列表+创建新节点选项 | | `components/lesson-plan-error-boundary.tsx` | **错误边界**:LessonPlanErrorBoundary 类组件,支持 fallback 和 onError 回调 | | `components/lesson-plan-skeleton.tsx` | **骨架屏**:VersionListSkeleton/QuestionBankSkeleton/KnowledgePointSkeleton/LessonPlanListSkeleton | | `components/block-renderer.tsx` | ⚠️ @deprecated Block 渲染器(已被 NodeEditor 替代,保留向后兼容) | -| `components/template-picker.tsx` | 模板选择器(i18n 已接入;V2-6:create 调用 tracker.track) | +| `components/template-picker.tsx` | 模板选择器(i18n 已接入;V2-6:create 调用 tracker.track;V3:加载并显示个人模板(调用 getLessonPlanTemplatesAction),分区显示系统/个人模板) | | `components/version-history-drawer.tsx` | 版本历史抽屉(i18n 已接入;V2-6:revert 调用 tracker.track) | | `components/knowledge-point-picker.tsx` | 知识点选择器(i18n 已接入) | | `components/question-bank-picker.tsx` | 题库选择器(i18n 已接入) | @@ -1949,6 +1971,7 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions" - `collectFromExamSubmission`:从考试提交记录中筛选得分 < 满分的题目,去重后批量插入 - `collectFromHomeworkSubmission`:从作业提交记录中筛选错题,去重后批量插入 - 自动关联知识点(通过 `questionsToKnowledgePoints` 表) +- subjectId 来源:`questions` 表无 subjectId 字段,考试错题从 `exams.subjectId` 获取;作业错题从 `homeworkAssignments.sourceExamId` 关联到源试卷的 `exams.subjectId` 获取(独立作业无学科归属则为 null) **文件清单**: | 文件 | 行数 | 职责 | @@ -2429,7 +2452,7 @@ shared/lib/{audit-logger, change-logger, auth-guard} → @/auth → shared/lib/* - `buildScopeClassFilter(scope: DataScope, currentUserId?: string): SQL | null`(新增 `currentUserId` 参数,`class_members` scope 内置 `eq(gradeRecords.studentId, currentUserId)` 过滤;P3-26:从 `lib/grade-utils.ts` 迁移至 `lib/scope-filter.ts`) **新增导出**: -- `assertClassInScope(scope: DataScope, classId: string): string | null`(`actions.ts`,校验 classId 是否在 scope 允许范围内,供 actions.ts 与 actions-analytics.ts 复用) +- `assertClassInScope(scope: DataScope, classId: string): string | null`(✅ v4-P2-6 从 `actions.ts` 迁移至 `lib/scope-check.ts`:校验 classId 是否在 scope 允许范围内,供 actions.ts 与 actions-analytics.ts 复用。迁移原因:actions.ts 是 "use server" 文件要求所有 export 为 async,而 assertClassInScope 是同步函数) - `PaginatedGradeRecords` 接口(`data-access.ts`,`{ records: GradeRecordListItem[]; total: number }`,配合 DB 层分页) ### 3.6.3 homework 模块签名变更 diff --git a/docs/architecture/005_architecture_data.json b/docs/architecture/005_architecture_data.json index ac9ef1a..9bfba83 100644 --- a/docs/architecture/005_architecture_data.json +++ b/docs/architecture/005_architecture_data.json @@ -5,7 +5,7 @@ "generatedAt": "2026-06-17", "formatVersion": "1.1", "rule": "每次文件修改后须同步更新本文件", - "lastUpdate": "grades 模块 v3/v4 P3 长期问题修复(2026-06-23):(v3-P3-1) batch-grade-entry.tsx 新增下载模板按钮,客户端生成 CSV 模板(含学生姓名/分数/备注列头 + BOM 支持 Excel UTF-8)。(v3-P3-2) grade-record-list.tsx 新增多选复选框(全选/单选)+ 批量删除工具栏 + 批量删除确认对话框;data-access.ts 新增 bulkDeleteGradeRecords(使用 inArray 一次性删除避免 N+1);actions.ts 新增 bulkDeleteGradeRecordsAction(GRADE_RECORD_MANAGE 权限 + 限制单次最多 500 条)。(v4-P3-2) batch-grade-entry.tsx 顶部新增可折叠新手引导提示框(4 步使用说明),使用 localStorage 记住用户关闭状态。i18n zh-CN/en grades.json 同步新增 batch.downloadTemplate/templateAriaLabel/templateStudentName/templateScore/templateRemark/templateFilename + batch.guide.* + list.bulkDelete/bulkDeleteConfirmation/bulkDeleteSelected/bulkDeleteSuccess/bulkDeleteFailed/selectAll/selectRow/clearSelection 键。--- 之前更新:V4 AI 模块深度增强(ai-module-v2 对标 Khanmigo/Duolingo Max/Squirrel AI/Century Tech):(V4-1) SSE 流式响应:shared/lib/ai/client.ts 新增 createAiChatCompletionStream(AsyncGenerator 逐 token 产出),新增 API 路由 /api/ai/chat/stream(POST,requirePermission(AI_CHAT)+checkDailyLimit+filterUserInput+学生苏格拉底系统提示+ReadableStream 流式输出+filterAiOutput+incrementDailyUsage+trackEvent),新增 hook useAiChatStream(fetch+ReadableStream reader+SSE 解析+AbortController 停止生成+localStorage 持久化最近 20 条)。(V4-2) Markdown 渲染:新增组件 AiMarkdownRenderer(react-markdown+remark-gfm,代码块/表格/列表+hover 复制按钮+memo 优化),AiChatPanel 全面重写(流式渲染+停止按钮+清空确认+建议提示词空状态+aria-live+错误展示)。(V4-3) 全局 AI 助手:新增组件 AiAssistantWidget(fixed 浮动按钮+Sheet 侧抽屉+usePathname 路由推断上下文+inferContextFromPath 映射 7 类场景系统提示:教师批改/备课/考试/学生错题本/学生作业/家长/管理员+useAiClientOptional 无 Provider 时隐藏+pulsing 绿色指示器),dashboard layout 全局注入 AiClientProvider+AiAssistantWidget。(V4-4) 内容安全:新增 services/content-safety.ts(filterUserInput 阻断暴力/自残/色情/毒品/黑客/PII 索取;filterAiOutput 输出二次过滤+学生场景阻断直接答案;checkDailyLimit 学生 50/教师 200/家长 30/管理员 500;incrementDailyUsage;getDailyLimit),COPPA/FERPA K12 合规。(V4-5) 多角色 AI 覆盖:types.ts 新增 ChildSummaryInput/Result、StudyPathInput/Result、AiUsageStats 类型 + AiService/AiClientService 接口扩展;schema.ts 新增 4 个 Zod schema;prompt-templates.ts 新增 CHILD_SUMMARY_SYSTEM_PROMPT(家庭教育顾问,家长友好语言)+ STUDY_PATH_SYSTEM_PROMPT(自适应学习路径设计师,3-7 步骤);ai-service.ts 新增 generateChildSummary(聚合成绩/出勤/错题本数据)+ recommendStudyPath(基于掌握度数据);actions.ts 新增 generateChildSummaryAction/recommendStudyPathAction(AI_CHAT 权限)+ getAiUsageStatsAction(AI_CONFIGURE 权限);新增组件 AiChildSummary(家长端:整体评估 Markdown+优势绿勾+改进橙警+家庭辅导建议+下一步徽章)、AiUsageDashboard(管理员端:4 统计卡片+按能力进度条+按角色徽章+Top 用户列表+最近活动日志)、AiStudyPath(学生端:当前等级横幅+学习路径步骤+连接线+状态图标+预估时间+激励消息)。(V4-6) i18n 修复与扩展:修复 AiGradingAssist/AiLessonContentGenerator/AiQuestionVariantGenerator 共 8 处错误 i18n 键引用(CardDescription 重复 title、label/placeholder/button 复用 generateContent、3 个变体类型标签全部显示「生成」);zh-CN/en ai.json 全面重写新增 chat.streaming/stopGeneration/copy/clearConfirm/suggestedPrompts、grading.description/batch*、lessonPrep.description/additionalContext/insertContent、exam.variantType.*/targetDifficulty/addVariant、parent.*、admin.*、studyPath.*、widget.*、safety.* 等键。架构文档 004/005 同步更新。" + "lastUpdate": "lesson-preparation 模块 V3 多角色课案查看(2026-06-23):(V3-1) 新增 publishLessonPlan/unpublishLessonPlan data-access 函数 + publishLessonPlanAction/unpublishLessonPlanAction Server Actions(LESSON_PLAN_PUBLISH 权限),支持课案 status=published 供学生/家长/教研组长只读查看。(V3-2) 新增 LessonPlanReadonlyView 只读画布组件(复用 React Flow,nodesDraggable=false, nodesConnectable=false)。(V3-3) LessonPlanCard/LessonPlanList/LessonPlanEditor 新增 viewMode prop(teacher/student/parent/admin/gradeHead)+ 发布/撤回按钮(AlertDialog 确认);LessonPlanEditor 新增 initialStatus prop。(V3-4) TextbookContentNode 新增 anchorableNodes/onCreateNewNode props,AnchorNodeSelector 重写为节点列表+创建新节点选项;NodeEditPanel 选中 textbook_content 时显示操作提示+锚点列表(含删除)。(V3-5) TemplatePicker 加载并显示个人模板,分区显示系统/个人模板。(V3-6) rf-mappers 增强:toRfNodes ctx 新增 anchorableNodes/onCreateNewNode;toRfEdges 锚点边颜色用 getNodeColor(anchor.nodeId) 替代硬编码,anchorId 存入 edge.data;fromRfEdges 从 e.data.anchorId 读取(回退 className 判断)。(V3-7) student/parent/grade_head/teaching_head 角色新增 LESSON_PLAN_READ 权限。(V3-8) DataScope class_members/children 新增可选 gradeIds? 字段,auth-guard resolveDataScope 通过 classEnrollments.innerJoin(classes) 预解析年级 ID。(V3-9) 新增 8 个路由:/student/lesson-plans、/student/lesson-plans/[planId]/view、/parent/lesson-plans、/parent/lesson-plans/[planId]/view、/admin/lesson-plans、/admin/lesson-plans/[planId]/view、/grade-head/lesson-plans、/grade-head/lesson-plans/[planId]/view。admin/student/parent/grade_head/teaching_head 导航新增课案相关入口。--- 之前更新:年级管理模块改进 v4-P2-6(2026-06-23):(1) school/data-access.ts 新增 getGradeOverviewStats() 函数 + GradeOverviewStats 接口(每个年级的 classCount/studentCount/teacherCount,动态导入 classes/data-access 避免循环依赖);GradesClient 新增 gradeStats prop,渲染年级概览卡片视图(班级/学生/教师数 + 年级主任/教学主任 + 快捷操作入口);admin/school/grades/page.tsx 新增 getGradeOverviewStats() 查询。(2) 新增 GradeInsightsFilters 组件(ChipNav 替代原生 form get,无整页刷新);admin/school/grades/insights/page.tsx 与 management/grade/insights/page.tsx 重写,添加 i18n + 表格 overflow-x-auto。(3) 修复 grades/actions.ts 中 assertClassInScope 同步函数位于 \"use server\" 文件的 bug,迁移至独立 lib/scope-check.ts 文件,actions.ts 与 actions-analytics.ts 均从 ./lib/scope-check 导入。school.json(en + zh-CN)新增 grades.gradeInsights 和 grades.gradeOverview 两个章节 i18n 键。--- 之前更新:grades 模块 v3/v4 P3 长期问题修复(2026-06-23):(v3-P3-1) batch-grade-entry.tsx 新增下载模板按钮,客户端生成 CSV 模板(含学生姓名/分数/备注列头 + BOM 支持 Excel UTF-8)。(v3-P3-2) grade-record-list.tsx 新增多选复选框(全选/单选)+ 批量删除工具栏 + 批量删除确认对话框;data-access.ts 新增 bulkDeleteGradeRecords(使用 inArray 一次性删除避免 N+1);actions.ts 新增 bulkDeleteGradeRecordsAction(GRADE_RECORD_MANAGE 权限 + 限制单次最多 500 条)。(v4-P3-2) batch-grade-entry.tsx 顶部新增可折叠新手引导提示框(4 步使用说明),使用 localStorage 记住用户关闭状态。i18n zh-CN/en grades.json 同步新增 batch.downloadTemplate/templateAriaLabel/templateStudentName/templateScore/templateRemark/templateFilename + batch.guide.* + list.bulkDelete/bulkDeleteConfirmation/bulkDeleteSelected/bulkDeleteSuccess/bulkDeleteFailed/selectAll/selectRow/clearSelection 键。--- 之前更新:V4 AI 模块深度增强(ai-module-v2 对标 Khanmigo/Duolingo Max/Squirrel AI/Century Tech):(V4-1) SSE 流式响应:shared/lib/ai/client.ts 新增 createAiChatCompletionStream(AsyncGenerator 逐 token 产出),新增 API 路由 /api/ai/chat/stream(POST,requirePermission(AI_CHAT)+checkDailyLimit+filterUserInput+学生苏格拉底系统提示+ReadableStream 流式输出+filterAiOutput+incrementDailyUsage+trackEvent),新增 hook useAiChatStream(fetch+ReadableStream reader+SSE 解析+AbortController 停止生成+localStorage 持久化最近 20 条)。(V4-2) Markdown 渲染:新增组件 AiMarkdownRenderer(react-markdown+remark-gfm,代码块/表格/列表+hover 复制按钮+memo 优化),AiChatPanel 全面重写(流式渲染+停止按钮+清空确认+建议提示词空状态+aria-live+错误展示)。(V4-3) 全局 AI 助手:新增组件 AiAssistantWidget(fixed 浮动按钮+Sheet 侧抽屉+usePathname 路由推断上下文+inferContextFromPath 映射 7 类场景系统提示:教师批改/备课/考试/学生错题本/学生作业/家长/管理员+useAiClientOptional 无 Provider 时隐藏+pulsing 绿色指示器),dashboard layout 全局注入 AiClientProvider+AiAssistantWidget。(V4-4) 内容安全:新增 services/content-safety.ts(filterUserInput 阻断暴力/自残/色情/毒品/黑客/PII 索取;filterAiOutput 输出二次过滤+学生场景阻断直接答案;checkDailyLimit 学生 50/教师 200/家长 30/管理员 500;incrementDailyUsage;getDailyLimit),COPPA/FERPA K12 合规。(V4-5) 多角色 AI 覆盖:types.ts 新增 ChildSummaryInput/Result、StudyPathInput/Result、AiUsageStats 类型 + AiService/AiClientService 接口扩展;schema.ts 新增 4 个 Zod schema;prompt-templates.ts 新增 CHILD_SUMMARY_SYSTEM_PROMPT(家庭教育顾问,家长友好语言)+ STUDY_PATH_SYSTEM_PROMPT(自适应学习路径设计师,3-7 步骤);ai-service.ts 新增 generateChildSummary(聚合成绩/出勤/错题本数据)+ recommendStudyPath(基于掌握度数据);actions.ts 新增 generateChildSummaryAction/recommendStudyPathAction(AI_CHAT 权限)+ getAiUsageStatsAction(AI_CONFIGURE 权限);新增组件 AiChildSummary(家长端:整体评估 Markdown+优势绿勾+改进橙警+家庭辅导建议+下一步徽章)、AiUsageDashboard(管理员端:4 统计卡片+按能力进度条+按角色徽章+Top 用户列表+最近活动日志)、AiStudyPath(学生端:当前等级横幅+学习路径步骤+连接线+状态图标+预估时间+激励消息)。(V4-6) i18n 修复与扩展:修复 AiGradingAssist/AiLessonContentGenerator/AiQuestionVariantGenerator 共 8 处错误 i18n 键引用(CardDescription 重复 title、label/placeholder/button 复用 generateContent、3 个变体类型标签全部显示「生成」);zh-CN/en ai.json 全面重写新增 chat.streaming/stopGeneration/copy/clearConfirm/suggestedPrompts、grading.description/batch*、lessonPrep.description/additionalContext/insertContent、exam.variantType.*/targetDifficulty/addVariant、parent.*、admin.*、studyPath.*、widget.*、safety.* 等键。架构文档 004/005 同步更新。" }, "architectureOverview": { "layers": [ @@ -250,7 +250,8 @@ "ELECTIVE_READ", "DASHBOARD_STUDENT_READ", "ERROR_BOOK_READ", - "ERROR_BOOK_MANAGE" + "ERROR_BOOK_MANAGE", + "LESSON_PLAN_READ" ], "parent": [ "EXAM_READ", @@ -264,7 +265,8 @@ "MESSAGE_READ", "MESSAGE_DELETE", "DASHBOARD_PARENT_READ", - "ERROR_BOOK_READ" + "ERROR_BOOK_READ", + "LESSON_PLAN_READ" ], "grade_head": [ "EXAM_CREATE", @@ -301,7 +303,8 @@ "DIAGNOSTIC_READ", "ELECTIVE_READ", "EXAM_PROCTOR_READ", - "ERROR_BOOK_ANALYTICS_READ" + "ERROR_BOOK_ANALYTICS_READ", + "LESSON_PLAN_READ" ], "teaching_head": [ "EXAM_CREATE", @@ -332,7 +335,8 @@ "MESSAGE_DELETE", "DIAGNOSTIC_READ", "ELECTIVE_READ", - "ERROR_BOOK_ANALYTICS_READ" + "ERROR_BOOK_ANALYTICS_READ", + "LESSON_PLAN_READ" ] }, "dataScopeTypes": { @@ -340,8 +344,8 @@ "owned": "仅自己创建的资源,含 userId 字段", "class_taught": "教师:所教班级,含 classIds[] 和可选 subjectIds[]", "grade_managed": "年级主任:所管年级,含 gradeIds[]", - "class_members": "学生:所在班级的成员数据", - "children": "家长:子女数据,含 childrenIds[]" + "class_members": "学生:所在班级的成员数据,含 classIds[] 和可选 gradeIds[](auth-guard 预解析)", + "children": "家长:子女数据,含 childrenIds[] 和可选 gradeIds[](auth-guard 预解析)" }, "modules": { "shared": { @@ -1718,13 +1722,14 @@ { "name": "DataScope", "file": "types/permissions.ts", - "definition": "DataScope = { type: 'all' } | { type: 'owned'; userId: string } | { type: 'class_members'; classIds: string[] } | { type: 'grade_managed'; gradeIds: string[] } | { type: 'class_taught'; classIds: string[]; subjectIds?: string[] } | { type: 'children'; childrenIds: string[] }", + "definition": "DataScope = { type: 'all' } | { type: 'owned'; userId: string } | { type: 'class_members'; classIds: string[]; gradeIds?: string[] } | { type: 'grade_managed'; gradeIds: string[] } | { type: 'class_taught'; classIds: string[]; subjectIds?: string[] } | { type: 'children'; childrenIds: string[]; gradeIds?: string[] }", "usedBy": [ "auth-guard", "exams/data-access", "homework/data-access", "dashboard/data-access", "grades/data-access", + "lesson-preparation/data-access", "parent/children/[studentId]/page.tsx" ] }, @@ -2879,6 +2884,19 @@ "usedBy": [ "exam-form.tsx" ] + }, + { + "name": "getExamsByGradeIdAction", + "permission": "EXAM_READ", + "signature": "(gradeId: string) => Promise>", + "purpose": "v4-P2-7 新增:年级仪表盘维度3,获取年级下所有考试 + 提交统计", + "deps": [ + "requirePermission", + "data-access.getExamsByGradeId" + ], + "usedBy": [ + "management/grade/dashboard" + ] } ], "dataAccess": [ @@ -3013,6 +3031,23 @@ "getGradesAction" ] }, + { + "name": "getExamsByGradeId", + "signature": "(params: { gradeId: string; scope: DataScope }) => Promise", + "file": "data-access.ts", + "purpose": "v4-P2-7 新增:年级仪表盘维度3,exams 表有直接 gradeId 字段,配合 examSubmissions 聚合提交数/已评分数/平均分,支持 scope 行级过滤", + "deps": [ + "shared.db", + "shared.db.schema.exams", + "shared.db.schema.examSubmissions", + "classes/data-access.getClassGradeIdsByClassIds", + "school/data-access.getSubjectOptions" + ], + "usedBy": [ + "exams/actions.getExamsByGradeIdAction", + "management/grade/dashboard" + ] + }, { "name": "GetExamsParams", "type": "type", @@ -6321,6 +6356,15 @@ "homework/data-access-classes.getHomeworkAssignmentsWithSubject", "homework/data-access-classes.getPublishedHomeworkAssignmentsWithSubject" ] + }, + { + "name": "getGradeOverviewStats", + "signature": "() => Promise", + "purpose": "v4-P2-6 新增:年级概览统计,返回每个年级的 classCount/studentCount/teacherCount,供年级管理卡片视图使用。动态导入 classes/data-access 避免循环依赖", + "usedBy": [ + "admin/school/grades/page.tsx", + "school/components/grades-view.tsx" + ] } ], "schema": [ @@ -6480,6 +6524,17 @@ "getOrgTree", "school/components/org-tree-nav.tsx" ] + }, + { + "name": "GradeOverviewStats", + "type": "interface", + "file": "data-access.ts", + "definition": "{ gradeId: string; classCount: number; studentCount: number; teacherCount: number }", + "purpose": "v4-P2-6 新增:年级概览统计接口,每个年级的班级数/学生数/教师数", + "usedBy": [ + "data-access.getGradeOverviewStats", + "school/components/grades-view.tsx" + ] } ], "components": [ @@ -6507,7 +6562,15 @@ }, { "name": "GradesClient", - "purpose": "年级管理客户端" + "file": "components/grades-view.tsx", + "purpose": "年级管理客户端(v4-P2-6 更新:新增 gradeStats prop,渲染年级概览卡片视图——每个年级卡片展示班级/学生/教师数 + 年级主任/教学主任 + 快捷操作入口)", + "props": "grades: GradeListItem[], schools: SchoolListItem[], staff: StaffOption[], gradeStats: GradeOverviewStats[]" + }, + { + "name": "GradeInsightsFilters", + "file": "components/grade-insights-filters.tsx", + "purpose": "v4-P2-6 新增:年级洞察筛选器,使用 ChipNav 替代原生 form get,点击 chip 即时通过 URL 参数切换,无整页刷新", + "props": "grades: Array<{id, name, schoolName}>, currentGradeId: string, buildHref: (gradeId: string) => string" }, { "name": "DepartmentsClient", @@ -7007,6 +7070,15 @@ "shared/lib/ai.testAiProviderConfig" ] }, + { + "name": "deleteAiProviderAction", + "permission": "AI_CONFIGURE", + "signature": "(input: { id: string }) => Promise>", + "purpose": "删除AI Provider(V3 新增:事务删除 + 若为默认则自动转移默认到最新记录)", + "deps": [ + "data-access.deleteAiProvider" + ] + }, { "name": "changePasswordAction", "file": "actions-password.ts", @@ -9174,6 +9246,21 @@ "teacher/grades/analytics" ] }, + { + "name": "getGradeDistributionByGradeId", + "signature": "(params: { gradeId; subjectId?; examId?; semester?; scope: DataScope }) => Promise", + "file": "data-access-analytics.ts", + "purpose": "v4-P2-7 新增:年级仪表盘维度1,通过 getClassesByGradeId 获取年级下所有班级,inArray 查询成绩记录,复用 computeGradeDistribution/computeGradeStats,返回整体分布 + 按班级拆分", + "deps": [ + "shared.db", + "shared.db.schema.gradeRecords", + "classes/data-access.getClassesByGradeId" + ], + "usedBy": [ + "grades/actions-analytics.getGradeDistributionByGradeIdAction", + "management/grade/dashboard" + ] + }, { "name": "getRankingTrend", "signature": "(studentId: string, subjectId?, semester?, scope?: DataScope) => Promise", @@ -9376,7 +9463,7 @@ "export.exportClassGradeReportToExcel", "export.exportStudentGradeRecordsToExcel", "export.formatDateForFile", - "actions.assertClassInScope" + "lib/scope-check.assertClassInScope" ], "usedBy": [ "grades/components/export-button.tsx", @@ -9423,6 +9510,16 @@ "teacher/grades/analytics" ] }, + { + "name": "getGradeDistributionByGradeIdAction", + "signature": "(params: { gradeId; subjectId?; examId?; semester? }) => Promise>", + "file": "actions-analytics.ts", + "permission": "GRADE_RECORD_READ", + "purpose": "v4-P2-7 新增:年级仪表盘维度1,获取年级整体 + 按班级拆分的成绩分布", + "usedBy": [ + "management/grade/dashboard" + ] + }, { "name": "getRankingTrendAction", "signature": "(studentId, subjectId?, semester?) => Promise", @@ -9433,21 +9530,6 @@ "待扩展" ] }, - { - "name": "assertClassInScope", - "signature": "(classId: string, ctx: AuthContext) => void", - "file": "actions.ts", - "permission": "internal", - "purpose": "P3 新增:校验 classId 是否在 ctx.dataScope 允许范围内,不在则抛 BusinessError。供 actions.ts 与 actions-analytics.ts 复用", - "usedBy": [ - "grades/actions.getGradeRecordsAction", - "grades/actions.getGradeRecordByIdAction", - "grades/actions.exportGradesAction", - "grades/actions-analytics.getGradeTrendAction", - "grades/actions-analytics.getSubjectComparisonAction", - "grades/actions-analytics.getGradeDistributionAction" - ] - }, { "name": "saveGradeDraftAction", "signature": "(params: { classId: string; subjectId: string; type: string; data: GradeDraftData }) => Promise>", @@ -9920,6 +10002,20 @@ "data-access-analytics.getGradeDistribution", "data-access-ranking.getRankingTrend" ] + }, + { + "name": "assertClassInScope", + "signature": "(scope: DataScope, classId: string) => string | null", + "file": "lib/scope-check.ts", + "purpose": "校验 classId 是否在 scope 允许范围内,返回错误消息字符串表示拒绝,返回 null 表示允许。all/children/grade_managed 允许;class_taught/class_members 校验 classIds 包含;owned 拒绝。P3 新增原位于 actions.ts,v4-P2-6 迁移至独立文件 lib/scope-check.ts(含 import \"server-only\"),原因:actions.ts 是 \"use server\" 文件要求所有 export 必须为 async,而本函数是同步函数", + "usedBy": [ + "grades/actions.getGradeRecordsAction", + "grades/actions.getGradeRecordByIdAction", + "grades/actions.exportGradesAction", + "grades/actions-analytics.getGradeTrendAction", + "grades/actions-analytics.getSubjectComparisonAction", + "grades/actions-analytics.getGradeDistributionAction" + ] } ], "statsService": [ @@ -10272,6 +10368,19 @@ "usedBy": [ "course-plan-detail.tsx" ] + }, + { + "name": "getGradeCoursePlanProgressAction", + "permission": "COURSE_PLAN_READ", + "signature": "(gradeId: string) => Promise>", + "purpose": "v4-P2-7 新增:年级仪表盘维度4,获取年级下所有班级的教学计划进度", + "deps": [ + "requirePermission", + "data-access.getGradeCoursePlanProgress" + ], + "usedBy": [ + "management/grade/dashboard" + ] } ], "dataAccess": [ @@ -10407,6 +10516,25 @@ "admin/course-plans/create/page.tsx", "admin/course-plans/[id]/edit/page.tsx" ] + }, + { + "name": "getGradeCoursePlanProgress", + "signature": "(params: { gradeId: string }) => Promise", + "file": "data-access.ts", + "purpose": "v4-P2-7 新增:年级仪表盘维度4,通过 getClassesByGradeId 获取年级下所有班级,inArray 查询 course_plans + course_plan_items,返回整体进度汇总 + 按班级/科目拆分的进度矩阵", + "deps": [ + "shared.db", + "shared.db.schema.coursePlans", + "shared.db.schema.coursePlanItems", + "shared.db.schema.classes", + "shared.db.schema.subjects", + "shared.db.schema.users", + "classes/data-access.getClassesByGradeId" + ], + "usedBy": [ + "course-plans/actions.getGradeCoursePlanProgressAction", + "management/grade/dashboard" + ] } ], "schemas": [ @@ -14678,6 +14806,16 @@ "file": "publish-service.ts", "purpose": "发布课案为作业(编排 homework/exams/classes,通过对方 data-access 调用 addExamQuestions/getStudentIdsByClassIds,无直查跨模块表;V2-1:抛出 PublishServiceError 错误码;V2-3:显式字段映射替代 as unknown as)" }, + { + "name": "publishLessonPlan", + "file": "data-access.ts", + "purpose": "发布课案:设置 status=published(V3 新增,供学生/家长/教研组长只读查看)" + }, + { + "name": "unpublishLessonPlan", + "file": "data-access.ts", + "purpose": "撤回发布:将 status 从 published 改为 draft(仅 published 课案可撤回;V3 新增)" + }, { "name": "suggestKnowledgePoints", "file": "ai-suggest.ts", @@ -14769,6 +14907,18 @@ "file": "actions-publish.ts", "purpose": "发布课案为作业" }, + { + "name": "publishLessonPlanAction", + "permission": "LESSON_PLAN_PUBLISH", + "file": "actions-publish.ts", + "purpose": "发布课案(设置 status=published,供学生/家长/教研组长只读查看;V3 新增)" + }, + { + "name": "unpublishLessonPlanAction", + "permission": "LESSON_PLAN_PUBLISH", + "file": "actions-publish.ts", + "purpose": "撤回发布(将 status 从 published 改为 draft;V3 新增)" + }, { "name": "getKnowledgePointOptionsAction", "permission": "LESSON_PLAN_READ", @@ -14900,6 +15050,7 @@ "components/lesson-plan-card.tsx", "components/lesson-plan-filters.tsx", "components/lesson-plan-editor.tsx", + "components/lesson-plan-readonly-view.tsx", "components/node-editor.tsx", "components/node-edit-panel.tsx", "components/nodes/lesson-node.tsx", @@ -14951,7 +15102,16 @@ "V2-3": "as unknown as 类型断言清零:8 处替换为显式类型映射函数(mapRowToLessonPlan/mapRowToListItem/mapRowToTemplate/mapRowToVersion)+ 类型守卫(isLessonPlanStatus/isTemplateType/isTemplateScope)", "V2-4": "MiniMap nodeColor 复用 lib/node-summary.ts 的 getNodeColor", "V2-5": "a11y 深度修复:lesson-plan-filters/exercise-block/inline-question-editor 的 select 添加 label htmlFor 关联;exercise-block 题目列表改为 ul/li;node-editor 画布添加 role=application + 键盘导航配置", - "V2-6": "Tracker 埋点接入:新增 useLessonPlanTrackerSafe hook,在 create/save/publish/revert/duplicate/archive 6 处调用 tracker.track" + "V2-6": "Tracker 埋点接入:新增 useLessonPlanTrackerSafe hook,在 create/save/publish/revert/duplicate/archive 6 处调用 tracker.track", + "V3-1": "课案发布/撤回:新增 publishLessonPlan/unpublishLessonPlan data-access 函数 + publishLessonPlanAction/unpublishLessonPlanAction Server Actions(LESSON_PLAN_PUBLISH 权限),支持课案 status=published 供学生/家长/教研组长只读查看", + "V3-2": "只读画布组件:新增 LessonPlanReadonlyView,复用 React Flow(nodesDraggable=false, nodesConnectable=false),供学生/家长/管理员/教研组长查看已发布课案", + "V3-3": "多角色视图:LessonPlanCard/LessonPlanList/LessonPlanEditor 新增 viewMode prop(teacher/student/parent/admin/gradeHead),动态跳转链接 + 发布/撤回按钮(AlertDialog 确认);LessonPlanEditor 新增 initialStatus prop", + "V3-4": "锚点选择器重写:TextbookContentNode 新增 anchorableNodes/onCreateNewNode props,AnchorNodeSelector 重写为节点列表+创建新节点选项;NodeEditPanel 选中 textbook_content 时显示操作提示+锚点列表(含删除功能)", + "V3-5": "模板分区显示:TemplatePicker 加载并显示个人模板(调用 getLessonPlanTemplatesAction),分区显示系统/个人模板", + "V3-6": "rf-mappers 增强:toRfNodes ctx 新增 anchorableNodes/onCreateNewNode 字段;toRfEdges 锚点边颜色使用 getNodeColor(anchor.nodeId) 替代硬编码,anchorId 存入 edge.data;fromRfEdges 从 e.data.anchorId 读取 anchorId(回退到 className 判断)", + "V3-7": "权限扩展:student/parent/grade_head/teaching_head 角色新增 LESSON_PLAN_READ 权限,可查看已发布课案", + "V3-8": "DataScope 扩展:class_members 和 children 新增可选 gradeIds? 字段,auth-guard resolveDataScope 通过 classEnrollments.innerJoin(classes) 预解析年级 ID", + "V3-9": "新增路由:/student/lesson-plans、/student/lesson-plans/[planId]/view、/parent/lesson-plans、/parent/lesson-plans/[planId]/view、/admin/lesson-plans、/admin/lesson-plans/[planId]/view" } }, "error-book": { @@ -17399,6 +17559,17 @@ "status": "resolved", "resolvedAt": "2026-06-18", "resolution": "getDemoStudentUser 从 homework 模块迁移至 users 模块 getCurrentStudentUser(通过 session + JOIN users/usersToRoles/roles 校验 student 角色);6 个 student 页面(dashboard/assignments/assignments-[assignmentId]/courses/textbooks/textbooks-[id]/schedule)改用 users 模块,消除对 homework 的虚假依赖;student/elective 改用 getAuthContext();homework 保留 re-export 向后兼容" + }, + { + "id": "v4-P2-6", + "severity": "P2", + "title": "年级管理模块改进:概览卡片视图 + 洞察筛选器 + assertClassInScope 迁移", + "file": "src/modules/school/data-access.ts, src/modules/school/components/grade-insights-filters.tsx, src/modules/school/components/grades-view.tsx, src/modules/grades/lib/scope-check.ts, src/app/(dashboard)/admin/school/grades/page.tsx, src/app/(dashboard)/admin/school/grades/insights/page.tsx, src/app/(dashboard)/management/grade/insights/page.tsx", + "problem": "(1) 年级管理页面缺少概览视图,管理员难以一目了然看到各年级规模;(2) 年级洞察页面使用原生 form get 切换年级导致整页刷新;(3) grades/actions.ts 中的 assertClassInScope 是同步函数但位于 \"use server\" 文件中,Next.js 要求 \"use server\" 文件所有 export 必须为 async,会导致构建错误", + "suggestion": "新增 getGradeOverviewStats data-access 函数 + GradesClient 卡片视图;新增 GradeInsightsFilters 组件使用 ChipNav;将 assertClassInScope 迁移至独立 lib/scope-check.ts 文件", + "status": "resolved", + "resolvedAt": "2026-06-23", + "resolution": "(1) 新增 getGradeOverviewStats() data-access 函数(返回 GradeOverviewStats[]:每个年级的 classCount/studentCount/teacherCount,动态导入 classes/data-access 避免循环依赖)+ GradeOverviewStats 接口;GradesClient 新增 gradeStats prop,渲染年级概览卡片视图(班级/学生/教师数 + 年级主任/教学主任 + 快捷操作入口);admin/school/grades/page.tsx 新增 getGradeOverviewStats() 查询并传入 GradesClient。(2) 新增 GradeInsightsFilters 组件(使用 ChipNav 替代原生 form get,点击 chip 即时通过 URL 参数切换,无整页刷新);admin/school/grades/insights/page.tsx 与 management/grade/insights/page.tsx 重写,使用 GradeInsightsFilters 替代原生 form get,添加 i18n,表格添加 overflow-x-auto。(3) 将 assertClassInScope 从 actions.ts 迁移至独立文件 lib/scope-check.ts(含 import \"server-only\"),actions.ts 与 actions-analytics.ts 均从 ./lib/scope-check 导入。school.json(en + zh-CN)新增 grades.gradeInsights 和 grades.gradeOverview 两个章节 i18n 键" } ], "routes": { @@ -17481,17 +17652,24 @@ "component": "GradesClient", "type": "client", "module": "school", - "permission": "school:manage" + "dataAccess": [ + "school/data-access.getGrades", + "school/data-access.getSchools", + "school/data-access.getStaffOptions", + "school/data-access.getGradeOverviewStats" + ], + "permission": "school:manage", + "description": "v4-P2-6 更新:新增 getGradeOverviewStats 查询,传入 gradeStats 到 GradesClient 渲染年级概览卡片视图" }, "/admin/school/grades/insights": { - "component": "年级作业洞察 + SchoolWideSummaryCard", + "component": "年级作业洞察 + SchoolWideSummaryCard + GradeInsightsFilters", "type": "server", "dataAccess": [ "classes/data-access.getGradeHomeworkInsights", "grades/data-access-analytics.getSchoolWideGradeSummary" ], "permission": "school:manage", - "description": "v3-P2 更新:顶部新增 SchoolWideSummaryCard 全校成绩汇总卡片(4 个统计卡片 + 各年级对比表格)" + "description": "v3-P2 更新:顶部新增 SchoolWideSummaryCard 全校成绩汇总卡片(4 个统计卡片 + 各年级对比表格);v4-P2-6 更新:重写页面使用 GradeInsightsFilters 替代原生 form get(ChipNav 无整页刷新),添加 i18n,表格添加 overflow-x-auto" }, "/admin/school/departments": { "component": "DepartmentsClient", @@ -17753,12 +17931,39 @@ "permission": "settings:admin", "description": "系统设置页面(学校信息/安全策略/文件上传/通知配置;权限:requirePermission(SETTINGS_ADMIN))" }, + "/admin/ai-settings": { + "component": "AiProviderSettingsCard + AiUsageDashboard", + "type": "server", + "module": "settings+ai", + "permission": "ai:configure", + "description": "AI 统一配置页(V3 新增:取代 /settings?tab=ai 和考试页面内嵌弹窗;管理 AI 服务商/API 密钥/使用统计;权限:requirePermission(AI_CONFIGURE))" + }, "/admin/error-book": { "component": "AdminErrorBookPage + ClassErrorOverview", "type": "server", "module": "error-book", "permission": "error_book:analytics_read", "description": "管理员错题分析页面(全校错题统计/薄弱知识点/学科分布/高频错题;权限:requirePermission(ERROR_BOOK_ANALYTICS_READ))" + }, + "/admin/lesson-plans": { + "component": "LessonPlanList (viewMode=admin) + 统计卡片", + "type": "server", + "module": "lesson-preparation", + "dataAccess": [ + "lesson-preparation/data-access.getLessonPlans (scope=all)" + ], + "permission": "lesson_plan:read", + "description": "V3 新增:管理员课案列表 + 统计卡片(全校课案总览;权限:requirePermission(LESSON_PLAN_READ);DataScope.all)" + }, + "/admin/lesson-plans/[planId]/view": { + "component": "LessonPlanReadonlyView", + "type": "server", + "module": "lesson-preparation", + "dataAccess": [ + "lesson-preparation/data-access.getLessonPlanById" + ], + "permission": "lesson_plan:read", + "description": "V3 新增:管理员查看课案详情(只读画布;权限:requirePermission(LESSON_PLAN_READ))" } }, "teacher": { @@ -18342,6 +18547,26 @@ ], "permission": "error_book:read", "description": "学生错题本页面(统计卡片/筛选/列表/手动添加/详情复习;权限:requirePermission(ERROR_BOOK_READ);DataScope.owned 仅查自己)" + }, + "/student/lesson-plans": { + "component": "LessonPlanList (viewMode=student)", + "type": "server", + "module": "lesson-preparation", + "dataAccess": [ + "lesson-preparation/data-access.getLessonPlans (status=published, scope=class_members)" + ], + "permission": "lesson_plan:read", + "description": "V3 新增:学生课案列表(仅查看 status=published 的课案;权限:requirePermission(LESSON_PLAN_READ);DataScope.class_members 按 gradeIds 过滤)" + }, + "/student/lesson-plans/[planId]/view": { + "component": "LessonPlanReadonlyView", + "type": "server", + "module": "lesson-preparation", + "dataAccess": [ + "lesson-preparation/data-access.getLessonPlanById" + ], + "permission": "lesson_plan:read", + "description": "V3 新增:学生只读查看已发布课案(复用 React Flow,nodesDraggable=false, nodesConnectable=false;权限:requirePermission(LESSON_PLAN_READ))" } }, "management": { @@ -18359,12 +18584,26 @@ "permission": "grade:manage" }, "/management/grade/insights": { - "component": "年级作业洞察", + "component": "年级作业洞察 + GradeInsightsFilters", "type": "server", "dataAccess": [ "classes/data-access.getGradeHomeworkInsights" ], - "permission": "grade:manage" + "permission": "grade:manage", + "description": "v4-P2-6 更新:重写页面使用 GradeInsightsFilters 替代原生 form get(ChipNav 无整页刷新),表格添加 overflow-x-auto 水平滚动" + }, + "/management/grade/dashboard": { + "component": "GradeDashboardPage + 4 Tab 面板", + "type": "server", + "module": "school", + "dataAccess": [ + "grades/data-access-analytics.getGradeDistributionByGradeId", + "classes/data-access.getGradeHomeworkInsights", + "exams/data-access.getExamsByGradeId", + "course-plans/data-access.getGradeCoursePlanProgress" + ], + "permission": "grade:read", + "description": "v4-P2-7 新增:年级仪表盘,4 个维度 Tab(成绩分布/作业完成/考试/课本进度),ChipNav URL 切换 Tab,仅加载当前 Tab 数据" } }, "parent": { @@ -18441,6 +18680,26 @@ ], "permission": "error_book:read", "description": "家长错题本页面(子女错题统计/薄弱知识点/高频错题;权限:requirePermission(ERROR_BOOK_READ);DataScope.children 仅查子女)" + }, + "/parent/lesson-plans": { + "component": "LessonPlanList (viewMode=parent)", + "type": "server", + "module": "lesson-preparation", + "dataAccess": [ + "lesson-preparation/data-access.getLessonPlans (status=published, scope=children)" + ], + "permission": "lesson_plan:read", + "description": "V3 新增:家长课案列表(仅查看子女所在班级 status=published 的课案;权限:requirePermission(LESSON_PLAN_READ);DataScope.children 按 gradeIds 过滤)" + }, + "/parent/lesson-plans/[planId]/view": { + "component": "LessonPlanReadonlyView", + "type": "server", + "module": "lesson-preparation", + "dataAccess": [ + "lesson-preparation/data-access.getLessonPlanById" + ], + "permission": "lesson_plan:read", + "description": "V3 新增:家长只读查看已发布课案(复用 React Flow,nodesDraggable=false, nodesConnectable=false;权限:requirePermission(LESSON_PLAN_READ))" } }, "root": { diff --git a/src/app/(dashboard)/admin/ai-settings/page.tsx b/src/app/(dashboard)/admin/ai-settings/page.tsx new file mode 100644 index 0000000..c8019db --- /dev/null +++ b/src/app/(dashboard)/admin/ai-settings/page.tsx @@ -0,0 +1,43 @@ +import type { Metadata } from "next" +import type { JSX } from "react" + +import { requirePermission } from "@/shared/lib/auth-guard" +import { Permissions } from "@/shared/types/permissions" +import { AiProviderSettingsCard } from "@/modules/settings/components/ai-provider-settings-card" +import { AiUsageDashboard } from "@/modules/ai/components/ai-usage-dashboard" + +export const metadata: Metadata = { + title: "AI 配置 - Next_Edu", + description: "统一管理 AI 服务商、API 密钥与使用统计", +} + +export const dynamic = "force-dynamic" + +/** + * AI 统一配置页 + * + * 作为 AI 模块的独立配置入口,取代: + * - /settings?tab=ai(已移除 AI 标签页) + * - 考试页面内嵌的 AI 配置弹窗(已改为跳转链接) + * + * 权限:AI_CONFIGURE(当前仅 admin 角色拥有) + */ +export default async function AiSettingsPage(): Promise { + await requirePermission(Permissions.AI_CONFIGURE) + return ( +
+
+
+

AI 配置

+
+ 统一管理 AI 服务商、API 密钥与使用统计 +
+
+
+
+ + +
+
+ ) +} diff --git a/src/modules/ai/components/ai-provider-selector.tsx b/src/modules/ai/components/ai-provider-selector.tsx index adf1c9e..78cf4a9 100644 --- a/src/modules/ai/components/ai-provider-selector.tsx +++ b/src/modules/ai/components/ai-provider-selector.tsx @@ -1,5 +1,6 @@ "use client" +import Link from "next/link" import { useTranslations } from "next-intl" import { Settings } from "lucide-react" import { @@ -17,14 +18,6 @@ import { SelectTrigger, SelectValue, } from "@/shared/components/ui/select" -import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/shared/components/ui/dialog" import { Button } from "@/shared/components/ui/button" import type { Control } from "react-hook-form" @@ -47,11 +40,6 @@ type AiProviderSelectorProps = { loading?: boolean /** Provider 标签映射 */ providerLabels?: Record - /** 管理面板触发器 */ - managePanel?: React.ReactNode - /** 管理面板打开状态 */ - manageOpen?: boolean - onManageOpenChange?: (open: boolean) => void } /** @@ -59,6 +47,8 @@ type AiProviderSelectorProps = { * * 可复用的表单字段组件,用于选择 AI Provider。 * 从 exam-ai-generator.tsx 抽取,支持在任何需要 AI Provider 选择的表单中复用。 + * + * V3:移除内嵌配置弹窗,"管理"按钮改为跳转到 /admin/ai-settings 统一配置页。 */ export function AiProviderSelector({ control, @@ -66,9 +56,6 @@ export function AiProviderSelector({ providers, loading = false, providerLabels, - managePanel, - manageOpen, - onManageOpenChange, }: AiProviderSelectorProps): React.ReactNode { const t = useTranslations("ai") @@ -80,28 +67,12 @@ export function AiProviderSelector({
{t("provider.label")} - {managePanel ? ( - - - - - - - {t("provider.manageTitle")} - {t("provider.manageDescription")} - - {managePanel} - - - ) : null} +
diff --git a/src/modules/exams/components/exam-form.tsx b/src/modules/exams/components/exam-form.tsx index 6008841..5cd13fd 100644 --- a/src/modules/exams/components/exam-form.tsx +++ b/src/modules/exams/components/exam-form.tsx @@ -25,8 +25,6 @@ export type { ExamFormValues } from "./exam-form-types" export function ExamForm() { const router = useRouter() const [isPending, startTransition] = useTransition() - const [providerDialogOpen, setProviderDialogOpen] = useState(false) - const [providerDialogKey, setProviderDialogKey] = useState(0) const [subjects, setSubjects] = useState<{ id: string; name: string }[]>([]) const [loadingSubjects, setLoadingSubjects] = useState(true) const [grades, setGrades] = useState<{ id: string; name: string }[]>([]) @@ -219,10 +217,6 @@ export function ExamForm() { aiProviders={aiProviders} setAiProviders={setAiProviders} loadingAiProviders={loadingAiProviders} - providerDialogOpen={providerDialogOpen} - setProviderDialogOpen={setProviderDialogOpen} - providerDialogKey={providerDialogKey} - setProviderDialogKey={setProviderDialogKey} handlePreview={preview.handlePreview} handleBackgroundPreview={preview.handleBackgroundPreview} previewLoading={preview.previewLoading} diff --git a/src/modules/layout/config/navigation.ts b/src/modules/layout/config/navigation.ts index 3e22507..a5ed70c 100644 --- a/src/modules/layout/config/navigation.ts +++ b/src/modules/layout/config/navigation.ts @@ -22,6 +22,7 @@ import { BookCopy, Files, BookX, + Sparkles, } from "lucide-react" import type { LucideIcon } from "lucide-react" import { Permissions } from "@/shared/types/permissions" @@ -134,6 +135,12 @@ export const NAV_CONFIG: Partial> = { href: "/admin/error-book", permission: Permissions.ERROR_BOOK_ANALYTICS_READ, }, + { + title: "课案管理", + icon: PenTool, + href: "/admin/lesson-plans", + permission: Permissions.LESSON_PLAN_READ, + }, { title: "Audit Logs", icon: ScrollText, @@ -146,6 +153,12 @@ export const NAV_CONFIG: Partial> = { ] }, ...COMMON_NAV_ITEMS, + { + title: "AI 配置", + icon: Sparkles, + href: "/admin/ai-settings", + permission: Permissions.AI_CONFIGURE, + }, { title: "Settings", icon: Settings, @@ -268,6 +281,7 @@ export const NAV_CONFIG: Partial> = { permission: Permissions.GRADE_MANAGE, items: [ { title: "年级班级", href: "/management/grade/classes" }, + { title: "年级仪表盘", href: "/management/grade/dashboard" }, { title: "年级洞察", href: "/management/grade/insights" }, ] }, @@ -287,6 +301,7 @@ export const NAV_CONFIG: Partial> = { permission: Permissions.GRADE_MANAGE, items: [ { title: "年级班级", href: "/management/grade/classes" }, + { title: "年级仪表盘", href: "/management/grade/dashboard" }, { title: "年级洞察", href: "/management/grade/insights" }, ] }, @@ -332,6 +347,7 @@ export const NAV_CONFIG: Partial> = { permission: Permissions.GRADE_MANAGE, items: [ { title: "年级班级", href: "/management/grade/classes" }, + { title: "年级仪表盘", href: "/management/grade/dashboard" }, { title: "年级洞察", href: "/management/grade/insights" }, ] }, @@ -392,6 +408,12 @@ export const NAV_CONFIG: Partial> = { href: "/student/grades", permission: Permissions.GRADE_RECORD_READ, }, + { + title: "我的课案", + icon: PenTool, + href: "/student/lesson-plans", + permission: Permissions.LESSON_PLAN_READ, + }, { title: "Attendance", icon: CalendarCheck, @@ -430,6 +452,12 @@ export const NAV_CONFIG: Partial> = { href: "/parent/grades", permission: Permissions.GRADE_RECORD_READ, }, + { + title: "孩子课案", + icon: PenTool, + href: "/parent/lesson-plans", + permission: Permissions.LESSON_PLAN_READ, + }, { title: "Attendance", icon: CalendarCheck, diff --git a/src/modules/settings/actions.ts b/src/modules/settings/actions.ts index 09adb72..0b52bb1 100644 --- a/src/modules/settings/actions.ts +++ b/src/modules/settings/actions.ts @@ -12,6 +12,7 @@ import { encryptAiApiKey, getAiErrorMessage, testAiProviderById, testAiProviderC import { countDefaultAiProviders, createAiProvider, + deleteAiProvider as deleteAiProviderRecord, getAiProviderForUpdate, getAiProviderSummaries as fetchAiProviderSummaries, updateAiProvider, @@ -181,3 +182,31 @@ export async function testAiProviderAction( return { success: false, message: getAiErrorMessage(error) } } } + +const DeleteAiProviderSchema = z.object({ + id: z.string().min(1), +}) + +/** + * 删除 AI Provider + * + * 如果删除的是默认 Provider,自动将最新的一条记录设为默认(若存在)。 + */ +export async function deleteAiProviderAction( + input: z.infer +): Promise> { + try { + await ensureUser() + const parsed = DeleteAiProviderSchema.safeParse(input) + if (!parsed.success) { + return { success: false, message: "Invalid provider id" } + } + await deleteAiProviderRecord(parsed.data.id) + revalidatePath("/admin/ai-settings") + revalidatePath("/settings") + return { success: true, message: "AI provider deleted", data: null } + } catch (error) { + if (error instanceof PermissionDeniedError) return { success: false, message: error.message } + return { success: false, message: "Failed to delete AI provider" } + } +} diff --git a/src/modules/settings/components/ai-provider-settings-card.tsx b/src/modules/settings/components/ai-provider-settings-card.tsx index c68b243..d86a4d0 100644 --- a/src/modules/settings/components/ai-provider-settings-card.tsx +++ b/src/modules/settings/components/ai-provider-settings-card.tsx @@ -6,12 +6,23 @@ import { z } from "zod" import { zodResolver } from "@hookform/resolvers/zod" import { useForm } from "react-hook-form" import { toast } from "sonner" -import { Loader2, Save, Sparkles } from "lucide-react" +import { Loader2, Save, Sparkles, Trash2 } from "lucide-react" import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/shared/components/ui/card" import { Button } from "@/shared/components/ui/button" import { Checkbox } from "@/shared/components/ui/checkbox" import { Label } from "@/shared/components/ui/label" +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@/shared/components/ui/alert-dialog" import { Form, FormControl, @@ -28,7 +39,7 @@ import { SelectTrigger, SelectValue, } from "@/shared/components/ui/select" -import { getAiProviderSummaries, testAiProviderAction, upsertAiProviderAction, type AiProviderSummary } from "@/modules/settings/actions" +import { deleteAiProviderAction, getAiProviderSummaries, testAiProviderAction, upsertAiProviderAction, type AiProviderSummary } from "@/modules/settings/actions" const ProviderSchema = z.enum(["zhipu", "openai", "gemini", "custom"]) @@ -242,6 +253,44 @@ export function AiProviderSettingsCard({ }) } + const handleDelete = () => { + const id = form.getValues("id") + if (!id?.trim()) { + toast.error(t("deleteNeedSelect")) + return + } + startTransition(async () => { + const result = await deleteAiProviderAction({ id: id.trim() }) + if (result.success) { + toast.success(result.message ?? t("deleteSuccess")) + const summariesResult = await getAiProviderSummaries() + if (summariesResult.success && summariesResult.data) { + const rows = summariesResult.data + setProviders(rows) + onProvidersChanged?.(rows) + if (rows.length > 0) { + const next = rows[0] + setSelectedId(next.id) + form.reset({ + id: next.id, + provider: next.provider, + baseUrl: next.baseUrl ?? "", + model: next.model, + apiKey: "", + isDefault: next.isDefault, + }) + } else { + resetToNew() + } + } else { + resetToNew() + } + } else { + toast.error(result.message ?? t("deleteFailure")) + } + }) + } + return ( @@ -339,32 +388,56 @@ export function AiProviderSettingsCard({ /> - - + + + + + + + {t("deleteConfirmTitle")} + {t("deleteConfirmDescription")} + + + {t("deleteCancel")} + {t("deleteConfirm")} + + + +
+ + +
diff --git a/src/modules/settings/components/settings-view.tsx b/src/modules/settings/components/settings-view.tsx index d93da79..79d73ea 100644 --- a/src/modules/settings/components/settings-view.tsx +++ b/src/modules/settings/components/settings-view.tsx @@ -4,14 +4,13 @@ import Link from "next/link" import { useRouter, useSearchParams } from "next/navigation" import { Suspense, type ReactNode } from "react" import { useTranslations } from "next-intl" -import { User, Palette, Lock, Bell, Sparkles } from "lucide-react" +import { User, Palette, Lock, Bell } from "lucide-react" import { signOut } from "next-auth/react" import { ThemePreferencesCard } from "@/modules/settings/components/theme-preferences-card" import { ProfileSettingsForm } from "@/modules/settings/components/profile-settings-form" import { PasswordChangeForm } from "@/modules/settings/components/password-change-form" import { NotificationPreferencesForm } from "@/modules/settings/components/notification-preferences-form" -import { AiProviderSettingsCard } from "@/modules/settings/components/ai-provider-settings-card" import { SecurityCenterCard } from "@/modules/settings/components/security-center-card" import { SettingsSectionErrorBoundary } from "@/modules/settings/components/settings-section-error-boundary" import { Button } from "@/shared/components/ui/button" @@ -31,8 +30,6 @@ import { } from "@/shared/components/ui/alert-dialog" import type { UserProfile } from "@/modules/users/data-access" import type { NotificationPreferences } from "@/modules/notifications/types" -import { usePermission } from "@/shared/hooks/use-permission" -import { Permissions } from "@/shared/types/permissions" interface SettingsViewProps { /** 页面副标题描述(i18n 键) */ @@ -49,7 +46,7 @@ interface SettingsViewProps { currentUserAgent?: string } -const VALID_TABS = ["general", "notifications", "appearance", "security", "ai"] as const +const VALID_TABS = ["general", "notifications", "appearance", "security"] as const type TabValue = (typeof VALID_TABS)[number] function isTabValue(value: string | null): value is TabValue { @@ -95,15 +92,12 @@ function SettingsViewInner({ const t = useTranslations("settings") const router = useRouter() const searchParams = useSearchParams() - const { hasPermission } = usePermission() const tabParam = searchParams.get("tab") - const canConfigureAi = hasPermission(Permissions.AI_CONFIGURE) - // 解析 tab 参数,对无权限的 tab(如非管理员的 ai)回退到 general + // 解析 tab 参数 function resolveTab(value: string | null): TabValue { if (!isTabValue(value)) return "general" - if (value === "ai" && !canConfigureAi) return "general" return value } const activeTab: TabValue = resolveTab(tabParam) @@ -151,12 +145,6 @@ function SettingsViewInner({ {t("tabs.security")} - {canConfigureAi ? ( - - - {t("tabs.ai")} - - ) : null} @@ -223,16 +211,6 @@ function SettingsViewInner({ - - {canConfigureAi ? ( - - - }> - - - - - ) : null} ) diff --git a/src/modules/settings/data-access.ts b/src/modules/settings/data-access.ts index 78cf589..466f980 100644 --- a/src/modules/settings/data-access.ts +++ b/src/modules/settings/data-access.ts @@ -111,6 +111,42 @@ export async function createAiProvider( }) } +/** + * 删除 AI Provider + * + * 如果删除的是默认 Provider,自动将最新的一条记录设为默认(若存在)。 + */ +export async function deleteAiProvider(id: string): Promise<{ wasDefault: boolean }> { + return await db.transaction(async (tx) => { + const [existing] = await tx + .select({ isDefault: aiProviders.isDefault }) + .from(aiProviders) + .where(eq(aiProviders.id, id)) + .limit(1) + + if (!existing) { + return { wasDefault: false } + } + + await tx.delete(aiProviders).where(eq(aiProviders.id, id)) + + // 如果删除的是默认 Provider,自动选一条最新的设为默认 + if (existing.isDefault) { + const [next] = await tx + .select({ id: aiProviders.id }) + .from(aiProviders) + .orderBy(desc(aiProviders.updatedAt)) + .limit(1) + + if (next) { + await tx.update(aiProviders).set({ isDefault: true }).where(eq(aiProviders.id, next.id)) + } + } + + return { wasDefault: existing.isDefault } + }) +} + // --- Password change operations --- export async function getUserPasswordHash( diff --git a/src/shared/i18n/messages/en/settings.json b/src/shared/i18n/messages/en/settings.json index 8c09dd4..766d8a9 100644 --- a/src/shared/i18n/messages/en/settings.json +++ b/src/shared/i18n/messages/en/settings.json @@ -227,7 +227,15 @@ "saveFailure": "Failed to save", "loadFailure": "Failed to load AI providers", "needKey": "Please enter API key to test", - "needTest": "Please test the configuration before saving" + "needTest": "Please test the configuration before saving", + "delete": "Delete", + "deleteNeedSelect": "Please select a provider to delete", + "deleteSuccess": "Provider deleted", + "deleteFailure": "Failed to delete provider", + "deleteConfirmTitle": "Confirm deletion", + "deleteConfirmDescription": "This action cannot be undone. If the deleted provider was the default, the most recent provider will be set as default automatically.", + "deleteConfirm": "Delete", + "deleteCancel": "Cancel" } }, "quickLinks": { diff --git a/src/shared/i18n/messages/zh-CN/settings.json b/src/shared/i18n/messages/zh-CN/settings.json index 26b6c8c..06a514a 100644 --- a/src/shared/i18n/messages/zh-CN/settings.json +++ b/src/shared/i18n/messages/zh-CN/settings.json @@ -227,7 +227,15 @@ "saveFailure": "保存失败", "loadFailure": "加载 AI 服务商失败", "needKey": "请输入 API 密钥进行测试", - "needTest": "保存前请先测试配置" + "needTest": "保存前请先测试配置", + "delete": "删除", + "deleteNeedSelect": "请先选择要删除的服务商", + "deleteSuccess": "已删除服务商", + "deleteFailure": "删除失败", + "deleteConfirmTitle": "确认删除", + "deleteConfirmDescription": "此操作不可撤销。删除后若该服务商为默认,将自动选择最新的服务商作为默认。", + "deleteConfirm": "确认删除", + "deleteCancel": "取消" } }, "quickLinks": {