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
1536
docs/superpowers/plans/2026-07-04-design-tokens-refactor.md
Normal file
1536
docs/superpowers/plans/2026-07-04-design-tokens-refactor.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,547 @@
|
||||
# 设计令牌专项重构 · 设计文档
|
||||
|
||||
> 创建日期:2026-07-04
|
||||
> 状态:Draft(待用户审查)
|
||||
> 执行方式:大爆炸式(单 PR)
|
||||
> 关联规则:`docs/architecture/004_architecture_impact_map.md`、`.trae/rules/project_rules.md`
|
||||
|
||||
---
|
||||
|
||||
## 1. 背景与目标
|
||||
|
||||
### 1.1 现状摘要
|
||||
|
||||
项目当前令牌散落在 [`src/app/globals.css`](file:///e:/Desktop/CICD/src/app/globals.css)(477 行)与 [`tailwind.config.ts`](file:///e:/Desktop/CICD/tailwind.config.ts)(极简,无 `theme.extend`)。已有 5 类令牌:
|
||||
|
||||
1. **shadcn 标准令牌**(HSL 格式,完整暗色)
|
||||
2. **chart-1~5 数据可视化令牌**(HSL,完整暗色)
|
||||
3. **sidebar 令牌**(HSL,完整暗色)
|
||||
4. **`--lp-*` 备课编辑器令牌**(HEX 格式,**无暗色**,**未在 `@theme inline` 中暴露**,无法 `bg-lp-paper`)
|
||||
5. **Material Design 3 Surface 映射令牌**(在 `@theme inline` 中,**经扫描零使用**,属死代码)
|
||||
|
||||
### 1.2 关键技术债
|
||||
|
||||
扫描结果:
|
||||
|
||||
| 问题类型 | 出现次数 | 文件数 |
|
||||
|---|---|---|
|
||||
| 硬编码颜色 `#xxx` | 91 处 | 15 文件 |
|
||||
| 硬编码字体 `'Inter'/'Fraunces'/'JetBrains Mono'` | 8 处 | 2 文件 |
|
||||
| 硬编码 `font-size: Npx` | 15 处 | 3 文件 |
|
||||
| Tailwind 任意值 `w-[Npx]/h-[Npx]/p-[Npx]/...` | 98 处 | 40 文件 |
|
||||
| 缺失:font/spacing/shadow/text-size 尺度令牌 | — | — |
|
||||
| `--lp-*` 无暗色 / 未暴露为 Tailwind 类 | — | — |
|
||||
| M3 Surface 令牌零使用(死代码) | — | — |
|
||||
|
||||
项目规则(`.trae/rules/project_rules.md`)明确禁止"任意值"和"动态拼接类名",但实际任意值已散落到 40 个文件,需要专项治理。
|
||||
|
||||
### 1.3 重构目标
|
||||
|
||||
- **核心目标**:全量体系化重建设计令牌,建立完整令牌契约,清理全部硬编码与任意值
|
||||
- **命名体系**:双层架构(Primitive + Semantic)+ shadcn 语义扩展
|
||||
- **覆盖范围**:font/size/leading/weight/radius/shadow/spacing/duration/ease/z-index 全阶梯
|
||||
- **暗色覆盖**:明暗双色全面覆盖(所有令牌都有 `:root` 与 `.dark` 双份)
|
||||
- **强制约束**:ESLint 规则 + 注释豁免,防止技术债复发
|
||||
|
||||
---
|
||||
|
||||
## 2. 令牌架构
|
||||
|
||||
### 2.1 双层架构
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│ Layer 1: Primitive (Raw Tokens) — 原始色板/字号/间距/阴影 │
|
||||
│ ───────────────────────────────────────────────────────────── │
|
||||
│ 命名: --color-zinc-50 ~ 950 │
|
||||
│ --color-stone-50 ~ 950 │
|
||||
│ --color-indigo-500/600 │
|
||||
│ --font-size-1 ~ --font-size-9 │
|
||||
│ --space-0 ~ --space-32 │
|
||||
│ --shadow-1 ~ --shadow-6 │
|
||||
│ --font-family-sans/serif/mono │
|
||||
│ 特征: 不直接使用,只被 Semantic 层引用 │
|
||||
│ 主题: 无主题差异(色板层不区分明暗) │
|
||||
└──────────────────────────────────────────────────────────────────┘
|
||||
▲ 引用
|
||||
│
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│ Layer 2: Semantic (Component/Context Tokens) — 语义层 │
|
||||
│ ───────────────────────────────────────────────────────────── │
|
||||
│ shadcn 兼容: --background / --foreground / --primary / ... │
|
||||
│ 扩展层级: --background-elevated / --text-secondary / ... │
|
||||
│ 模块命名空间: --lp-* (lesson-preparation) │
|
||||
│ chart/sidebar: --chart-1~5 / --sidebar-* │
|
||||
│ 特征: 业务代码唯一引用入口,Tailwind 通过 @theme inline 暴露 │
|
||||
│ 主题: :root 与 .dark 双份定义,所有令牌都有暗色 │
|
||||
└──────────────────────────────────────────────────────────────────┘
|
||||
▲ 引用
|
||||
│
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│ Layer 3: Tailwind 类 (via @theme inline) │
|
||||
│ ───────────────────────────────────────────────────────────── │
|
||||
│ bg-background / text-foreground / bg-primary / ... │
|
||||
│ bg-lp-paper / text-lp-inline-node-meta / ... │
|
||||
│ font-sans / font-serif / font-mono │
|
||||
│ text-size-2 / leading-snug / weight-semibold │
|
||||
│ radius-md / shadow-2 / space-4 / duration-fast / z-modal │
|
||||
└──────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2.2 核心原则
|
||||
|
||||
- 业务代码 **只引用 Semantic 层**(直接 `var(--background)` 或 Tailwind 类 `bg-background`),禁止引用 Primitive 层
|
||||
- Primitive 层只为 Semantic 层提供"原材料",便于未来多主题切换(只改 Semantic 层映射,不动 Primitive)
|
||||
- shadcn 命名严格保留(`--background`/`--foreground`/`--primary` 等),`shared/components/ui/*` 零改动
|
||||
- `--lp-*` 保留命名空间(已有 9 文件使用,改名风险大),但补全暗色与 Tailwind 暴露
|
||||
|
||||
### 2.3 文件分布
|
||||
|
||||
当前 [`globals.css`](file:///e:/Desktop/CICD/src/app/globals.css) 477 行,补全令牌后估算 1200+ 行,超过项目规则硬性上限 1000 行。拆分为令牌专题目录:
|
||||
|
||||
```
|
||||
src/app/
|
||||
├─ globals.css # 仅保留 @import、base layer、组件硬编码样式清理后的样式
|
||||
└─ styles/
|
||||
└─ tokens/
|
||||
├─ primitive.css # Layer 1: 原始色板/字号/间距/阴影阶梯
|
||||
├─ semantic-light.css # Layer 2: :root 语义令牌(明色)
|
||||
├─ semantic-dark.css # Layer 2: .dark 语义令牌(暗色)
|
||||
├─ lesson-preparation.css # --lp-* 令牌(明暗双份)
|
||||
├─ tailwind-theme.css # @theme inline 暴露规则
|
||||
└─ index.css # 汇总 @import 入口
|
||||
```
|
||||
|
||||
`globals.css` 顶部改为:
|
||||
|
||||
```css
|
||||
@import "tailwindcss";
|
||||
@import "./styles/tokens/index.css";
|
||||
@plugin "tailwindcss-animate";
|
||||
@plugin "@tailwindcss/typography";
|
||||
@custom-variant dark (&:where(.dark, .dark *));
|
||||
```
|
||||
|
||||
### 2.4 Tailwind v4 集成
|
||||
|
||||
[`tailwind.config.ts`](file:///e:/Desktop/CICD/tailwind.config.ts) 保持极简(只 content + plugins),所有令牌通过 `@theme inline` 在 `tailwind-theme.css` 中暴露。
|
||||
|
||||
---
|
||||
|
||||
## 3. Primitive 层(原始阶梯)
|
||||
|
||||
### 3.1 色板(以 Zinc 为基准中性色,与现有 shadcn 一致)
|
||||
|
||||
```css
|
||||
/* primitive.css */
|
||||
:root {
|
||||
/* Zinc 中性色板(shadcn 默认) */
|
||||
--color-zinc-50: 0 0% 99%;
|
||||
--color-zinc-100: 240 4.8% 95.9%;
|
||||
--color-zinc-200: 240 5.9% 90%;
|
||||
--color-zinc-300: 240 4.8% 83.9%;
|
||||
--color-zinc-400: 240 5% 64.9%;
|
||||
--color-zinc-500: 240 3.8% 46.1%;
|
||||
--color-zinc-600: 240 5.2% 33.9%;
|
||||
--color-zinc-700: 240 5.3% 26.1%;
|
||||
--color-zinc-800: 240 5.9% 10%;
|
||||
--color-zinc-900: 240 5.9% 3.9%;
|
||||
--color-zinc-950: 240 10% 3.9%;
|
||||
|
||||
/* Stone 暖灰(用于 lp-* 纸感,与现有 --lp-paper-edge #f8f8f7 对齐) */
|
||||
--color-stone-50: 60 4.8% 95.9%;
|
||||
--color-stone-100: 60 5.1% 90%;
|
||||
--color-stone-200: 20 5.9% 90%;
|
||||
--color-stone-300: 24 5.7% 82.9%;
|
||||
--color-stone-400: 24 5.4% 63.9%;
|
||||
--color-stone-500: 25 5.1% 44.7%;
|
||||
--color-stone-600: 33 5% 39.8%;
|
||||
--color-stone-700: 30 5.2% 32.7%;
|
||||
--color-stone-800: 12 6.5% 31.4%;
|
||||
--color-stone-900: 24 10% 10%;
|
||||
--color-stone-950: 20 14.3% 4.1%;
|
||||
|
||||
/* Indigo 强调色(lp-interaction 用) */
|
||||
--color-indigo-500: 238.7 83.5% 66.7%;
|
||||
--color-indigo-600: 238.6 84.5% 59.8%;
|
||||
}
|
||||
```
|
||||
|
||||
**说明**: 仅记录 Primitive 原料,业务代码不直接引用。
|
||||
|
||||
### 3.2 字号阶梯(替代硬编码 13.5px/14px/12.5px 等)
|
||||
|
||||
| Token | 值 | 用途 |
|
||||
|---|---|---|
|
||||
| `--font-size-1` | 12px | 元信息/角色标签 |
|
||||
| `--font-size-2` | 13px | inline-node body |
|
||||
| `--font-size-3` | 13.5px | inline-node 主文(备课纸感) |
|
||||
| `--font-size-4` | 14px | 标题/按钮 |
|
||||
| `--font-size-5` | 16px | 正文(Fraunces 16px) |
|
||||
| `--font-size-6` | 18px | H2 |
|
||||
| `--font-size-7` | 20px | H1 |
|
||||
| `--font-size-8` | 24px | 区块标题 |
|
||||
| `--font-size-9` | 32px | 页面标题 |
|
||||
|
||||
### 3.3 间距阶梯(覆盖 98 处任意值)
|
||||
|
||||
| Token | 值 | 替代示例 |
|
||||
|---|---|---|
|
||||
| `--space-0` | 0 | |
|
||||
| `--space-0_5` | 0.125rem (2px) | `p-[2px]` |
|
||||
| `--space-1` | 0.25rem (4px) | `p-[4px]` |
|
||||
| `--space-1_5` | 0.375rem (6px) | `gap-[6px]` |
|
||||
| `--space-2` | 0.5rem (8px) | |
|
||||
| `--space-2_5` | 0.625rem (10px) | `p-[10px]` |
|
||||
| `--space-3` | 0.75rem (12px) | |
|
||||
| `--space-3_5` | 0.875rem (14px) | `p-[14px]` |
|
||||
| `--space-4` | 1rem (16px) | |
|
||||
| `--space-5` | 1.25rem (20px) | `m-[20px]` |
|
||||
| `--space-6` | 1.5rem (24px) | |
|
||||
| `--space-7` | 1.75rem (28px) | `w-[28px]` (lp-tb-btn) |
|
||||
| `--space-8` | 2rem (32px) | |
|
||||
| `--space-10` | 2.5rem (40px) | |
|
||||
| `--space-12` | 3rem (48px) | |
|
||||
| `--space-16` | 4rem (64px) | |
|
||||
| `--space-18` | 4.5rem (72px) | `px-[72px]` (lp-paper-toolbar) |
|
||||
|
||||
### 3.4 阴影阶梯(替代硬编码 box-shadow)
|
||||
|
||||
| Token | 值 | 替代 |
|
||||
|---|---|---|
|
||||
| `--shadow-1` | `0 1px 2px rgba(15,15,15,0.04)` | shadow-xs |
|
||||
| `--shadow-2` | `0 1px 3px rgba(15,15,15,0.06), 0 1px 2px rgba(15,15,15,0.04)` | shadow-sm |
|
||||
| `--shadow-3` | `0 4px 6px rgba(15,15,15,0.05), 0 2px 4px rgba(15,15,15,0.04)` | shadow-md |
|
||||
| `--shadow-4` | `0 1px 2px rgba(15,15,15,0.04), 0 8px 24px rgba(15,15,15,0.04)` | lp-paper-shadow |
|
||||
| `--shadow-5` | `0 1px 2px rgba(15,15,15,0.06), 0 12px 36px rgba(15,15,15,0.08)` | lp-paper-shadow-active |
|
||||
| `--shadow-6` | `0 10px 15px rgba(15,15,15,0.1), 0 4px 6px rgba(15,15,15,0.05)` | shadow-xl |
|
||||
|
||||
### 3.5 字体家族(替代 8 处硬编码)
|
||||
|
||||
| Token | 值 | 用途 |
|
||||
|---|---|---|
|
||||
| `--font-family-sans` | `'Inter', system-ui, sans-serif` | UI 元素 |
|
||||
| `--font-family-serif` | `'Fraunces', Georgia, serif` | 主文/备课纸感 |
|
||||
| `--font-family-mono` | `'JetBrains Mono', monospace` | 角色标签 |
|
||||
|
||||
---
|
||||
|
||||
## 4. Semantic 层(shadcn 扩展 + lp-* 升级 + M3 清理)
|
||||
|
||||
### 4.1 shadcn 标准令牌(零改动,确保 ui/ 兼容)
|
||||
|
||||
`--background`/`--foreground`/`--card`/`--popover`/`--primary`/`--secondary`/`--muted`/`--accent`/`--destructive`/`--border`/`--input`/`--ring`/`--radius` 全部保留现有 HSL 值,`:root` 与 `.dark` 双份定义不变。
|
||||
|
||||
### 4.2 语义层扩展(新增层级)
|
||||
|
||||
| Token | 明色 HSL | 暗色 HSL | 用途 |
|
||||
|---|---|---|---|
|
||||
| `--background-elevated` | `0 0% 100%` | `240 6% 10%` | 浮层/card-on-card |
|
||||
| `--background-sunken` | `240 4.8% 95.9%` | `240 6% 8%` | 凹陷区/inset |
|
||||
| `--text-primary` | `= --foreground` | `= --foreground` | 主文 |
|
||||
| `--text-secondary` | `240 3.8% 46.1%` | `240 5% 64.9%` | 副文(= muted-foreground) |
|
||||
| `--text-tertiary` | `240 4% 65%` | `240 5% 50%` | 占位/元信息 |
|
||||
| `--border-strong` | `240 5.9% 70%` | `240 5% 40%` | 强调边框 |
|
||||
| `--border-subtle` | `240 5.9% 95%` | `240 5% 18%` | 弱边框 |
|
||||
|
||||
**说明**: `--text-secondary` 等是 `--muted-foreground` 的语义别名,业务代码优先用语义名。
|
||||
|
||||
### 4.3 chart-1~5 / sidebar 令牌(零改动)
|
||||
|
||||
保留现有 HSL 值,`:root` 与 `.dark` 双份定义不变。
|
||||
|
||||
### 4.4 --lp-* 令牌升级(HEX→HSL + 暗色 + Tailwind 暴露)
|
||||
|
||||
**明色(`:root`)**:
|
||||
|
||||
| Token | 旧 HEX | 新 HSL | 说明 |
|
||||
|---|---|---|---|
|
||||
| `--lp-paper` | `#fefefe` | `0 0% 99.6%` | 纸面 |
|
||||
| `--lp-paper-edge` | `#f8f8f7` | `60 9% 97%` | 纸边(对齐 stone-100) |
|
||||
| `--lp-paper-shadow` | (box-shadow) | `= --shadow-4` 引用 | 阴影令牌化 |
|
||||
| `--lp-paper-shadow-active` | (box-shadow) | `= --shadow-5` 引用 | 阴影令牌化 |
|
||||
| `--lp-anchor-range` | `rgba(28,25,23,0.08)` | `20 14.3% 4.1% / 0.08` | 引用 stone-950 |
|
||||
| `--lp-anchor-range-active` | `rgba(28,25,23,0.16)` | `20 14.3% 4.1% / 0.16` | |
|
||||
| `--lp-anchor-point` | `#1c1917` | `20 14.3% 4.1%` | = stone-950 |
|
||||
| `--lp-inline-node-border` | `#d6d3d1` | `30 5.7% 82.9%` | = stone-300 |
|
||||
| `--lp-inline-node-text` | `#44403c` | `24 5.4% 26.9%` | ≈ stone-700 微调 |
|
||||
| `--lp-inline-node-meta` | `#a8a29e` | `30 5% 64.9%` | = stone-400 |
|
||||
| `--lp-interaction` | `#6366f1` | `238.6 84.5% 59.8%` | = indigo-600 |
|
||||
|
||||
**12 个 --lp-dot-* 节点色点**: 全部映射到 zinc/stone 色板对应值,统一为 HSL。
|
||||
|
||||
**暗色(`.dark`)**: 全部补齐。例如:
|
||||
|
||||
- `--lp-paper: 240 6% 10%`(暗背景纸面)
|
||||
- `--lp-paper-edge: 240 6% 8%`
|
||||
- `--lp-anchor-point: 0 0% 98%`(反相)
|
||||
- `--lp-interaction: 238.7 83.5% 66.7%`(= indigo-500,暗色微亮)
|
||||
|
||||
### 4.5 M3 Surface 映射令牌清理
|
||||
|
||||
[`globals.css:185-198`](file:///e:/Desktop/CICD/src/app/globals.css#L185-L198) 中的 M3 令牌零使用(已扫描确认),全部删除:
|
||||
|
||||
- `--color-surface` / `--color-on-surface` / `--color-on-surface-variant`
|
||||
- `--color-surface-container-lowest/low/high/highest`
|
||||
- `--color-outline-variant` / `--color-outline`
|
||||
- `--color-error` / `--color-tertiary` / `--color-tertiary-container`
|
||||
|
||||
### 4.6 globals.css 硬编码样式令牌化
|
||||
|
||||
| 硬编码 | 令牌化后 |
|
||||
|---|---|
|
||||
| `.lp-tb-btn { width: 28px; ... font-family: 'Inter'; font-size: 13px; color: var(--foreground) }` | `width: var(--space-7); font-family: var(--font-family-sans); font-size: var(--font-size-2); color: hsl(var(--foreground))` |
|
||||
| `.lp-inline-node { font-family: 'Inter'; font-size: 13.5px; ... border-left: 2px solid var(--lp-inline-node-border); color: var(--lp-inline-node-text) }` | 全部用令牌引用 |
|
||||
| `.lp-qa-role.teacher { color: #1c1917 }` | `color: hsl(var(--lp-anchor-point))` |
|
||||
| `.lp-qa-role.student { color: #6b7280 }` | `color: hsl(var(--color-zinc-500))` |
|
||||
| `.lp-inline-node-title { color: #1a1a1a }` | `color: hsl(var(--lp-anchor-point))` |
|
||||
| `.lp-inline-node-body { color: #404040 }` | `color: hsl(var(--lp-inline-node-text))` |
|
||||
| `.lp-qa-content { color: #404040 }` | 同上 |
|
||||
| `.lp-qa-prompt { color: #525252 }` | `color: hsl(var(--color-zinc-600))` |
|
||||
| `.range-anchor { background-color: var(--node-color, #1976d2) }` | fallback 改为 `hsl(var(--lp-interaction))` |
|
||||
|
||||
---
|
||||
|
||||
## 5. 尺度令牌补齐清单
|
||||
|
||||
### 5.1 圆角阶梯
|
||||
|
||||
| Token | 值 | Tailwind 类 |
|
||||
|---|---|---|
|
||||
| `--radius-sm` | `calc(var(--radius) - 4px)` | `rounded-sm` |
|
||||
| `--radius-md` | `calc(var(--radius) - 2px)` | `rounded-md` |
|
||||
| `--radius-lg` | `var(--radius)` = `0.5rem` | `rounded-lg` |
|
||||
| `--radius-xl` | `calc(var(--radius) + 4px)` | `rounded-xl` |
|
||||
| `--radius-2xl` | `calc(var(--radius) + 8px)` | `rounded-2xl` |
|
||||
| `--radius-full` | `9999px` | `rounded-full` |
|
||||
|
||||
### 5.2 行高与字重
|
||||
|
||||
| Token | 值 |
|
||||
|---|---|
|
||||
| `--leading-tight` | 1.2 |
|
||||
| `--leading-snug` | 1.35 |
|
||||
| `--leading-normal` | 1.5 |
|
||||
| `--leading-relaxed` | 1.65 |
|
||||
| `--leading-loose` | 1.8 |
|
||||
| `--weight-regular` | 400 |
|
||||
| `--weight-medium` | 500 |
|
||||
| `--weight-semibold` | 600 |
|
||||
| `--weight-bold` | 700 |
|
||||
|
||||
### 5.3 动效与 z-index
|
||||
|
||||
| Token | 值 | Tailwind 类 |
|
||||
|---|---|---|
|
||||
| `--duration-fast` | 150ms | `duration-fast` |
|
||||
| `--duration-normal` | 200ms | `duration-normal` |
|
||||
| `--duration-slow` | 300ms | `duration-slow` |
|
||||
| `--ease-in` | `cubic-bezier(0.4, 0, 1, 1)` | `ease-in` |
|
||||
| `--ease-out` | `cubic-bezier(0, 0, 0.2, 1)` | `ease-out` |
|
||||
| `--ease-in-out` | `cubic-bezier(0.4, 0, 0.2, 1)` | `ease-in-out` |
|
||||
| `--z-dropdown` | 1000 | `z-dropdown` |
|
||||
| `--z-sticky` | 1100 | `z-sticky` |
|
||||
| `--z-modal` | 1300 | `z-modal` |
|
||||
| `--z-popover` | 1400 | `z-popover` |
|
||||
| `--z-toast` | 1500 | `z-toast` |
|
||||
|
||||
---
|
||||
|
||||
## 6. 硬编码清理映射表(分级)
|
||||
|
||||
### 6.1 91 处 #hex 颜色 — 按文件分类
|
||||
|
||||
| 文件 | 处数 | 清理策略 |
|
||||
|---|---|---|
|
||||
| `app/globals.css` | 32 | 全部令牌化(见 4.6 节) |
|
||||
| `app/manifest.ts` | 2 | 保留(theme_color 元数据,需字面量),加注释 `// arbitrary-value: PWA manifest requires literal hex` |
|
||||
| `app/layout.tsx` | 2 | 检查后令牌化或注释 |
|
||||
| `shared/components/ui/chart.tsx` | 5 | 令牌化(用 `--chart-1~5`) |
|
||||
| `modules/notifications/channels/email-channel.ts` | 7 | 保留(邮件 HTML 内联样式需字面量),加注释 |
|
||||
| `modules/textbooks/components/knowledge-graph.tsx` | 10 | 令牌化(用 `--chart-*` 或新增 `--graph-node-*`) |
|
||||
| `modules/textbooks/components/force-graph.tsx` | 17 | 同上 |
|
||||
| `modules/lesson-preparation/components/paper-editor/*` | ~6 | 令牌化(用 `--lp-*`) |
|
||||
| `modules/attendance/components/attendance-grade-correlation-card.tsx` | 3 | 令牌化(用 `--chart-*`) |
|
||||
| `modules/classes/components/my-classes-grid.tsx` | 2 | 令牌化 |
|
||||
| `modules/lesson-preparation/components/version-diff-view.tsx` | 1 | 令牌化(用 `--lp-*` 或新增 `--diff-add`/`--diff-remove`) |
|
||||
|
||||
**新增语义令牌**:
|
||||
|
||||
- `--diff-add` / `--diff-add-bg`(版本对比新增)
|
||||
- `--diff-remove` / `--diff-remove-bg`(版本对比删除)
|
||||
- `--graph-node-1` ~ `--graph-node-6`(知识图谱节点色)
|
||||
|
||||
### 6.2 8 处硬编码 font-family — 全部令牌化
|
||||
|
||||
| 文件 | 处数 | 令牌化 |
|
||||
|---|---|---|
|
||||
| `app/globals.css` | 7 | `var(--font-family-sans)` / `var(--font-family-serif)` / `var(--font-family-mono)` |
|
||||
| `modules/lesson-preparation/components/paper-editor/textbook-tiptap-editor.tsx` | 1 | 同上 |
|
||||
|
||||
### 6.3 15 处硬编码 font-size — 全部令牌化
|
||||
|
||||
| 文件 | 处数 | 令牌化 |
|
||||
|---|---|---|
|
||||
| `app/globals.css` | 13 | `var(--font-size-N)` |
|
||||
| `modules/notifications/channels/email-channel.ts` | 1 | 保留(邮件 HTML),加注释 |
|
||||
| `modules/lesson-preparation/components/paper-editor/textbook-tiptap-editor.tsx` | 1 | `var(--font-size-N)` |
|
||||
|
||||
### 6.4 98 处 Tailwind 任意值 — 分级清理
|
||||
|
||||
**Tier 1 — 直接映射 Tailwind 默认阶梯**(约 40 处):
|
||||
|
||||
- `w-[28px]` → `w-7`(28px ≈ 1.75rem,但 w-7 = 1.75rem ✓)
|
||||
- `gap-[6px]` → `gap-1.5`(6px = 0.375rem,gap-1.5 ✓)
|
||||
- `p-[10px]` → 需新增 `--space-2_5`(10px = 0.625rem)
|
||||
- `text-[13px]` → `text-size-2`(13px = `--font-size-2`)
|
||||
|
||||
**Tier 2 — 新增间距令牌后引用**(约 30 处):
|
||||
|
||||
- `w-[72px]` → `w-space-18`(`--space-18` = 4.5rem)
|
||||
- `h-[16px]` → `h-space-4`(`--space-4` = 1rem)
|
||||
- `p-[20px]` → `p-space-5`(`--space-5` = 1.25rem)
|
||||
|
||||
**Tier 3 — 注释豁免**(约 28 处):
|
||||
|
||||
- 图表 SVG 尺寸(如 `w-[400px]` 在 chart 容器)→ 加注释 `// arbitrary-value: chart canvas fixed size`
|
||||
- 邮件 HTML 内联样式
|
||||
- loading.tsx 占位骨架特殊尺寸
|
||||
|
||||
**ESLint 规则**: `tailwindcss/no-arbitrary-value: error`,允许通过 `// eslint-disable-next-line` + 注释理由豁免。
|
||||
|
||||
---
|
||||
|
||||
## 7. 强制约束机制
|
||||
|
||||
### 7.1 ESLint 规则新增
|
||||
|
||||
**新增依赖**: `eslint-plugin-tailwindcss`(需验证与 Tailwind v4 兼容性,若不兼容则自定义规则)
|
||||
|
||||
**`eslint.config.mjs` 新增规则**:
|
||||
|
||||
```javascript
|
||||
{
|
||||
// 禁止 Tailwind 任意值(允许 eslint-disable + 注释豁免)
|
||||
'tailwindcss/no-arbitrary-value': 'error',
|
||||
// 禁止 TSX/CSS 中硬编码颜色字面量(白名单:globals.css primitive 定义、email-channel.ts、manifest.ts)
|
||||
'no-restricted-syntax': [
|
||||
'error',
|
||||
{
|
||||
selector: "Literal[value=/#[0-9a-fA-F]{3,8}/]",
|
||||
message: '禁止硬编码 hex 颜色,使用设计令牌 var(--*) 或 Tailwind 类 bg-*',
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
**自定义规则文件**: `eslint-rules/no-hardcoded-design-tokens.js`
|
||||
|
||||
- 检测 TSX/TS 中 `'Inter'`/`'Fraunces'`/`'JetBrains Mono'` 字面量
|
||||
- 检测 `font-family:` CSS 属性中的字面量字体名
|
||||
- 检测 `font-size: Npx` 硬编码
|
||||
- 白名单:`globals.css`、`styles/tokens/*.css`、`email-channel.ts`、`manifest.ts`
|
||||
|
||||
### 7.2 项目规则更新
|
||||
|
||||
[`.trae/rules/project_rules.md`](file:///e:/Desktop/CICD/.trae/rules/project_rules.md) "Tailwind 规范"章节新增:
|
||||
|
||||
```markdown
|
||||
### 设计令牌规范(强制)
|
||||
|
||||
- **禁止硬编码颜色**: TSX/TS/CSS 中不得出现 `#hex` 颜色字面量,统一使用 `var(--*)` 或 Tailwind 类
|
||||
- **禁止硬编码字体**: 不得出现 `'Inter'`/`'Fraunces'`/`'JetBrains Mono'` 字面量,使用 `var(--font-family-sans/serif/mono)`
|
||||
- **禁止硬编码字号**: 不得出现 `font-size: Npx`,使用 `var(--font-size-1~9)`
|
||||
- **禁止 Tailwind 任意值**: 不得使用 `w-[Npx]`/`h-[Npx]`/`p-[Npx]` 等,映射到 `--space-*` 或 Tailwind 默认阶梯
|
||||
- **豁免场景**(需 `// arbitrary-value: <reason>` 注释):
|
||||
- PWA manifest(`app/manifest.ts`)
|
||||
- 邮件 HTML 内联样式(`email-channel.ts`)
|
||||
- 图表 SVG 固定画布尺寸
|
||||
- loading.tsx 占位骨架
|
||||
- **令牌文件分布**: `src/app/styles/tokens/`(primitive/semantic-light/semantic-dark/lesson-preparation/tailwind-theme)
|
||||
- **改令牌必同步图**: 修改令牌定义后,同步更新 `docs/architecture/004` 与 `005`
|
||||
```
|
||||
|
||||
### 7.3 Lint 命令不变
|
||||
|
||||
`npm run lint` 与 `npx tsc --noEmit` 保持零错误零警告。
|
||||
|
||||
---
|
||||
|
||||
## 8. 验证策略与架构图同步
|
||||
|
||||
### 8.1 验证清单
|
||||
|
||||
| 验证项 | 命令/方法 | 通过标准 |
|
||||
|---|---|---|
|
||||
| TypeScript | `npx tsc --noEmit` | 零错误 |
|
||||
| ESLint | `npm run lint` | 零错误零警告 |
|
||||
| 视觉回归 | `npm run test:visual` | 快照无差异(或更新后人工确认) |
|
||||
| 备课编辑器视觉 | 手动检查 `teacher/lesson-plans/[id]/edit` | 纸感、inline-node、师生对话样式无变化 |
|
||||
| 暗色模式 | 切换暗色主题 | 所有页面可读,无对比度问题 |
|
||||
| shadcn ui 组件 | 抽查 button/card/dialog/table | 零回归 |
|
||||
|
||||
### 8.2 架构图同步(项目规则强制)
|
||||
|
||||
修改完成后,同步更新:
|
||||
|
||||
| 文档 | 同步内容 |
|
||||
|---|---|
|
||||
| [`docs/architecture/004_architecture_impact_map.md`](file:///e:/Desktop/CICD/docs/architecture/004_architecture_impact_map.md) | 新增"设计令牌体系"章节(双层架构图、文件分布、令牌分类) |
|
||||
| [`docs/architecture/005_architecture_data.json`](file:///e:/Desktop/CICD/docs/architecture/005_architecture_data.json) | `modules.shared.exports` 新增 `design-tokens` 节点;`dependencyMatrix` 新增令牌依赖关系 |
|
||||
| [`docs/troubleshooting/known-issues.md`](file:///e:/Desktop/CICD/docs/troubleshooting/known-issues.md) | 新增"设计令牌"问题分类,记录 Tailwind v4 + ESLint 兼容性、HEX→HSL 转换、任意值豁免规则 |
|
||||
|
||||
### 8.3 大爆炸式执行顺序(单 PR 内)
|
||||
|
||||
1. 创建 `src/app/styles/tokens/` 6 个文件(primitive/semantic-light/semantic-dark/lesson-preparation/tailwind-theme/index)
|
||||
2. `globals.css` 改为 `@import` 引入,删除已迁移内容
|
||||
3. `globals.css` 中剩余硬编码样式令牌化(lp-tb-btn/lp-inline-node/lp-qa-*)
|
||||
4. 全局扫描替换:91 处 `#hex` → 令牌引用(按 6.1 表)
|
||||
5. 全局扫描替换:8 处 font-family + 15 处 font-size → 令牌引用(按 6.2/6.3 表)
|
||||
6. 全局扫描替换:98 处任意值 → Tier 1/2/3 分级处理(按 6.4 表)
|
||||
7. ESLint 规则新增 + 自定义规则文件
|
||||
8. 项目规则文档更新
|
||||
9. 架构图同步(004/005)
|
||||
10. `docs/troubleshooting/known-issues.md` 更新
|
||||
11. `npm run lint` + `npx tsc --noEmit` + `npm run test:visual` 验证
|
||||
12. 单次 commit:`refactor(design-tokens): 全量体系化重建设计令牌`
|
||||
|
||||
---
|
||||
|
||||
## 9. 风险与缓解
|
||||
|
||||
| 风险 | 缓解 |
|
||||
|---|---|
|
||||
| Tailwind v4 + `eslint-plugin-tailwindcss` 兼容性未知 | 实施时先验证,不兼容则自定义 AST 规则 |
|
||||
| 大爆炸式改动 50+ 文件,review 困难 | 严格按 8.3 执行顺序,每步独立验证;视觉回归测试保障 |
|
||||
| `--lp-*` HEX→HSL 转换可能有色差 | 视觉回归测试 + 手动备课编辑器检查 |
|
||||
| 98 处任意值分级清理可能遗漏 | Tier 3 注释豁免需明确理由,ESLint 规则锁定 |
|
||||
| 暗色补全可能对比度不足 | 暗色模式手动抽查所有页面 |
|
||||
| shadcn ui 组件理论零改动但需验证 | 抽查 button/card/dialog/table 4 个核心组件 |
|
||||
|
||||
---
|
||||
|
||||
## 10. 范围与非目标
|
||||
|
||||
### 10.1 范围内
|
||||
|
||||
- 令牌底座重建(Primitive + Semantic 双层)
|
||||
- 尺度令牌补齐(font/size/leading/weight/radius/shadow/spacing/duration/ease/z-index)
|
||||
- `--lp-*` 升级(HEX→HSL + 暗色 + Tailwind 暴露)
|
||||
- M3 Surface 死代码清理
|
||||
- 91 处硬编码颜色清理
|
||||
- 8 处硬编码 font-family 清理
|
||||
- 15 处硬编码 font-size 清理
|
||||
- 98 处 Tailwind 任意值清理
|
||||
- ESLint 强制约束规则
|
||||
- 项目规则文档更新
|
||||
- 架构图同步(004/005)
|
||||
- known-issues.md 更新
|
||||
|
||||
### 10.2 范围外(非目标)
|
||||
|
||||
- 第三主题(如 high-contrast、paper 纸感)预留 — YAGNI
|
||||
- `shared/components/ui/*` shadcn 组件内部样式改动 — 理论零改动,仅验证
|
||||
- `cn()` 工具函数修改 — 不变
|
||||
- `tailwind.config.ts` 内容扩展 — 保持极简
|
||||
- 设计令牌文档站(Storybook 等)— 不在本期范围
|
||||
- 多主题切换架构 — 仅明暗双色
|
||||
@@ -545,6 +545,112 @@ export function AnnouncementPagination({ page, pageSize, total, basePath, status
|
||||
|
||||
---
|
||||
|
||||
## 备课模块审核问题(lesson-preparation audit)
|
||||
|
||||
### Server Action 权限常量引用规则
|
||||
|
||||
| 规则 | 正确写法 | 错误写法 |
|
||||
|------|---------|---------|
|
||||
| 权限校验必须引用 `Permissions` 常量 | `await requirePermission(Permissions.LESSON_PLAN_READ)` | `await requirePermission("lesson_plan:read")` |
|
||||
| Server Action 返回值统一用 `ActionState<T>` | `Promise<ActionState<null>>` / `Promise<ActionState<{ planId: string }>>` | `Promise<ActionState>` |
|
||||
|
||||
涉及文件:`actions-analytics.ts:38,61,76,91`、`actions.ts:142,248,263,282`
|
||||
|
||||
### TypeScript `as` 断言规则
|
||||
|
||||
| 规则 | 正确写法 | 错误写法 |
|
||||
|------|---------|---------|
|
||||
| 字面量收窄用类型守卫 | `isLessonPlanStatus(v) ? v : "draft"` | `"published" as LessonPlanStatus` |
|
||||
| DB JSON 字段转换用类型守卫 | `isLessonPlanDocument(content) ? content : null` | `content as unknown as LessonPlanDocument` |
|
||||
| select onChange 值用类型守卫 | `isTeachingStage(v) && updateNode(id, { stage: v })` | `updateNode(id, { stage: v as TeachingStage })` |
|
||||
| 判别联合字段提取用类型守卫 | `isInteractionBlockData(node.data) && ...` | `node.data as InteractionBlockData` |
|
||||
| AI patch 合并到节点 data 用注册表守卫 | `mergeBlockDataPatch(node, patch)`(内部用 `BLOCK_DATA_GUARDS: Record<BlockType, BlockDataGuard>` 校验,失败回退原 data) | `{ ...node.data, ...patch } as BlockData` |
|
||||
| DB enum 字段安全窄化用 `toXxx` 辅助函数 | `toLessonPlanStatus(r.status)` / `toMessageReportReason(r.reason)`(守卫失败返回 fallback 默认值) | `r.status as LessonPlanStatus` / `r.reason as MessageReportReason` |
|
||||
| 类型守卫参数类型放宽到 `unknown` | `function isRichTextBlockData(data: unknown): data is RichTextBlockData`(兼容 `BlockDataGuard = (data: unknown) => data is BlockData` 注册表签名) | `function isRichTextBlockData(data: BlockData): data is RichTextBlockData`(无法赋值给 `BlockDataGuard`,触发逆变错误) |
|
||||
| AI 输出 patch 类型用 `Record<string, unknown>` | `interface NodeContentUpdate { data: Record<string, unknown> }`(诚实反映 Zod `z.record(z.string(), z.unknown())` 校验后的 untrusted 输出,强制调用方走 `mergeBlockDataPatch`) | `interface NodeContentUpdate { data: Partial<BlockData> }`(伪装成可信类型,调用方直接 `as BlockData` 断言) |
|
||||
| switch 分发用类型守卫而非 `as` | `case "objective": return isObjectiveBlockData(data) ? flattenObjective(data, t) : []` | `case "objective": return flattenObjective(data as ObjectiveBlockData, t)` |
|
||||
| TextbookContentNode 分支用 `unknown` 中间变量 | `const merged: unknown = { ...n, ...patch }; return merged as TextbookContentNode`(结构类型不兼容 Block.data 联合,从 unknown 收窄需 `as`) | `return { ...n, ...patch } as unknown as TextbookContentNode`(双重断言) |
|
||||
|
||||
严重违规双重断言:
|
||||
- `structure-tree.tsx:72` `{ ...textbookNode, type: "textbook_content" } as unknown as Block`
|
||||
- `version-diff-viewer.tsx:38` `selectedVersion.content as unknown as LessonPlanDocument`
|
||||
|
||||
**类型守卫专项重构(2026-07-04)涉及文件**:
|
||||
- `lesson-preparation/lib/type-guards.ts` — 11 个 `isXxxBlockData` 守卫参数放宽到 `unknown`,新增 `mergeBlockDataPatch` + `BLOCK_DATA_GUARDS` 注册表
|
||||
- `lesson-preparation/lib/export.ts` — 14 处 `as` 替换为 `isXxxBlockData` 守卫
|
||||
- `lesson-preparation/data-access-calendar.ts` — 新增 `toLessonPlanStatus`,移除 6 处 `as LessonPlanStatus`
|
||||
- `lesson-preparation/data-access-review.ts` — 移除 5 处 `as`(含 `as LessonPlanStatus` / `as ReviewDecision`)
|
||||
- `lesson-preparation/hooks/editor-slice.ts` — `updateNode` 显式标注 `AnyLessonPlanNode` 返回类型 + `unknown` 中间变量
|
||||
- `lesson-preparation/hooks/use-node-ai-assist.ts` — 4 处 `as BlockData` 替换为 `mergeBlockDataPatch`
|
||||
- `lesson-preparation/lib/ai-node-assist.ts` — `NodeContentUpdate.data` 改为 `Record<string, unknown>`,2 处 `as Partial<BlockData>` 移除
|
||||
- `messaging/lib/type-guards.ts`(新建)— `isRecipientRole` / `isMessageReportReason` / `isMessageReportStatus` + `toMessageReportReason` / `toMessageReportStatus`
|
||||
- `messaging/data-access.ts` — 5 处字面量 `as RecipientRole` 移除 + 2 处 `mapMessageReport` 改用 `toXxx` 辅助
|
||||
- `messaging/components/message-report-block.tsx` — `setReason(v as MessageReportReason)` 替换为 `if (isMessageReportReason(v)) setReason(v)`
|
||||
|
||||
### 非空断言 `!.` 规则
|
||||
|
||||
| 规则 | 正确写法 | 错误写法 |
|
||||
|------|---------|---------|
|
||||
| 数组首元素先做存在性判断 | `const row = rows[0]; if (!row) return; row.resolved` | `rows[0]!.resolved` |
|
||||
| Map.get 后做空值处理 | `const arr = chapterMap.get(chId); if (arr) arr.push(kp)` | `chapterMap.get(chId)!.push(kp)` |
|
||||
|
||||
涉及文件:`data-access-comments.ts:113`、`data-access-review.ts:51,83,217`、`data-access-substitutes.ts:131`、`lib/curriculum-coverage.ts:90`
|
||||
|
||||
### ESLint 零警告规则
|
||||
|
||||
| 规则 | 正确写法 | 错误写法 |
|
||||
|------|---------|---------|
|
||||
| effect 中读 localStorage 用 `useEffectEvent` 或加依赖 | `useEffect(() => { setRecentIds(readRecentTextbookIds()) }, [])` 改用 `useSyncExternalStore` 或初始化函数 | `useEffect(() => { setRecentIds(readRecentTextbookIds()) }, [])` 触发 set-state-in-effect |
|
||||
| 禁止 `eslint-disable-next-line` | 修正依赖数组或用 `useCallback` 包裹 | `// eslint-disable-next-line react-hooks/exhaustive-deps` |
|
||||
|
||||
涉及文件:`template-picker.tsx:72`(error)、`schedule-dialog.tsx:53`(warning)、`lesson-plan-editor.tsx:81`(disable)
|
||||
|
||||
### Tailwind 任意值规则
|
||||
|
||||
| 规则 | 正确写法 | 错误写法 |
|
||||
|------|---------|---------|
|
||||
| 优先映射 Tailwind 默认阶梯 | `min-h-[40px]` → `min-h-10`、`min-w-[80px]` → `min-w-20` | 保留 `min-h-[40px]` 裸任意值 |
|
||||
| 无法令牌化的固定尺寸加 `arbitrary-value:` 豁免注释 | `{/* arbitrary-value: dialog fixed width */}` 置于 JSX 元素上方 | 裸用 `w-[680px]` `min-h-[120px]` `text-[10px]` 无注释 |
|
||||
| JSX 子元素上下文用 `{/* */}` 注释 | `<div>{/* arbitrary-value: ... */}\n<span .../>` | `//` 会渲染为文本 |
|
||||
| `return (` / `&& (` 等 JS 表达式上下文用 `//` 注释 | `return (\n // arbitrary-value: ...\n <div/>)` | `{/* */}` 在 `()` 内触发语法错误 |
|
||||
| Tiptap editorProps.attributes 等 JS 对象用 `//` 注释 | `attributes: {\n // arbitrary-value: tiptap editor fixed size\n class: "..."}` | `{/* */}` 在 JS 对象内非法 |
|
||||
|
||||
涉及 lesson-preparation 模块 23 文件(Task 11 已清理):Tier 1 替换 6 处(min-h-10/min-h-20/min-w-20),Tier 3 豁免 26 处(dialog 宽度/textarea min-h/badge text-[10px]/tiptap 等)
|
||||
|
||||
### i18n 翻译文件对称性规则
|
||||
|
||||
| 规则 | 正确写法 | 错误写法 |
|
||||
|------|---------|---------|
|
||||
| zh-CN 与 en 必须 key 完全对称 | 同步增删 key,CI 校验 key 差集 | 单边新增 key,导致 fallback |
|
||||
| 缺失 key 必须补齐 | `analytics.publishedPlans` 等同步到 zh-CN | en 有 12 个 analytics.* key 但 zh-CN 缺失 |
|
||||
|
||||
涉及文件:`zh-CN/lesson-preparation.json`(缺 12 个 analytics.* key)、`en/lesson-preparation.json`(缺 `analytics.templateUsage`)
|
||||
|
||||
### i18n 硬编码中文规则
|
||||
|
||||
| 规则 | 正确写法 | 错误写法 |
|
||||
|------|---------|---------|
|
||||
| 用户可见文本必须走 i18n | `t("v4.contextMenu.copySuffix")` | `${src.title}(副本)` 硬编码 |
|
||||
| 导出/打印文本通过 i18n 注入 | `flattenObjective(data, t)` 接受翻译函数 | `dimensionLabel: { knowledge: "知识与技能" }` 硬编码 |
|
||||
| 师生角色标签用 i18n | `t("v4.interaction.roleTeacher")` | `turn.role === "teacher" ? "师" : "生"` 硬编码 |
|
||||
|
||||
严重违规:
|
||||
- [editor-slice.ts:181](file:///e:/Desktop/CICD/src/modules/lesson-preparation/hooks/editor-slice.ts#L181) `title: \`${src.title}(副本)\`` 硬编码"副本"
|
||||
- [lib/export.ts:165-271](file:///e:/Desktop/CICD/src/modules/lesson-preparation/lib/export.ts#L165) 5 个 label 映射表全硬编码中文(dimensionLabel/importMethodLabels/homeworkTypeLabels/blackboardLayoutLabels/reflectionAspectLabels)
|
||||
- [lib/export.ts:246](file:///e:/Desktop/CICD/src/modules/lesson-preparation/lib/export.ts#L246) `item.source === "inline" ? "课案内新建" : "题库"` 硬编码
|
||||
- [lib/export.ts:271](file:///e:/Desktop/CICD/src/modules/lesson-preparation/lib/export.ts#L271) `turn.role === "teacher" ? "师" : "生"` 硬编码
|
||||
|
||||
### AI Prompt 中文常量规则(允许)
|
||||
|
||||
| 规则 | 正确写法 | 错误写法 |
|
||||
|------|---------|---------|
|
||||
| AI prompt 提取为模块常量 | `const AI_SUGGEST_PROMPT_TEMPLATE = \`...\`` | 散落在函数体内 |
|
||||
| AI prompt 不强制 i18n | 开发期调优的固定 prompt,可保持中文 | — |
|
||||
|
||||
合规文件:`ai-suggest.ts:31`、`lib/ai-differentiation.ts:80,92,103` 已提取为常量
|
||||
|
||||
---
|
||||
|
||||
## 二十、i18n Namespace 与文件名一致性规则
|
||||
|
||||
| 规则 | 正确写法 | 错误写法 |
|
||||
@@ -694,3 +800,158 @@ Zod schema 中存储的 i18n 键(如 `"error.titleRequired"`)在服务端翻
|
||||
| 规则 | 正确写法 | 错误写法 |
|
||||
|------|---------|---------|
|
||||
| 动态键翻译前必须用 t.has() 守卫 | `if (t.has(msg)) { return t(msg); } return msg;` | `return t(msg);` — 键不存在时抛 MISSING_MESSAGE |
|
||||
|
||||
---
|
||||
|
||||
## 设计令牌替换规则(chart/graph 组件 #hex 清理)
|
||||
|
||||
| 规则 | 正确写法 | 错误写法 |
|
||||
|------|---------|---------|
|
||||
| chart 颜色配置用 `--chart-1~5` 令牌 | `color: "hsl(var(--chart-1))"` | `color: "#e11d48"` |
|
||||
| graph 节点色用 `--graph-node-1~6` 令牌 | `"hsl(var(--graph-node-1))"` | `"#3b82f6"` |
|
||||
| 节点默认/未评估色用 `--muted-foreground` | `"hsl(var(--muted-foreground))"` | `"#6b7280"` / `"#94a3b8"` |
|
||||
| 超过 6 色的调色板按顺序循环复用 graph-node-1~6 | `["hsl(var(--graph-node-1))", ..., "hsl(var(--graph-node-6))", "hsl(var(--graph-node-1))", ...]` | 保留 8/12 个 #hex 不处理 |
|
||||
| **recharts CSS 属性选择器中的 #hex 不可替换** | `[&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50`(#ccc/#fff 是 recharts 默认输出值,选择器需原样匹配) | `[stroke='hsl(var(--border))']`(选择器无法匹配 recharts 实际输出,功能失效) |
|
||||
|
||||
涉及文件:
|
||||
- `src/shared/components/ui/chart.tsx:54` — 5 处 `#ccc`/`#fff` 保留(CSS 属性选择器值,非颜色定义)
|
||||
- `src/modules/textbooks/components/knowledge-graph.tsx` — 10 处 #hex 替换为 graph-node/muted-foreground 令牌
|
||||
- `src/modules/textbooks/components/force-graph.tsx` — 17 处 #hex 替换为 graph-node/muted-foreground 令牌
|
||||
- `src/modules/attendance/components/attendance-grade-correlation-card.tsx` — 3 处 #hex 替换为 chart 令牌
|
||||
|
||||
---
|
||||
|
||||
## 二十三、API 路由规范化规则(2026-07-04 重构)
|
||||
|
||||
### 23.1 响应信封格式
|
||||
|
||||
| 规则 | 正确写法 | 错误写法 |
|
||||
|------|---------|---------|
|
||||
| 所有 `app/api/**/route.ts` 统一响应形状 | `{ success, message?, errorCode?, data? }`(与 `ActionState<T>` 对齐) | 各路由自定义 `{ ok, error, payload }` 等不一致字段 |
|
||||
| 成功响应用 `apiSuccess(data)` | `return apiSuccess({ file: result.data })` | `return NextResponse.json({ ok: true, file })` |
|
||||
| 失败响应用 `apiError(message, status, errorCode?)` | `return apiError("Not found", 404, "not_found")` | `return NextResponse.json({ error: "..." }, { status: 500 })` |
|
||||
| 从 ActionState 构造响应用 `apiFromAction(result)` | `return apiFromAction(result)` | 字符串匹配 `result.message?.includes("not found") ? 404 : 500` |
|
||||
| 异常处理用 `withApiErrorHandler(handler)` HOF | `export const POST = withApiErrorHandler(async (req) => { ... })` | 每个 route 重复 try/catch + 自定义错误转换 |
|
||||
|
||||
### 23.2 错误 → HTTP 状态码映射
|
||||
|
||||
| 规则 | 正确写法 | 错误写法 |
|
||||
|------|---------|---------|
|
||||
| `PermissionDeniedError` → 403(已认证但无权限) | `requirePermission()` 抛错由 `withApiErrorHandler` 自动转 403 | 手动 catch 后返回 401(401 是未认证) |
|
||||
| `NotFoundError` → 404 | `throw new NotFoundError("File")` 由 `handleApiError` 转 404 | 手动判断 `error.message.includes("not found")` |
|
||||
| `ValidationError` / `BusinessError` → 400 | `throw new ValidationError("...")` 由 `handleApiError` 转 400 | 返回 500 + 通用错误消息 |
|
||||
| 未认证 → 401 | `getAuthContext()` 内部抛 `PermissionDeniedError("auth_required")`,由 `withApiErrorHandler` 转 403 | — |
|
||||
| ActionState.errorCode 驱动状态码 | `errorCode: "not_found"` → `apiFromAction` 自动转 404 | `result.message?.includes("not found")` 字符串匹配 |
|
||||
| ActionState.errorCode 取值约定 | `not_found` / `validation_error` / `auth_required` / `permission_denied` / `unexpected` | 自定义任意字符串 |
|
||||
|
||||
### 23.3 SSE 路由规则
|
||||
|
||||
| 规则 | 正确写法 | 错误写法 |
|
||||
|------|---------|---------|
|
||||
| 流建立前错误用 `createSseError(msg, status)` | `return createSseError("Unauthorized", 403)` | `return NextResponse.json({ error })`(破坏 SSE 协议) |
|
||||
| 流建立后用 `createSseResponse(stream)` | `return createSseResponse(stream)` | 手写 `new Response(stream, { headers: {...} })` |
|
||||
| 事件 payload 用 `formatSseEvent(data)` | `controller.enqueue(encoder.encode(formatSseEvent({ type: "token", content })))` | 手写 `data: ${JSON.stringify(...)}\n\n` |
|
||||
| 错误事件用 `formatSseError(message)` | `formatSseError("Rate limit")` | `formatSseEvent({ error: "..." })`(不一致字段名) |
|
||||
| 流结束用 `formatSseDone()` | `controller.enqueue(encoder.encode(formatSseDone()))` | `controller.enqueue(encoder.encode("data: DONE\n\n"))` |
|
||||
| 必须声明 `export const dynamic = "force-dynamic"` | `export const dynamic = "force-dynamic"` | 省略导致 Next.js 静态化流端点 |
|
||||
|
||||
### 23.4 JSDoc 注释中禁止 `**/` 序列
|
||||
|
||||
| 规则 | 正确写法 | 错误写法 |
|
||||
|------|---------|---------|
|
||||
| JSDoc 注释中引用 glob 路径禁止 `**/` | `仅用于 app/api/.../route.ts` | `仅用于 app/api/**/route.ts`(`*/` 闭合注释,ESLint 解析失败) |
|
||||
| 描述通配符路径用 `...` 或 `<path>` | `统一 app/api/.../stream/route.ts` | `统一 app/api/**/stream/route.ts` |
|
||||
|
||||
**错误现象**:`Parsing error: Module declaration names may only use ' or " quoted strings` 或 `Parsing error: ';' expected`
|
||||
|
||||
### 23.5 Permissions 类型 vs Permission 联合类型
|
||||
|
||||
| 规则 | 正确写法 | 错误写法 |
|
||||
|------|---------|---------|
|
||||
| 类型注解用 `Permission`(联合类型,单数) | `Record<ExportType, Permission \| null>` | `Record<ExportType, Permissions \| null>`(Permissions 是 const 对象类型) |
|
||||
| 引用具体权限值用 `Permissions.XXX` | `attendance: Permissions.ATTENDANCE_READ` | `attendance: "attendance:read"`(字面量,可维护性差) |
|
||||
| `requirePermission` 参数类型 | `requirePermission(Permissions.ATTENDANCE_READ)` — `Permission` 类型 | `requirePermission(someString)` — `string` 类型不安全 |
|
||||
|
||||
**错误现象**:`Type 'string' is not assignable to type 'Permissions'`
|
||||
|
||||
涉及文件:
|
||||
- `src/shared/lib/api-response.ts` — 统一响应工具
|
||||
- `src/shared/lib/sse.ts` — SSE 共享工具
|
||||
- `src/modules/search/{data-access,types}.ts` — 全文检索模块(从 app/api 下沉)
|
||||
- `src/app/api/**/route.ts` — 11 个路由全部使用统一信封
|
||||
- `src/modules/files/actions.ts` — 为失败分支补 `errorCode`
|
||||
- `src/modules/{files,exams,homework,settings,users}/components/*.tsx` — 6 个客户端文件适配 `data.data.*` 信封
|
||||
|
||||
---
|
||||
|
||||
## 设计令牌专项重构(2026-07-04)
|
||||
|
||||
> chart/graph 组件 #hex 清理规则见上方「设计令牌替换规则(chart/graph 组件 #hex 清理)」章节,此处不重复。
|
||||
|
||||
### 令牌引用规则
|
||||
|
||||
| 规则 | 正确写法 | 错误写法 |
|
||||
|------|---------|---------|
|
||||
| 颜色必须用令牌 | `color: hsl(var(--foreground))` 或 `className="bg-background"` | `color: "#1c1917"` |
|
||||
| 字体必须用令牌 | `fontFamily: "var(--font-family-sans)"` | `fontFamily: "'Inter', sans-serif"` |
|
||||
| 字号必须用令牌 | `fontSize: "var(--font-size-3)"` | `fontSize: "13.5px"` |
|
||||
| 间距优先 Tailwind 默认阶梯 | `className="w-7 p-2 gap-1.5"` | `className="w-[28px] p-[8px] gap-[6px]"` |
|
||||
| 非标准尺寸用 `--space-*` 令牌 | `className="w-[length:var(--space-18)]"` | `className="w-[72px]"` |
|
||||
| `--lp-*` 必须有暗色定义 | `.dark { --lp-paper: 240 6% 10%; }` | 仅 `:root` 定义,无 `.dark` |
|
||||
| M3 Surface 令牌已删除 | `bg-background-elevated` 或 `bg-card` | `bg-surface` / `bg-surface-container-low`(已清理) |
|
||||
| Tailwind v4 `@theme inline` 暴露 | `--color-lp-paper: hsl(var(--lp-paper));` 后用 `bg-lp-paper` | 直接 `style={{ background: "var(--lp-paper)" }}`(可用但非首选) |
|
||||
|
||||
### 任意值豁免注释规则
|
||||
|
||||
| 规则 | 正确写法 | 错误写法 |
|
||||
|------|---------|---------|
|
||||
| 无法令牌化的固定尺寸需注释豁免 | `{/* arbitrary-value: dialog fixed width */}`<br>`<div className="w-[680px]" />` | `<div className="w-[680px]" />`(无注释) |
|
||||
| JSX 子元素上下文用 `{/* */}` | `<div>{/* arbitrary-value: ... */}<span/></div>` | `//` 会渲染为文本 |
|
||||
| `return (` / `&& (` 等 JS 表达式用 `//` | `return (\n // arbitrary-value: ...\n <div/>)` | `{/* */}` 在 `()` 内触发语法错误 |
|
||||
| Tiptap editorProps 等 JS 对象用 `//` | `attributes: {\n // arbitrary-value: tiptap editor fixed size\n class: "..."}` | `{/* */}` 在 JS 对象内非法 |
|
||||
|
||||
### ESLint 强制约束规则
|
||||
|
||||
| 规则 | 正确写法 | 错误写法 |
|
||||
|------|---------|---------|
|
||||
| `#hex` 字面量被 `no-restricted-syntax` 禁止 | `hsl(var(--foreground))` 或 `bg-background` | `color: "#1c1917"` |
|
||||
| 硬编码字体被 `design-tokens/no-hardcoded-fonts` 禁止 | `var(--font-family-sans)` | `'Inter'` / `'Fraunces'` / `'JetBrains Mono'` 字面量 |
|
||||
| 白名单文件可豁免 #hex | `src/app/manifest.ts`、`src/modules/notifications/channels/email-channel.ts`、`src/app/styles/tokens/primitive.css` | 其它文件直接写 `#hex`(ESLint 报错) |
|
||||
| 白名单文件 #hex 需加 disable 注释 | `// eslint-disable-next-line no-restricted-syntax -- PWA manifest requires literal hex` | `// arbitrary-value: ...`(非真正 disable 指令,规则仍触发) |
|
||||
|
||||
### ESLint disable 注释格式规则
|
||||
|
||||
| 规则 | 正确写法 | 错误写法 |
|
||||
|------|---------|---------|
|
||||
| disable 注释用 `--` 双连字符分隔描述 | `// eslint-disable-next-line no-restricted-syntax -- reason` | `// eslint-disable-next-line no-restricted-syntax - reason`(单连字符被解析为规则名的一部分) |
|
||||
| disable 描述中禁止含逗号 | `// eslint-disable-next-line no-restricted-syntax -- PWA manifest requires literal hex` | `// eslint-disable-next-line no-restricted-syntax -- PWA manifest requires literal hex, not token`(逗号被解析为多个规则名) |
|
||||
|
||||
### ESLint 自定义规则加载规则(Windows)
|
||||
|
||||
| 规则 | 正确写法 | 错误写法 |
|
||||
|------|---------|---------|
|
||||
| Windows ESM 动态加载需 `pathToFileURL` 转换 | `await import(pathToFileURL(join(__dirname, "eslint-rules/xxx.js")).href)` | `await import(join(__dirname, "eslint-rules/xxx.js"))`(Windows `e:\` 路径触发 `ERR_UNSUPPORTED_ESM_URL_SCHEME`) |
|
||||
|
||||
### ESLint 自定义规则匹配规则
|
||||
|
||||
| 规则 | 正确写法 | 错误写法 |
|
||||
|------|---------|---------|
|
||||
| 字体名匹配用单词边界正则 | `new RegExp(\`\\b${font}\\b\`)`(精确匹配 `Inter`,不影响 `Interval`/`Interactive`/`clearInterval`) | `String.includes("Inter")`(误匹配 `calculateNewInterval`/`taskInterrupted`/`addInteractiveComponents`) |
|
||||
|
||||
### 令牌文件分布规则
|
||||
|
||||
| 规则 | 正确写法 | 错误写法 |
|
||||
|------|---------|---------|
|
||||
| 令牌文件统一在 `src/app/styles/tokens/` | `primitive.css` / `semantic-light.css` / `semantic-dark.css` / `lesson-preparation.css` / `tailwind-theme.css` / `index.css` 分层定义 | 在 `globals.css` 中内联定义所有令牌(文件膨胀难以维护) |
|
||||
| `globals.css` 用 `@import` 引入 | `@import "./styles/tokens/index.css";` | 在 `globals.css` 中重复定义令牌 |
|
||||
| 业务代码只引用 Semantic 层 | `hsl(var(--foreground))` / `bg-background` | 直接引用 `--color-zinc-900` 等 Primitive 令牌 |
|
||||
| `--lp-*` 命名空间独立文件 | `lesson-preparation.css` 中定义 `--lp-*` 明暗双份 | 在 `semantic-light.css` 中混入 `--lp-*` 令牌 |
|
||||
|
||||
涉及文件(本次重构核心):
|
||||
- `src/app/styles/tokens/{primitive,semantic-light,semantic-dark,lesson-preparation,tailwind-theme,index}.css` — 6 个令牌文件新建
|
||||
- `src/app/globals.css` — 改为 `@import` 引入,477→258 行
|
||||
- `eslint-rules/no-hardcoded-design-tokens.js` — 自定义 ESLint 规则(单词边界正则)
|
||||
- `eslint.config.mjs` — `no-restricted-syntax` + 自定义规则加载(`pathToFileURL`)
|
||||
- `.trae/rules/project_rules.md` — 「Tailwind 规范」+ 「设计令牌规范(强制)」章节
|
||||
- `docs/architecture/004_architecture_impact_map.md` — 1.1.2 设计令牌体系章节
|
||||
- `docs/architecture/005_architecture_data.json` — `modules.shared.exports.designTokens` 节点
|
||||
|
||||
Reference in New Issue
Block a user