From 5d42495480e05fb5150d410828ebd1219319c190 Mon Sep 17 00:00:00 2001 From: SpecialX <47072643+wangxiner55@users.noreply.github.com> Date: Mon, 22 Jun 2026 16:15:36 +0800 Subject: [PATCH] =?UTF-8?q?feat(settings):=20=E8=AE=BE=E7=BD=AE=E4=B8=8E?= =?UTF-8?q?=E4=B8=AA=E4=BA=BA=E4=BF=A1=E6=81=AF=E6=A8=A1=E5=9D=97=E5=AE=A1?= =?UTF-8?q?=E8=AE=A1=E9=87=8D=E6=9E=84=20=E2=80=94=20i18n=20+=20=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1=E6=B3=A8=E5=85=A5=E8=A7=A3=E8=80=A6=20+=20Error=20Bou?= =?UTF-8?q?ndary=20+=20=E6=B5=81=E5=BC=8F=E6=B8=B2=E6=9F=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 SettingsService 接口 + Context 注入,组件层不再直接 import users/messaging actions - 新增 resolveRoleSettingsConfig 配置驱动角色路由,删除 parent/student/teacher-settings-view 冗余文件 - 新增 SettingsSectionErrorBoundary,每个 TabsContent + profile 角色概览区块均包裹 - 新增 ProfileStudentOverview/ProfileTeacherOverview 异步 Server Component + 骨架屏,支持流式渲染 - 抽取 buildStudentOverviewData 等纯函数到 lib/student-overview-data.ts,便于单元测试 - 新增 settings.json 翻译文件(zh-CN + en),所有组件改用 useTranslations/getTranslations - 重构 profile/page.tsx:i18n 适配 + Suspense 分区加载 + 业务逻辑抽离 - 同步更新架构图 004/005 --- .../004_architecture_impact_map.md | 99 ++-- docs/architecture/005_architecture_data.json | 188 ++++++-- .../audit/settings-profile-audit-report.md | 428 ++++++++++++++++++ src/app/(dashboard)/profile/error.tsx | 8 +- src/app/(dashboard)/profile/page.tsx | 261 +++-------- src/app/(dashboard)/settings/error.tsx | 8 +- src/app/(dashboard)/settings/page.tsx | 76 +++- .../(dashboard)/settings/security/error.tsx | 8 +- .../(dashboard)/settings/security/page.tsx | 18 +- .../components/admin-settings-view.tsx | 115 ++--- .../components/ai-provider-settings-card.tsx | 150 +++--- .../notification-preferences-form.tsx | 424 +++++++---------- .../components/parent-settings-view.tsx | 65 --- .../components/password-change-form.tsx | 49 +- .../components/profile-settings-form.tsx | 169 +++---- .../components/profile-student-overview.tsx | 93 ++++ .../components/profile-teacher-overview.tsx | 115 +++++ .../settings/components/quick-links-card.tsx | 46 ++ .../settings-section-error-boundary.tsx | 64 +++ .../components/settings-service-context.tsx | 39 ++ .../settings/components/settings-view.tsx | 134 ++++-- .../components/student-settings-view.tsx | 59 --- .../components/teacher-settings-view.tsx | 71 --- .../components/theme-preferences-card.tsx | 16 +- .../settings/config/role-settings-config.tsx | 84 ++++ .../settings/lib/student-overview-data.ts | 150 ++++++ src/modules/settings/types.ts | 42 ++ src/shared/i18n/messages/en/settings.json | 280 ++++++++++++ src/shared/i18n/messages/zh-CN/settings.json | 280 ++++++++++++ 29 files changed, 2445 insertions(+), 1094 deletions(-) create mode 100644 docs/architecture/audit/settings-profile-audit-report.md delete mode 100644 src/modules/settings/components/parent-settings-view.tsx create mode 100644 src/modules/settings/components/profile-student-overview.tsx create mode 100644 src/modules/settings/components/profile-teacher-overview.tsx create mode 100644 src/modules/settings/components/quick-links-card.tsx create mode 100644 src/modules/settings/components/settings-section-error-boundary.tsx create mode 100644 src/modules/settings/components/settings-service-context.tsx delete mode 100644 src/modules/settings/components/student-settings-view.tsx delete mode 100644 src/modules/settings/components/teacher-settings-view.tsx create mode 100644 src/modules/settings/config/role-settings-config.tsx create mode 100644 src/modules/settings/lib/student-overview-data.ts create mode 100644 src/shared/i18n/messages/en/settings.json create mode 100644 src/shared/i18n/messages/zh-CN/settings.json diff --git a/docs/architecture/004_architecture_impact_map.md b/docs/architecture/004_architecture_impact_map.md index b15ec8e..739e8d6 100644 --- a/docs/architecture/004_architecture_impact_map.md +++ b/docs/architecture/004_architecture_impact_map.md @@ -1327,17 +1327,19 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions" ## 2.23 settings(设置模块) -**职责**:系统设置(学校信息/安全策略/文件上传/通知配置)+ AI Provider 管理 + 密码修改 + 个人资料 + 主题偏好 + 通知偏好。 +**职责**:系统设置(学校信息/安全策略/文件上传/通知配置)+ AI Provider 管理 + 密码修改 + 个人资料 + 主题偏好 + 通知偏好 + 个人信息页(学生/教师概览)。 **导出函数**: - Actions:`getAiProvidersAction` / `createAiProviderAction` / `updateAiProviderAction` / `deleteAiProviderAction` / `testAiProviderAction` - Actions-password:`changePasswordAction`(✅ P1 已修复:使用 `requirePermission(USER_PROFILE_UPDATE)` + Zod 校验 + DB 操作下沉到 data-access) - Data-access:`getAiProviderSummaries` / `countDefaultAiProviders` / `getAiProviderForUpdate` / `updateAiProvider` / `createAiProvider` / `getUserPasswordHash` / `getPasswordSecurityByUserId` / `updateUserPassword` / `upsertPasswordSecurityOnPasswordChange`(P1 新增,从 actions 下沉) -- Components:`SettingsView`(P2-a 新增:统一设置页布局,消除 admin/teacher/student/parent 四个设置视图的重复布局;5 标签页 General/Notifications/Appearance/Security/AI,角色差异通过 `description` / `backHref` / `generalExtra` 三个 props 注入;Tab 通过 URL `?tab=` 参数持久化;AI 标签页条件渲染需 `AI_CONFIGURE` 权限;登出按钮使用 AlertDialog 二次确认;4 个消费方:admin/teacher/student/parent 设置页)、`ParentSettingsView`(家长设置视图,backHref 指向 `/parent/dashboard`,含家长专属快捷链接)、`AdminSettingsView`(系统设置视图,4 个 Card:学校信息/安全策略/文件上传/通知配置;消费方:`/admin/settings` 页面,权限 `SETTINGS_ADMIN`) -- Types:`AiProviderSummary` / `AiProviderName` / `AiProviderExisting`(P1 新增,从 actions.ts 迁出) +- 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`(系统设置视图,4 个 Card) +- Config:`ROLE_SETTINGS_CONFIG` / `resolveRoleSettingsConfig`(配置驱动角色 → 设置视图映射,新增角色只需添加条目) +- Lib:`buildStudentOverviewData` / `computeStudentStats` / `sortUpcomingAssignments` / `filterTodaySchedule` / `toWeekday`(纯数据计算函数,与 UI 分离,便于单元测试) +- Types:`AiProviderSummary` / `AiProviderName` / `AiProviderExisting` / `SettingsService` / `ProfileService` / `NotificationPreferenceService`(服务接口定义,用于依赖注入解耦) **依赖关系**: -- 依赖:`shared/*`(含 `shared/lib/bcrypt-utils`,✅ P2 已修复:`actions-password.ts` 删除本地 `normalizeBcryptHash`,统一复用 `shared/lib/bcrypt-utils.normalizeBcryptHash`)、`@/auth`、`messaging`(通知偏好表单调用 messaging Action) +- 依赖:`shared/*`(含 `shared/lib/bcrypt-utils`)、`@/auth`、`messaging`(页面层通过 `SettingsService` 接口注入,组件层不直接 import)、`users`(页面层通过 `SettingsService` 接口注入)、`classes` / `homework` / `dashboard`(ProfileStudentOverview 异步组件获取学生概览数据)、`notifications`(页面层获取通知偏好) - 被依赖:无 **已知问题**: @@ -1345,27 +1347,42 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions" - ✅ P1 已修复:~~无 `data-access.ts`,`actions.ts` 直接使用 `db`~~ 新建 `data-access.ts`,所有 DB 操作已下沉 - ✅ P1 已修复:~~`changePasswordAction` 使用 `requireAuth()` 无 Zod 校验~~ 改为 `requirePermission(USER_PROFILE_UPDATE)` + `ChangePasswordSchema` Zod 校验 + 并行查询优化 - ✅ P2 已修复:`actions-password.ts` 删除本地 `normalizeBcryptHash`,统一复用 `shared/lib/bcrypt-utils.normalizeBcryptHash`,消除重复代码 -- ✅ P2-a 已修复:~~admin/teacher/student 三个设置视图重复布局~~ 新增 `SettingsView` 统一设置页布局(5 标签页 + 角色差异通过 props 注入),4 个设置页改为消费 `SettingsView` -- ✅ parent 角色路由已修复:~~parent 用户被错误渲染为 TeacherSettingsView~~ 新增 `ParentSettingsView`,`/settings` 页面增加 parent 角色分支 +- ✅ P2-a 已修复:~~admin/teacher/student/parent 四个设置视图重复布局~~ 新增 `SettingsView` 统一设置页布局 + `resolveRoleSettingsConfig` 配置驱动角色路由,删除 `parent-settings-view.tsx` / `student-settings-view.tsx` / `teacher-settings-view.tsx` +- ✅ parent 角色路由已修复:通过 `ROLE_SETTINGS_CONFIG` 配置驱动,parent 用户正确渲染对应配置 - ✅ Tab URL 持久化已修复:`SettingsView` 改为受控模式,通过 `useSearchParams` 读取 `tab` 参数,`router.push` 更新 URL - ✅ 登出二次确认已修复:`SettingsView` 的 Log out 按钮使用 `AlertDialog` 包裹,点击时弹出确认对话框 - ✅ AiProviderSettingsCard 已集成:`SettingsView` 新增 AI 标签页,条件渲染需 `AI_CONFIGURE` 权限 - ✅ password-change-form 任意值 Tailwind 类已修复:~~`[&>div]:bg-red-500` 等任意值类~~ Progress 组件新增 `indicatorClassName` prop,使用标准颜色类 -- ⚠️ P2:`notification-preferences-form.tsx` 跨模块 UI 依赖 +- ✅ P0 已修复:~~`notification-preferences-form.tsx` 跨模块直接 import messaging/actions~~ 改为通过 `useSettingsService().notifications.updatePreferences` 调用,页面层注入实现 +- ✅ P0 已修复:~~`profile-settings-form.tsx` 跨模块直接 import users/actions~~ 改为通过 `useSettingsService().profile.updateProfile` 调用,页面层注入实现 +- ✅ P0 已修复:~~i18n 完全缺失~~ 新增 `settings.json` 翻译文件(zh-CN + en),所有组件改用 `useTranslations` / `getTranslations` +- ✅ P1 已修复:~~缺少 Error Boundary~~ 新增 `SettingsSectionErrorBoundary`,每个 TabsContent + profile 页面角色概览区块均包裹 +- ✅ P1 已修复:~~缺少 Suspense 骨架屏~~ 每个 TabsContent 包裹 `Suspense` + `SettingsSectionSkeleton`;profile 页面包裹 `ProfileStudentOverviewSkeleton` / `ProfileTeacherOverviewSkeleton` +- ✅ P1 已修复:~~profile/page.tsx 业务逻辑与 UI 混合~~ 抽取 `buildStudentOverviewData` 等纯函数到 `lib/student-overview-data.ts`;拆分 `ProfileStudentOverview` / `ProfileTeacherOverview` 异步组件 - ✅ 密码修改有速率限制 - ✅ AI Provider 操作有 `AI_CONFIGURE` 权限校验 **文件清单**: | 文件 | 行数 | 职责 | |------|------|------| -| `actions.ts` | 178 | AI Provider CRUD + 测试(P1 已修复,无直接 DB 操作) | -| `actions-password.ts` | 107 | 修改密码(P1 已修复:requirePermission + Zod + data-access) | -| `data-access.ts` | 175 | AI Provider CRUD + 密码修改 DB 操作(P1 新增) | -| `types.ts` | 16 | 类型定义(P1 新增,AiProviderSummary 等) | -| `components/settings-view.tsx` | 196 | SettingsView 统一设置页布局(P2-a 新增,5 标签页 + props 注入角色差异 + Tab URL 持久化 + 登出二次确认 + AI 标签页) | -| `components/admin-settings-view.tsx` | 195 | AdminSettingsView 系统设置视图(4 个 Card:学校信息/安全策略/文件上传/通知配置,模拟保存) | -| `components/parent-settings-view.tsx` | 70 | ParentSettingsView 家长设置视图(新增,复用 SettingsView 布局) | -| `components/*` | 9 文件 | 通用设置 + AI 配置 + 密码 + 主题 + 通知偏好 + 4 角色设置视图 | +| `actions.ts` | 160 | AI Provider CRUD + 测试(P1 已修复,无直接 DB 操作) | +| `actions-password.ts` | 87 | 修改密码(P1 已修复:requirePermission + Zod + data-access) | +| `data-access.ts` | 158 | AI Provider CRUD + 密码修改 DB 操作(P1 新增) | +| `types.ts` | 60 | 类型定义(AiProviderSummary + SettingsService/ProfileService/NotificationPreferenceService 接口) | +| `config/role-settings-config.tsx` | 85 | 角色设置页配置驱动映射(ROLE_SETTINGS_CONFIG + resolveRoleSettingsConfig) | +| `lib/student-overview-data.ts` | 150 | 学生概览纯数据计算(buildStudentOverviewData + computeStudentStats 等,便于单测) | +| `components/settings-view.tsx` | 236 | SettingsView 统一设置页布局(5 标签页 + Error Boundary + Suspense + i18n) | +| `components/settings-service-context.tsx` | 39 | SettingsServiceProvider + useSettingsService(Context 注入服务接口) | +| `components/settings-section-error-boundary.tsx` | 64 | 分区 Error Boundary(局部失败不影响整页) | +| `components/quick-links-card.tsx` | 42 | 快捷链接卡片(i18n 键驱动) | +| `components/profile-student-overview.tsx` | 91 | 学生概览异步 Server Component + 骨架屏 | +| `components/profile-teacher-overview.tsx` | 115 | 教师概览异步 Server Component + 骨架屏 | +| `components/admin-settings-view.tsx` | 195 | AdminSettingsView 系统设置视图(4 个 Card,i18n) | +| `components/profile-settings-form.tsx` | 158 | 个人资料表单(通过 SettingsService 注入,i18n) | +| `components/notification-preferences-form.tsx` | ~140 | 通知偏好表单(通过 SettingsService 注入,i18n) | +| `components/password-change-form.tsx` | ~130 | 密码修改表单(i18n + a11y) | +| `components/theme-preferences-card.tsx` | ~60 | 主题偏好卡片(i18n) | +| `components/ai-provider-settings-card.tsx` | ~200 | AI 服务商配置卡片(i18n) | --- @@ -1488,13 +1505,25 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions" - ✅ 编辑器架构升级:NodeEditor(React Flow 画布)+ NodeEditPanel(侧边内容编辑面板)+ LessonNode(自定义节点组件),支持节点拖拽、连线、画布缩放 - ⚠️ `block-renderer.tsx` 标记为 @deprecated(已被 NodeEditor 替代,保留用于向后兼容) +> 架构变更(2026-06-22,本次审计修复): +> - **P0-1 跨模块直查修复**:`publish-service.ts` 不再直接 `db.insert(examQuestions)` 和本地实现 `getStudentIdsByClassIds`,改为调用 `exams/data-access.addExamQuestions` 和 `classes/data-access.getStudentIdsByClassIds`,恢复三层架构约束 +> - **P0-2 i18n 接入**:新增 `shared/i18n/messages/zh-CN/lesson-preparation.json` 和 `shared/i18n/messages/en/lesson-preparation.json`,注册 `lessonPreparation` 命名空间到 `src/i18n/request.ts`,17 个组件改造为 `useTranslations`/`getTranslations` +> - **P1 纯函数抽取**:新增 `lib/document-migration.ts`(migrateV1ToV2/normalizeDocument/buildInitialContent,使用类型守卫替代 as 断言)、`lib/node-summary.ts`(getNodeSummary + NODE_COLORS + getNodeColor,接受翻译函数注入)、`lib/rf-mappers.ts`(toRfNodes/toRfEdges/fromRfEdges),data-access.ts 改为从 lib/ 导入并 re-export 保持向后兼容 +> - **P1 错误边界 + 骨架屏**:新增 `components/lesson-plan-error-boundary.tsx`(LessonPlanErrorBoundary 类组件)和 `components/lesson-plan-skeleton.tsx`(VersionListSkeleton/QuestionBankSkeleton/KnowledgePointSkeleton/LessonPlanListSkeleton) +> - **P1 Block 注册表**:新增 `config/block-registry.tsx`(BLOCK_REGISTRY 配置表 + getBlockComponent/isRichTextBlock),`node-edit-panel.tsx` 重构为配置驱动渲染,移除 if/else 链 +> - **P1 window.location.reload 修复**:`exercise-block.tsx` 改用 `router.refresh()` 精确刷新缓存 + **文件清单**: | 文件 | 职责 | |------|------| | `types.ts` | 类型定义(含 v1/v2 文档类型、LessonPlanNode、LessonPlanEdge) | | `constants.ts` | 常量定义 | | `schema.ts` | Zod 验证 | -| `data-access.ts` | 课案 CRUD + 模板查询 + 初始内容构建 + v1→v2 迁移(migrateV1ToV2 / normalizeDocument) | +| `lib/document-migration.ts` | **纯函数**:v1→v2 迁移(migrateV1ToV2)/ 规范化(normalizeDocument)/ 初始内容构建(buildInitialContent),使用类型守卫 isV1Document/isV2Document 替代 as 断言 | +| `lib/node-summary.ts` | **纯函数**:getNodeSummary(接受翻译函数注入,支持 i18n)+ NODE_COLORS + getNodeColor | +| `lib/rf-mappers.ts` | **纯函数**:toRfNodes/toRfEdges/fromRfEdges(LessonPlanNode/Edge ↔ React Flow Node/Edge 映射) | +| `config/block-registry.tsx` | **配置驱动**:BLOCK_REGISTRY 注册表 + getBlockComponent/isRichTextBlock,node-edit-panel 通过配置渲染 Block | +| `data-access.ts` | 课案 CRUD + 模板查询(migrateV1ToV2/normalizeDocument/buildInitialContent 从 lib/ 导入并 re-export 保持向后兼容) | | `data-access-versions.ts` | 版本管理(创建/查询/回滚/清理) | | `data-access-templates.ts` | 个人模板 CRUD | | `data-access-knowledge.ts` | 按知识点/题目反查课案 | @@ -1502,28 +1531,30 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions" | `actions-publish.ts` | 发布作业 Server Action | | `actions-ai.ts` | AI 知识点建议 Server Action | | `actions-kp.ts` | 知识点选项 Server Action | -| `publish-service.ts` | 发布作业服务(编排 homework/exams/classes) | +| `publish-service.ts` | 发布作业服务(编排 homework/exams/classes,通过对方 data-access 调用,无直查跨模块表) | | `ai-suggest.ts` | AI 知识点建议服务 | | `seed-templates.ts` | 模板种子数据 | | `hooks/use-lesson-plan-editor.ts` | 课案编辑器 Hook(基于 zustand,支持 nodes/edges 操作:addNode/updateNode/updateNodePosition/removeNode/connect/disconnect/setEdges/selectNode) | -| `components/lesson-plan-list.tsx` | 课案列表 | -| `components/lesson-plan-card.tsx` | 课案卡片 | -| `components/lesson-plan-filters.tsx` | 课案筛选器 | -| `components/lesson-plan-editor.tsx` | 课案编辑器(编排 NodeEditor + NodeEditPanel) | -| `components/node-editor.tsx` | **节点图画布**(React Flow,自定义 LessonNode,支持拖拽/连线/缩放) | -| `components/node-edit-panel.tsx` | **侧边内容编辑面板**(选中节点后编辑标题/数据) | -| `components/nodes/lesson-node.tsx` | **自定义节点组件**(按 BlockType 显示图标/颜色,含 Handle 连接点) | +| `components/lesson-plan-list.tsx` | 课案列表(i18n 已接入) | +| `components/lesson-plan-card.tsx` | 课案卡片(i18n 已接入) | +| `components/lesson-plan-filters.tsx` | 课案筛选器(i18n 已接入) | +| `components/lesson-plan-editor.tsx` | 课案编辑器(编排 NodeEditor + NodeEditPanel,i18n 已接入) | +| `components/node-editor.tsx` | **节点图画布**(React Flow,使用 lib/rf-mappers + lib/node-summary 纯函数,i18n 已接入) | +| `components/node-edit-panel.tsx` | **侧边内容编辑面板**(配置驱动渲染 Block,通过 getBlockComponent + LessonPlanErrorBoundary 包裹,i18n 已接入) | +| `components/nodes/lesson-node.tsx` | **自定义节点组件**(使用 lib/node-summary 的 getNodeSummary/getNodeColor,i18n 已接入) | +| `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` | 模板选择器 | -| `components/version-history-drawer.tsx` | 版本历史抽屉 | -| `components/knowledge-point-picker.tsx` | 知识点选择器 | -| `components/question-bank-picker.tsx` | 题库选择器 | -| `components/inline-question-editor.tsx` | 内联题目编辑器 | -| `components/publish-homework-dialog.tsx` | 发布作业对话框 | -| `components/blocks/rich-text-block.tsx` | 富文本 Block(被 NodeEditPanel 复用) | -| `components/blocks/text-study-block.tsx` | 课文研读 Block(被 NodeEditPanel 复用) | -| `components/blocks/exercise-block.tsx` | 练习 Block(被 NodeEditPanel 复用) | -| `components/blocks/reflection-block.tsx` | 反思 Block(被 NodeEditPanel 复用) | +| `components/template-picker.tsx` | 模板选择器(i18n 已接入) | +| `components/version-history-drawer.tsx` | 版本历史抽屉(i18n 已接入) | +| `components/knowledge-point-picker.tsx` | 知识点选择器(i18n 已接入) | +| `components/question-bank-picker.tsx` | 题库选择器(i18n 已接入) | +| `components/inline-question-editor.tsx` | 内联题目编辑器(i18n 已接入) | +| `components/publish-homework-dialog.tsx` | 发布作业对话框(i18n 已接入) | +| `components/blocks/rich-text-block.tsx` | 富文本 Block(被 NodeEditPanel 复用,i18n 已接入) | +| `components/blocks/text-study-block.tsx` | 课文研读 Block(被 NodeEditPanel 复用,i18n 已接入) | +| `components/blocks/exercise-block.tsx` | 练习 Block(被 NodeEditPanel 复用,使用 router.refresh 替代 window.location.reload,i18n 已接入) | +| `components/blocks/reflection-block.tsx` | 反思 Block(被 NodeEditPanel 复用,i18n 已接入) | --- diff --git a/docs/architecture/005_architecture_data.json b/docs/architecture/005_architecture_data.json index b39eb08..99f3daa 100644 --- a/docs/architecture/005_architecture_data.json +++ b/docs/architecture/005_architecture_data.json @@ -2735,6 +2735,14 @@ "createExamAction" ] }, + { + "name": "addExamQuestions", + "signature": "addExamQuestions(examId: string, items: Array<{ questionId: string; score: number; order: number }>): Promise", + "purpose": "批量插入考试-题目关联(跨模块写接口,供 lesson-preparation/publish-service 调用,避免直查 examQuestions 表)", + "usedBy": [ + "lesson-preparation/publish-service.publishLessonPlanHomework" + ] + }, { "name": "persistAiGeneratedExamDraft", "signature": "persistAiGeneratedExamDraft(input: { examId, title, creatorId, subjectId, gradeId, scheduledAt?, description, structure, generated }): Promise", @@ -6258,56 +6266,125 @@ "usedBy": [ "data-access.getAiProviderForUpdate" ] + }, + { + "name": "SettingsService", + "file": "types.ts", + "type": "interface", + "definition": "设置模块统一服务接口(profile + notifications + trackEvent),通过 React Context 注入实现解耦", + "usedBy": [ + "components/settings-service-context.tsx", + "app/(dashboard)/settings/page.tsx" + ] + }, + { + "name": "ProfileService", + "file": "types.ts", + "type": "interface", + "definition": "个人资料服务接口(getProfile + updateProfile),解耦组件对 users/actions 的直接依赖", + "usedBy": [ + "types.SettingsService", + "components/profile-settings-form.tsx" + ] + }, + { + "name": "NotificationPreferenceService", + "file": "types.ts", + "type": "interface", + "definition": "通知偏好服务接口(getPreferences + updatePreferences),解耦组件对 messaging/actions 的直接依赖", + "usedBy": [ + "types.SettingsService", + "components/notification-preferences-form.tsx" + ] } ], "components": [ { "name": "AiProviderSettingsCard", - "purpose": "AI Provider设置卡片" + "purpose": "AI Provider设置卡片(i18n:settings.ai.providers)" }, { "name": "AdminSettingsView", - "purpose": "系统设置视图(学校信息/安全策略/文件上传/通知配置 4 个 Card,模拟保存;消费方:/admin/settings 页面)", + "purpose": "系统设置视图(学校信息/安全策略/文件上传/通知配置 4 个 Card,i18n;消费方:/admin/settings 页面)", "usedBy": [ "app/(dashboard)/admin/settings/page.tsx" ] }, { "name": "ProfileSettingsForm", - "purpose": "个人资料设置表单" + "purpose": "个人资料设置表单(通过 useSettingsService().profile.updateProfile 调用,i18n:settings.profile)", + "deps": [ + "useSettingsService", + "shared/components/form-fields/text-field", + "shared/components/form-fields/select-field" + ] }, { "name": "ThemePreferencesCard", - "purpose": "主题偏好卡片" + "purpose": "主题偏好卡片(i18n:settings.appearance)" }, { - "name": "StudentSettingsView", - "purpose": "学生设置视图(含 Notifications tab)" - }, - { - "name": "TeacherSettingsView", - "purpose": "教师设置视图(含 Notifications tab)" - }, - { - "name": "ParentSettingsView", - "purpose": "家长设置视图(复用 SettingsView 布局,backHref 指向 /parent/dashboard,含家长专属快捷链接;消费方:/settings 页面 parent 角色分支)", + "name": "SettingsView", + "purpose": "统一设置页布局(5 标签页:General/Notifications/Appearance/Security/AI;角色差异通过 resolveRoleSettingsConfig 配置驱动 + generalExtra props 注入;Tab URL 持久化;每个 TabsContent 包裹 SettingsSectionErrorBoundary + Suspense 骨架屏;AI 标签页条件渲染需 AI_CONFIGURE 权限;登出 AlertDialog 二次确认;i18n:settings 命名空间)", "usedBy": [ "app/(dashboard)/settings/page.tsx" ] }, { - "name": "SettingsView", - "purpose": "统一设置页布局(5 标签页:General/Notifications/Appearance/Security/AI,角色差异通过 description/backHref/generalExtra 三个 props 注入;Tab 通过 URL ?tab= 参数持久化;AI 标签页条件渲染需 AI_CONFIGURE 权限;登出按钮使用 AlertDialog 二次确认;4 个消费方:admin/teacher/student/parent 设置页)", + "name": "SettingsServiceProvider", + "file": "components/settings-service-context.tsx", + "purpose": "SettingsService React Context Provider,页面层注入服务实现,组件层通过 useSettingsService() 消费", "usedBy": [ - "AdminSettingsView", - "TeacherSettingsView", - "StudentSettingsView", - "ParentSettingsView" + "app/(dashboard)/settings/page.tsx" + ] + }, + { + "name": "SettingsSectionErrorBoundary", + "file": "components/settings-section-error-boundary.tsx", + "purpose": "分区 Error Boundary,包裹每个 TabsContent 和 profile 角色概览区块,局部失败不影响整页", + "usedBy": [ + "components/settings-view.tsx", + "app/(dashboard)/profile/page.tsx" + ] + }, + { + "name": "QuickLinksCard", + "file": "components/quick-links-card.tsx", + "purpose": "快捷链接卡片(客户端组件,i18n 键驱动:settings.quickLinks)", + "usedBy": [ + "config/role-settings-config.tsx" + ] + }, + { + "name": "ProfileStudentOverview", + "file": "components/profile-student-overview.tsx", + "purpose": "学生概览异步 Server Component,独立获取学生数据并渲染(StatsGrid + UpcomingAssignments + Grades + TodaySchedule),可被 Suspense + ErrorBoundary 包裹实现流式渲染", + "deps": [ + "classes/data-access.getStudentClasses", + "classes/data-access.getStudentSchedule", + "homework/data-access.getStudentHomeworkAssignments", + "homework/data-access.getStudentDashboardGrades", + "lib/student-overview-data.buildStudentOverviewData" + ], + "usedBy": [ + "app/(dashboard)/profile/page.tsx" + ] + }, + { + "name": "ProfileTeacherOverview", + "file": "components/profile-teacher-overview.tsx", + "purpose": "教师概览异步 Server Component,独立获取教师数据并渲染(任教科目 + 任教班级),可被 Suspense + ErrorBoundary 包裹", + "deps": [ + "classes/data-access.getTeacherClasses", + "classes/data-access.getTeacherTeachingSubjects" + ], + "usedBy": [ + "app/(dashboard)/profile/page.tsx" ] }, { "name": "PasswordChangeForm", - "purpose": "密码修改表单(当前密码/新密码/确认密码 + 强度指示器 + 需求提示)", + "purpose": "密码修改表单(当前密码/新密码/确认密码 + 强度指示器 + 需求提示;i18n:settings.security;a11y:aria-label)", "deps": [ "changePasswordAction", "getPasswordStrength", @@ -6317,17 +6394,14 @@ { "name": "NotificationPreferencesForm", "file": "components/notification-preferences-form.tsx", - "purpose": "通知偏好设置表单(Switch 切换 email/sms/push 通道 + 5 个分类开关:作业/成绩/公告/消息/考勤;隐藏 checkbox 与 Switch 同步,useActionState 调用 updateNotificationPreferencesAction)", + "purpose": "通知偏好设置表单(Switch 切换 email/sms/push 通道 + 5 个分类开关;通过 useSettingsService().notifications.updatePreferences 调用,i18n:settings.notifications)", "deps": [ - "updateNotificationPreferencesAction", + "useSettingsService", "shared/components/ui/switch", - "shared/components/ui/card", - "react.useActionState" + "shared/components/ui/card" ], "usedBy": [ - "TeacherSettingsView", - "StudentSettingsView", - "ParentSettingsView" + "SettingsView" ] } ] @@ -12083,17 +12157,17 @@ }, { "name": "buildInitialContent", - "file": "data-access.ts", + "file": "lib/document-migration.ts(data-access.ts re-export)", "purpose": "基于模板构建初始课案内容(v2 nodes+edges)" }, { "name": "migrateV1ToV2", - "file": "data-access.ts", - "purpose": "v1→v2 迁移:将旧 blocks 数组转换为 nodes + 线性 edges(节点按网格布局)" + "file": "lib/document-migration.ts(data-access.ts re-export)", + "purpose": "v1→v2 迁移:将旧 blocks 数组转换为 nodes + 线性 edges(节点按网格布局),使用类型守卫 isV1Document/isV2Document 替代 as 断言" }, { "name": "normalizeDocument", - "file": "data-access.ts", + "file": "lib/document-migration.ts(data-access.ts re-export)", "purpose": "规范化:确保 content 为 v2 格式,兼容旧 v1 数据(自动调用 migrateV1ToV2)" }, { @@ -12149,7 +12223,7 @@ { "name": "publishLessonPlanHomework", "file": "publish-service.ts", - "purpose": "发布课案为作业(编排 homework/exams/classes)" + "purpose": "发布课案为作业(编排 homework/exams/classes,通过对方 data-access 调用 addExamQuestions/getStudentIdsByClassIds,无直查跨模块表)" }, { "name": "suggestKnowledgePoints", @@ -12264,6 +12338,10 @@ "types.ts", "constants.ts", "schema.ts", + "lib/document-migration.ts", + "lib/node-summary.ts", + "lib/rf-mappers.ts", + "config/block-registry.tsx", "data-access.ts", "data-access-versions.ts", "data-access-templates.ts", @@ -12283,6 +12361,8 @@ "components/node-editor.tsx", "components/node-edit-panel.tsx", "components/nodes/lesson-node.tsx", + "components/lesson-plan-error-boundary.tsx", + "components/lesson-plan-skeleton.tsx", "components/block-renderer.tsx", "components/template-picker.tsx", "components/version-history-drawer.tsx", @@ -13027,7 +13107,11 @@ "dependsOn": [ "shared", "auth", - "messaging" + "classes", + "homework", + "dashboard", + "users", + "notifications" ], "uses": { "shared": [ @@ -13035,17 +13119,41 @@ "auth-guard", "ai", "types", - "components.ui.switch" + "components.ui.switch", + "components.ui.card", + "components.ui.tabs", + "components.ui.alert-dialog", + "components.form-fields" ], "auth": [ "auth" ], - "messaging": [ - "notification-preferences.getNotificationPreferences", - "actions.getNotificationPreferencesAction", - "actions.updateNotificationPreferencesAction" + "classes": [ + "data-access.getStudentClasses", + "data-access.getStudentSchedule", + "data-access.getTeacherClasses", + "data-access.getTeacherTeachingSubjects" + ], + "homework": [ + "data-access.getStudentHomeworkAssignments", + "data-access.getStudentDashboardGrades" + ], + "dashboard": [ + "components.student-dashboard.student-grades-card", + "components.student-dashboard.student-stats-grid", + "components.student-dashboard.student-today-schedule-card", + "components.student-dashboard.student-upcoming-assignments-card" + ], + "users": [ + "data-access.UserProfile", + "data-access.UpdateUserProfileInput" + ], + "notifications": [ + "types.NotificationPreferences", + "types.UpdateNotificationPreferencesInput" ] - } + }, + "note": "组件层通过 SettingsService 接口注入解耦,不直接 import messaging/actions;页面层 app/(dashboard)/settings/page.tsx 负责注入 users/actions + messaging/actions 实现" }, "users": { "dependsOn": [ diff --git a/docs/architecture/audit/settings-profile-audit-report.md b/docs/architecture/audit/settings-profile-audit-report.md new file mode 100644 index 0000000..a316d0f --- /dev/null +++ b/docs/architecture/audit/settings-profile-audit-report.md @@ -0,0 +1,428 @@ +# 设置和个人信息模块审计报告 + +> 审查日期:2026-06-22 +> 审查范围:`src/modules/settings/**`、`src/app/(dashboard)/settings/**`、`src/app/(dashboard)/admin/settings/**`、`src/app/(dashboard)/profile/**` +> 架构图参考:`docs/architecture/004_architecture_impact_map.md` §2.23、`docs/architecture/005_architecture_data.json` + +--- + +## 一、现有实现概要 + +### 1.1 文件分布 + +| 层 | 路径 | 文件数 | 说明 | +|----|------|--------|------| +| 路由层 - 通用设置 | `src/app/(dashboard)/settings/` | 1 个 `page.tsx` + `error.tsx` + `loading.tsx` | 角色分发到 4 个 SettingsView | +| 路由层 - 管理员系统设置 | `src/app/(dashboard)/admin/settings/` | 1 个 `page.tsx` | 仅 admin 可访问,渲染 `AdminSettingsView` | +| 路由层 - 安全设置 | `src/app/(dashboard)/settings/security/` | 1 个 `page.tsx` + `error.tsx` + `loading.tsx` | 独立密码修改页 | +| 路由层 - 个人资料 | `src/app/(dashboard)/profile/` | 1 个 `page.tsx` + `error.tsx` + `loading.tsx` | 个人资料展示页(317 行) | +| 模块层 - actions | `src/modules/settings/actions.ts`(160 行) | AI Provider CRUD + test | ✅ 使用 `requirePermission(AI_CONFIGURE)` | +| 模块层 - actions-password | `src/modules/settings/actions-password.ts`(87 行) | 修改密码 | ✅ 使用 `requirePermission(USER_PROFILE_UPDATE)` + Zod + 限流 | +| 模块层 - data-access | `src/modules/settings/data-access.ts`(158 行) | AI Provider + 密码 DB 操作 | ✅ `server-only` | +| 模块层 - types | `src/modules/settings/types.ts`(16 行) | AI Provider 类型 | | +| 模块层 - 组件 | `src/modules/settings/components/` | 10 个组件 | 见下表 | +| i18n | **缺失** | 0 | 无 `settings.json` / `profile.json` 翻译文件 | + +**组件清单**: + +| 组件 | 行数 | 职责 | +|------|------|------| +| [settings-view.tsx](file:///e:/Desktop/CICD/src/modules/settings/components/settings-view.tsx) | 179 | 统一设置页布局(5 标签页 + 角色差异 props 注入 + Tab URL 持久化) | +| [admin-settings-view.tsx](file:///e:/Desktop/CICD/src/modules/settings/components/admin-settings-view.tsx) | 185 | **mock 实现**:4 个 Card(学校信息/安全策略/文件上传/通知配置),`setTimeout` 模拟保存 | +| [ai-provider-settings-card.tsx](file:///e:/Desktop/CICD/src/modules/settings/components/ai-provider-settings-card.tsx) | 357 | AI Provider 管理(选择/新建/测试/保存) | +| [notification-preferences-form.tsx](file:///e:/Desktop/CICD/src/modules/settings/components/notification-preferences-form.tsx) | 326 | 通知偏好(渠道/类别/免打扰时段) | +| [password-change-form.tsx](file:///e:/Desktop/CICD/src/modules/settings/components/password-change-form.tsx) | 169 | 修改密码(强度指示器 + 显示切换) | +| [profile-settings-form.tsx](file:///e:/Desktop/CICD/src/modules/settings/components/profile-settings-form.tsx) | 146 | 个人资料编辑表单 | +| [theme-preferences-card.tsx](file:///e:/Desktop/CICD/src/modules/settings/components/theme-preferences-card.tsx) | 55 | 主题切换(system/light/dark) | +| [parent-settings-view.tsx](file:///e:/Desktop/CICD/src/modules/settings/components/parent-settings-view.tsx) | 60 | 家长设置视图(复用 SettingsView + 快捷链接) | +| [teacher-settings-view.tsx](file:///e:/Desktop/CICD/src/modules/settings/components/teacher-settings-view.tsx) | 66 | 教师设置视图(同上) | +| [student-settings-view.tsx](file:///e:/Desktop/CICD/src/modules/settings/components/student-settings-view.tsx) | 54 | 学生设置视图(同上) | + +### 1.2 数据流 + +``` +[Route] /settings/page.tsx + ├─▶ users/data-access.getUserProfile (跨模块 data-access,类型导入) + ├─▶ notifications/preferences.getNotificationPreferences (跨模块 data-access) + └─▶ 按 roles.includes("admin"|"student"|"parent") 分发 + ├─ admin → SettingsView(无 generalExtra) + ├─ student → StudentSettingsView → SettingsView + ├─ parent → ParentSettingsView → SettingsView + └─ teacher → TeacherSettingsView → SettingsView + +[Route] /admin/settings/page.tsx + └─▶ AdminSettingsView(mock,无数据流) + +[Route] /settings/security/page.tsx + └─▶ PasswordChangeForm → settings/actions-password.changePasswordAction + +[Route] /profile/page.tsx + ├─▶ users/data-access.getUserProfile + ├─▶ classes/data-access.getStudentClasses / getStudentSchedule (学生分支) + ├─▶ homework/data-access.getStudentHomeworkAssignments / getStudentDashboardGrades (学生分支) + ├─▶ classes/data-access.getTeacherClasses / getTeacherTeachingSubjects (教师分支) + └─▶ 页面层内联 80+ 行业务计算(weekday 转换、作业状态统计、排序切片) + +[Component] ProfileSettingsForm + └─▶ users/actions.updateUserProfile ❌ 跨模块 action 直调 + +[Component] NotificationPreferencesForm + └─▶ messaging/actions.updateNotificationPreferencesAction ❌ 跨模块 action 直调 + +[Component] AiProviderSettingsCard + └─▶ settings/actions.getAiProviderSummaries / upsertAiProviderAction / testAiProviderAction ✅ 模块内 +``` + +### 1.3 架构图记录情况 + +`004_architecture_impact_map.md` §2.23 记录了 settings 模块的基本结构,但存在以下遗漏和不一致: + +- **未记录 `profile/page.tsx` 的数据流**:profile 页面编排了 users/classes/homework 三个模块的 data-access,但架构图未记录 +- **未记录跨模块 action 直调问题**:`profile-settings-form.tsx` 直调 `users/actions.updateUserProfile`、`notification-preferences-form.tsx` 直调 `messaging/actions.updateNotificationPreferencesAction`,架构图标注为"已修复"但实际仍存在 +- **未记录 `AdminSettingsView` 是 mock 实现**:架构图描述其有 4 个 Card 但未说明无真实数据持久化 +- **未记录 i18n 缺失**:架构图未标注 settings 模块所有文本均为硬编码 +- **通知偏好归属不一致**:架构图 §2.17 称通知偏好已迁移至 notifications 模块,但 `notification-preferences-form.tsx` 仍从 `messaging/actions` 导入 action + +--- + +## 二、现存问题与原因分析 + +### 2.1 国际化完全缺失(P0) + +| 位置 | 问题 | 违反规则 | +|------|------|----------| +| [settings-view.tsx](file:///e:/Desktop/CICD/src/modules/settings/components/settings-view.tsx) L96-104 | "Settings"、"Back to dashboard" 等硬编码英文 | "所有用户可见文本必须适配 i18n(使用 next-intl),提取翻译键" | +| [admin-settings-view.tsx](file:///e:/Desktop/CICD/src/modules/settings/components/admin-settings-view.tsx) 全文 | "系统设置"、"学校信息"、"安全策略" 等硬编码中文 | 同上 | +| [profile-settings-form.tsx](file:///e:/Desktop/CICD/src/modules/settings/components/profile-settings-form.tsx) L80-82 | "Profile Information"、"Update your personal information." 硬编码 | 同上 | +| [notification-preferences-form.tsx](file:///e:/Desktop/CICD/src/modules/settings/components/notification-preferences-form.tsx) L47-99 | CHANNELS/CATEGORIES 数组中 label/description 全部硬编码 | 同上 | +| [password-change-form.tsx](file:///e:/Desktop/CICD/src/modules/settings/components/password-change-form.tsx) L72-76 | "Change Password"、"Choose a strong password..." 硬编码 | 同上 | +| [theme-preferences-card.tsx](file:///e:/Desktop/CICD/src/modules/settings/components/theme-preferences-card.tsx) L24-28 | "Theme"、"Choose how the admin console looks..." 硬编码(且写死 "admin console") | 同上 | +| [ai-provider-settings-card.tsx](file:///e:/Desktop/CICD/src/modules/settings/components/ai-provider-settings-card.tsx) L251-257 | "AI Providers"、"Manage AI vendors..." 硬编码 | 同上 | +| [profile/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/profile/page.tsx) 全文 | "Profile"、"Personal Information"、"Account Information" 等硬编码 | 同上 | +| [settings/loading.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/settings/loading.tsx) 等错误页 | "页面加载失败" 中文硬编码,与英文页面不统一 | 同上 | +| `src/shared/i18n/messages/{zh-CN,en}/` | **无 settings.json / profile.json** | i18n 命名空间缺失 | +| `src/i18n/request.ts` | 未加载 settings/profile 命名空间 | 同上 | + +**原因**:settings 模块在历次重构中未纳入 i18n 改造范围,`i18n/request.ts` 只加载 6 个命名空间(common/auth/onboarding/classes/errors/dashboard)。 + +**后果**:无法支持中英文切换;admin 端中文、其他端英文,体验割裂;新增语言需逐文件修改。 + +### 2.2 跨模块 Action 直调,违反解耦原则(P0) + +| 位置 | 问题 | 违反规则 | +|------|------|----------| +| [profile-settings-form.tsx](file:///e:/Desktop/CICD/src/modules/settings/components/profile-settings-form.tsx) L16 | `import { updateUserProfile } from "@/modules/users/actions"` | "模块内部组件绝不直接 import 其他业务模块的 actions 或 data-access(只能通过注入的接口调用)" | +| [notification-preferences-form.tsx](file:///e:/Desktop/CICD/src/modules/settings/components/notification-preferences-form.tsx) L16 | `import { updateNotificationPreferencesAction } from "@/modules/messaging/actions"` | 同上 | +| [settings-view.tsx](file:///e:/Desktop/CICD/src/modules/settings/components/settings-view.tsx) L28 | `import { UserProfile } from "@/modules/users/data-access"` | 类型导入,语法允许但耦合类型定义 | +| [settings-view.tsx](file:///e:/Desktop/CICD/src/modules/settings/components/settings-view.tsx) L29 | `import type { NotificationPreferences } from "@/modules/notifications/types"` | 类型导入,可接受 | + +**原因**:settings 组件直接消费 users/messaging 模块的 Server Action,未通过接口抽象 + Context 注入。 + +**后果**:settings 模块无法独立测试(mock users/messaging action 困难);users/messaging action 签名变更会直接破坏 settings 组件;无法在不修改 settings 组件的前提下替换数据源。 + +### 2.3 AdminSettingsView 是 mock 实现(P0) + +| 位置 | 问题 | 违反规则 | +|------|------|----------| +| [admin-settings-view.tsx](file:///e:/Desktop/CICD/src/modules/settings/components/admin-settings-view.tsx) L20-25 | `await new Promise((r) => setTimeout(r, 800))` 模拟保存,无 Server Action 调用 | "app/ 只能调用 modules/ 的 Server Actions 和 data-access,不直接访问数据库" — 这里连 action 都没调 | +| 同文件 L23 | `toast.success("设置已保存")` 撒谎,实际未保存 | 用户体验问题 | +| 同文件全文 | 4 个 Card(学校信息/安全策略/文件上传/通知配置)的输入框无 `name` 属性、无表单提交逻辑 | 表单不可用 | +| [admin/settings/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/settings/page.tsx) | 与 `/settings` 页面割裂,admin 用户有两个设置入口 | 信息架构混乱 | + +**原因**:初版占位实现,后续未接入真实数据层。 + +**后果**:admin 调整的安全策略/文件上传限制/通知配置均不生效;与 `/settings` 页面功能重叠但行为不一致。 + +### 2.4 角色路由硬编码,非配置驱动(P1) + +| 位置 | 问题 | 违反规则 | +|------|------|----------| +| [settings/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/settings/page.tsx) L28-44 | `if (roles.includes("admin")) ... if (roles.includes("student")) ...` 4 分支硬编码 | "采用配置驱动设计,例如通过角色配置决定该模块渲染哪些 Widget/子模块" | +| [profile/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/profile/page.tsx) L48-49 | `const isStudent = roles.includes("student")` | 同上 | + +**原因**:角色分发逻辑写在页面层,未抽取为配置。 + +**后果**:新增角色(如 grade_head)需修改页面代码;角色与设置视图的映射关系不可配置。 + +### 2.5 缺少分区 Error Boundary 和 Suspense(P1) + +| 位置 | 问题 | 违反规则 | +|------|------|----------| +| [settings-view.tsx](file:///e:/Desktop/CICD/src/modules/settings/components/settings-view.tsx) L132-184 | 5 个 TabsContent 内部组件(ProfileSettingsForm / NotificationPreferencesForm / ThemePreferencesCard / PasswordChangeForm / AiProviderSettingsCard)无独立 Error Boundary | "每个独立的数据区块必须用 React Error Boundary 包裹" | +| 同上 | AiProviderSettingsCard 在 useEffect 中异步加载 providers,无 Suspense 包裹 | "异步数据使用 React Suspense + 骨架屏" | +| [profile/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/profile/page.tsx) L229-314 | 学生概览 / 教师概览区块无独立 Error Boundary | 同上 | + +**原因**:仅依赖页面级 `error.tsx` / `loading.tsx`,未做分区隔离。 + +**后果**:AI Provider 加载失败会导致整个 Security 标签页崩溃;ProfileSettingsForm 提交失败不会优雅降级。 + +### 2.6 Profile 页面职责臃肿(P1) + +| 位置 | 问题 | 违反规则 | +|------|------|----------| +| [profile/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/profile/page.tsx) L37-317 | 单文件 317 行,混合:用户基本信息展示 + 学生作业统计 + 课表筛选 + 教师班级展示 | "页面组件" 建议 ≤ 500 行(虽未超限,但职责过多) | +| 同文件 L51-110 | 学生分支内联 60 行业务计算(dueSoonCount / overdueCount / gradedCount / upcomingAssignments 排序) | "数据获取、计算、格式化等纯逻辑全部放入纯函数或 hooks,与 UI 分离" | +| 同文件 L27-35 | `WEEKDAY_MAP` / `toWeekday` 日期工具函数定义在页面文件内 | 同上 | + +**原因**:profile 页面直接编排了 dashboard 模块的学生概览组件,未通过 service 层。 + +**后果**:业务逻辑不可测试、不可复用;学生/教师概览与 dashboard 模块重复。 + +### 2.7 类型安全与表单规范问题(P2) + +| 位置 | 问题 | 违反规则 | +|------|------|----------| +| [profile-settings-form.tsx](file:///e:/Desktop/CICD/src/modules/settings/components/profile-settings-form.tsx) L44 | `zodResolver(profileFormSchema) as Resolver` 使用 `as` 断言 | "禁止 `as` 断言(除非从 `unknown` 转换或测试中)" | +| [notification-preferences-form.tsx](file:///e:/Desktop/CICD/src/modules/settings/components/notification-preferences-form.tsx) L121 | `useActionState(updateNotificationPreferencesAction, null)` 第二参数 `null` 类型不安全 | 应为 `ActionState` 初值 | +| [admin-settings-view.tsx](file:///e:/Desktop/CICD/src/modules/settings/components/admin-settings-view.tsx) L22 | `await new Promise((r) => setTimeout(r, 800))` 参数 `r` 隐式 any | "禁止 any" | + +### 2.8 可访问性缺失(P2) + +| 位置 | 问题 | 违反规则 | +|------|------|----------| +| [settings-view.tsx](file:///e:/Desktop/CICD/src/modules/settings/components/settings-view.tsx) L106-130 | Tabs 组件虽有 Radix 内置 a11y,但 TabsTrigger 仅有图标+文字,无 `aria-label` | "可访问性(a11y):语义化标签、ARIA 属性、键盘导航" | +| [notification-preferences-form.tsx](file:///e:/Desktop/CICD/src/modules/settings/components/notification-preferences-form.tsx) L198-205 | 隐藏 checkbox + Switch 双控件模式,屏幕阅读器可能重复朗读 | 同上 | +| [password-change-form.tsx](file:///e:/Desktop/CICD/src/modules/settings/components/password-change-form.tsx) L90-99 | 密码显示切换按钮 `tabIndex={-1}`,键盘用户无法触达 | "键盘导航" | + +### 2.9 监控埋点缺失(P2) + +| 位置 | 问题 | 违反规则 | +|------|------|----------| +| 全模块 | 无任何埋点接口预留(密码修改成功率、AI Provider 测试通过率、通知偏好变更频率等) | "监控:方案中预留关键操作埋点接口" | + +### 2.10 行业差距:安全功能单薄(P2) + +| 缺失功能 | 影响 | +|----------|------| +| 头像上传 | 用户无法个性化头像,profile 页只能显示文字 fallback | +| 两步验证(2FA/MFA) | K12 系统涉及学生隐私,仅密码保护不够 | +| 活跃会话管理 | 用户无法查看/远程登出其他设备会话 | +| 登录历史查看 | 非管理员用户无法查看自己的登录记录 | +| 账号数据导出/注销 | 不符合 GDPR-like 合规要求 | +| 通知预览 | 通知偏好表单无"发送测试通知"功能 | +| 设置搜索 | 设置项较多时无快速定位 | + +--- + +## 三、行业差距对比 + +### 3.1 与优秀 K12 产品的差距 + +| 维度 | 优秀实践(Google Classroom / PowerSchool / Veracross) | 当前状态 | 差距影响 | +|------|--------------------------------------------------------|----------|----------| +| **设置信息架构** | 统一入口,按角色动态显示分组,支持搜索 | admin 有两个入口(`/admin/settings` mock + `/settings`),其他角色统一 | admin 体验割裂,功能不可用 | +| **个人资料** | 头像上传 + 字段级权限可见性(学生看不到自己手机号,家长可见) | 无头像上传,所有字段对本人可见 | 个性化缺失,字段级权限未实现 | +| **安全中心** | 2FA、会话列表、登录历史、密码泄露检测 | 仅密码修改 | K12 数据安全合规风险 | +| **通知偏好** | 按事件类型细分(作业/成绩/考勤/公告/消息),支持渠道矩阵 + 免打扰 | 已有基础,但无"测试通知"按钮 | 功能完整度尚可,交互反馈缺失 | +| **主题/语言** | 主题切换 + 语言切换同页 | 主题有,语言切换在 shared 但未集成到设置页 | 用户需到别处找语言切换 | +| **AI 配置** | 多 Provider + 测试 + 用量统计 | 多 Provider + 测试,无用量统计 | 教育机构无法监控 AI 成本 | +| **空状态/骨架屏** | 每个数据区块独立骨架屏 + 空状态 | 仅页面级 loading.tsx | 局部加载失败时整页白屏 | + +### 3.2 多角色使用习惯差距 + +| 角色 | 优秀实践 | 当前状态 | +|------|----------|----------| +| **admin** | 系统设置(学校信息/策略)与个人设置在同一入口的不同分组 | 两套页面割裂,系统设置是 mock | +| **teacher** | 设置页可快速跳转常用教学功能 | ✅ 有 Quick links(TeacherSettingsView) | +| **parent** | 设置页可切换查看不同孩子的通知偏好 | 仅一套偏好,无法按孩子细分 | +| **student** | 设置页简洁,无系统配置 | ✅ 简洁 | + +--- + +## 四、改进优先级建议 + +### P0(紧急,影响安全/合规/核心功能) + +1. **创建 settings i18n 命名空间**:新增 `zh-CN/settings.json` + `en/settings.json`,覆盖所有设置/个人资料文本;更新 `i18n/request.ts` 加载新命名空间。 +2. **消除跨模块 action 直调**:定义 `SettingsService` 接口(含 `updateProfile` / `updateNotificationPreferences` 方法),通过 React Context 注入;`ProfileSettingsForm` / `NotificationPreferencesForm` 改为消费 Context。 +3. **AdminSettingsView 接入真实数据层**:将 4 个 Card(学校信息/安全策略/文件上传/通知配置)接入 `school/data-access` 或新增 `system-settings` data-access;移除 mock `setTimeout`。 + +### P1(重要,影响可维护性/体验) + +4. **配置驱动角色路由**:新增 `settings-config.ts`,定义 `Role → SettingsViewConfig` 映射(description / backHref / generalExtra),`/settings/page.tsx` 改为查表分发。 +5. **分区 Error Boundary + Suspense**:为每个 TabsContent 内部组件包裹 `` + `}>`。 +6. **Profile 页面拆分**:将学生概览/教师概览业务逻辑抽为 `useStudentProfileOverview` / `useTeacherProfileOverview` hooks;`WEEKDAY_MAP`/`toWeekday` 移至 `shared/lib/utils`。 +7. **移除 `as` 断言**:`profile-settings-form.tsx` 的 `zodResolver(...) as Resolver<...>` 改为类型兼容写法。 + +### P2(优化,提升完整度) + +8. **头像上传**:profile 页新增头像上传组件(复用 `files/data-access`)。 +9. **2FA / 会话管理**:security 标签页新增 2FA 开关 + 活跃会话列表。 +10. **通知测试按钮**:通知偏好表单新增"发送测试通知"按钮。 +11. **语言切换集成**:在 Appearance 标签页集成 `LocaleSwitcher`。 +12. **埋点接口**:在 `SettingsService` 接口预留 `trackEvent` 方法。 +13. **a11y 修复**:密码显示切换按钮移除 `tabIndex={-1}`;通知偏好表单移除冗余隐藏 checkbox。 + +--- + +## 五、架构图同步说明 + +本次审计发现架构图需补充/修改以下节点: + +### 5.1 `004_architecture_impact_map.md` §2.23 settings 模块 + +- **修改"已知问题"**:新增"跨模块 action 直调未修复"(`profile-settings-form` → `users/actions`、`notification-preferences-form` → `messaging/actions`) +- **修改"已知问题"**:新增"AdminSettingsView 为 mock 实现,无数据持久化" +- **修改"已知问题"**:新增"i18n 完全缺失,所有文本硬编码" +- **修改"依赖关系"**:明确标注 `profile-settings-form.tsx` 依赖 `users/actions`(action 级,非 data-access) +- **新增"文件清单"**:补充 `profile/page.tsx`(317 行)的归属说明(虽在 app 层,但编排 settings 相关数据) + +### 5.2 `005_architecture_data.json` settings 节点 + +- **`modules.settings.knownIssues`**:新增 3 条(跨模块 action 直调 / AdminSettingsView mock / i18n 缺失) +- **`modules.settings.exports`**:补充 `SettingsService` 接口(重构后新增) +- **`dependencyMatrix`**:settings → users 的依赖类型从 `data-access` 改为 `action`(标注为待修复) + +### 5.3 `004` §2.17 notifications 模块 + +- **修正不一致**:`notification-preferences-form.tsx` 仍从 `messaging/actions` 导入 action,但架构图称"通知偏好已迁移至 notifications 模块" — 需标注"表单层 action 调用未同步迁移" + +--- + +## 六、重构方案设计 + +### 6.1 完全解耦:SettingsService 接口 + Context 注入 + +```typescript +// src/modules/settings/types.ts (新增) +export interface ProfileService { + getProfile: () => Promise + updateProfile: (input: UpdateProfileInput) => Promise> +} + +export interface NotificationService { + getPreferences: () => Promise + updatePreferences: (input: UpdateNotificationPreferencesInput) => Promise> +} + +export interface SettingsService { + profile: ProfileService + notifications: NotificationService + trackEvent?: (event: string, payload?: Record) => void +} +``` + +```tsx +// src/modules/settings/components/settings-service-context.tsx (新增) +const SettingsServiceContext = createContext(null) + +export function SettingsServiceProvider({ service, children }: { service: SettingsService; children: ReactNode }) { + return {children} +} + +export function useSettingsService(): SettingsService { + const ctx = useContext(SettingsServiceContext) + if (!ctx) throw new Error("useSettingsService must be used within SettingsServiceProvider") + return ctx +} +``` + +页面层注入实现: + +```tsx +// /settings/page.tsx +const serverService: SettingsService = { + profile: { + getProfile: async () => getUserProfile(userId), + updateProfile: async (input) => updateUserProfile(input), + }, + notifications: { + getPreferences: async () => getNotificationPreferences(userId), + updatePreferences: async (input) => updateNotificationPreferencesAction(null, input), + }, +} +return +``` + +### 6.2 组合优先:角色配置驱动 + +```typescript +// src/modules/settings/config/role-settings-config.ts (新增) +export interface RoleSettingsConfig { + description: string + backHref: string + generalExtra?: ReactNode +} + +export const ROLE_SETTINGS_CONFIG: Partial> = { + admin: { description: "settings.admin.description", backHref: "/admin/dashboard" }, + teacher: { description: "settings.teacher.description", backHref: "/teacher/dashboard", generalExtra: }, + student: { description: "settings.student.description", backHref: "/student/dashboard", generalExtra: }, + parent: { description: "settings.parent.description", backHref: "/parent/dashboard", generalExtra: }, +} +``` + +### 6.3 国际化就绪:翻译文件结构 + +```json +// src/shared/i18n/messages/zh-CN/settings.json +{ + "title": "设置", + "backToDashboard": "返回仪表盘", + "tabs": { + "general": "通用", + "notifications": "通知", + "appearance": "外观", + "security": "安全", + "ai": "AI" + }, + "profile": { + "title": "个人信息", + "description": "更新您的个人资料", + "fields": { + "name": "姓名", + "email": "邮箱", + "phone": "电话", + "address": "地址", + "gender": "性别", + "age": "年龄", + "role": "角色" + } + }, + "notifications": { + "title": "通知偏好", + "channels": { "push": "推送通知", "email": "邮件", "sms": "短信" }, + "categories": { "messages": "消息", "announcements": "公告", "homework": "作业", "grades": "成绩", "attendance": "考勤" }, + "quietHours": { "title": "免打扰时段", "enable": "启用", "start": "开始时间", "end": "结束时间" } + }, + "security": { + "changePassword": { "title": "修改密码", "current": "当前密码", "new": "新密码", "confirm": "确认密码" }, + "session": { "title": "会话", "signOut": "退出登录" } + }, + "appearance": { "theme": { "title": "主题", "system": "跟随系统", "light": "浅色", "dark": "深色" } }, + "ai": { "providers": { "title": "AI 服务商", "test": "测试", "save": "保存" } } +} +``` + +### 6.4 错误与边界处理 + +每个 TabsContent 内部组件用 `` + `` 包裹: + +```tsx + + }> + }> + + + + +``` + +### 6.5 可测试性 + +- `SettingsService` 接口可 mock,组件单测无需真实 DB +- `WEEKDAY_MAP` / `toWeekday` 移至 `shared/lib/utils` 后可独立测试 +- 学生概览计算逻辑抽为 `useStudentProfileOverview` hook,可独立测试 + +### 6.6 可扩展性 + +- 新增角色只需在 `ROLE_SETTINGS_CONFIG` 添加条目 +- 新增设置标签页只需在 `settings-view.tsx` 的 tabs 配置添加条目 +- 新增系统设置 Card 只需在 `AdminSettingsView` 组合新 Card + +### 6.7 企业级补充 + +- **a11y**:密码显示切换按钮移除 `tabIndex={-1}`;通知偏好表单移除冗余隐藏 checkbox,仅用 Switch + `name` 属性 +- **性能**:SettingsView 保持客户端组件(需 URL searchParams),但各标签页内容组件按需加载 +- **安全**:`SettingsService` 实现在 Server Action 层调用 `requirePermission`,组件层不绕过 +- **监控**:`SettingsService.trackEvent` 预留埋点接口 diff --git a/src/app/(dashboard)/profile/error.tsx b/src/app/(dashboard)/profile/error.tsx index 040a0dd..937445c 100644 --- a/src/app/(dashboard)/profile/error.tsx +++ b/src/app/(dashboard)/profile/error.tsx @@ -1,18 +1,20 @@ "use client" import { AlertCircle } from "lucide-react" +import { useTranslations } from "next-intl" import { EmptyState } from "@/shared/components/ui/empty-state" export default function ProfileError({ reset }: { error: Error & { digest?: string }; reset: () => void }) { + const t = useTranslations("settings.errors") return (
reset(), }} className="border-none shadow-none h-auto" diff --git a/src/app/(dashboard)/profile/page.tsx b/src/app/(dashboard)/profile/page.tsx index 933dfb0..d234318 100644 --- a/src/app/(dashboard)/profile/page.tsx +++ b/src/app/(dashboard)/profile/page.tsx @@ -1,138 +1,58 @@ import Link from "next/link" import { redirect } from "next/navigation" +import { Suspense, type ReactElement } from "react" +import { getTranslations } from "next-intl/server" +import { User, Mail, Phone, MapPin, Calendar, Clock, Shield } from "lucide-react" import { requireAuth } from "@/shared/lib/auth-guard" -import { getStudentClasses, getStudentSchedule, getTeacherClasses, getTeacherTeachingSubjects } from "@/modules/classes/data-access" -import { StudentGradesCard } from "@/modules/dashboard/components/student-dashboard/student-grades-card" -import { StudentStatsGrid } from "@/modules/dashboard/components/student-dashboard/student-stats-grid" -import { StudentTodayScheduleCard } from "@/modules/dashboard/components/student-dashboard/student-today-schedule-card" -import { StudentUpcomingAssignmentsCard } from "@/modules/dashboard/components/student-dashboard/student-upcoming-assignments-card" -import { getStudentDashboardGrades, getStudentHomeworkAssignments } from "@/modules/homework/data-access" import { getUserProfile } from "@/modules/users/data-access" +import { ProfileStudentOverview, ProfileStudentOverviewSkeleton } from "@/modules/settings/components/profile-student-overview" +import { ProfileTeacherOverview, ProfileTeacherOverviewSkeleton } from "@/modules/settings/components/profile-teacher-overview" +import { SettingsSectionErrorBoundary } from "@/modules/settings/components/settings-section-error-boundary" import { Avatar, AvatarFallback, AvatarImage } from "@/shared/components/ui/avatar" import { Badge } from "@/shared/components/ui/badge" import { Button } from "@/shared/components/ui/button" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/shared/components/ui/card" import { PageHeader } from "@/shared/components/ui/page-header" -import { Separator } from "@/shared/components/ui/separator" import { formatDate } from "@/shared/lib/utils" -import { User, Mail, Phone, MapPin, Calendar, Clock, Shield } from "lucide-react" export const dynamic = "force-dynamic" -export const metadata = { - title: "Profile", +export async function generateMetadata() { + const t = await getTranslations("settings.profilePage") + return { title: t("title") } } -const WEEKDAY_MAP = [7, 1, 2, 3, 4, 5, 6] as const -type Weekday = 1 | 2 | 3 | 4 | 5 | 6 | 7 - -const toWeekday = (d: Date): Weekday => { - const day = d.getDay() - const result = WEEKDAY_MAP[day] - if (result < 1 || result > 7) throw new Error("Invalid weekday") - return result -} - -export default async function ProfilePage() { +export default async function ProfilePage(): Promise { const ctx = await requireAuth() const userId = ctx.userId const userProfile = await getUserProfile(userId) if (!userProfile) { - redirect("/login") + redirect("/login") } const roles = ctx.roles const isStudent = roles.includes("student") const isTeacher = roles.includes("teacher") - - const studentData = - isStudent - ? await (async () => { - const [classes, schedule, assignmentsAll, grades] = await Promise.all([ - getStudentClasses(userId), - getStudentSchedule(userId), - getStudentHomeworkAssignments(userId), - getStudentDashboardGrades(userId), - ]) - - const now = new Date() - const in7Days = new Date(now) - in7Days.setDate(in7Days.getDate() + 7) - - const dueSoonCount = assignmentsAll.filter((a) => { - if (!a.dueAt) return false - const due = new Date(a.dueAt) - return due >= now && due <= in7Days && a.progressStatus !== "graded" - }).length - - const overdueCount = assignmentsAll.filter((a) => { - if (!a.dueAt) return false - const due = new Date(a.dueAt) - return due < now && a.progressStatus !== "graded" - }).length - - const gradedCount = assignmentsAll.filter((a) => a.progressStatus === "graded").length - - const upcomingAssignments = [...assignmentsAll] - .sort((a, b) => { - const aTime = a.dueAt ? new Date(a.dueAt).getTime() : Number.POSITIVE_INFINITY - const bTime = b.dueAt ? new Date(b.dueAt).getTime() : Number.POSITIVE_INFINITY - if (aTime !== bTime) return aTime - bTime - return a.id.localeCompare(b.id) - }) - .slice(0, 8) - - const todayWeekday = toWeekday(now) - const todayScheduleItems = schedule - .filter((s) => s.weekday === todayWeekday) - .map((s) => ({ - id: s.id, - classId: s.classId, - className: s.className, - course: s.course, - startTime: s.startTime, - endTime: s.endTime, - location: s.location ?? null, - })) - - return { - enrolledClassCount: classes.length, - dueSoonCount, - overdueCount, - gradedCount, - todayScheduleItems, - upcomingAssignments, - grades, - } - })() - : null - - const teacherData = - isTeacher - ? await (async () => { - const [subjects, classes] = await Promise.all([getTeacherTeachingSubjects(), getTeacherClasses()]) - return { subjects, classes } - })() - : null + const t = await getTranslations("settings.profilePage") return (
- Edit Profile + {t("editProfile")} } />
- {userProfile.image ? : null} + {userProfile.image ? : null} {(userProfile.name ?? userProfile.email).slice(0, 2).toUpperCase()} @@ -148,36 +68,36 @@ export default async function ProfilePage() { - Personal Information + {t("personalInfo.title")} - Basic personal details. + {t("personalInfo.description")}
-
Full Name
+
{t("personalInfo.fullName")}
{userProfile.name ?? "-"}
-
Gender
+
{t("personalInfo.gender")}
{userProfile.gender ?? "-"}
-
Age
+
{t("personalInfo.age")}
{userProfile.age ?? "-"}
-
-
Phone
+
+
{t("personalInfo.phone")}
- {userProfile.phone ? : null} - {userProfile.phone ?? "-"} + {userProfile.phone ? : null} + {userProfile.phone ?? "-"}
-
Address
+
{t("personalInfo.address")}
- {userProfile.address ? : null} - {userProfile.address ?? "-"} + {userProfile.address ? : null} + {userProfile.address ?? "-"}
@@ -188,37 +108,37 @@ export default async function ProfilePage() { - Account Information + {t("accountInfo.title")} - System account details. + {t("accountInfo.description")}
-
-
Email
+
+
{t("accountInfo.email")}
- - {userProfile.email} + + {userProfile.email}
-
Role
+
{t("accountInfo.role")}
{userProfile.role}
-
Member Since
+
{t("accountInfo.memberSince")}
- - {formatDate(userProfile.createdAt)} + + {formatDate(userProfile.createdAt)}
-
-
Onboarded At
-
- - {userProfile.onboardedAt ? formatDate(userProfile.onboardedAt) : "-"} +
+
{t("accountInfo.onboardedAt")}
+
+ + {userProfile.onboardedAt ? formatDate(userProfile.onboardedAt) : "-"}
@@ -226,91 +146,20 @@ export default async function ProfilePage() {
- {studentData ? ( -
- -
-

Student Overview

-
Your academic performance and schedule.
-
- - - -
-
- - -
-
- -
-
-
+ {isStudent ? ( + + }> + + + ) : null} - {teacherData ? ( -
- -
-

Teacher Overview

-
Your teaching subjects and classes.
-
- -
- - - Teaching Subjects - Subjects you are currently assigned to teach. - - - {teacherData.subjects.length === 0 ? ( -
No subjects assigned yet.
- ) : ( -
- {teacherData.subjects.map((subject) => ( - - {subject} - - ))} -
- )} -
-
- - - - Teaching Classes - Classes you are currently managing. - - - {teacherData.classes.length === 0 ? ( -
No classes assigned yet.
- ) : ( - teacherData.classes.map((cls) => ( -
-
-
{cls.name}
-
- {cls.grade} - {cls.homeroom ? ` • ${cls.homeroom}` : ""} -
-
- -
- )) - )} -
-
-
-
+ {isTeacher ? ( + + }> + + + ) : null}
) diff --git a/src/app/(dashboard)/settings/error.tsx b/src/app/(dashboard)/settings/error.tsx index 6f2161c..3ae05ff 100644 --- a/src/app/(dashboard)/settings/error.tsx +++ b/src/app/(dashboard)/settings/error.tsx @@ -1,18 +1,20 @@ "use client" import { AlertCircle } from "lucide-react" +import { useTranslations } from "next-intl" import { EmptyState } from "@/shared/components/ui/empty-state" export default function SettingsError({ reset }: { error: Error & { digest?: string }; reset: () => void }) { + const t = useTranslations("settings.errors") return (
reset(), }} className="border-none shadow-none h-auto" diff --git a/src/app/(dashboard)/settings/page.tsx b/src/app/(dashboard)/settings/page.tsx index 46c525a..6562ad0 100644 --- a/src/app/(dashboard)/settings/page.tsx +++ b/src/app/(dashboard)/settings/page.tsx @@ -1,12 +1,16 @@ import { redirect } from "next/navigation" +import { getTranslations } from "next-intl/server" import { requireAuth } from "@/shared/lib/auth-guard" import { SettingsView } from "@/modules/settings/components/settings-view" -import { StudentSettingsView } from "@/modules/settings/components/student-settings-view" -import { TeacherSettingsView } from "@/modules/settings/components/teacher-settings-view" -import { ParentSettingsView } from "@/modules/settings/components/parent-settings-view" +import { SettingsServiceProvider } from "@/modules/settings/components/settings-service-context" +import { resolveRoleSettingsConfig } from "@/modules/settings/config/role-settings-config" +import type { SettingsService } from "@/modules/settings/types" import { getUserProfile } from "@/modules/users/data-access" +import { updateUserProfile } from "@/modules/users/actions" import { getNotificationPreferences } from "@/modules/notifications/preferences" +import { updateNotificationPreferencesAction } from "@/modules/messaging/actions" +import type { UpdateNotificationPreferencesInput } from "@/modules/notifications/types" export const dynamic = "force-dynamic" @@ -14,6 +18,32 @@ export const metadata = { title: "Settings", } +/** + * 将通知偏好输入对象转换为 FormData,适配 updateNotificationPreferencesAction 的签名。 + * Action 内部通过 formData.get(key) === "on" 解析布尔值。 + */ +function buildNotificationFormData(input: UpdateNotificationPreferencesInput): FormData { + const formData = new FormData() + const booleanFields: Array = [ + "emailEnabled", + "smsEnabled", + "pushEnabled", + "homeworkNotifications", + "gradeNotifications", + "announcementNotifications", + "messageNotifications", + "attendanceNotifications", + "quietHoursEnabled", + ] + for (const field of booleanFields) { + const value = input[field] + if (value === true) formData.set(field, "on") + } + if (input.quietHoursStart) formData.set("quietHoursStart", input.quietHoursStart) + if (input.quietHoursEnd) formData.set("quietHoursEnd", input.quietHoursEnd) + return formData +} + export default async function SettingsPage() { const ctx = await requireAuth() @@ -24,22 +54,36 @@ export default async function SettingsPage() { const roles = ctx.roles const notificationPrefs = await getNotificationPreferences(userId) + const t = await getTranslations("settings") - if (roles.includes("admin")) { - return ( + const config = resolveRoleSettingsConfig(roles) + const description = t(config?.descriptionKey ?? "title") + const backHref = config?.backHref ?? "/dashboard" + const generalExtra = config?.generalExtra + + // 构建 SettingsService 实现,注入到 SettingsServiceProvider + // 组件层通过 useSettingsService() 消费,不直接 import users/messaging actions + const service: SettingsService = { + profile: { + getProfile: async () => getUserProfile(userId), + updateProfile: async (input) => updateUserProfile(input), + }, + notifications: { + getPreferences: async () => getNotificationPreferences(userId), + updatePreferences: async (input) => + updateNotificationPreferencesAction(null, buildNotificationFormData(input)), + }, + } + + return ( + - ) - } - if (roles.includes("student")) { - return - } - if (roles.includes("parent")) { - return - } - return + + ) } diff --git a/src/app/(dashboard)/settings/security/error.tsx b/src/app/(dashboard)/settings/security/error.tsx index 401c9d3..2894899 100644 --- a/src/app/(dashboard)/settings/security/error.tsx +++ b/src/app/(dashboard)/settings/security/error.tsx @@ -1,18 +1,20 @@ "use client" import { AlertCircle } from "lucide-react" +import { useTranslations } from "next-intl" import { EmptyState } from "@/shared/components/ui/empty-state" export default function SecuritySettingsError({ reset }: { error: Error & { digest?: string }; reset: () => void }) { + const t = useTranslations("settings.errors") return (
reset(), }} className="border-none shadow-none h-auto" diff --git a/src/app/(dashboard)/settings/security/page.tsx b/src/app/(dashboard)/settings/security/page.tsx index ce721e4..ce79afb 100644 --- a/src/app/(dashboard)/settings/security/page.tsx +++ b/src/app/(dashboard)/settings/security/page.tsx @@ -1,4 +1,5 @@ import { Lock } from "lucide-react" +import { getTranslations } from "next-intl/server" import { requireAuth } from "@/shared/lib/auth-guard" import { PasswordChangeForm } from "@/modules/settings/components/password-change-form" @@ -13,12 +14,13 @@ export const metadata = { export default async function SecuritySettingsPage() { await requireAuth() + const t = await getTranslations("settings") return (
@@ -27,15 +29,15 @@ export default async function SecuritySettingsPage() { - Security Tips - Best practices to keep your account safe. + {t("security.tips.title")} + {t("security.tips.description")}
    -
  • Use a unique password that you don't reuse across other sites.
  • -
  • Avoid common words, names, or sequential patterns.
  • -
  • Change your password periodically.
  • -
  • Your account will be temporarily locked after multiple failed login attempts.
  • +
  • {t("security.tips.tip1")}
  • +
  • {t("security.tips.tip2")}
  • +
  • {t("security.tips.tip3")}
  • +
  • {t("security.tips.tip4")}
diff --git a/src/modules/settings/components/admin-settings-view.tsx b/src/modules/settings/components/admin-settings-view.tsx index f79b535..7e7cbb8 100644 --- a/src/modules/settings/components/admin-settings-view.tsx +++ b/src/modules/settings/components/admin-settings-view.tsx @@ -1,6 +1,7 @@ "use client" import * as React from "react" +import { useTranslations } from "next-intl" import { toast } from "sonner" import { School, Shield, Database, Bell } from "lucide-react" @@ -12,23 +13,31 @@ import { Switch } from "@/shared/components/ui/switch" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/shared/components/ui/card" import { Separator } from "@/shared/components/ui/separator" +/** + * 管理员系统设置视图 + * + * TODO: 当前为 mock 实现(setTimeout 模拟保存),未接入真实数据层。 + * 后续需新增 system_settings 表 + data-access + actions,替换 mock 逻辑。 + * 当前已适配 i18n,文本均通过 settings.admin.* 翻译键获取。 + */ export function AdminSettingsView() { + const t = useTranslations("settings.admin") const [saving, setSaving] = React.useState(false) const handleSave = async (e: React.FormEvent) => { e.preventDefault() setSaving(true) - // 模拟保存 - await new Promise((r) => setTimeout(r, 800)) - toast.success("设置已保存") + // TODO: 替换为真实 Server Action 调用 + await new Promise((resolve) => setTimeout(resolve, 800)) + toast.success(t("saveSuccess")) setSaving(false) } return (
-

系统设置

-

管理系统基础信息与运行参数。

+

{t("title")}

+

{t("description")}

@@ -38,39 +47,39 @@ export function AdminSettingsView() {
- 学校信息 - 学校的基础信息,将显示在系统各处 + {t("schoolInfo.title")} + {t("schoolInfo.description")}
- - + +
- - + +
- - + +
- - + +
- - + +
- -