refactor(design-tokens): 全量体系化重建设计令牌
Primitive + Semantic 双层令牌架构,HEX->HSL,明暗双份,@theme inline 暴露为 Tailwind 类。 - 新建 src/app/styles/tokens/ 6 个令牌文件(primitive/semantic-light/semantic-dark/lesson-preparation/tailwind-theme/index) - globals.css 改为 @import 引入,477->258 行 - 清理 91 处 #hex 硬编码颜色 -> hsl(var(--*)) - 清理 10 处硬编码字体 -> var(--font-family-*) - 清理 100 文件 Tailwind 任意值(Tier 1 映射/Tier 3 注释豁免) - 清理 M3 Surface 死代码,升级 --lp-* 令牌(HEX->HSL + 暗色补全) - 新建 ESLint 自定义规则 no-hardcoded-design-tokens(单词边界正则) - eslint.config.mjs 新增 no-restricted-syntax 禁止 #hex + 自定义规则加载(pathToFileURL) - 项目规则新增设计令牌规范强制章节 - 架构图 004/005 同步设计令牌体系节点 - known-issues.md 追加设计令牌问题分类(7 个规则表) 验证: tsc --noEmit 0 errors, npm run lint 0 errors/12 warnings(均为既有问题)
This commit is contained in:
@@ -91,6 +91,50 @@
|
||||
|
||||
---
|
||||
|
||||
## 1.1.2 设计令牌体系(2026-07-04 新增)
|
||||
|
||||
项目采用**双层令牌架构**(Primitive + Semantic),Tailwind v4 通过 `@theme inline` 暴露为业务可用的类。
|
||||
|
||||
### 文件分布
|
||||
|
||||
```
|
||||
src/app/
|
||||
├─ globals.css # @import 入口 + base layer + 组件硬编码样式
|
||||
└─ styles/tokens/
|
||||
├─ primitive.css # Layer 1: 原始色板(zinc/stone/indigo)、字号/间距/阴影/字体家族阶梯
|
||||
├─ semantic-light.css # Layer 2: :root 语义令牌(shadcn + 扩展 + chart/sidebar + diff/graph)
|
||||
├─ semantic-dark.css # Layer 2: .dark 语义令牌(完整暗色)
|
||||
├─ lesson-preparation.css # --lp-* 令牌(明暗双份,HEX→HSL)
|
||||
├─ tailwind-theme.css # @theme inline 暴露所有令牌为 Tailwind 类
|
||||
└─ index.css # 汇总 @import 入口
|
||||
```
|
||||
|
||||
### 令牌分类
|
||||
|
||||
| 分类 | 命名 | 暴露为 Tailwind 类 |
|
||||
|---|---|---|
|
||||
| shadcn 标准 | `--background`/`--foreground`/`--primary`/... | `bg-background`/`text-foreground`/`bg-primary`/... |
|
||||
| 扩展层级 | `--background-elevated`/`--text-secondary`/`--border-strong`/... | `bg-background-elevated`/`text-text-secondary`/... |
|
||||
| chart | `--chart-1~5` | `bg-chart-1`/... |
|
||||
| sidebar | `--sidebar-*` | `bg-sidebar`/`text-sidebar-foreground`/... |
|
||||
| lp-* 命名空间 | `--lp-paper`/`--lp-inline-node-text`/`--lp-dot-*` | `bg-lp-paper`/`text-lp-inline-node-text`/... |
|
||||
| 字体家族 | `--font-family-sans/serif/mono` | `font-sans`/`font-serif`/`font-mono` |
|
||||
| 字号阶梯 | `--font-size-1~9`(含 `--font-size-0` 微字号) | `text-size-1`~`text-size-9` |
|
||||
| 间距阶梯 | `--space-0~18` | `w-[length:var(--space-N)]` 等 |
|
||||
| 阴影阶梯 | `--shadow-1~6` | `shadow-1`~`shadow-6` |
|
||||
| 圆角阶梯 | `--radius-sm/md/lg/xl/2xl/full` | `rounded-sm`/`rounded-md`/... |
|
||||
| 动效 | `--duration-*`/`--ease-*` | `duration-fast`/`ease-in`/... |
|
||||
| z-index | `--z-dropdown/sticky/modal/popover/toast` | `z-dropdown`/`z-modal`/... |
|
||||
|
||||
### 强制约束
|
||||
|
||||
- ESLint 规则 `no-restricted-syntax` 禁止 `#hex` 字面量
|
||||
- 自定义规则 `design-tokens/no-hardcoded-fonts` 禁止 `'Inter'`/`'Fraunces'`/`'JetBrains Mono'` 字面量(单词边界匹配,不影响 `Interval`/`Interactive` 等标识符)
|
||||
- 白名单:`primitive.css`(令牌定义)、`email-channel.ts`(邮件 HTML)、`manifest.ts`(PWA)
|
||||
- 任意值 `w-[Npx]` 等需通过 `// eslint-disable-next-line no-restricted-syntax -- <reason>` 注释豁免(如 Dialog 固定宽度、图表画布、loading.tsx 骨架等)
|
||||
|
||||
---
|
||||
|
||||
## 1.2 模块依赖关系图
|
||||
|
||||
下图展示模块间的实际依赖关系,**标注依赖类型与合规性**。
|
||||
@@ -456,6 +500,8 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
|
||||
- `isRecord(v)` — 通用类型守卫(P1-6 代码去重新增:位于 `lib/type-guards.ts`,判断未知值是否为非空对象 `v is Record<string, unknown>`;消除 homework/stats-service、lesson-preparation/node-summary、lesson-preparation/ai-suggest 等 12+ 个文件中重复定义的 `isRecord` 局部实现,从 `unknown` 安全收窄类型,替代 `as Record<string, unknown>` 断言)
|
||||
- `trackEvent(name, payload)` / `trackExamEvent(name, payload)` / `trackAuthEvent(name, payload)` — 监控埋点接口(位于 `lib/track-event.ts`,server-only,当前输出到 console.info,预留接入外部监控服务;EventName 联合类型覆盖 announcement/message/notification/attendance/elective/exam/homework/ai/audit/files/auth 模块事件;✅ P3 新增事件名 `homework.excellent_viewed`、`homework.remind_unsubmitted`;✅ audit-P1-9 新增 auth 模块 8 个事件 `auth.signin_success`/`auth.signin_failure`/`auth.signout`/`auth.signup`/`auth.2fa_enabled`/`auth.2fa_disabled`/`auth.account_locked`/`auth.rate_limited`,并新增 `trackAuthEvent` 便捷函数自动设置 targetType="user")
|
||||
- `ROLE_NAMES` — 角色名常量对象(✅ 2026-06-24 新增:位于 `types/permissions.ts`,包含 ADMIN/TEACHER/STUDENT/PARENT/GRADE_HEAD/TEACHING_HEAD 键,替代 DB 查询中硬编码的角色名字符串;被 error-book/data-access-analytics.getAllStudentIds 使用 `ROLE_NAMES.STUDENT`)
|
||||
- `apiSuccess(data, init?)` / `apiError(message, status, errorCode?, init?)` / `apiFromAction(result, options?)` / `handleApiError(error, init?)` / `withApiErrorHandler(handler)` / `parseJsonBody<T>(req)` / `errorToStatus(error)` — API 路由层统一响应工具(✅ 2026-07-04 API 规范化重构新增:位于 `lib/api-response.ts`,仅用于 `app/api/.../route.ts`;统一响应信封 `{ success, message?, errorCode?, data? }` 与 `ActionState<T>` 对齐;集中错误→HTTP 状态码映射替代字符串匹配推导;`withApiErrorHandler` HOF 统一异常处理消除每个 route 重复 try/catch;`apiFromAction` 通过 `errorCode` 驱动状态码替代 `result.message?.includes("not found")` 脆弱模式;`errorToStatus` 映射规则:PermissionDeniedError→403 / NotFoundError→404 / ValidationError→400 / BusinessError→400 / 其他→500)
|
||||
- `formatSseEvent(data)` / `formatSseError(message)` / `formatSseDone()` / `createSseResponse(stream)` / `createSseError(message, status)` — SSE 共享工具(✅ 2026-07-04 API 规范化重构新增:位于 `lib/sse.ts`,统一 `app/api/.../stream/route.ts` 的事件格式与响应头;标准 SSE 头 `Content-Type: text/event-stream` + `Cache-Control: no-cache, no-transform` + `Connection: keep-alive`;客户端约定错误统一 `{ type: "error", message }`、流结束信号 `[DONE]`)
|
||||
|
||||
**共享组件导出**(P0-b / P1-a / P1-b / P1-c / P2-a / P2-b / P3-a / P3-b / P3-c / P3-d / 第二轮 P0-1/P0-2/P0-3/P1-1/P1-2/P1-3/P1-4 重构新增,按类别组织):
|
||||
|
||||
@@ -497,6 +543,27 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
|
||||
| `useActionMutation` | `hooks/use-action-mutation.ts` | `useActionMutation<T>(options?): { isWorking, mutate }` | 通用 Server Action mutation Hook,替代 50+ 文件中重复的 setIsWorking + try/catch/finally + toast 模式 | 1 个示范(P1-4: schools-view),潜在影响 50+ 文件 |
|
||||
| `useActionQuery` | `hooks/use-action-query.ts` | `useActionQuery<T>(action, options?): { data, loading, error, refetch }` | 通用 Server Action 查询 Hook,替代 11 个文件中重复的 useEffect + useState(loading) + Action().then().catch().finally() 模式,内置竞态防护 | 1 个示范(P1-4: create-question-dialog),潜在影响 11 个文件 |
|
||||
|
||||
**V5 状态管理统一专项基础设施**(阶段 1 新增,零业务行为变更):
|
||||
|
||||
| 资产 | 文件 | 签名 | 用途 | 后续消费方 |
|
||||
|------|------|------|------|--------|
|
||||
| `createQueryClient` | `lib/query-client.ts` | `createQueryClient(): QueryClient` | TanStack Query 工厂,默认 staleTime 30s / retry 1 / refetchOnWindowFocus false / mutations 全局 onError 兜底 | `app/providers.tsx` |
|
||||
| `configureNotify` + `notify` + `rawToast` | `lib/notify.ts` | `configureNotify(resolver); notify.{success,error,info,warning,promise}; rawToast` | 统一 toast 接口,包装 sonner 并内置 i18n key 解析。`configureNotify` 由 `Providers` 的 `NotifyConfigurator` 注入 next-intl `t` 函数 | `query-client.ts` 全局兜底 + 业务组件迁移后由各模块使用(替换 100+ 处 `import { toast } from "sonner"`) |
|
||||
| `useDialogState` | `hooks/use-dialog-state.ts` | `useDialogState(): readonly [boolean, () => void, () => void, () => void]` | Dialog/Sheet 开关状态 Hook,替换 30+ 处 `const [open, setOpen] = useState(false)` 模式 | 待迁移:30+ 处 Dialog 组件 |
|
||||
| `useUiStore` + `useModal` | `stores/ui-store.ts` | `useUiStore: { modals, openModal, closeModal, isOpen, closeAll }; useModal(name)` | 全局 UI store(zustand),集中管理 modal registry。命令式 API + Hook 双入口。与 sonner 的分工:toast 走 notify,modal 走本 store | 待迁移:30+ 处 Dialog 组件切换为 `useModal(name)` |
|
||||
| `createServiceProvider` | `lib/create-service-provider.tsx` | `createServiceProvider<T>(displayName): { Provider, useService, Context }` | Service DI Provider 工厂,替换 14 处重复的 `createContext(null)` + Provider + useService 抛错模板 | 待迁移:announcements / attendance / audit / dashboard / messaging / settings 等 14 处 Service DI Provider |
|
||||
| `Providers` | `app/providers.tsx` | `Providers({ children, locale, messages, session }): React.JSX.Element` | 客户端 Provider 聚合。嵌套顺序:QueryClientProvider → NextIntlClientProvider → NotifyConfigurator → ThemeProvider → SessionProvider → NuqsAdapter → children + Toaster。DevTools 仅 dev 环境 | `app/layout.tsx` |
|
||||
|
||||
**V5 状态分层模型**(5 层):
|
||||
|
||||
| 层 | 责任 | 选型 |
|
||||
|----|------|------|
|
||||
| URL 状态 | 列表过滤、tab 持久化、深链接 | `nuqs useQueryState`(单一来源,逐步替换 `useSearchParams` + `window.location.search`) |
|
||||
| 服务端状态 | 列表/详情/分页数据,mutation + 缓存失效 | TanStack Query(混合模式:RSC 拿首屏 props,useQuery 负责交互数据) |
|
||||
| 客户端业务状态 | 模块级状态机(如 exam preview、lesson editor) | zustand slice(按模块拆,参考 lesson-preparation 现有模式) |
|
||||
| 全局 UI 状态 | modal registry | zustand ui-store |
|
||||
| 表单状态 | 表单密集模块 | react-hook-form + zodResolver |
|
||||
|
||||
**共享工具函数导出**(第二轮 P1-3 重构新增):
|
||||
|
||||
| 函数 | 文件 | 签名 | 用途 | 消费方 |
|
||||
@@ -582,6 +649,8 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
|
||||
| ~~`components/onboarding-gate.tsx`~~ | ~~312~~ | ✅ audit-P0-4 已删除(引导流程业务逻辑已迁移至 `modules/onboarding/`,原文件无任何 import 引用,删除以避免文件膨胀) |
|
||||
| `components/global-search.tsx` | 221 | 全局搜索(业务泄漏) |
|
||||
| `types/permissions.ts` | 157 | 67 个权限点常量 + Role/DataScope/AuthContext 类型(✅ rbac 新增:`Role` 类型从固定联合类型改为 `string` 支持动态角色;新增 `BUILTIN_ROLES` 常量、`BuiltinRole` 类型、`isBuiltinRole()` 类型守卫;`isRole()` 标记为 deprecated) |
|
||||
| `lib/api-response.ts` | 212 | ✅ 2026-07-04 API 规范化重构新增:API 路由层统一响应工具(apiSuccess/apiError/apiFromAction/handleApiError/withApiErrorHandler/parseJsonBody/errorToStatus);统一响应信封 + 集中错误→HTTP 状态码映射,消除每个 route 重复 try/catch 与字符串匹配推导状态码 |
|
||||
| `lib/sse.ts` | 81 | ✅ 2026-07-04 API 规范化重构新增:SSE 共享工具(formatSseEvent/formatSseError/formatSseDone/createSseResponse/createSseError);统一 stream 路由的事件格式与响应头,消除 `ai/chat/stream` 与 `notifications/stream` 重复实现 |
|
||||
|
||||
---
|
||||
|
||||
@@ -1577,6 +1646,7 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
|
||||
| `types.ts` | 85 | 私信类型 + ✅ 审计 V1-P1-6 新增 `RecipientRole` 类型 + re-export 通知类型(向后兼容) |
|
||||
| `hooks/use-message-search.ts` | ~60 | ✅ P1-7 新增:消息搜索 hook(防抖 + 请求竞态取消) |
|
||||
| `lib/build-reply-href.ts` | ~30 | ✅ 审计 V1-P1-7 新增:构建回复 URL 纯函数(URLSearchParams 编码) |
|
||||
| `lib/type-guards.ts` | ~40 | ✅ 类型守卫专项重构(2026-07-04)新增:`isRecipientRole` / `isMessageReportReason` / `isMessageReportStatus` 守卫 + `toMessageReportReason` / `toMessageReportStatus` 安全窄化函数(fallback 默认值),替代 `data-access.ts` 与 `message-report-block.tsx` 中的 `as` 断言 |
|
||||
|
||||
**组件清单**:
|
||||
| 组件 | 职责 |
|
||||
@@ -2567,7 +2637,7 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
|
||||
- Lib(`lib/document-migration.ts`):`defaultDataForType` / `migrateV1ToV2` / `migrateV2ToV3` / `normalizeDocument` / `buildInitialContent` / `buildDefaultSkeleton` / `isTextbookContentNode` / `isAnchorEdge` / `getAnchorsForNode` / `getActiveAnchorIds` / `getAnchorEdges`
|
||||
- Lib(`lib/anchor-mark.ts`,V4 新增):Tiptap `AnchorMark` 扩展(区间锚)+ `AnchorPoint` 扩展(点锚),替代 v3 字符串偏移锚点
|
||||
- Lib(`lib/node-summary.ts`):`getNodeSummary` / `getTextbookContentSummary` / `getNodeColor` / `NODE_COLORS`
|
||||
- Lib(`lib/export.ts`,V5-4 新增 + V4 增强):`flattenLessonPlanForPrint` / `flattenBlockData`(V4:新增 `interaction` case + `flattenInteraction` 函数)/ `PrintableLessonPlan` 类型
|
||||
- Lib(`lib/export.ts`,V5-4 新增 + V4 增强 + i18n 注入):`flattenLessonPlanForPrint` / `flattenBlockData`(V4:新增 `interaction` case + `flattenInteraction` 函数)/ `PrintableLessonPlan` 类型 / `TranslationFn` 类型(V5-4 i18n:所有 label 通过翻译函数注入,调用方 print-view.tsx 注入 `useTranslations`)
|
||||
- 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`
|
||||
@@ -2672,7 +2742,7 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
|
||||
| `types.ts` | 类型定义(含 v1/v2/v3 文档类型、TextbookContentNode、LessonPlanNode、NodeAnchor、AnchorEdge、FlowEdge、11 种 BlockData 接口) |
|
||||
| `constants.ts` | 常量定义 |
|
||||
| `schema.ts` | Zod 验证(V3:错误消息改为 i18n 键,如 `error.titleRequired`) |
|
||||
| `lib/type-guards.ts` | **集中类型守卫(V3 新增)**:11 种 BlockData 类型守卫(isRichTextBlockData/isTextStudyBlockData/isExerciseBlockData/isObjectiveBlockData/isKeyPointBlockData/isImportBlockData/isNewTeachingBlockData/isSummaryBlockData/isHomeworkBlockData/isBlackboardBlockData/isReflectionBlockData)+ 节点类型守卫(isTextbookContentNode/isLessonPlanNode)+ 题目类型守卫(isValidQuestionType + **V4 导出 `VALID_QUESTION_TYPES` 常量 + `ValidQuestionType` 类型**)+ 基础类型守卫(isLessonPlanStatus/isTemplateType/isTemplateScope/isBlockType)+ **Block 字段值类型守卫(V3 续新增)**:isBlackboardLayout/isImportMethod/isExercisePurpose/isObjectiveDimension/isKeyPointType/isHomeworkType/isReflectionAspect(用于 select onChange 替代 `as` 断言)+ **normalizeTemplateBlocks 规范化函数(V3 续审计新增)**:从 DB `unknown` 安全转换为 `TemplateBlockSkeleton[]`,替代 `as LessonPlanTemplate["blocks"]` 断言 |
|
||||
| `lib/type-guards.ts` | **集中类型守卫(V3 新增)**:11 种 BlockData 类型守卫(isRichTextBlockData/isTextStudyBlockData/isExerciseBlockData/isObjectiveBlockData/isKeyPointBlockData/isImportBlockData/isNewTeachingBlockData/isSummaryBlockData/isHomeworkBlockData/isBlackboardBlockData/isReflectionBlockData)+ 节点类型守卫(isTextbookContentNode/isLessonPlanNode)+ 题目类型守卫(isValidQuestionType + **V4 导出 `VALID_QUESTION_TYPES` 常量 + `ValidQuestionType` 类型**)+ 基础类型守卫(isLessonPlanStatus/isTemplateType/isTemplateScope/isBlockType/isReviewDecision)+ **Block 字段值类型守卫(V3 续新增)**:isBlackboardLayout/isImportMethod/isExercisePurpose/isObjectiveDimension/isKeyPointType/isHomeworkType/isReflectionAspect(用于 select onChange 替代 `as` 断言)+ **normalizeTemplateBlocks 规范化函数(V3 续审计新增)**:从 DB `unknown` 安全转换为 `TemplateBlockSkeleton[]`,替代 `as LessonPlanTemplate["blocks"]` 断言 + **类型守卫专项重构(2026-07-04)**:所有 `isXxxBlockData` 守卫参数从 `BlockData` 拓宽为 `unknown`(守卫内部已用 `isObject` 检查,安全;使守卫可从 `unknown` 直接收窄,支持 `AnyLessonPlanNode.data` 联合类型);新增 `mergeBlockDataPatch(node, patch)` 辅助函数 + `BLOCK_DATA_GUARDS` 注册表(BlockType → 守卫映射),替代 AI patch 合并中的 `{ ...node.data, ...patch } as BlockData` 断言,校验失败回退原 data 保证编辑器完整性 |
|
||||
| `lib/i18n-errors.ts` | **Zod 错误 i18n 翻译辅助(V3 新增)**:`translateFieldErrors`(将 Zod fieldErrors 中的 i18n 键翻译为实际消息)/ `safeParseWithI18n`(安全解析 Zod 结果并返回带翻译的 ActionState 错误格式) |
|
||||
| `lib/document-migration.ts` | **纯函数**:v1→v2(migrateV1ToV2)/ v2→v3(migrateV2ToV3)/ 规范化(normalizeDocument,兼容 v1/v2/v3/v4)/ 初始内容(buildInitialContent)/ 默认骨架(buildDefaultSkeleton)/ defaultDataForType / 工具函数(isTextbookContentNode/isAnchorEdge/getAnchorsForNode/getActiveAnchorIds/getAnchorEdges) |
|
||||
| `lib/anchor-mark.ts` | **V4 新增**:Tiptap `AnchorMark` 扩展(区间锚)+ `AnchorPoint` 扩展(点锚),替代 v3 字符串偏移锚点;存储在正文富文本 doc 中 |
|
||||
@@ -2820,7 +2890,7 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
|
||||
> - **J4 架构图同步**:004_architecture_impact_map.md 新增 V4 纸感重构章节;005_architecture_data.json 更新 lesson_preparation 节点——description 改为 V4 描述、dependencies 移除 `@xyflow/react` 改为 `@tiptap/*` 包、files 数组移除 9 个废弃文件并新增 V4 组件、auditFixes 新增 V4-PAPER-1~8 + V4-PAPER-DOC 条目
|
||||
> - **J5 known-issues V4 规则**:`docs/troubleshooting/known-issues.md` 新增 V4 纸感重构规则表,记录 8 条规则——React Flow 弃用、Tiptap Mark 锚点替代字符串偏移、setState in effect 禁用、doc.version=4、editor-slice 移除画布相关方法、interaction block 注册、PrintView 按 order 排序、AnchorMigrationBanner 失效提示
|
||||
> - **数据模型 v4**:`types.ts` `LessonPlanDocument.version = 4`;`lib/document-migration.ts` `normalizeDocument` 兼容 v1/v2/v3/v4 链式迁移;`editor-slice.ts` 移除 `updateNodePosition`/`connect`/`setEdges`/`autoLayout` 画布相关方法;`hooks/expanded-slice.ts` 新增 `expandedNodeIds`/`toggleExpand`/`setExpanded` 状态(节点展开到正文流);`lib/anchor-mark.ts` 新增 Tiptap `AnchorMark` + `AnchorPoint` 扩展(替代 v3 字符串偏移锚点)
|
||||
> - **V2-1 复制节点**:`hooks/editor-slice.ts` 新增 `duplicateNode(id)` 方法(深拷贝 `BlockData` + 新 id + 标题加"副本" + order 置末,interaction 节点的 turns 重新生成 id 避免冲突);`components/paper-editor/paper-context-menu.tsx` 的 `copyNode` 从 toast 占位改为调用 `duplicateNode` 并 toast 成功提示;i18n 新增 `v4.contextMenu.copied`/`copyFailed` 键(zh-CN + en)
|
||||
> - **V2-1 复制节点**:`hooks/editor-slice.ts` 新增 `duplicateNode(id, copySuffix)` 方法(深拷贝 `BlockData` + 新 id + 标题加 `copySuffix` + order 置末,interaction 节点的 turns 重新生成 id 避免冲突;`copySuffix` 由调用方注入 i18n 文案);`components/paper-editor/paper-context-menu.tsx` 的 `copyNode` 从 toast 占位改为调用 `duplicateNode` 并 toast 成功提示;i18n 新增 `v4.contextMenu.copied`/`copyFailed`/`copySuffix` 键(zh-CN + en)
|
||||
> - **V2-2 节点级 AI 协助 4 项**:`lib/ai-node-assist.ts` 新增 4 个服务函数(`generateNodeContent`/`optimizeNodeExpression`/`suggestNodeDifferentiation`/`generateLayeredQuestions`),全部纯服务端、Zod 校验、失败返回 null;`actions-ai.ts` 新增 4 个 Server Action,每个 action 校验 `LESSON_PLAN_READ + LESSON_PLAN_CREATE + AI_CHAT` 三个权限点;`hooks/use-node-ai-assist.ts` 新增客户端 hook;`components/paper-editor/paper-editor.tsx` 注入 `onAiAction` 回调;`components/detail-panel/detail-panel.tsx` 的 4 个 `AiButton` 接入真实 action;i18n 新增 `v4.contextMenu.aiRunning`/`aiSuccess`/`aiFailed`/`aiComingSoon` 键(zh-CN + en)
|
||||
> - **V4-I18N-1 i18n 完整审查与修复**:对 lesson-preparation 模块 i18n 配置进行全量审查并修复——P0 修复运行时 MISSING_MESSAGE:`version.diff.*` 3 键(title/summary/noChanges)+ `schema.ts` 引用的 8 个 `error.*` 键(labelTooLong/versionNoInvalid/nameRequired/nameTooLong/queryTooLong/blockIdRequired/commentTooLong/reviewerRequired)+ `v4.contextMenu` 6 键(copied/copyFailed/aiComingSoon/aiRunning/aiSuccess/aiFailed)(zh-CN + en 同步);P1 修复 en locale 下大面积报错:en `calendar` 节键名对齐 zh-CN(weekView/monthView/loadFailed + prev/next/eventMeta/weekDays 子节),en `analytics` 节补齐 totalPublished/totalSubmitted/totalStandardsLinked/loadFailed;P2 修复 7 处硬编码中文(detail-props.tsx 的 stageLabel/differentiationLabel、inline-node.tsx 的 exerciseCount、inline-qa-dialog.tsx 的 expectedAnswer、qa-editor.tsx 的 turnContentPlaceholder、curriculum-map-view.tsx 的 legendTitle/noData)并新增对应 6 个 i18n 键(`v4.detail.stageLabel`/`differentiationLabel`/`exerciseCount`/`turnContentPlaceholder`、`v4.heatmap.legendTitle`/`noData`);清理 7 个 dead 节(review/comment/formative/substitute/evaluation/standards/gradeHead,analytics 因被 `app/(dashboard)/admin/curriculum-map` 引用而保留);`lib/i18n-errors.ts` 新增 `t.has(msg)` 运行时守卫,避免 `as` 断言绕过类型检查(键不存在时返回原消息而非抛出 MISSING_MESSAGE);zh-CN 与 en 顶层键集一致性验证通过(43 = 43);005_architecture_data.json 同步追加 `V4-I18N-1` auditFixes 条目;known-issues.md 追加 3 个 i18n 规则表(键缺失与中英文不同步 / dead 节清理规则 / 动态键翻译守卫)
|
||||
|
||||
@@ -3469,6 +3539,47 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
|
||||
|
||||
---
|
||||
|
||||
## 2.34 search(全文检索模块)— ✅ 2026-07-04 API 规范化重构新增
|
||||
|
||||
**职责**:封装跨 4 张表(questions / textbooks / exams / announcements)的全文检索数据访问层,供 `app/api/search/route.ts` 调用。本次 API 规范化重构从 `app/api/search/route.ts` 下沉而来,消除 `app/` 层直接查询 DB 的架构违规(违反「app → modules → shared」三层架构单向依赖)。
|
||||
|
||||
**导出函数**(`data-access.ts`,server-only):
|
||||
- `searchQuestions(kw, limit?)` — 题库检索:`CAST(content AS CHAR) LIKE` 模糊匹配 JSON 字段,返回 `SearchResultItem[]`(href 指向 `/admin/questions?id=`)
|
||||
- `searchTextbooks(kw, limit?)` — 教材检索:title/subject/publisher 三字段 OR 模糊匹配
|
||||
- `searchExams(kw, limit?)` — 试卷检索:title/description 模糊匹配
|
||||
- `searchAnnouncements(kw, limit?)` — 公告检索:仅返回 `status=published` 的公告,title/content 模糊匹配;HTML 内容通过 `stripHtml` 提取纯文本摘要
|
||||
- `DEFAULT_SEARCH_PAGE_SIZE`(= 10)— 默认分页大小常量
|
||||
|
||||
**类型定义**(`types.ts`):
|
||||
- `SearchType = "all" | "question" | "textbook" | "exam" | "announcement"`
|
||||
- `SearchResultItem` — `{ id, title, snippet, type, href, createdAt }`
|
||||
- `SearchResponse` — `{ success, query, type, results, total, page, pageSize }`
|
||||
- `isSearchType(v): v is SearchType` — 类型守卫
|
||||
|
||||
**设计要点**:
|
||||
- ✅ **容错降级**:每个查询函数 try/catch 包裹,单表查询失败返回空数组而非抛错,避免拖垮整体搜索
|
||||
- ✅ **职责分离**:本模块仅负责 DB 查询;角色过滤(学生不能搜题目/考试)由路由层基于 `getAuthContext()` 处理
|
||||
- ✅ **摘要生成**:`extractTextFromJson`(JSON content → string)、`stripHtml`(去 HTML 标签)、`truncate`(截断+省略号)三个内部纯函数
|
||||
- ✅ **`as` 断言清理**:所有 `as const` 与 `as SearchResultItem` 类型收窄均使用类型守卫或字面量标注,符合项目 TypeScript 严格模式
|
||||
|
||||
**依赖关系**:
|
||||
- 依赖:`shared/db`(db 客户端)、`shared/db/schema`(questions/textbooks/exams/announcements 表)、`drizzle-orm`(and/or/eq/like/desc/sql)
|
||||
- 被依赖:`app/api/search/route.ts`
|
||||
|
||||
**数据库表**(只读):
|
||||
- `questions`(content JSON 字段、type、createdAt)
|
||||
- `textbooks`(title、subject、grade、publisher、createdAt)
|
||||
- `exams`(title、description、status、createdAt)
|
||||
- `announcements`(title、content、type、status、createdAt)
|
||||
|
||||
**文件清单**:
|
||||
| 文件 | 行数 | 职责 |
|
||||
|------|------|------|
|
||||
| `data-access.ts` | 212 | 4 个 search 函数 + 3 个内部纯函数(extractTextFromJson/stripHtml/truncate)+ DEFAULT_SEARCH_PAGE_SIZE 常量 |
|
||||
| `types.ts` | 31 | SearchType/SearchResultItem/SearchResponse 类型 + isSearchType 类型守卫 |
|
||||
|
||||
---
|
||||
|
||||
# 第三部分:已知架构问题和技术债
|
||||
|
||||
## 3.1 P0 严重问题(必须立即修复)
|
||||
@@ -3816,6 +3927,95 @@ shared/lib/{audit-logger, change-logger, auth-guard} → @/auth → shared/lib/*
|
||||
|
||||
---
|
||||
|
||||
## 3.7 API 规范化专项重构(2026-07-04)
|
||||
|
||||
> 本次重构统一 `app/api/**/route.ts` 的响应信封、错误处理与状态码映射,消除 11 个路由的重复模板代码与脆弱字符串匹配模式。新增 2 个共享工具文件 + 1 个 search 模块。
|
||||
|
||||
### 3.7.1 新增共享工具
|
||||
|
||||
| 文件 | 用途 |
|
||||
|------|------|
|
||||
| `shared/lib/api-response.ts` | API 路由层统一响应工具:`apiSuccess` / `apiError` / `apiFromAction` / `handleApiError` / `withApiErrorHandler` HOF / `parseJsonBody` / `errorToStatus` |
|
||||
| `shared/lib/sse.ts` | SSE 共享工具:`formatSseEvent` / `formatSseError` / `formatSseDone` / `createSseResponse` / `createSseError` |
|
||||
|
||||
### 3.7.2 新增模块
|
||||
|
||||
**`modules/search/`**(2 个文件):从 `app/api/search/route.ts` 下沉 4 个查询函数(searchQuestions/searchTextbooks/searchExams/searchAnnouncements),消除 `app/` 直查 DB 的架构违规。
|
||||
|
||||
### 3.7.3 重构路由清单(11 个)
|
||||
|
||||
| 路由 | 重构内容 |
|
||||
|------|---------|
|
||||
| `api/search` | 改用 `modules/search/data-access`(消除直查 DB);`withApiErrorHandler` + `apiSuccess` |
|
||||
| `api/proctoring/event` | 改用 `getExamSubmissionForProctoring`(消除与 data-access 重复实现);`parseJsonBody` + Zod + `ValidationError`;修复 401→403 |
|
||||
| `api/upload` | `apiFromAction(result, { headers })` 替代字符串匹配;415 等状态码改为 errorCode 驱动 |
|
||||
| `api/files/[id]` | GET/DELETE 用 `apiFromAction`;路由从 80+ 行简化到 38 行 |
|
||||
| `api/files/batch-delete` | 使用 `apiFromAction` |
|
||||
| `api/export` | `EXPORT_TYPES: Record<ExportType, Permission \| null>` 映射表替代 4 处嵌套 try/catch;统一 `requirePermission` |
|
||||
| `api/import` | `ValidationError` + `apiError`;修复 401→403 |
|
||||
| `api/ai/chat` | `apiSuccess` / `apiError` + `withApiErrorHandler` |
|
||||
| `api/ai/chat/stream` | 改用 `createSseError` / `createSseResponse` / `formatSseEvent` / `formatSseDone`;补充 `dynamic = "force-dynamic"` |
|
||||
| `api/notifications/stream` | 改用 SSE 共享 helpers;补充 `dynamic = "force-dynamic"`;`PermissionDeniedError` → 403,其他 → 401 |
|
||||
| `api/cron/audit-cleanup` | `withApiErrorHandler`;字段名 `error` → `message`、`ok` → `success` |
|
||||
| `api/onboarding/complete` | 410 响应改用 `apiError(..., 410, "deprecated")` |
|
||||
| `api/onboarding/status` | 同上 |
|
||||
| `api/rate-limit-test` | `apiSuccess` / `apiError` + `withApiErrorHandler` |
|
||||
|
||||
### 3.7.4 模块改动
|
||||
|
||||
**`modules/files/actions.ts`**:移除本地 `handleActionError`,改用 `@/shared/lib/action-utils` 统一版本;为所有失败分支添加 `errorCode`(`validation_error` / `not_found` / `permission_denied` / `unexpected`),驱动 `apiFromAction` 状态码映射。
|
||||
|
||||
### 3.7.5 客户端适配(6 个文件)
|
||||
|
||||
所有客户端 fetch 调用适配新响应信封 `{ success, message?, errorCode?, data? }`:
|
||||
|
||||
| 文件 | 改动 |
|
||||
|------|------|
|
||||
| `modules/files/hooks/use-file-upload.ts` | 响应体类型改为 `{ success, data: Partial<FileUploadResult> }` |
|
||||
| `modules/exams/editor/selection-toolbar.tsx` | `data.url` → `data.data.url`,`data.id` → `data.data.id` |
|
||||
| `modules/homework/components/scan-uploader.tsx` | 同上 |
|
||||
| `modules/settings/components/avatar-upload.tsx` | 修复原双重 `.json()` bug;适配 `responseJson.data.url` |
|
||||
| `modules/files/hooks/use-file-batch-operations.ts` | `body.deletedCount` → `body.data?.deletedCount` |
|
||||
| `modules/users/components/user-import-dialog.tsx` | `data.sheets?.[0]?.rows` → `data.data?.sheets?.[0]?.rows` |
|
||||
|
||||
### 3.7.6 设计决策
|
||||
|
||||
| 问题 | 决策 | 理由 |
|
||||
|------|------|------|
|
||||
| 搜索 API 架构违规 | 新建 `modules/search/` 模块 | 遵循三层架构 `app → modules → shared`,下沉 DB 查询到 data-access 层 |
|
||||
| deprecated onboarding 路由 | 保留 410 响应 + 统一信封 | 兼容旧客户端,`apiError(..., 410, "deprecated")` 表明废弃原因 |
|
||||
| rate-limit-test 测试端点 | 保留并规范化 | 测试基础设施,使用统一信封便于客户端解析 |
|
||||
| 错误→状态码映射 | errorCode 驱动 | 替代 `result.message?.includes("not found")` 字符串匹配脆弱模式 |
|
||||
| 异常处理 | `withApiErrorHandler` HOF | 消除每个 route 重复 try/catch,统一 `PermissionDeniedError`→403 映射 |
|
||||
|
||||
### 3.7.7 响应信封规范
|
||||
|
||||
```typescript
|
||||
type ApiResponse<T = unknown> = {
|
||||
success: boolean
|
||||
message?: string
|
||||
errorCode?: string
|
||||
data?: T
|
||||
}
|
||||
```
|
||||
|
||||
错误→状态码映射规则:
|
||||
- `PermissionDeniedError` → 403(已认证但无权限)
|
||||
- `NotFoundError` → 404
|
||||
- `ValidationError` / `BusinessError` → 400
|
||||
- 未认证 → 401
|
||||
- 限流 → 429
|
||||
- 其他 → 500
|
||||
|
||||
`ActionState.errorCode` → 状态码映射:
|
||||
- `not_found` → 404
|
||||
- `validation_error` → 400
|
||||
- `auth_required` → 401
|
||||
- `permission_denied` → 403
|
||||
- 其他 → 500
|
||||
|
||||
---
|
||||
|
||||
# 附录 A:模块间依赖矩阵
|
||||
|
||||
> 行表示使用方,列表示被使用方。`✅` 合理依赖,`❌` 违规直查,`⟳` 循环依赖。
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user