Files
NextEdu/docs/architecture/003_ui_refactoring_plan.md

415 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
> ⚠️ **已归档文档**
> 本文档是 2026-06-16 架构审核后的 UI 代码结构重构计划(评分 7.2/10描述的是待修复的质量问题与重构方案。
> 当前重构计划已执行完毕,组件拆分、复用抽象、测试保障等改进已落地,最新架构状态详见 [004 架构影响地图](./004_architecture_impact_map.md)。
> 保留用于历史参考,不再维护。
---
# UI 代码结构重构计划
> 基于 2026-06-16 架构审核,整体评分 7.2/10
> 目标补齐工程细节短板达到企业级标准8.5+
---
## 审核评分总览
| 维度 | 评分 | 状态 |
|------|------|------|
| 目录结构分层 | 8/10 | 良好 |
| 组件设计 | 6.5/10 | 需重构 |
| 复用与抽象 | 6/10 | 需重构 |
| 样式组织 | 8.5/10 | 优秀 |
| 可维护性 | 7/10 | 一般 |
| 性能相关 | 7/10 | 一般 |
| 测试与质量保障 | 5.5/10 | 需重构 |
| 安全性 | 7.5/10 | 良好 |
---
## P0 — 紧急(阻塞级质量问题)
### P0-1 拆分巨型组件
**问题:** exam-form.tsx1623 行、textbook-reader.tsx744 行)严重违反单一职责,难以维护和测试。
#### exam-form.tsx 拆分方案
```
src/modules/exams/components/
exam-form.tsx ← 保留为容器组件(~200 行),组合子组件
exam-basic-info-form.tsx ← 考试基本信息(名称、科目、时间)
exam-ai-generator.tsx ← AI 生成试卷功能
exam-structure-editor.tsx ← 试卷结构编辑
exam-question-selector.tsx ← 题目选择器
exam-preview-panel.tsx ← 试卷预览面板
exam-form-actions.tsx ← 表单操作按钮组
```
#### textbook-reader.tsx 拆分方案
```
src/modules/textbooks/components/
textbook-reader.tsx ← 保留为容器组件(~150 行)
textbook-content-panel.tsx ← 内容阅读面板Markdown 渲染)
knowledge-point-list.tsx ← 知识点列表
knowledge-graph.tsx ← 知识图谱可视化
knowledge-point-dialogs.tsx ← 创建/编辑知识点 Dialog
textbook-editor-panel.tsx ← 编辑模式面板
use-text-selection.ts ← 文本选择逻辑 Hook
use-knowledge-point-actions.ts ← 知识点操作逻辑 Hook
```
**验收标准:**
- 单文件不超过 300 行
- 每个子组件职责单一,可独立测试
- 容器组件仅负责组合和状态分发
---
### P0-2 修复无障碍a11y缺陷
**问题:** 全项目仅 7 处 `aria-label`,但 180 处 `onClick`;存在 `<div onClick>` 非语义化标签。
#### 修复清单
1. **图标按钮补 aria-label**
- 搜索所有 `<Button.*size="icon"` 或仅含图标的按钮,补充 `aria-label`
- 涉及文件exam-actions.tsx、question-columns.tsx、exam-columns.tsx、breadcrumb.tsx 等
2. **替换 `<div onClick>` 为语义化标签**
- textbook-reader.tsx:434 — 知识点卡片 → `<button>`
- 搜索所有 `<div onClick` / `<span onClick` 模式,逐一替换
3. **表单控件补 label**
- 确认所有 `<Input>` / `<Select>` 有关联 `<Label htmlFor>``aria-label`
4. **添加 skip-link**
- Root Layout 或 Dashboard Layout 添加:
```tsx
<a href="#main-content" className="sr-only focus:not-sr-only ...">
Skip to main content
</a>
```
- `<main>` 添加 `id="main-content"`
**验收标准:**
- axe-core 扫描零严重违规
- 所有图标按钮有 `aria-label`
- 无 `<div onClick>` / `<span onClick>` 模式
---
## P1 — 重要(架构级改进)
### P1-1 创建自定义 Hooks 层
**问题:** 整个项目零自定义 Hook逻辑耦合在组件中。
#### 通用 Hookssrc/shared/hooks/
```
src/shared/hooks/
use-action-with-toast.ts ← Server Action + toast 反馈
use-async-action.ts ← 异步操作 loading/error 状态
use-debounce.ts ← 防抖
use-media-query.ts ← 响应式断点
use-local-storage.ts ← 本地存储
```
示例实现:
```typescript
// src/shared/hooks/use-action-with-toast.ts
"use client"
import { useTransition } from "react"
import { toast } from "sonner"
import type { ActionState } from "@/shared/types/action-state"
export function useActionWithToast<T>(): { isPending: boolean; execute: (action: () => Promise<ActionState<T>>) => Promise<void> } {
const [isPending, startTransition] = useTransition()
const execute = async (action: () => Promise<ActionState<T>>): Promise<void> => {
startTransition(async () => {
const result = await action()
if (result.success) {
toast.success(result.message || "操作成功")
} else {
toast.error(result.message || "操作失败")
}
})
}
return { isPending, execute }
}
```
#### 模块级 Hookssrc/modules/*/hooks/
```
src/modules/exams/hooks/
use-exam-form.ts ← 考试表单状态管理
use-exam-filters.ts ← 考试筛选逻辑
src/modules/textbooks/hooks/
use-text-selection.ts ← 文本选择 + 知识点创建
use-knowledge-point-actions.ts ← 知识点 CRUD 操作
src/modules/homework/hooks/
use-homework-submission.ts ← 作业提交逻辑
```
**验收标准:**
- 组件中无超过 3 个 `useState` 的逻辑块(应提取为 Hook
- 通用 Hook 有单元测试
---
### P1-2 添加动画降级prefers-reduced-motion
**问题:** 全项目零处 `prefers-reduced-motion` 引用,不符合 WCAG 2.1。
#### 方案
1. **globals.css 添加全局降级**
```css
@layer base {
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
}
```
2. **tailwindcss-animate 适配**
确认 `tailwindcss-animate` 生成的动画类在 `prefers-reduced-motion: reduce` 下被正确降级。如不支持,在 globals.css 中补充:
```css
@media (prefers-reduced-motion: reduce) {
.animate-accordion-down,
.animate-accordion-up {
animation: none !important;
}
}
```
**验收标准:**
- 操作系统开启"减少动态效果"后,页面无动画
- Lighthouse Accessibility 评分 ≥ 90
---
### P1-3 引入 Next.js 图片与字体优化
#### next/image
- 搜索所有 `<img>` 标签,替换为 `next/image`
- 配置 `next.config.ts` 的 `images.remotePatterns`(如有外部图片源)
#### next/font
```typescript
// src/app/layout.tsx
import { Inter } from "next/font/google"
const inter = Inter({
subsets: ["latin"],
display: "swap",
variable: "--font-inter",
})
```
**验收标准:**
- Lighthouse Performance 无 "Properly size images" 警告
- 无 "Eliminate render-blocking resources" 字体相关警告
---
## P2 — 改进(质量提升)
### P2-1 补充组件单元测试
**问题:** 零组件测试、零 Hook 测试、零工具函数测试。
#### 测试目录结构co-location 模式)
```
src/modules/exams/components/
exam-card.tsx
exam-card.test.tsx ← 新增
src/shared/hooks/
use-action-with-toast.ts
use-action-with-toast.test.ts ← 新增
src/shared/lib/
utils.ts
utils.test.ts ← 新增
```
#### 优先级
1. **高**`useActionWithToast`、`cn()`、`formatDate()`
2. **高**ExamCard、QuestionColumns、HomeworkAssignmentForm
3. **中**:各模块 data-access.tsmock DB 测试)
4. **低**UI 基础组件shadcn/ui 已有上游测试)
#### 配置更新
```typescript
// vitest.config.ts 添加
test: {
include: ["src/**/*.test.{ts,tsx}"],
}
```
**验收标准:**
- 关键路径覆盖率 > 80%
- CI 中 `npm run test:integration` 包含单元测试
---
### P2-2 统一 Tailwind v4 配置
**问题:** `tailwind.config.ts`v3 风格)与 `globals.css` 的 `@theme inline`v4 风格)并存。
#### 方案
1. 将 `tailwind.config.ts` 中的 `theme.extend` 迁移至 `globals.css` 的 `@theme inline` 块
2. 删除 `tailwind.config.ts` 中与 CSS 重复的定义
3. 保留 `tailwind.config.ts` 仅用于 `plugins` 和 `content` 配置(如 v4 仍需要)
**验收标准:**
- 无重复的主题定义
- `npm run build` 无 Tailwind deprecation 警告
---
### P2-3 清理 Mock 数据
**问题:** `src/modules/exams/mock-data.ts` 和 `src/modules/questions/mock-data.ts` 残留在生产代码中。
#### 方案
```
src/mocks/ ← 新增
exam-data.ts ← 从 modules/exams/mock-data.ts 迁移
question-data.ts ← 从 modules/questions/mock-data.ts 迁移
tests/mocks/ ← 或放测试目录
exam-factories.ts ← 使用 @faker-js/faker 生成
question-factories.ts
```
- 删除 `src/modules/*/mock-data.ts`
- 确认无生产代码引用 mock 数据
**验收标准:**
- `src/modules/` 下无 `mock-data.ts`
- 生产构建不包含 mock 数据
---
## P3 — 优化(锦上添花)
### P3-1 i18n 文案提取
**问题:** 硬编码中文字案散布在组件中。
#### 方案
- 引入 `next-intl` 或轻量 i18n 方案
- 提取所有用户可见文案到 `src/shared/i18n/zh-CN.json`
- 代码中通过 `t("knowledgePoint.created")` 引用
### P3-2 替换原生 confirm/alert
**问题:** textbook-reader.tsx 使用 `confirm()`,与项目 UI 风格不一致。
#### 方案
- 创建 `useConfirmDialog` Hook封装 AlertDialog
- 全局搜索 `confirm(` 和 `alert(`,逐一替换
### P3-3 react-markdown 安全加固
**问题:** textbook-reader.tsx 渲染用户内容remarkGfm 启用 HTML 后需防 XSS。
#### 方案
```typescript
import rehypeSanitize from "rehype-sanitize"
<ReactMarkdown
remarkPlugins={[remarkGfm, remarkBreaks]}
rehypePlugins={[rehypeSanitize]} ← 新增
>
```
### P3-4 formatDate 国际化
**问题:** `utils.ts` 中 `formatDate` 硬编码 `"en-US"`。
#### 方案
```typescript
export function formatDate(date: string | Date, locale = "zh-CN"): string {
return new Intl.DateTimeFormat(locale, {
year: "numeric",
month: "short",
day: "numeric",
}).format(new Date(date))
}
```
### P3-5 AI 聊天接口速率限制
**问题:** `/api/ai/chat` 需确认是否有服务端速率限制。
#### 方案
- 使用 `@upstash/ratelimit` 或自实现基于 IP/用户的速率限制
- 建议限制:每用户每分钟 10 次请求
---
## 执行时间线
| 阶段 | 内容 | 预计周期 |
|------|------|---------|
| 第 1 周 | P0-1 exam-form.tsx 拆分 | 3 天 |
| 第 1 周 | P0-1 textbook-reader.tsx 拆分 | 2 天 |
| 第 2 周 | P0-2 无障碍修复 | 3 天 |
| 第 2 周 | P1-1 创建 Hooks 层 | 2 天 |
| 第 3 周 | P1-2 动画降级 | 1 天 |
| 第 3 周 | P1-3 图片/字体优化 | 2 天 |
| 第 3-4 周 | P2-1 补充单元测试 | 5 天 |
| 第 4 周 | P2-2 + P2-3 配置清理 | 2 天 |
| 持续 | P3 优化项 | 随迭代推进 |
---
## 验收检查清单
- [ ] 单文件不超过 300 行
- [ ] axe-core 扫描零严重 a11y 违规
- [ ] Lighthouse Accessibility ≥ 90
- [ ] Lighthouse Performance ≥ 85
- [ ] 关键路径测试覆盖率 > 80%
- [ ] `npm run build` 零错误零警告
- [ ] `npm run lint` 零错误
- [ ] `npm run typecheck` 零错误
- [ ] 无 `<div onClick>` / `<span onClick>` 模式
- [ ] 所有图标按钮有 `aria-label`
- [ ] `prefers-reduced-motion` 降级生效
- [ ] 生产构建不含 mock 数据