435 lines
20 KiB
Markdown
435 lines
20 KiB
Markdown
# Frontend Engineering Standards (Next_Edu)
|
||
|
||
**Status**: ACTIVE
|
||
**Owner**: Frontend Team
|
||
**Scope**: Next.js App Router 前端工程规范(编码、目录、交互、样式、数据流、质量门禁)
|
||
**Applies To**: `src/app/*`, `src/modules/*`, `src/shared/*`, `docs/design/*`
|
||
|
||
---
|
||
|
||
## 0. 目标与非目标
|
||
|
||
### 0.1 目标
|
||
|
||
- 让新加入的前端工程师在 30 分钟内完成对齐并开始稳定迭代
|
||
- 保证 UI 一致性(Design Token + Shadcn/UI 复用)
|
||
- 充分利用 App Router + RSC(Server-First)降低 bundle、提升性能
|
||
- 保证类型安全与可维护性(Vertical Slice、数据访问边界清晰)
|
||
- 形成可执行的质量门禁(lint/typecheck/build 与评审清单)
|
||
|
||
### 0.2 非目标
|
||
|
||
- 不规定具体业务模块的需求细节(业务规则以 `docs/design/*` 与 PRD 为准)
|
||
- 不引入与当前仓库技术栈不一致的新框架/库(新增依赖需明确收益与替代方案)
|
||
|
||
---
|
||
|
||
## 1. 接手流程(Onboarding Checklist)
|
||
|
||
### 1.1 先读什么(按顺序)
|
||
|
||
- 设计系统与 UI 规范:[docs/design/design_system.md](file:///c:/Users/xiner/Desktop/CICD/docs/design/design_system.md)
|
||
- 角色路由与目录规范:[docs/architecture/002_role_based_routing.md](file:///c:/Users/xiner/Desktop/CICD/docs/architecture/002_role_based_routing.md)
|
||
- 项目架构总览:[ARCHITECTURE.md](file:///c:/Users/xiner/Desktop/CICD/ARCHITECTURE.md)
|
||
- 你将要改动的模块实现文档:`docs/design/00*_*.md`
|
||
|
||
### 1.2 开发前对齐(必须)
|
||
|
||
- 核对 Design Tokens 与暗色模式变量:
|
||
- Tailwind 语义色映射:[tailwind.config.ts](file:///c:/Users/xiner/Desktop/CICD/tailwind.config.ts)
|
||
- CSS 变量定义:[src/app/globals.css](file:///c:/Users/xiner/Desktop/CICD/src/app/globals.css)
|
||
- 盘点可复用 UI 组件:`src/shared/components/ui/*`
|
||
- 盘点通用工具(`cn` 等):[src/shared/lib/utils.ts](file:///c:/Users/xiner/Desktop/CICD/src/shared/lib/utils.ts)
|
||
|
||
### 1.3 环境变量与配置校验(必须)
|
||
|
||
- 统一使用 `@t3-oss/env-nextjs` 的 `env` 入口读取环境变量,禁止在业务代码中散落 `process.env.*`
|
||
- Schema 定义与校验入口:[src/env.mjs](file:///c:/Users/xiner/Desktop/CICD/src/env.mjs)
|
||
- 任何新增环境变量:
|
||
- 必须先在 `src/env.mjs` 增加 schema
|
||
- 必须在 docs 中更新部署/运行说明(就近更新对应模块文档或全局架构文档)
|
||
|
||
### 1.3 本地跑通(推荐顺序)
|
||
|
||
- 安装依赖:`npm install`
|
||
- 启动开发:`npm run dev`
|
||
- 质量检查:
|
||
- `npm run lint`
|
||
- `npm run typecheck`
|
||
- `npm run build`
|
||
|
||
---
|
||
|
||
## 2. 核心工程原则(必须遵守)
|
||
|
||
### 2.1 Vertical Slice(按业务功能组织)
|
||
|
||
- 业务必须放在 `src/modules/<feature>/*`
|
||
- `src/app/*` 是路由层,只负责:
|
||
- 布局组合(layout)
|
||
- 读取 `searchParams` / `params`
|
||
- 调用模块的数据访问函数(`data-access.ts`)
|
||
- 组合模块组件渲染
|
||
- 通用能力放在 `src/shared/*`:
|
||
- 通用 UI:`src/shared/components/ui/*`
|
||
- 通用工具:`src/shared/lib/*`
|
||
- DB 与 schema:`src/shared/db/*`
|
||
|
||
### 2.2 Server-First(默认 Server Component)
|
||
|
||
- 默认写 Server Component
|
||
- 只有在需要以下能力时,才把“最小子组件”标记为 Client Component:
|
||
- `useState/useEffect/useMemo`(与交互/浏览器相关)
|
||
- DOM 事件(`onClick/onChange` 等)
|
||
- `useRouter/usePathname` 等客户端导航 hooks
|
||
- Radix/Portal 类组件需要客户端(Dialog/Dropdown 等通常在 client 内组合使用)
|
||
|
||
### 2.3 不重复造轮子(Shadcn/UI 优先)
|
||
|
||
- 禁止手写 Modal/Dropdown/Tooltip 等基础交互容器
|
||
- 优先组合 `src/shared/components/ui/*`(Button/Card/Dialog/DropdownMenu/AlertDialog/Skeleton/EmptyState 等)
|
||
- 若现有基础组件无法满足需求:
|
||
1. 优先通过 Composition 在业务模块里封装“业务组件”
|
||
2. 仅在存在 bug 或需要全局一致性调整时,才考虑改动 `src/shared/components/ui/*`(并在 PR 中明确影响面)
|
||
|
||
### 2.4 Client Component 引用边界(强制)
|
||
|
||
- 禁止在 Client Component 中导入任何“服务端实现”代码(例如 DB 实例、data-access、server-only 模块)
|
||
- Client Component 允许导入:
|
||
- `src/shared/components/ui/*`(基础 UI)
|
||
- `src/shared/lib/*`(纯前端工具函数)
|
||
- Server Actions(`"use server"` 导出的 action 函数)
|
||
- 类型定义必须使用 `import type`(避免把服务端依赖带入 client bundle)
|
||
- 所有 `data-access.ts` 必须包含 `import "server-only"`,并将其视为强制安全边界(不是可选优化)
|
||
|
||
---
|
||
|
||
## 3. 目录与路由规范
|
||
|
||
### 3.1 路由目录(App Router)
|
||
|
||
- 认证域:`src/app/(auth)/*`
|
||
- 控制台域(共享 App Shell):`src/app/(dashboard)/*`
|
||
- 角色域:`src/app/(dashboard)/teacher|student|admin/*`
|
||
- `/dashboard` 作为入口页(重定向/分发到具体角色 dashboard):参考 [002_role_based_routing.md](file:///c:/Users/xiner/Desktop/CICD/docs/architecture/002_role_based_routing.md)
|
||
|
||
### 3.2 页面文件职责
|
||
|
||
- `page.tsx`:页面组装(RSC),不承载复杂交互
|
||
- `loading.tsx`:路由级加载态(Skeleton)
|
||
- `error.tsx`:路由级错误边界(友好 UI)
|
||
- `not-found.tsx`:路由级 404
|
||
|
||
### 3.3 错误处理与用户反馈(Error Handling & Feedback)
|
||
|
||
- 路由级错误(404/500/未捕获异常):
|
||
- 交由 `not-found.tsx` / `error.tsx` 处理
|
||
- 禁止在 `error.tsx` 里弹 Toast 堆栈刷屏,错误页输出必须友好且可恢复(例如提供 retry / 返回入口)
|
||
- 业务操作反馈(表单提交/按钮操作/行级动作):
|
||
- 统一由 Client Component 在调用 Server Action 后触发 `sonner` toast
|
||
- 只在“成功”或“明确失败(业务/校验错误)”时触发 toast;未知异常由 action 归一为失败 message
|
||
|
||
### 3.4 异步组件与 Suspense(Streaming)
|
||
|
||
- 对于数据加载超过 300ms 的非核心 UI 区块(例如:仪表盘某张统计卡片/图表/第三方数据块):
|
||
- 必须用 `<Suspense fallback={<Skeleton />}>` 包裹,以避免全页阻塞
|
||
- 禁止在 `page.tsx` 顶层用多个串行 `await` 造成瀑布请求:
|
||
- 多个独立请求必须使用 `Promise.all`
|
||
- 或拆分为多个 async 子组件并行流式渲染(用 `Suspense` 分段展示)
|
||
|
||
### 3.3 动态渲染策略(避免 build 阶段查库)
|
||
|
||
当页面在渲染时会查询数据库或依赖 request-time 数据,且无法安全静态化时:
|
||
|
||
- 在页面入口显式声明:
|
||
- `export const dynamic = "force-dynamic"`
|
||
- 该策略已用于教师端班级与作业相关页面,见相应 design 文档(例如教师班级模块更新记录)
|
||
|
||
---
|
||
|
||
## 4. 模块内文件结构(强制)
|
||
|
||
每个业务模块使用统一结构(可按复杂度增减,但命名必须一致):
|
||
|
||
```
|
||
src/modules/<feature>/
|
||
├── components/ # 仅该模块使用的 UI 组件(可含 client 组件)
|
||
├── actions.ts # Server Actions(写入/变更 + revalidatePath)
|
||
├── data-access.ts # 数据查询与聚合(server-only + cache)
|
||
├── schema.ts # Zod schema(若需要)
|
||
└── types.ts # 类型定义(与 DB/DTO 对齐)
|
||
```
|
||
|
||
约束:
|
||
|
||
- `actions.ts` 必须包含 `"use server"`
|
||
- `data-access.ts` 必须包含 `import "server-only"`(防止误导入到 client bundle)
|
||
- 复杂页面组件必须下沉到 `src/modules/<feature>/components/*`,路由层只做组装
|
||
|
||
---
|
||
|
||
## 5. Server / Client 边界与拆分策略
|
||
|
||
### 5.1 最小化 Client Component 的落地方式
|
||
|
||
- 页面保持 RSC
|
||
- 把需要交互的部分抽成独立 `components/*` 子组件并标记 `"use client"`
|
||
- Client 组件向上暴露“数据变化事件”,由 Server Action 完成写入并 `revalidatePath`
|
||
|
||
### 5.4 Hydration 一致性(必须)
|
||
|
||
- 所有 Client Component 的首屏渲染必须保证与 SSR 产出的 HTML 一致
|
||
- 禁止在 render 分支中使用:
|
||
- `typeof window !== "undefined"` 之类的 server/client 分支
|
||
- `Date.now()` / `Math.random()` 等不稳定输入
|
||
- 依赖用户 locale 的时间格式化(除非服务端与客户端完全一致并带 snapshot)
|
||
- 对于 Radix 等组件生成的动态 aria/id 导致的属性差异:
|
||
- 优先通过组件封装确保首屏稳定
|
||
- 若确认差异不可避免且不影响交互,可在最小范围使用 `suppressHydrationWarning`
|
||
|
||
### 5.2 页面必须只做“拼装”,功能模块必须独立
|
||
|
||
- 任何功能模块都必须在 `src/modules/<feature>/components/*` 内独立实现
|
||
- `page.tsx` 只负责:
|
||
- 读取 `params/searchParams`
|
||
- 调用 `data-access.ts` 获取数据
|
||
- 以组合方式拼装模块组件(不在 page 内实现具体交互与复杂 UI)
|
||
- 行数不是拆分依据,只是“路由层变厚”的信号;一旦出现成块的功能 UI,应立即下沉到模块组件
|
||
|
||
### 5.3 什么时候允许在 Client 中做“局部工作台”
|
||
|
||
当交互复杂到“页面需要类似 SPA 的局部体验”,允许将工作台容器作为 Client:
|
||
|
||
- 典型场景:三栏工作台、拖拽排序编辑器、复杂筛选器组合、富交互表格
|
||
- 但仍要求:
|
||
- 初始数据由 RSC 获取并传入 Client
|
||
- 写操作通过 Server Actions
|
||
- UI 状态尽量 URL 化(能分享/回溯)
|
||
|
||
---
|
||
|
||
## 6. 样式与 UI 一致性(Design System 强制项)
|
||
|
||
### 6.1 Token 优先(语义化颜色/圆角)
|
||
|
||
- 颜色必须使用语义 token:
|
||
- `bg-background`, `bg-card`, `bg-muted`, `text-foreground`, `text-muted-foreground`, `border-border` 等
|
||
- 禁止硬编码颜色值(`#fff`/`rgb()`)与随意引入灰度(如 `bg-gray-100`)
|
||
- 圆角、边框、阴影遵循设计系统:
|
||
- 常规组件使用 `rounded-md` 等语义半径(由 `--radius` 映射)
|
||
|
||
### 6.2 className 规范
|
||
|
||
- 所有条件样式必须使用 `className={cn(...)}`
|
||
- `cn` 入口为 `@/shared/lib/utils`
|
||
|
||
### 6.3 禁止 Arbitrary Values(默认)
|
||
|
||
- 默认禁止 `w-[123px]` 等任意值
|
||
- 只有在设计系统或现有实现明确允许、并且无法用 token/栅格解决时,才可使用,并在 PR 描述说明原因
|
||
|
||
### 6.4 微交互与状态(必须有)
|
||
|
||
- 按钮 hover:必须有 transition(现有 Button 组件已内置)
|
||
- 列表项 hover:使用 `hover:bg-muted/50` 等轻量反馈
|
||
- Loading:必须使用 `Skeleton`(路由级 `loading.tsx` 或组件内 skeleton)
|
||
- Empty:必须使用 `EmptyState`
|
||
- Toast:统一使用 `sonner`
|
||
|
||
---
|
||
|
||
## 7. 图标规范(lucide-react)
|
||
|
||
- 统一使用 `lucide-react`
|
||
- 图标尺寸统一:默认 `h-4 w-4`,需要强调时 `h-5 w-5`
|
||
- 颜色使用语义化:例如 `text-muted-foreground`
|
||
|
||
---
|
||
|
||
## 8. 数据流规范(查询、写入、状态)
|
||
|
||
### 8.1 查询(data-access.ts)
|
||
|
||
- 所有查询放在 `src/modules/<feature>/data-access.ts`
|
||
- 需要复用/去重的查询优先用 `cache` 包裹(React cache)
|
||
- 查询函数返回“UI 直接可消费的 DTO”,避免页面层再做复杂映射
|
||
|
||
### 8.2 写入(actions.ts)
|
||
|
||
- 所有写操作必须通过 Server Actions
|
||
- 每个 action:
|
||
- 校验输入(Zod 或手写 guard)
|
||
- 执行 DB 写入
|
||
- 必须 `revalidatePath`(以页面为单位)
|
||
|
||
### 8.3 Server Action 返回结构(统一反馈协议)
|
||
|
||
- 所有 Server Action 必须返回统一结构,用于前端统一处理 toast 与表单错误
|
||
- 统一使用类型:[src/shared/types/action-state.ts](file:///c:/Users/xiner/Desktop/CICD/src/shared/types/action-state.ts)
|
||
|
||
```ts
|
||
export type ActionState<T = void> = {
|
||
success: boolean
|
||
message?: string
|
||
errors?: Record<string, string[]>
|
||
data?: T
|
||
}
|
||
```
|
||
|
||
约束:
|
||
|
||
- `errors` 必须对齐 `zod` 的 `error.flatten().fieldErrors` 结构
|
||
- 禁止在各模块内重复定义自有的 ActionState 类型
|
||
|
||
### 8.4 Toast 触发时机(强制)
|
||
|
||
- Client Component 在调用 Server Action 后:
|
||
- `success: true`:触发 `toast.success(message)`(或使用模块内约定的成功文案)
|
||
- `success: false`:
|
||
- 存在 `errors`:优先渲染表单字段错误;可选触发 `toast.error(message)`
|
||
- 不存在 `errors`:触发 `toast.error(message || "Action failed")`
|
||
- 对于路由级异常与边界错误,禁止用 toast 替代 `error.tsx`
|
||
|
||
### 8.5 URL State(nuqs 优先)
|
||
|
||
- 列表页筛选/分页/Tab/排序等“可分享状态”必须放 URL
|
||
- 使用 `nuqs` 做类型安全的 query state 管理
|
||
|
||
### 8.6 Data Access 权限边界(Security / IDOR 防护)
|
||
|
||
- `data-access.ts` 不是纯 DTO 映射层,必须承担数据归属权校验
|
||
- 允许两种合规方式(二选一,但模块内必须统一):
|
||
- **方式 A(强制传参)**:所有 data-access 函数显式接收 `actor`(userId/role)并在查询条件中约束归属(例如 teacherId)
|
||
- **方式 B(函数内获取)**:data-access 函数首行获取 session/user 并校验 role/归属,再执行查询
|
||
- 禁止把权限校验放在 page.tsx 或 client 组件中作为唯一屏障
|
||
|
||
---
|
||
|
||
## 9. 数据完整性与 Seed 规则(禁止 Mock)
|
||
|
||
项目默认不使用 Mock 数据。
|
||
|
||
当某功能缺失实际数据,开发者必须把数据补齐到数据库与种子数据中,而不是在前端临时模拟。
|
||
|
||
执行规范:
|
||
|
||
- 若缺失的是“表结构/字段/关系”:
|
||
- 修改 `src/shared/db/schema.ts` 与 `src/shared/db/relations.ts`(按既有模式)
|
||
- 生成并提交 Drizzle migration(`drizzle/*.sql`)
|
||
- 若缺失的是“可演示的业务数据”:
|
||
- 更新 `scripts/seed.ts`,确保 `npm run db:seed` 可一键生成可用数据
|
||
- 文档同步(必须):
|
||
- 在 [schema-changelog.md](file:///c:/Users/xiner/Desktop/CICD/docs/db/schema-changelog.md) 记录本次新增/变更的数据表、字段、索引与外键
|
||
- 在对应模块的 `docs/design/00*_*.md` 中补充“新增了哪些数据/为什么需要/如何验证(db:seed + 页面路径)”
|
||
|
||
### 9.1 Seed 分层(降低阻塞)
|
||
|
||
- Seed 分为两类:
|
||
- **Baseline Seed**:全项目必备的最小集合(核心用户/角色/基础字典数据等),保证任何页面都不因“数据空”而无法进入流程
|
||
- **Scenario Seed(按模块)**:面向具体模块的可演示数据包(例如:班级/题库/试卷/作业),用于复现与验证该模块交互
|
||
- 任何模块新增数据依赖,必须以 “Scenario Seed” 的形式落到 `scripts/seed.ts`,而不是把数据要求隐含在前端逻辑里
|
||
|
||
### 9.2 Seed 可复现与数据锚点(保证跨模块联动)
|
||
|
||
- Seed 必须可重复执行(idempotent),避免开发环境多次执行后产生脏数据与重复数据
|
||
- 对跨模块联动依赖的关键实体,必须提供可稳定引用的数据锚点:
|
||
- 固定标识(如固定 email/slug/title 组合)或可预测 ID(按现有 seed 约定)
|
||
- 文档必须写明锚点是什么、依赖它的模块有哪些、如何验证
|
||
- 禁止在 UI 里依赖“随机生成数据顺序”来定位实体(例如 “取第一条记录作为 demo 用户” 这类逻辑应退化为明确锚点)
|
||
|
||
### 9.3 外部服务的例外(仅限 Adapter Mock)
|
||
|
||
- 内部业务数据严格遵守“DB + Migration + Seed”,不允许 Mock
|
||
- 仅当对接外部不可控服务(支付/短信/第三方 AI 流式等)且无法用本地 seed 复现时:
|
||
- 允许在 `src/shared/lib/mock-adapters/*` 建立 mock 适配器
|
||
- 必须先定义 Adapter 接口,再提供真实实现与 mock 实现(业务模块只能依赖接口,不可直接依赖某个具体实现)
|
||
- 该 mock 仅用于外部服务交互层,禁止承载内部业务数据
|
||
|
||
---
|
||
|
||
## 10. 表单规范(react-hook-form + zod)
|
||
|
||
- 表单统一使用 `react-hook-form` + `@hookform/resolvers` + `zod`
|
||
- 错误提示放在输入框下方:
|
||
- 字号 `text-xs`
|
||
- 颜色 `text-destructive`
|
||
- 破坏性操作必须二次确认(`AlertDialog`)
|
||
- 提交中按钮禁用并展示 loading(可使用 `useFormStatus` 或本地 state)
|
||
|
||
---
|
||
|
||
## 11. 质量门禁与评审清单(PR 必须过)
|
||
|
||
### 11.1 本地必须通过
|
||
|
||
- `npm run lint`
|
||
- `npm run typecheck`
|
||
- `npm run build`
|
||
|
||
### 11.2 代码评审清单(Reviewer 逐项检查)
|
||
|
||
- 目录结构是否符合 Vertical Slice(路由层是否保持“薄”)
|
||
- 页面是否只做拼装(功能 UI 是否全部下沉到模块组件)
|
||
- Server/Client 边界是否最小化(是否把整页误标 client)
|
||
- 是否复用 `src/shared/components/ui/*`,是否重复实现基础交互
|
||
- 是否使用语义化 token(颜色/圆角/间距),是否引入硬编码颜色与大量 arbitrary values
|
||
- Loading/Empty/Error 是否齐全(Skeleton/EmptyState/error.tsx)
|
||
- 列表页筛选是否 URL 化(nuqs),是否支持刷新/分享
|
||
- 写操作是否通过 Server Action 且正确 `revalidatePath`
|
||
- 是否避免 Mock(数据是否通过迁移 + seed 补齐,且 docs/db 与模块文档已同步)
|
||
- 是否引入不必要的依赖与重型客户端逻辑
|
||
|
||
### 11.3 Commit 规范(Git History)
|
||
|
||
- 推荐遵循 Conventional Commits:
|
||
- `feat:` 新功能
|
||
- `fix:` 修复 bug
|
||
- `docs:` 文档更新
|
||
- `refactor:` 重构(无功能变化)
|
||
- `chore:` 工程杂项
|
||
- 约束:
|
||
- 单次提交必须聚焦一个意图,避免把大范围格式化与功能修改混在一起
|
||
- 涉及 DB 迁移与 seed 变更时,commit message 必须包含模块/领域关键词,便于追溯
|
||
|
||
---
|
||
|
||
## 12. 文档同步规则(Docs Sync)
|
||
|
||
以下情况必须同步更新文档(就近放在 `docs/design/*` 或 `docs/architecture/*`):
|
||
|
||
- 新增“全局交互模式”(例如:新的工作台/拖拽范式/跨模块复用交互)
|
||
- 新增“全局组件”或改变基础 UI 行为(影响 `src/shared/components/ui/*`)
|
||
- 新增关键路由结构或权限/角色策略
|
||
|
||
### 12.1 业务组件可发现性(可选但推荐)
|
||
|
||
- 对 `src/modules/<feature>/components` 内的复杂业务组件(例如:试卷编辑器、排课表、工作台):
|
||
- 推荐在对应的 `docs/design/00*_*.md` 增加“用法示例 + 关键 props + 截图”
|
||
- 若团队资源允许,可引入 Storybook 作为可视化组件目录(不作为硬性门禁)
|
||
|
||
---
|
||
|
||
## 13. Performance Essentials(必须遵守)
|
||
|
||
- 图片:
|
||
- 强制使用 `next/image` 替代 `<img>`(SVG 或已明确无需优化的极小图标除外)
|
||
- 头像等外部域名资源必须配置并明确缓存策略
|
||
- 字体:
|
||
- 强制使用 `next/font` 管理字体加载
|
||
- 禁止在 CSS 中 `@import` 外部字体 URL(避免 CLS 与阻塞渲染)
|
||
- 依赖:
|
||
- 禁止引入重型动画库作为默认方案;复杂动效需按需加载并解释收益
|
||
- 大体积 Client 组件必须拆分与动态加载,并通过 `Suspense` 提供 skeleton fallback
|
||
|
||
---
|
||
|
||
## 14. 参考实现(从现有代码学习的路径)
|
||
|
||
- 设计系统与 UI 组件清单:[docs/design/design_system.md](file:///c:/Users/xiner/Desktop/CICD/docs/design/design_system.md)
|
||
- Auth:路由层 RSC + 表单 client 拆分模式:[docs/design/001_auth_ui_implementation.md](file:///c:/Users/xiner/Desktop/CICD/docs/design/001_auth_ui_implementation.md)
|
||
- 教师端班级模块:URL state + client 交互组件 + server actions 的组合:[docs/design/002_teacher_dashboard_implementation.md](file:///c:/Users/xiner/Desktop/CICD/docs/design/002_teacher_dashboard_implementation.md)
|
||
- 教材工作台:RSC 拉初始数据 + client 工作台容器接管交互:[docs/design/003_textbooks_module_implementation.md](file:///c:/Users/xiner/Desktop/CICD/docs/design/003_textbooks_module_implementation.md)
|
||
- 题库:nuqs 驱动筛选 + TanStack Table + CRUD actions:[docs/design/004_question_bank_implementation.md](file:///c:/Users/xiner/Desktop/CICD/docs/design/004_question_bank_implementation.md)
|
||
- 考试组卷:拖拽编辑器(@dnd-kit)+ structure JSON 模型:[docs/design/005_exam_module_implementation.md](file:///c:/Users/xiner/Desktop/CICD/docs/design/005_exam_module_implementation.md)
|
||
- 作业:冻结 exam → assignment 的域模型 + 学生作答/教师批改闭环:[docs/design/006_homework_module_implementation.md](file:///c:/Users/xiner/Desktop/CICD/docs/design/006_homework_module_implementation.md)
|