- 全项目逐文件审查: 4 份审计报告(shared/core-business/management/new-modules) - 重写 004 架构影响地图: 图优先 + 模块依赖图 + 数据流 + 调用链 + 问题分级 - 更新 005 结构化数据: 新增 architectureOverview/moduleDependencyGraph/knownIssues/dbTables 节点 - 更新 006 功能清单: 143 项功能标注实现状态, P0 覆盖率 80%->92% - 更新 007 差距审计: v2->v3, P0 完成 69%->84%, 新增架构技术债章节 - 更新 001 项目概览: 6 角色/54 权限/26 模块/54 表 - 新增 docs/README.md 文档索引 - 归档 11 份过时文档(002x2/003/designx8) 标注 - 更新 work_log
415 lines
11 KiB
Markdown
415 lines
11 KiB
Markdown
> ⚠️ **已归档文档**
|
||
> 本文档是 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.tsx(1623 行)、textbook-reader.tsx(744 行)严重违反单一职责,难以维护和测试。
|
||
|
||
#### 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,逻辑耦合在组件中。
|
||
|
||
#### 通用 Hooks(src/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>() {
|
||
const [isPending, startTransition] = useTransition()
|
||
|
||
const execute = async (action: () => Promise<ActionState<T>>) => {
|
||
startTransition(async () => {
|
||
const result = await action()
|
||
if (result.success) {
|
||
toast.success(result.message || "操作成功")
|
||
} else {
|
||
toast.error(result.message || "操作失败")
|
||
}
|
||
})
|
||
}
|
||
|
||
return { isPending, execute }
|
||
}
|
||
```
|
||
|
||
#### 模块级 Hooks(src/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.ts(mock 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") {
|
||
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 数据
|