完整性更新
现在已经实现了大部分基础功能
This commit is contained in:
434
docs/architecture/003_frontend_engineering_standards.md
Normal file
434
docs/architecture/003_frontend_engineering_standards.md
Normal file
@@ -0,0 +1,434 @@
|
||||
# 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)
|
||||
@@ -75,3 +75,128 @@ This release introduces homework-related tables and hardens foreign key names to
|
||||
### 4. Impact Analysis
|
||||
* **Performance**: Minimal. New indexes are scoped to common homework access patterns.
|
||||
* **Data Integrity**: High. Foreign keys enforce referential integrity for homework workflow.
|
||||
|
||||
## v1.3.0 - Classes Domain (Teacher Class Management)
|
||||
**Date:** 2025-12-31
|
||||
**Migration ID:** `0003_petite_newton_destine`
|
||||
**Author:** Principal Database Architect
|
||||
|
||||
### 1. Summary
|
||||
This release introduces the core schema for teacher class management: classes, enrollments, and schedules.
|
||||
|
||||
### 2. Changes
|
||||
|
||||
#### 2.1 Tables: Classes Domain
|
||||
* **Action**: `CREATE TABLE`
|
||||
* **Tables**:
|
||||
* `classes`
|
||||
* `class_enrollments`
|
||||
* `class_schedule`
|
||||
* **Reason**: Support teacher-owned classes, student enrollment lists, and weekly schedules.
|
||||
|
||||
#### 2.2 Enum: Enrollment Status
|
||||
* **Action**: `ADD ENUM`
|
||||
* **Enum**: `class_enrollment_status` = (`active`, `inactive`)
|
||||
* **Reason**: Provide a stable status field for filtering active enrollments.
|
||||
|
||||
#### 2.3 Foreign Keys & Indexes
|
||||
* **Action**: `ADD FOREIGN KEY`, `CREATE INDEX`
|
||||
* **Key Relationships**:
|
||||
* `classes.teacher_id` -> `users.id` (cascade delete)
|
||||
* `class_enrollments.class_id` -> `classes.id` (cascade delete)
|
||||
* `class_enrollments.student_id` -> `users.id` (cascade delete)
|
||||
* `class_schedule.class_id` -> `classes.id` (cascade delete)
|
||||
* **Indexes**:
|
||||
* `classes_teacher_idx`, `classes_grade_idx`
|
||||
* `class_enrollments_class_idx`, `class_enrollments_student_idx`
|
||||
* `class_schedule_class_idx`, `class_schedule_class_day_idx`
|
||||
|
||||
### 3. Migration Strategy
|
||||
* **Up**: Run standard Drizzle migration. Ensure `DATABASE_URL` points to the intended schema (e.g., `next_edu`).
|
||||
* **Down**: Not provided. Dropping these tables is destructive and should be handled explicitly per environment.
|
||||
|
||||
### 4. Impact Analysis
|
||||
* **Performance**: Indexes align with common query patterns (teacher listing, enrollment filtering, per-class schedule).
|
||||
* **Data Integrity**: High. Foreign keys enforce ownership and membership integrity across teacher/classes/students.
|
||||
|
||||
## v1.4.0 - Classes Domain Enhancements (School Name & Subject Teachers)
|
||||
**Date:** 2026-01-07
|
||||
**Migration ID:** `0005_add_class_school_subject_teachers`
|
||||
**Author:** Frontend/Fullstack Engineering
|
||||
|
||||
### 1. Summary
|
||||
This release extends the Classes domain to support school-level sorting and per-subject teacher assignment defaults.
|
||||
|
||||
### 2. Changes
|
||||
|
||||
#### 2.1 Table: `classes`
|
||||
* **Action**: `ADD COLUMN`
|
||||
* **Field**: `school_name` (varchar(255), nullable)
|
||||
* **Reason**: Enable sorting/grouping by school name, then grade, then class name.
|
||||
|
||||
#### 2.2 Table: `class_subject_teachers`
|
||||
* **Action**: `CREATE TABLE`
|
||||
* **Primary Key**: (`class_id`, `subject`)
|
||||
* **Columns**:
|
||||
* `class_id` (varchar(128), FK -> `classes.id`, cascade delete)
|
||||
* `subject` (enum: `语文/数学/英语/美术/体育/科学/社会/音乐`)
|
||||
* `teacher_id` (varchar(128), FK -> `users.id`, set null on delete)
|
||||
* `created_at`, `updated_at`
|
||||
* **Reason**: Maintain a stable default “subject list” per class while allowing admin/teacher to assign the actual teacher per subject.
|
||||
|
||||
### 3. Migration Strategy
|
||||
* **Up**: Run standard Drizzle migration.
|
||||
* **Down**: Not provided. Dropping assignment history is destructive.
|
||||
|
||||
### 4. Impact Analysis
|
||||
* **Performance**: Minimal. Table is small (8 rows per class) and indexed by class/teacher.
|
||||
* **Data Integrity**: High. Composite PK prevents duplicates per class/subject; FKs enforce referential integrity.
|
||||
|
||||
## v1.4.1 - Classes Domain Enhancements (School/Grade Normalization)
|
||||
**Date:** 2026-01-07
|
||||
**Migration ID:** `0006_faithful_king_bedlam`
|
||||
**Author:** Frontend/Fullstack Engineering
|
||||
|
||||
### 1. Summary
|
||||
This release extends the `classes` table to support normalized school and grade references.
|
||||
|
||||
### 2. Changes
|
||||
|
||||
#### 2.1 Table: `classes`
|
||||
* **Action**: `ADD COLUMN`
|
||||
* **Fields**:
|
||||
* `school_id` (varchar(128), nullable)
|
||||
* `grade_id` (varchar(128), nullable)
|
||||
* **Reason**: Enable filtering and sorting by canonical school/grade entities instead of relying on free-text fields.
|
||||
|
||||
### 3. Migration Strategy
|
||||
* **Up**: Run standard Drizzle migration.
|
||||
* **Down**: Not provided. Dropping columns is destructive.
|
||||
|
||||
### 4. Impact Analysis
|
||||
* **Performance**: Minimal. Indexing and joins can be added as usage evolves.
|
||||
* **Data Integrity**: Medium. Existing rows remain valid (nullable fields); application-level validation can enforce consistency.
|
||||
|
||||
## v1.5.0 - Classes Domain Feature (Invitation Code)
|
||||
**Date:** 2026-01-08
|
||||
**Migration ID:** `0007_add_class_invitation_code`
|
||||
**Author:** Frontend/Fullstack Engineering
|
||||
|
||||
### 1. Summary
|
||||
This release introduces a 6-digit invitation code on `classes` to support join-by-code enrollment.
|
||||
|
||||
### 2. Changes
|
||||
|
||||
#### 2.1 Table: `classes`
|
||||
* **Action**: `ADD COLUMN` + `ADD UNIQUE CONSTRAINT`
|
||||
* **Field**: `invitation_code` (varchar(6), nullable, unique)
|
||||
* **Reason**: Allow students to enroll into a class using a short code, while ensuring uniqueness across all classes.
|
||||
|
||||
### 3. Migration Strategy
|
||||
* **Up**: Run standard Drizzle migration.
|
||||
* **Backfill**: Optional. Existing classes can keep `NULL` or be populated via application-level actions.
|
||||
* **Down**: Not provided. Removing a unique constraint/column is destructive.
|
||||
|
||||
### 4. Impact Analysis
|
||||
* **Performance**: Minimal. Uniqueness is enforced via an index.
|
||||
* **Data Integrity**: High. Unique constraint prevents code collisions and simplifies server-side enrollment checks.
|
||||
|
||||
@@ -59,6 +59,12 @@ Demonstrates the new **JSON Structure** field (`exams.structure`).
|
||||
]
|
||||
```
|
||||
|
||||
### 2.5 Classes / Enrollment / Schedule
|
||||
Seeds the teacher class management domain.
|
||||
* **Classes**: Creates at least one class owned by a teacher user.
|
||||
* **Enrollments**: Links students to classes via `class_enrollments` (default status: `active`).
|
||||
* **Schedule**: Populates `class_schedule` with weekday + start/end times for timetable validation.
|
||||
|
||||
## 3. How to Run
|
||||
|
||||
### Prerequisites
|
||||
|
||||
@@ -94,7 +94,120 @@ React 的 hydration 过程对 HTML 有效性要求极高。将 `div` 放入 `p`
|
||||
2. `teacher-stats.tsx`
|
||||
3. `teacher-schedule.tsx`
|
||||
|
||||
## 5. 下一步计划
|
||||
- 将 Mock Data 对接到真实的 API 端点 (React Server Actions)。
|
||||
- 实现 "Quick Actions" (快捷操作) 的具体功能。
|
||||
- 为 Submissions 和 Schedule 添加 "View All" (查看全部) 跳转导航。
|
||||
## 5. 更新记录(2026-01-04)
|
||||
- 教师仪表盘从 Mock Data 切换为真实数据查询:`/teacher/dashboard` 组合 `getTeacherClasses`、`getClassSchedule`、`getHomeworkSubmissions({ creatorId })` 渲染 KPI / 今日课表 / 最近提交。
|
||||
- Quick Actions 落地为真实路由跳转(创建作业、查看列表等)。
|
||||
- Schedule / Submissions 增加 “View All” 跳转到对应列表页(并携带筛选参数)。
|
||||
|
||||
---
|
||||
|
||||
## 6. 教师端班级管理模块(真实数据接入记录)
|
||||
|
||||
**日期**: 2025-12-31
|
||||
**范围**: 教师端「我的班级 / 学生 / 课表」页面与 MySQL(Drizzle) 真数据对接
|
||||
|
||||
### 6.1 页面入口与路由
|
||||
|
||||
班级管理相关页面位于:
|
||||
- `src/app/(dashboard)/teacher/classes/my/page.tsx`
|
||||
- `src/app/(dashboard)/teacher/classes/students/page.tsx`
|
||||
- `src/app/(dashboard)/teacher/classes/schedule/page.tsx`
|
||||
|
||||
为避免构建期/预渲染阶段访问数据库导致失败,以上页面显式启用动态渲染:
|
||||
- `export const dynamic = "force-dynamic"`
|
||||
|
||||
### 6.2 模块结构(Vertical Slice)
|
||||
|
||||
班级模块采用垂直切片架构,代码位于 `src/modules/classes/`:
|
||||
```
|
||||
src/modules/classes/
|
||||
├── components/
|
||||
│ ├── my-classes-grid.tsx
|
||||
│ ├── students-filters.tsx
|
||||
│ ├── students-table.tsx
|
||||
│ ├── schedule-filters.tsx
|
||||
│ └── schedule-view.tsx
|
||||
├── data-access.ts
|
||||
└── types.ts
|
||||
```
|
||||
|
||||
其中 `data-access.ts` 负责班级、学生、课表三类查询的服务端数据读取,并作为页面层唯一的数据入口。
|
||||
|
||||
### 6.3 数据库表与迁移
|
||||
|
||||
新增班级领域表:
|
||||
- `classes`
|
||||
- `class_enrollments`
|
||||
- `class_schedule`
|
||||
|
||||
对应 Drizzle Schema:
|
||||
- `src/shared/db/schema.ts`
|
||||
- `src/shared/db/relations.ts`
|
||||
|
||||
对应迁移文件:
|
||||
- `drizzle/0003_petite_newton_destine.sql`
|
||||
|
||||
外键关系(核心):
|
||||
- `classes.teacher_id` -> `users.id`
|
||||
- `class_enrollments.class_id` -> `classes.id`
|
||||
- `class_enrollments.student_id` -> `users.id`
|
||||
- `class_schedule.class_id` -> `classes.id`
|
||||
|
||||
索引(核心):
|
||||
- `classes_teacher_idx`, `classes_grade_idx`
|
||||
- `class_enrollments_class_idx`, `class_enrollments_student_idx`
|
||||
- `class_schedule_class_idx`, `class_schedule_class_day_idx`
|
||||
|
||||
### 6.4 Seed 数据
|
||||
|
||||
Seed 脚本已覆盖班级相关数据,以便在开发环境快速验证页面渲染与关联关系:
|
||||
- `scripts/seed.ts`
|
||||
- 运行命令:`npm run db:seed`
|
||||
|
||||
### 6.5 开发过程中的问题与处理
|
||||
|
||||
- 端口占用(EADDRINUSE):开发服务器端口被占用时,通过更换端口启动规避(例如 `next dev -p <port>`)。
|
||||
- Next dev 锁文件:出现 `.next/dev/lock` 无法获取锁时,需要确保只有一个 dev 实例在运行,并清理残留 lock。
|
||||
- 头像资源 404:移除 Header 中硬编码的本地头像资源引用,避免 `public/avatars/...` 不存在导致的 404 噪音(见 `src/modules/layout/components/site-header.tsx`)。
|
||||
- 班级人数统计查询失败:`class_enrollments` 表实际列名为 `class_enrollment_status`,修复查询中引用的列名以恢复教师端班级列表渲染。
|
||||
|
||||
### 6.6 班级详情页(聚合视图 + Schedule Builder + Homework 统计)
|
||||
|
||||
**日期**: 2026-01-04
|
||||
**入口**: `src/app/(dashboard)/teacher/classes/my/[id]/page.tsx`
|
||||
|
||||
聚合数据在单次 RSC 请求内并发获取:
|
||||
- 学生:`getClassStudents({ classId })`
|
||||
- 课表:`getClassSchedule({ classId })`
|
||||
- 作业统计:`getClassHomeworkInsights({ classId, limit })`(包含 latest、历史列表、overallScores、以及每次作业的 scoreStats:avg/median)
|
||||
|
||||
页面呈现:
|
||||
- 顶部 KPI 卡片:学生数、课表条目数、作业数、整体 avg/median
|
||||
- Latest homework:目标人数、提交数、批改数、avg/median,直达作业与提交列表
|
||||
- Students / Schedule 预览:提供 View all 跳转到完整列表页
|
||||
- Homework history 表格:支持通过 URL query `?hw=all|active|overdue` 过滤作业记录,并展示每条作业的 avg/median
|
||||
|
||||
课表编辑能力复用既有 Builder:
|
||||
- 组件:`src/modules/classes/components/schedule-view.tsx`(新增/编辑/删除课表项)
|
||||
- 数据变更:`src/modules/classes/actions.ts`
|
||||
|
||||
### 6.7 班级邀请码(6 位码)加入与管理
|
||||
|
||||
**日期**: 2026-01-08
|
||||
**范围**: 为班级新增 6 位邀请码,支持学生通过输入邀请码加入班级;教师可查看与刷新邀请码
|
||||
|
||||
#### 6.7.1 数据结构
|
||||
- 表:`classes`
|
||||
- 字段:`invitation_code`(varchar(6),unique,可为空)
|
||||
- 迁移:`drizzle/0007_add_class_invitation_code.sql`
|
||||
|
||||
#### 6.7.2 教师端能力
|
||||
- 在「我的班级」卡片中展示邀请码。
|
||||
- 提供“刷新邀请码”操作:生成新的 6 位码并写入数据库(确保唯一性)。
|
||||
|
||||
#### 6.7.3 学生端能力
|
||||
- 提供“通过邀请码加入班级”的入口,输入 6 位码后完成报名。
|
||||
- 写库操作设计为幂等:重复提交同一个邀请码不会生成重复报名记录,已有记录会被更新为有效状态。
|
||||
|
||||
#### 6.7.4 Seed 支持
|
||||
- `scripts/seed.ts` 为示例班级补充 `invitationCode`,便于在开发环境直接验证加入流程。
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Textbooks Module Implementation Details
|
||||
|
||||
**Date**: 2025-12-23
|
||||
**Updated**: 2025-12-31
|
||||
**Author**: DevOps Architect
|
||||
**Module**: Textbooks (`src/modules/textbooks`)
|
||||
|
||||
@@ -27,6 +28,13 @@ src/
|
||||
│ ├── page.tsx
|
||||
│ └── loading.tsx
|
||||
│
|
||||
│ └── student/
|
||||
│ └── learning/
|
||||
│ └── textbooks/ # 学生端只读阅读(Server Components)
|
||||
│ ├── page.tsx # 列表页(复用筛选与卡片)
|
||||
│ └── [id]/ # 详情页(阅读器)
|
||||
│ └── page.tsx
|
||||
│
|
||||
├── modules/
|
||||
│ └── textbooks/ # 业务模块
|
||||
│ ├── actions.ts # Server Actions (增删改)
|
||||
@@ -34,6 +42,7 @@ src/
|
||||
│ ├── types.ts # 类型定义 (Schema-aligned)
|
||||
│ └── components/ # 模块私有组件
|
||||
│ ├── textbook-content-layout.tsx # [核心] 三栏布局工作台
|
||||
│ ├── textbook-reader.tsx # [新增] 学生端只读阅读器(URL state)
|
||||
│ ├── chapter-sidebar-list.tsx # 递归章节树
|
||||
│ ├── knowledge-point-panel.tsx # 知识点管理面板
|
||||
│ ├── create-chapter-dialog.tsx # 章节创建弹窗
|
||||
@@ -64,6 +73,11 @@ src/
|
||||
* **Optimistic UI**: 虽然使用 Server Actions,但通过本地状态 (`useState`) 实现了操作的即时反馈(如保存正文后立即退出编辑模式)。
|
||||
* **Feedback**: 使用 `sonner` (`toast`) 提供操作成功或失败的提示。
|
||||
|
||||
### 3.3 学生端阅读体验(Read-Only Reader)
|
||||
* **两栏阅读**:左侧章节树,右侧正文渲染(Markdown)。
|
||||
* **URL State**:选中章节通过 `?chapterId=` 写入 URL,支持刷新/分享后保持定位(nuqs)。
|
||||
* **只读边界**:学生端不暴露创建/删除/编辑/知识点管理入口,避免误用教师工作台能力。
|
||||
|
||||
## 4. 数据流与逻辑 (Data Flow)
|
||||
|
||||
### 4.1 Server Actions
|
||||
@@ -71,18 +85,22 @@ src/
|
||||
* `createChapterAction`: 创建章节(支持嵌套)。
|
||||
* `updateChapterContentAction`: 更新正文内容。
|
||||
* `createKnowledgePointAction`: 创建知识点并自动关联当前章节。
|
||||
* `deleteKnowledgePointAction`: 删除知识点并刷新详情页数据。
|
||||
* `updateTextbookAction`: 更新教材元数据(Title, Subject, Grade, Publisher)。
|
||||
* `deleteTextbookAction`: 删除教材及其关联数据。
|
||||
* `delete...Action`: 处理删除逻辑。
|
||||
|
||||
### 4.2 数据访问层 (Data Access)
|
||||
* **Mock Implementation**: 目前在 `data-access.ts` 中使用内存数组模拟数据库操作,并人为增加了延迟 (`setTimeout`) 以测试 Loading 状态。
|
||||
* **Type Safety**: 定义了严格的 TypeScript 类型 (`Chapter`, `KnowledgePoint`, `UpdateTextbookInput`),确保前后端数据契约一致。
|
||||
* **DB Implementation**: 教材模块已接入真实数据库访问,`data-access.ts` 使用 `drizzle-orm` 直接查询并返回教材、章节、知识点数据。
|
||||
* **章节树构建**: 章节采用父子关系存储,通过一次性拉取后在内存中构建嵌套树结构,避免 N+1 查询。
|
||||
* **级联删除**: 删除章节时会同时删除其子章节以及关联的知识点,确保数据一致性。
|
||||
* **Type Safety**: 定义严格的 TypeScript 类型(如 `Chapter`, `KnowledgePoint`, `UpdateTextbookInput`),保证数据契约与 UI 组件一致。
|
||||
|
||||
## 5. 组件复用
|
||||
* 使用了 `src/shared/components/ui` 中的 Shadcn 组件:
|
||||
* `Dialog`, `ScrollArea`, `Card`, `Button`, `Input`, `Textarea`, `Select`.
|
||||
* `Collapsible` 用于实现递归章节树。
|
||||
* `AlertDialog` 用于危险操作的二次确认(删除章节/删除知识点)。
|
||||
* 图标库统一使用 `lucide-react`.
|
||||
|
||||
## 6. Settings 功能实现 (New)
|
||||
@@ -92,7 +110,39 @@ src/
|
||||
* **Edit**: 修改教材的基本信息。
|
||||
* **Delete**: 提供红色删除按钮,二次确认后执行删除并跳转回列表页。
|
||||
|
||||
## 7. 后续计划 (Next Steps)
|
||||
* [ ] **富文本编辑器**: 集成 Tiptap 替换现有的 Markdown Textarea,支持更丰富的格式。
|
||||
* [ ] **拖拽排序**: 实现章节树的拖拽排序 (`dnd-kit`)。
|
||||
* [ ] **数据库对接**: 将 `data-access.ts` 中的 Mock 逻辑替换为真实的 `drizzle-orm` 数据库调用。
|
||||
## 7. 关键更新记录 (Changelog)
|
||||
|
||||
### 7.1 数据与页面
|
||||
* 教材模块从 Mock 切换为真实 DB:新增教材/章节/知识点的数据访问与 Server Actions 刷新策略。
|
||||
* 列表页支持过滤/搜索:通过 query 参数驱动,统一空状态反馈。
|
||||
|
||||
### 7.2 章节侧边栏与弹窗
|
||||
* 修复子章节创建弹窗“闪现后消失”:改为受控 Dialog 状态管理。
|
||||
* 修复移动端操作按钮不可见/被遮挡:调整布局与可见性策略,确保小屏可点。
|
||||
* 删除章节使用确认弹窗并提供删除中状态。
|
||||
|
||||
### 7.3 Markdown 阅读体验
|
||||
* 阅读模式使用 `react-markdown` 渲染章节内容,支持 GFM(表格/任务列表等)。
|
||||
* 启用 Typography(`prose`)排版样式,使 `h1/h2/...` 在视觉上有明显层级差异。
|
||||
* 修复阅读模式内容区无法滚动:为 flex 容器补齐 `min-h-0` 等必要约束。
|
||||
|
||||
### 7.4 知识点删除交互
|
||||
* 删除知识点从浏览器 `confirm()` 升级为 `AlertDialog`:
|
||||
* 显示目标名称、危险样式按钮
|
||||
* 删除中禁用交互并显示 loading 文案
|
||||
* 删除成功后刷新页面数据
|
||||
|
||||
### 7.5 学生端 Textbooks 列表与阅读页(New)
|
||||
* 新增学生端路由:
|
||||
* `/student/learning/textbooks`:教材列表页(RSC),复用筛选组件(nuqs)与卡片布局。
|
||||
* `/student/learning/textbooks/[id]`:教材阅读页(RSC + client 阅读器容器),章节选择与阅读不跳页。
|
||||
* 复用与适配:
|
||||
* `TextbookCard` 增加可配置跳转基地址,避免学生端卡片误跳到教师端详情页。
|
||||
* 新增 `TextbookReader`(client)用于只读阅读体验:左侧章节树 + 右侧正文渲染,章节定位 URL 化(`chapterId`)。
|
||||
* 质量门禁:
|
||||
* 通过 `npm run lint / typecheck / build`。
|
||||
|
||||
## 8. 后续计划 (Next Steps)
|
||||
* [ ] **富文本编辑器**: 集成编辑器替换当前 Markdown Textarea,提升编辑体验。
|
||||
* [ ] **拖拽排序**: 实现章节树拖拽排序与持久化。
|
||||
* [ ] **知识点能力增强**: 支持编辑、排序、分层(如需要)。
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
|
||||
- `getHomeworkAssignments`:作业列表(可按 creatorId/ids)
|
||||
- `getHomeworkAssignmentById`:作业详情(含目标人数、提交数统计)
|
||||
- `getHomeworkSubmissions`:提交列表(可按 assignmentId)
|
||||
- `getHomeworkSubmissions`:提交列表(可按 assignmentId/classId/creatorId)
|
||||
- `getHomeworkSubmissionDetails`:提交详情(题目内容 + 学生答案 + 分值/顺序)
|
||||
|
||||
### 4.2 学生侧查询
|
||||
@@ -151,3 +151,120 @@
|
||||
|
||||
- `npm run typecheck`: 通过
|
||||
- `npm run lint`: 0 errors(仓库其他位置存在 warnings,与本模块新增功能无直接关联)
|
||||
|
||||
---
|
||||
|
||||
## 9. 部署与环境变量(CI/CD)
|
||||
|
||||
### 9.1 本地开发
|
||||
|
||||
- 本地开发使用项目根目录的 `.env` 提供 `DATABASE_URL`
|
||||
- `.env` 仅用于本机开发,不应写入真实生产库凭据
|
||||
|
||||
### 9.2 CI 构建与部署(Gitea)
|
||||
|
||||
工作流位于:[ci.yml](file:///c:/Users/xiner/Desktop/CICD/.gitea/workflows/ci.yml)
|
||||
|
||||
- 构建阶段(`npm run build`)不依赖数据库连接:作业相关页面在构建时不会静态预渲染执行查库
|
||||
- 部署阶段通过 `docker run -e DATABASE_URL=...` 在运行时注入数据库连接串
|
||||
- 需要在 Gitea 仓库 Secrets 配置 `DATABASE_URL`(生产环境 MySQL 连接串)
|
||||
- CI 中关闭 Next.js telemetry:设置 `NEXT_TELEMETRY_DISABLED=1`
|
||||
|
||||
### 9.3 Next.js 渲染策略(避免 build 阶段查库)
|
||||
|
||||
作业模块相关页面在渲染时会进行数据库查询,因此显式标记为动态渲染以避免构建期预渲染触发数据库连接:
|
||||
|
||||
- 教师端作业列表:[assignments/page.tsx](file:///c:/Users/xiner/Desktop/CICD/src/app/(dashboard)/teacher/homework/assignments/page.tsx)
|
||||
|
||||
---
|
||||
|
||||
## 10. 实现更新(2026-01-05)
|
||||
|
||||
### 10.1 教师端作业详情页组件化(按 Vertical Slice 拆分)
|
||||
|
||||
将 `/teacher/homework/assignments/[id]` 页面调整为“只负责组装”,把可复用展示逻辑下沉到模块内组件:
|
||||
|
||||
- 页面组装:[page.tsx](file:///c:/Users/xiner/Desktop/CICD/src/app/(dashboard)/teacher/homework/assignments/%5Bid%5D/page.tsx)
|
||||
- 题目错误概览卡片(overview):[homework-assignment-question-error-overview-card.tsx](file:///c:/Users/xiner/Desktop/CICD/src/modules/homework/components/homework-assignment-question-error-overview-card.tsx)
|
||||
- 题目错误明细卡片(details):[homework-assignment-question-error-details-card.tsx](file:///c:/Users/xiner/Desktop/CICD/src/modules/homework/components/homework-assignment-question-error-details-card.tsx)
|
||||
- 试卷预览/错题工作台容器卡片:[homework-assignment-exam-content-card.tsx](file:///c:/Users/xiner/Desktop/CICD/src/modules/homework/components/homework-assignment-exam-content-card.tsx)
|
||||
|
||||
### 10.2 题目点击联动:试卷预览 ↔ 错题详情
|
||||
|
||||
在“试卷预览”中点击题目后,右侧联动展示该题的统计与错答列表(按学生逐条展示,不做合并):
|
||||
|
||||
- 工作台(选择题目、拼装左右面板):[homework-assignment-exam-error-explorer.tsx](file:///c:/Users/xiner/Desktop/CICD/src/modules/homework/components/homework-assignment-exam-error-explorer.tsx)
|
||||
- 试卷预览面板(可选中题目):[homework-assignment-exam-preview-pane.tsx](file:///c:/Users/xiner/Desktop/CICD/src/modules/homework/components/homework-assignment-exam-preview-pane.tsx)
|
||||
- 错题详情面板(错误人数/错误率/错答列表):[homework-assignment-question-error-detail-panel.tsx](file:///c:/Users/xiner/Desktop/CICD/src/modules/homework/components/homework-assignment-question-error-detail-panel.tsx)
|
||||
|
||||
### 10.3 统计数据增强:返回逐学生错答
|
||||
|
||||
为满足“错答列表逐条展示学生姓名 + 答案”的需求,作业统计查询返回每题的错答明细(包含学生信息):
|
||||
|
||||
- 数据访问:[getHomeworkAssignmentAnalytics](file:///c:/Users/xiner/Desktop/CICD/src/modules/homework/data-access.ts)
|
||||
- 类型定义:[types.ts](file:///c:/Users/xiner/Desktop/CICD/src/modules/homework/types.ts)
|
||||
|
||||
### 10.4 加载优化:Client Wrapper 动态分包
|
||||
|
||||
由于 `next/dynamic({ ssr: false })` 不能在 Server Component 内使用,工作台动态加载通过 Client wrapper 进行隔离:
|
||||
|
||||
- Client wrapper:[homework-assignment-exam-error-explorer-lazy.tsx](file:///c:/Users/xiner/Desktop/CICD/src/modules/homework/components/homework-assignment-exam-error-explorer-lazy.tsx)
|
||||
- 入口卡片(Server Component,渲染 wrapper):[homework-assignment-exam-content-card.tsx](file:///c:/Users/xiner/Desktop/CICD/src/modules/homework/components/homework-assignment-exam-content-card.tsx)
|
||||
|
||||
### 10.5 校验
|
||||
|
||||
- `npm run lint`: 通过
|
||||
- `npm run typecheck`: 通过
|
||||
- `npm run build`: 通过
|
||||
|
||||
---
|
||||
|
||||
## 11. 学生成绩图表与排名(2026-01-06)
|
||||
|
||||
### 11.1 目标
|
||||
|
||||
在学生主页(Dashboard)展示:
|
||||
|
||||
- 最近已批改作业的成绩趋势(百分比折线)
|
||||
- 最近若干次已批改作业明细(标题、得分、时间)
|
||||
- 班级排名(基于班级内作业总体得分百分比)
|
||||
|
||||
### 11.2 数据访问与计算口径
|
||||
|
||||
数据由 Homework 模块统一提供聚合查询,避免页面层拼 SQL:
|
||||
|
||||
- 新增查询:[getStudentDashboardGrades](file:///c:/Users/xiner/Desktop/CICD/src/modules/homework/data-access.ts)
|
||||
- `trend`:取该学生所有 `graded` 提交中“每个 assignment 最新一次”的集合,按时间升序取最近 10 个
|
||||
- `recent`:对 `trend` 再按时间降序取最近 5 条,用于表格展示
|
||||
- `maxScore`:通过 `homework_assignment_questions` 汇总每个 assignment 的总分(SUM(score))
|
||||
- `percentage`:`score / maxScore * 100`
|
||||
- `ranking`:
|
||||
- 班级选择:取该学生最早创建的一条 active enrollment 作为当前班级
|
||||
- 班级作业集合:班级内所有学生的 targets 合并得到 assignment 集合
|
||||
- 计分口径:班级内“每个学生 × 每个 assignment”取最新一次 graded 提交,累加得分与满分,得到总体百分比
|
||||
- 排名:按总体百分比降序排序(百分比相同按 studentId 作为稳定排序因子)
|
||||
|
||||
### 11.3 类型定义
|
||||
|
||||
为 Dashboard 聚合数据提供显式类型:
|
||||
|
||||
- [types.ts](file:///c:/Users/xiner/Desktop/CICD/src/modules/homework/types.ts)
|
||||
- `StudentHomeworkScoreAnalytics`
|
||||
- `StudentRanking`
|
||||
- `StudentDashboardGradeProps`
|
||||
|
||||
### 11.4 页面与组件接入
|
||||
|
||||
- 学生主页页面负责“取数 + 计算基础计数 + 传参”:
|
||||
- [student/dashboard/page.tsx](file:///c:/Users/xiner/Desktop/CICD/src/app/(dashboard)/student/dashboard/page.tsx)
|
||||
- 取数:`getStudentDashboardGrades(student.id)`
|
||||
- 传入:`<StudentDashboard grades={grades} />`
|
||||
- 展示组件负责渲染卡片:
|
||||
- [student-view.tsx](file:///c:/Users/xiner/Desktop/CICD/src/modules/dashboard/components/student-view.tsx)
|
||||
- 趋势图:使用内联 `svg polyline` 渲染折线,避免引入额外图表依赖
|
||||
|
||||
### 11.5 校验
|
||||
|
||||
- `npm run lint`: 通过
|
||||
- `npm run typecheck`: 通过
|
||||
- `npm run build`: 通过
|
||||
|
||||
164
docs/design/007_school_module_implementation.md
Normal file
164
docs/design/007_school_module_implementation.md
Normal file
@@ -0,0 +1,164 @@
|
||||
# 学校基础数据模块(School)实现文档与更新记录
|
||||
|
||||
**日期**: 2026-01-07
|
||||
**作者**: Frontend Team
|
||||
**状态**: 已实现
|
||||
|
||||
## 1. 范围
|
||||
|
||||
本文档覆盖管理端「School」域的基础数据维护页面(Schools / Departments / Academic Year / Grades),并记录相关实现约束与关键更新,遵循 [003_frontend_engineering_standards.md](file:///c:/Users/xiner/Desktop/CICD/docs/architecture/003_frontend_engineering_standards.md) 的工程规范(Vertical Slice、Server/Client 边界、质量门禁)。
|
||||
|
||||
## 2. 路由入口(Admin)
|
||||
|
||||
School 域路由位于 `src/app/(dashboard)/admin/school/*`,均显式声明 `export const dynamic = "force-dynamic"` 以避免构建期预渲染触发数据库访问。
|
||||
|
||||
- `/admin/school`:入口重定向到 Classes(当前落点不在 `school` 模块内)
|
||||
实现:[page.tsx](file:///c:/Users/xiner/Desktop/CICD/src/app/(dashboard)/admin/school/page.tsx)
|
||||
- `/admin/school/schools`:学校维护(增删改)
|
||||
实现:[page.tsx](file:///c:/Users/xiner/Desktop/CICD/src/app/(dashboard)/admin/school/schools/page.tsx)
|
||||
- `/admin/school/departments`:部门维护(增删改)
|
||||
实现:[page.tsx](file:///c:/Users/xiner/Desktop/CICD/src/app/(dashboard)/admin/school/departments/page.tsx)
|
||||
- `/admin/school/academic-year`:学年维护(增删改 + 设为当前学年)
|
||||
实现:[page.tsx](file:///c:/Users/xiner/Desktop/CICD/src/app/(dashboard)/admin/school/academic-year/page.tsx)
|
||||
- `/admin/school/grades`:年级维护(增删改 + 指派年级组长/教研组长)
|
||||
实现:[page.tsx](file:///c:/Users/xiner/Desktop/CICD/src/app/(dashboard)/admin/school/grades/page.tsx)
|
||||
- `/admin/school/grades/insights`:年级维度作业统计(跨班级聚合)
|
||||
实现:[page.tsx](file:///c:/Users/xiner/Desktop/CICD/src/app/(dashboard)/admin/school/grades/insights/page.tsx)
|
||||
|
||||
## 3. 模块结构(Vertical Slice)
|
||||
|
||||
School 模块位于 `src/modules/school`:
|
||||
|
||||
```
|
||||
src/modules/school/
|
||||
├── components/
|
||||
│ ├── schools-view.tsx
|
||||
│ ├── departments-view.tsx
|
||||
│ ├── academic-year-view.tsx
|
||||
│ └── grades-view.tsx
|
||||
├── actions.ts
|
||||
├── data-access.ts
|
||||
├── schema.ts
|
||||
└── types.ts
|
||||
```
|
||||
|
||||
边界约束:
|
||||
- `data-access.ts` 包含 `import "server-only"`,仅用于服务端查询与 DTO 组装。
|
||||
- `actions.ts` 包含 `"use server"`,写操作统一通过 Server Actions 并 `revalidatePath`。
|
||||
- `components/*` 为 Client 交互层(表单、Dialog、筛选、行级操作),调用 Server Actions 并用 `sonner` toast 反馈。
|
||||
|
||||
## 4. 数据访问(data-access.ts)
|
||||
|
||||
实现:[data-access.ts](file:///c:/Users/xiner/Desktop/CICD/src/modules/school/data-access.ts)
|
||||
|
||||
- `getSchools(): Promise<SchoolListItem[]>`
|
||||
- `getDepartments(): Promise<DepartmentListItem[]>`
|
||||
- `getAcademicYears(): Promise<AcademicYearListItem[]>`
|
||||
- `getGrades(): Promise<GradeListItem[]>`
|
||||
- join `schools` 获取 `school.name`
|
||||
- 收集 `gradeHeadId/teachingHeadId` 并批量查询 `users` 以组装 `StaffOption`
|
||||
- `getStaffOptions(): Promise<StaffOption[]>`
|
||||
- 角色过滤 `teacher/admin`
|
||||
- 排序 `name/email`,用于 Select 列表可用性
|
||||
- `getGradesForStaff(staffId: string): Promise<GradeListItem[]>`
|
||||
- 用于按负责人(年级组长/教研组长)反查关联年级
|
||||
|
||||
返回 DTO 类型定义位于:[types.ts](file:///c:/Users/xiner/Desktop/CICD/src/modules/school/types.ts)
|
||||
|
||||
## 5. 写操作(actions.ts)
|
||||
|
||||
实现:[actions.ts](file:///c:/Users/xiner/Desktop/CICD/src/modules/school/actions.ts)
|
||||
|
||||
通用约束:
|
||||
- 输入校验:统一使用 [schema.ts](file:///c:/Users/xiner/Desktop/CICD/src/modules/school/schema.ts) 的 Zod schema
|
||||
- 返回结构:统一使用 [ActionState](file:///c:/Users/xiner/Desktop/CICD/src/shared/types/action-state.ts)
|
||||
- 刷新策略:对目标页面路径执行 `revalidatePath`
|
||||
|
||||
Departments:
|
||||
- `createDepartmentAction(formData)`
|
||||
- `updateDepartmentAction(departmentId, formData)`
|
||||
- `deleteDepartmentAction(departmentId)`
|
||||
|
||||
Academic Year:
|
||||
- `createAcademicYearAction(formData)`
|
||||
- `updateAcademicYearAction(academicYearId, formData)`
|
||||
- `deleteAcademicYearAction(academicYearId)`
|
||||
- 当 `isActive=true` 时,通过事务把其它学年置为非激活,保证唯一激活学年
|
||||
|
||||
Schools:
|
||||
- `createSchoolAction(formData)`
|
||||
- `updateSchoolAction(schoolId, formData)`
|
||||
- `deleteSchoolAction(schoolId)`
|
||||
- 删除后会同时刷新 `/admin/school/schools` 与 `/admin/school/grades`
|
||||
|
||||
Grades:
|
||||
- `createGradeAction(formData)`
|
||||
- `updateGradeAction(gradeId, formData)`
|
||||
- `deleteGradeAction(gradeId)`
|
||||
|
||||
## 6. UI 组件(components/*)
|
||||
|
||||
Schools:
|
||||
- 实现:[schools-view.tsx](file:///c:/Users/xiner/Desktop/CICD/src/modules/school/components/schools-view.tsx)
|
||||
- 交互:列表 + Dialog 表单(新增/编辑)+ 删除确认(AlertDialog)
|
||||
|
||||
Departments:
|
||||
- 实现:[departments-view.tsx](file:///c:/Users/xiner/Desktop/CICD/src/modules/school/components/departments-view.tsx)
|
||||
- 交互:列表 + Dialog 表单(新增/编辑)+ 删除确认(AlertDialog)
|
||||
|
||||
Academic Year:
|
||||
- 实现:[academic-year-view.tsx](file:///c:/Users/xiner/Desktop/CICD/src/modules/school/components/academic-year-view.tsx)
|
||||
- 交互:列表 + Dialog 表单(新增/编辑)+ 删除确认(AlertDialog)+ 设为当前学年(isActive)
|
||||
|
||||
Grades:
|
||||
- 实现:[grades-view.tsx](file:///c:/Users/xiner/Desktop/CICD/src/modules/school/components/grades-view.tsx)
|
||||
- 交互:列表展示 + URL 驱动筛选(搜索/学校/负责人/排序)+ Dialog 表单(新增/编辑)+ 删除确认(AlertDialog)
|
||||
- 负责人指派:
|
||||
- 年级组长(gradeHeadId)
|
||||
- 教研组长(teachingHeadId)
|
||||
|
||||
## 7. 关键交互与规则(Grades)
|
||||
|
||||
页面入口(RSC 组装)在服务端并发拉取三类数据:
|
||||
- 年级列表:`getGrades()`
|
||||
- 学校选项:`getSchools()`
|
||||
- 负责人候选:`getStaffOptions()`
|
||||
|
||||
实现:[page.tsx](file:///c:/Users/xiner/Desktop/CICD/src/app/(dashboard)/admin/school/grades/page.tsx)
|
||||
|
||||
### 7.1 URL State(筛选/排序)
|
||||
|
||||
Grades 列表页的筛选状态 URL 化(`nuqs`):
|
||||
- `q`:关键字(匹配 grade/school)
|
||||
- `school`:学校过滤(`all` 或具体 schoolId)
|
||||
- `head`:负责人过滤(全部 / 两者缺失 / 缺年级组长 / 缺教研组长)
|
||||
- `sort`:排序(默认/名称/更新时间等)
|
||||
|
||||
实现:[grades-view.tsx](file:///c:/Users/xiner/Desktop/CICD/src/modules/school/components/grades-view.tsx)
|
||||
|
||||
### 7.2 表单校验
|
||||
|
||||
Grades 的新增/编辑表单在客户端做轻量校验:
|
||||
- 必填:`schoolId`、`name`
|
||||
- `order`:非负整数
|
||||
- 去重:同一学校下年级名称不允许重复(忽略大小写 + 规范化空格)
|
||||
|
||||
说明:
|
||||
- 服务端写入前仍会经过 `UpsertGradeSchema` 校验(schema.ts),避免仅依赖客户端校验。
|
||||
|
||||
### 7.3 负责人选择(Radix Select)
|
||||
|
||||
Radix Select 约束:`SelectItem` 的 `value` 不能为 `""`(空字符串),否则会触发运行时错误。
|
||||
|
||||
当前实现策略:
|
||||
- UI 中 “未设置” 选项使用占位值 `__none__`
|
||||
- 在 `onValueChange` 中将 `__none__` 映射回 `""` 存入本地表单 state
|
||||
- 提交时依旧传递空字符串,由 `UpsertGradeSchema` 将其归一为 `null`
|
||||
|
||||
实现:[grades-view.tsx](file:///c:/Users/xiner/Desktop/CICD/src/modules/school/components/grades-view.tsx)
|
||||
|
||||
## 2. 更新记录(2026-01-07)
|
||||
|
||||
- 修复 Add Grades 弹窗报错:将 4 处 `<SelectItem value=\"\">` 替换为占位值 `__none__`,并在 `onValueChange` 中映射回 `\"\"`,保持“可清空选择/显示 placeholder”的行为不变。
|
||||
- 修复新建年级按钮不可用:创建/编辑表单在状态变化时触发实时校验更新,避免校验状态滞后导致提交被禁用。
|
||||
- 质量门禁:本地通过 `npm run lint` 与 `npm run typecheck`。
|
||||
Reference in New Issue
Block a user