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:
@@ -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