# 组件化重构专项实施计划 > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 拆分 9 个巨型文件、收敛 49 个重复组件到共享底座、补全 ErrorBoundary/StatsGrid/SkeletonCard 底座 **Architecture:** 两阶段 — Phase 1 收敛+补全底座(ErrorBoundary 抽取 + StatsGrid/SkeletonCard 新增),Phase 2 按模块垂直闭环(3 批风险分级:低/中/高) **Tech Stack:** Next.js 16 / React 19 / TypeScript strict / Tailwind v4 / next-intl / shadcn/ui **Spec:** `docs/superpowers/specs/2026-07-06-component-refactoring-design.md` --- ## 文件结构总览 ### Phase 1 新增/修改文件 | 文件 | 操作 | 职责 | |------|------|------| | `src/shared/components/ui/error-boundary.tsx` | 新增 | 基础 ErrorBoundary 类组件(从 section-error-boundary.tsx 抽取) | | `src/shared/components/ui/stats-grid.tsx` | 新增 | StatsGrid 容器,基于 StatCard 渲染网格 | | `src/shared/components/ui/skeleton.tsx` | 扩展 | 新增 SkeletonCard variant(保留原 Skeleton 原语) | | `src/shared/components/section-error-boundary.tsx` | 修改 | 内部改用 ErrorBoundary 基础组件 | | `src/shared/components/route-error.tsx` | 修改 | 内部改用 ErrorBoundary 基础组件 | | `src/shared/components/widget-boundary.tsx` | 修改 | 内部改用 ErrorBoundary 基础组件 | ### Phase 2 模块层(按批次) 每批次删除模块层 `*-filters.tsx`/`*-stats-cards.tsx`/`*-skeleton.tsx`/`*-error-boundary.tsx`,迁移到底座。巨型文件拆为容器+子组件。 --- ## Phase 1:底座收敛与补全 ### Task 1: 抽取 ErrorBoundary 基础组件 **Files:** - Create: `src/shared/components/ui/error-boundary.tsx` - [ ] **Step 1: 创建 error-boundary.tsx** ```tsx "use client" import { Component, type ErrorInfo, type ReactNode } from "react" /** * 基础 ErrorBoundary 类组件。 * 不直接使用,请使用 SectionErrorBoundary / RouteErrorBoundary / WidgetBoundary 等 preset。 * * 支持两种降级形式: * 1. `fallback`(函数 `(error, reset) => ReactNode`)— 完全自定义降级 UI(优先级最高) * 2. `fallback`(ReactNode)— 静态降级 UI */ interface ErrorBoundaryProps { children: ReactNode /** 自定义降级 UI:可为静态 ReactNode 或函数式 (error, reset) => ReactNode */ fallback?: ReactNode | ((error: Error, reset: () => void) => ReactNode) /** 错误回调(用于埋点/监控) */ onError?: (error: Error, info: ErrorInfo) => void } interface ErrorBoundaryState { hasError: boolean error: Error | null } export class ErrorBoundary extends Component { constructor(props: ErrorBoundaryProps) { super(props) this.state = { hasError: false, error: null } } static getDerivedStateFromError(error: Error): ErrorBoundaryState { return { hasError: true, error } } componentDidCatch(error: Error, errorInfo: ErrorInfo): void { console.error("[ErrorBoundary]", error, errorInfo) this.props.onError?.(error, errorInfo) } private handleReset = (): void => { this.setState({ hasError: false, error: null }) } render(): ReactNode { if (this.state.hasError && this.state.error) { const { fallback } = this.props if (typeof fallback === "function") { return fallback(this.state.error, this.handleReset) } if (fallback !== undefined) { return fallback } return null } return this.props.children } } ``` - [ ] **Step 2: 验证 tsc + lint** Run: `npx tsc --noEmit` Expected: 零错误 Run: `npm run lint -- --filter src/shared/components/ui/error-boundary.tsx` Expected: 零错误 - [ ] **Step 3: Commit** ```bash git add src/shared/components/ui/error-boundary.tsx git commit -m "feat(shared): 新增 ErrorBoundary 基础组件" ``` --- ### Task 2: 收敛 SectionErrorBoundary 为 preset **Files:** - Modify: `src/shared/components/section-error-boundary.tsx` - [ ] **Step 1: 重写 section-error-boundary.tsx** 将原文件(148 行)改写为:删除内部 `SectionErrorBoundaryBase` 类,改用 `ErrorBoundary` 基础组件。保留外部 API(`SectionErrorBoundary` 函数式 + i18n)不变。 ```tsx "use client" import type { JSX } from "react" import type { ReactNode, ErrorInfo } from "react" import { useTranslations } from "next-intl" import { AlertCircle, RefreshCw } from "lucide-react" import { Button } from "@/shared/components/ui/button" import { ErrorBoundary } from "@/shared/components/ui/error-boundary" interface SectionErrorBoundaryProps { children: ReactNode /** i18n namespace,默认 "common" */ namespace?: string /** 自定义降级 UI(函数形式,优先级最高) */ fallback?: (error: Error, reset: () => void) => ReactNode /** 显式覆盖标题 */ title?: string /** 显式覆盖描述 */ description?: string /** 显式覆盖重试按钮文案 */ retryLabel?: string /** 错误回调 */ onError?: (error: Error, info: ErrorInfo) => void } /** * 组件级 Error Boundary preset:包裹独立数据区块,防止单区块错误导致整页崩溃。 * 自动使用 next-intl 提供本地化文案;如需完全自定义可传入 `fallback` 函数。 * * @example * * * */ export function SectionErrorBoundary({ children, namespace = "common", fallback, title, description, retryLabel, onError, }: SectionErrorBoundaryProps): JSX.Element { const t = useTranslations(namespace) const effectiveTitle = title ?? t("error.boundaryTitle") const effectiveDescription = description ?? t("error.boundaryDescription") const effectiveRetryLabel = retryLabel ?? t("error.retry") const defaultFallback = (_error: Error, reset: () => void): ReactNode => (
) return ( {children} ) } ``` - [ ] **Step 2: 验证外部 API 不变** Run: `npx tsc --noEmit` Expected: 零错误 Run: `npm run lint` Expected: 零新增错误 - [ ] **Step 3: Grep 确认引用未受影响** Run Grep: `SectionErrorBoundary` in `src/` Expected: 所有引用文件(modules 层 error-boundary preset、各模块页面)类型检查通过 - [ ] **Step 4: Commit** ```bash git add src/shared/components/section-error-boundary.tsx git commit -m "refactor(shared): SectionErrorBoundary 收敛为 ErrorBoundary preset" ``` --- ### Task 3: 收敛 RouteErrorBoundary 为 preset **Files:** - Modify: `src/shared/components/route-error.tsx` - [ ] **Step 1: 重写 route-error.tsx** `RouteErrorBoundary` 当前不使用类组件(仅函数式 + EmptyState),但其语义是路由级错误边界。为保持一致性,改为基于 `ErrorBoundary` 的 preset,但保留 `reset` prop(来自 Next.js `error.tsx` 的 `reset()` 函数)。 ```tsx "use client" import type { ReactNode } from "react" import { AlertCircle } from "lucide-react" import { useTranslations } from "next-intl" import { EmptyState } from "@/shared/components/ui/empty-state" import { ErrorBoundary } from "@/shared/components/ui/error-boundary" /** * 通用路由错误边界组件 preset。 * 供各模块 error.tsx 复用。 * * @example * // app/(dashboard)/admin/attendance/error.tsx * "use client" * import { RouteErrorBoundary } from "@/shared/components/route-error" * export default function Error({ reset }: { error: Error; reset: () => void }) { * return * } */ export function RouteErrorBoundary({ reset, namespace = "attendance", }: { reset: () => void namespace?: string }): ReactNode { const t = useTranslations(namespace) return ( (
reset(), }} className="border-none shadow-none h-auto" />
)} > {/* RouteErrorBoundary 作为 error.tsx 的渲染组件,children 不适用,直接渲染 fallback */} {null}
) } ``` 注意:`RouteErrorBoundary` 实际上是在 `error.tsx` 中被调用渲染错误 UI,而非包裹 children。因此这里 `children` 为 `null`,仅用 `ErrorBoundary` 渲染 fallback。若未来需要包裹式用法,可扩展。 - [ ] **Step 2: 验证** Run: `npx tsc --noEmit` Expected: 零错误 Run: `npm run lint` Expected: 零新增错误 - [ ] **Step 3: Commit** ```bash git add src/shared/components/route-error.tsx git commit -m "refactor(shared): RouteErrorBoundary 收敛为 ErrorBoundary preset" ``` --- ### Task 4: 收敛 WidgetBoundary 为 preset **Files:** - Modify: `src/shared/components/widget-boundary.tsx` - [ ] **Step 1: 重写 widget-boundary.tsx** 删除内部 `WidgetErrorBoundary` 类,改用 `ErrorBoundary`。保留 `WidgetSkeleton` 和 `Suspense` 三合一能力。 ```tsx "use client" import { Suspense, type ReactNode } from "react" import { AlertCircle } from "lucide-react" import { useTranslations } from "next-intl" import { Button } from "@/shared/components/ui/button" import { Skeleton } from "@/shared/components/ui/skeleton" import { ErrorBoundary } from "@/shared/components/ui/error-boundary" interface WidgetBoundaryProps { children: ReactNode title?: string skeletonHeight?: number fallbackDescription?: string retryLabel?: string } function WidgetSkeleton({ height, loadingAriaLabel, }: { height: number loadingAriaLabel: string }): ReactNode { return (
) } export function WidgetBoundary({ children, title, skeletonHeight = 200, fallbackDescription, retryLabel, }: WidgetBoundaryProps): ReactNode { const t = useTranslations("common") const effectiveTitle = title ?? t("widget.block") const effectiveFallbackDescription = fallbackDescription ?? t("widget.defaultFallback") const effectiveRetryLabel = retryLabel ?? t("widget.retry") const loadFailedMessage = t("widget.loadFailed", { title: effectiveTitle }) const retryAriaLabel = t("widget.retryAriaLabel", { title: effectiveTitle }) const loadingAriaLabel = t("widget.loadingAriaLabel", { title: effectiveTitle }) const errorFallback = (_error: Error, reset: () => void): ReactNode => (
) return ( } > {children} ) } ``` - [ ] **Step 2: 验证** Run: `npx tsc --noEmit` Expected: 零错误 Run: `npm run lint` Expected: 零新增错误 - [ ] **Step 3: Commit** ```bash git add src/shared/components/widget-boundary.tsx git commit -m "refactor(shared): WidgetBoundary 收敛为 ErrorBoundary preset" ``` --- ### Task 5: 新增 StatsGrid 容器 **Files:** - Create: `src/shared/components/ui/stats-grid.tsx` - [ ] **Step 1: 创建 stats-grid.tsx** ```tsx import type { ComponentType, ReactNode } from "react" import { cn } from "@/shared/lib/utils" import { StatCard } from "@/shared/components/ui/stat-card" /** * 统计卡片网格容器:基于 StatCard 渲染响应式网格。 * * 覆盖以下重复模式: * - attendance-stats-cards / error-book-stats-cards / elective-stats-cards * - practice-stats-cards / practice-overview-stats-cards * - grade-stats-card / analytics-stats-cards */ interface StatItem { /** 卡片标题 */ label: string /** 统计数值 */ value: string | number /** 图标组件 */ icon?: ComponentType<{ className?: string }> /** 描述文本 */ description?: string /** 图标颜色类名 */ color?: string /** 是否高亮 */ highlight?: boolean /** 点击跳转链接 */ href?: string /** 数值自定义类名 */ valueClassName?: string } interface StatsGridProps { /** 统计项列表 */ items: StatItem[] /** 列数(默认响应式:mobile=1, md=2, lg=4) */ columns?: 2 | 3 | 4 /** 加载态 */ isLoading?: boolean /** 容器额外类名 */ className?: string } const columnsClass: Record<2 | 3 | 4, string> = { 2: "grid-cols-1 md:grid-cols-2", 3: "grid-cols-1 md:grid-cols-2 lg:grid-cols-3", 4: "grid-cols-1 md:grid-cols-2 lg:grid-cols-4", } export function StatsGrid({ items, columns = 4, isLoading = false, className, }: StatsGridProps): ReactNode { return (
{items.map((item) => ( ))}
) } ``` - [ ] **Step 2: 验证** Run: `npx tsc --noEmit` Expected: 零错误 Run: `npm run lint` Expected: 零新增错误 - [ ] **Step 3: Commit** ```bash git add src/shared/components/ui/stats-grid.tsx git commit -m "feat(shared): 新增 StatsGrid 容器组件" ``` --- ### Task 6: 扩展 Skeleton(新增 SkeletonCard variant) **Files:** - Modify: `src/shared/components/ui/skeleton.tsx` - [ ] **Step 1: 扩展 skeleton.tsx** 保留原 `Skeleton` 原语,新增 `SkeletonCard` 配置驱动变体。 ```tsx import type { ReactNode } from "react" import { cn } from "@/shared/lib/utils" import { Card, CardContent, CardHeader } from "@/shared/components/ui/card" function Skeleton({ className, ...props }: React.HTMLAttributes) { return (
) } interface SkeletonCardProps { variant: "table" | "list" | "chart" | "form" | "stats-grid" rows?: number className?: string } /** * 配置驱动骨架卡片:按 variant 渲染不同布局的骨架占位。 * * 覆盖以下重复模式: * - message-list-skeleton / announcement-list-skeleton → variant="list" * - audit-log-table-skeleton → variant="table" * - class-skeleton / school-skeleton → variant="form" * - ai-skeleton → variant="chart" * - dashboard-loading-skeleton → variant="stats-grid" * - lesson-plan-skeleton → variant="form"(备课编辑器布局) */ export function SkeletonCard({ variant, rows = 5, className, }: SkeletonCardProps): ReactNode { switch (variant) { case "table": return ( {Array.from({ length: rows }).map((_, i) => (
))}
) case "list": return (
{Array.from({ length: rows }).map((_, i) => (
))}
) case "chart": return ( ) case "form": return ( {Array.from({ length: rows }).map((_, i) => (
))}
) case "stats-grid": return (
{Array.from({ length: 4 }).map((_, i) => ( ))}
) } } export { Skeleton, SkeletonCard } ``` - [ ] **Step 2: 验证** Run: `npx tsc --noEmit` Expected: 零错误 Run: `npm run lint` Expected: 零新增错误 - [ ] **Step 3: Grep 确认原 Skeleton 引用未受影响** Run Grep: `from "@/shared/components/ui/skeleton"` in `src/` Expected: 所有引用仍可正常导入 `Skeleton`(原 named export 保留) - [ ] **Step 4: Commit** ```bash git add src/shared/components/ui/skeleton.tsx git commit -m "feat(shared): Skeleton 新增 SkeletonCard variant" ``` --- ### Task 7: Phase 1 整体验证 + 架构文档同步 **Files:** - Modify: `docs/architecture/004_architecture_impact_map.md` - Modify: `docs/architecture/005_architecture_data.json` - Modify: `docs/troubleshooting/known-issues.md` - [ ] **Step 1: 全量验证** Run: `npx tsc --noEmit` Expected: 零错误 Run: `npm run lint` Expected: 零新增错误 - [ ] **Step 2: 同步 004 架构图** 在 `docs/architecture/004_architecture_impact_map.md` 的 `shared/components` 章节,新增条目: ```markdown | 组件 | 路径 | 职责 | |------|------|------| | ErrorBoundary | ui/error-boundary.tsx | 基础 ErrorBoundary 类组件,preset 的底座 | | StatsGrid | ui/stats-grid.tsx | 统计卡片网格容器,基于 StatCard | | SkeletonCard | ui/skeleton.tsx | 配置驱动骨架卡片(table/list/chart/form/stats-grid) | ``` 并在文末新增"组件化重构专项"章节标题(内容随批次补充)。 - [ ] **Step 3: 同步 005 JSON** 在 `docs/architecture/005_architecture_data.json` 的 `sharedComponents` 节点新增: ```json { "errorBoundary": { "path": "shared/components/ui/error-boundary.tsx", "type": "class", "exports": ["ErrorBoundary"], "role": "基础 ErrorBoundary,preset 底座" }, "statsGrid": { "path": "shared/components/ui/stats-grid.tsx", "type": "function", "exports": ["StatsGrid"], "role": "统计卡片网格容器" }, "skeletonCard": { "path": "shared/components/ui/skeleton.tsx", "type": "function", "exports": ["Skeleton", "SkeletonCard"], "role": "骨架原语 + 配置驱动变体" } } ``` 更新 `lastUpdate` 字段,追加"组件化重构 Phase 1:底座收敛与补全"。 - [ ] **Step 4: known-issues.md 新增章节** 在 `docs/troubleshooting/known-issues.md` 末尾新增: ```markdown ## 组件化重构专项 - Phase 1(2026-07-06) ### 底座组件使用规则 | 规则 | 正确写法 | 错误写法 | |------|---------|---------| | 错误边界必须用 preset | `` | 自行实现类组件 | | 统计卡片必须用 StatsGrid | `` | 手写 grid + StatCard 循环 | | 骨架卡片必须用 SkeletonCard | `` | 手写 Card + Skeleton 布局 | | 新增 *-filters.tsx 禁止 | 用 `` 组合 | 新建模块专属筛选器文件 | ``` - [ ] **Step 5: Commit** ```bash git add docs/architecture/004_architecture_impact_map.md docs/architecture/005_architecture_data.json docs/troubleshooting/known-issues.md git commit -m "docs(architecture): 同步组件化重构 Phase 1 底座节点" ``` --- ## Phase 2 - 第 1 批:低风险模块(grades / attendance / error-book / elective) ### Task 8: grades 模块闭环 **Files:** - Modify: `src/modules/grades/components/batch-grade-entry.tsx` (>450 行) - Modify: `src/modules/grades/components/grade-record-list.tsx` (>450 行) - Delete: `src/modules/grades/components/grade-stats-card.tsx` - Delete: `src/modules/grades/components/grade-filters.tsx` - Delete: `src/modules/grades/components/grade-query-filters.tsx` - Delete: `src/modules/grades/components/analytics-filters.tsx` - [ ] **Step 1: 模块审计** Read 以下文件,记录外部 API 与依赖关系: - `src/modules/grades/components/batch-grade-entry.tsx` - `src/modules/grades/components/grade-record-list.tsx` - `src/modules/grades/components/grade-stats-card.tsx` - `src/modules/grades/components/grade-filters.tsx` - `src/modules/grades/components/grade-query-filters.tsx` - `src/modules/grades/components/analytics-filters.tsx` Grep 所有引用上述文件的 import 语句,记录调用方清单。 - [ ] **Step 2: 拆分 batch-grade-entry.tsx** 按职责拆为容器 + 子组件(保持 `batch-grade-entry.tsx` 作为容器,外部 API 不变): - `batch-grade-entry.tsx` — 容器,负责状态编排与 Server Action 调用 - `batch-grade-entry-form.tsx` — 表单子组件(学生列表 + 分数输入网格) - `batch-grade-entry-toolbar.tsx` — 工具栏子组件(保存/取消/导入按钮) 目标:容器 ≤300 行,子组件各 ≤200 行。 - [ ] **Step 3: 拆分 grade-record-list.tsx** 按职责拆为: - `grade-record-list.tsx` — 容器,列表查询与分页 - `grade-record-row.tsx` — 行渲染子组件 - `grade-record-detail.tsx` — 详情弹窗子组件 目标:容器 ≤300 行,子组件各 ≤200 行。 - [ ] **Step 4: 迁移 grade-stats-card 到 StatsGrid** 在调用 `grade-stats-card.tsx` 的页面中,替换为: ```tsx import { StatsGrid } from "@/shared/components/ui/stats-grid" const items = [ { label: "平均分", value: avg, icon: TrendingUp, color: "text-emerald-500" }, { label: "最高分", value: max, icon: ArrowUp, color: "text-blue-500" }, // ... ] ``` 删除 `grade-stats-card.tsx`。 - [ ] **Step 5: 迁移 grade-filters / grade-query-filters / analytics-filters 到 FilterBar** 在调用方页面中,替换为 `` + `` + Select 组件组合: ```tsx import { FilterBar, FilterSearchInput } from "@/shared/components/ui/filter-bar" import { Select } from "@/shared/components/ui/select" ``` 删除 `grade-filters.tsx`、`grade-query-filters.tsx`、`analytics-filters.tsx`。 - [ ] **Step 6: 验证** Run: `npx tsc --noEmit` Expected: 零错误 Run: `npm run lint` Expected: 零新增错误 Run Grep: `grade-stats-card|grade-filters|grade-query-filters|analytics-filters` in `src/` Expected: 仅在删除的文件中出现(已删除则无匹配) 手动回归:访问成绩列表页、批量录入页、分析页,确认筛选/统计/列表行为正常。 - [ ] **Step 7: Commit** ```bash git add src/modules/grades/ git commit -m "refactor(grades): 组件化重构 - 拆分巨型文件 + 迁移到底座" ``` --- ### Task 9: attendance 模块闭环 **Files:** - Delete: `src/modules/attendance/components/attendance-stats-cards.tsx` - Delete: `src/modules/attendance/components/attendance-stats-card.tsx`(单数,与复数重复) - Delete: `src/modules/attendance/components/attendance-filters.tsx` - [ ] **Step 1: 模块审计** Read 上述 3 个文件 + 调用方页面,记录外部 API。 - [ ] **Step 2: 迁移 attendance-stats-cards / attendance-stats-card 到 StatsGrid** 合并单数与复数版本,统一替换为 ``。删除 `attendance-stats-cards.tsx` 与 `attendance-stats-card.tsx`。 - [ ] **Step 3: 迁移 attendance-filters 到 FilterBar** 调用方替换为 `` 组合。删除 `attendance-filters.tsx`。 - [ ] **Step 4: 验证** Run: `npx tsc --noEmit` + `npm run lint` Expected: 零新增错误 Run Grep: `attendance-stats-cards|attendance-stats-card|attendance-filters` in `src/` Expected: 无匹配 手动回归:考勤列表页、统计页。 - [ ] **Step 5: Commit** ```bash git add src/modules/attendance/ git commit -m "refactor(attendance): 组件化重构 - 迁移到 StatsGrid/FilterBar 底座" ``` --- ### Task 10: error-book 模块闭环 **Files:** - Delete: `src/modules/error-book/components/error-book-stats-cards.tsx` - Delete: `src/modules/error-book/components/analytics-stats-cards.tsx` - Delete: `src/modules/error-book/components/error-book-filters.tsx` - [ ] **Step 1: 模块审计** Read 上述 3 个文件 + 调用方页面。 - [ ] **Step 2: 迁移 stats-cards 到 StatsGrid** `error-book-stats-cards.tsx` 与 `analytics-stats-cards.tsx` 均替换为 ``。删除原文件。 - [ ] **Step 3: 迁移 error-book-filters 到 FilterBar** 调用方替换为 `` 组合。删除 `error-book-filters.tsx`。 - [ ] **Step 4: 验证** Run: `npx tsc --noEmit` + `npm run lint` Expected: 零新增错误 Run Grep: `error-book-stats-cards|analytics-stats-cards|error-book-filters` in `src/` Expected: 无匹配 手动回归:错题本列表页、分析页。 - [ ] **Step 5: Commit** ```bash git add src/modules/error-book/ git commit -m "refactor(error-book): 组件化重构 - 迁移到 StatsGrid/FilterBar 底座" ``` --- ### Task 11: elective 模块闭环 **Files:** - Delete: `src/modules/elective/components/elective-stats-cards.tsx` - Delete: `src/modules/elective/components/elective-filters.tsx` - [ ] **Step 1: 模块审计** Read 上述 2 个文件 + 调用方页面。 - [ ] **Step 2: 迁移 elective-stats-cards 到 StatsGrid** 调用方替换为 ``。删除 `elective-stats-cards.tsx`。 - [ ] **Step 3: 迁移 elective-filters 到 FilterBar** 调用方替换为 `` 组合。删除 `elective-filters.tsx`。 - [ ] **Step 4: 验证** Run: `npx tsc --noEmit` + `npm run lint` Expected: 零新增错误 Run Grep: `elective-stats-cards|elective-filters` in `src/` Expected: 无匹配 手动回归:选修课列表页、统计页。 - [ ] **Step 5: Commit** ```bash git add src/modules/elective/ git commit -m "refactor(elective): 组件化重构 - 迁移到 StatsGrid/FilterBar 底座" ``` --- ### Task 12: 第 1 批收尾 - 文档同步 **Files:** - Modify: `docs/architecture/004_architecture_impact_map.md` - Modify: `docs/architecture/005_architecture_data.json` - Modify: `docs/troubleshooting/known-issues.md` - [ ] **Step 1: 同步 004 / 005** 更新 grades/attendance/error-book/elective 模块的 `exports` 清单(删除旧组件、新增子组件)。更新 `lastUpdate`。 - [ ] **Step 2: known-issues.md 补充第 1 批章节** 记录第 1 批遇到的拆分模式与底座使用规则(速查手册风格)。 - [ ] **Step 3: Commit** ```bash git add docs/ git commit -m "docs(architecture): 同步组件化重构第 1 批(grades/attendance/error-book/elective)" ``` --- ## Phase 2 - 第 2 批:中风险模块(homework / exams / textbooks) ### Task 13: homework 模块闭环 **Files:** - Modify: `src/modules/homework/components/homework-take-view.tsx` (428 行) - Modify: `src/modules/homework/components/homework-grading-view.tsx` (>450 行) - Delete: `src/modules/homework/components/assignment-filters.tsx` - [ ] **Step 1: 模块审计** Read 上述 3 个文件 + 调用方页面。 - [ ] **Step 2: 拆分 homework-take-view.tsx** 按作答流程拆为: - `homework-take-view.tsx` — 容器(题目导航 + 自动保存 + 提交) - `homework-take-question.tsx` — 单题作答子组件 - `homework-take-sidebar.tsx` — 题目导航侧边栏 目标:容器 ≤300 行,子组件各 ≤200 行。 - [ ] **Step 3: 拆分 homework-grading-view.tsx** 按批改流程拆为: - `homework-grading-view.tsx` — 容器(学生切换 + 评分 + 保存) - `homework-grading-panel.tsx` — 单题评分面板 - `homework-grading-toolbar.tsx` — 工具栏(上/下学生、保存、批改进度) - [ ] **Step 4: 迁移 assignment-filters 到 FilterBar** 删除 `assignment-filters.tsx`,调用方替换为 `` 组合。 - [ ] **Step 5: 验证** Run: `npx tsc --noEmit` + `npm run lint` Expected: 零新增错误 Run Grep: `assignment-filters` in `src/` Expected: 无匹配 手动回归:作业作答页、批改页、列表页。 - [ ] **Step 6: Commit** ```bash git add src/modules/homework/ git commit -m "refactor(homework): 组件化重构 - 拆分作答/批改视图 + 迁移 FilterBar" ``` --- ### Task 14: exams 模块闭环 **Files:** - Modify: `src/modules/exams/components/exam-assembly.tsx` (>450 行) - Delete: `src/modules/exams/components/exam-filters.tsx` - [ ] **Step 1: 模块审计** Read 上述 2 个文件 + 调用方页面。 - [ ] **Step 2: 拆分 exam-assembly.tsx** 按组卷流程拆为: - `exam-assembly.tsx` — 容器(题目池 + 已选题目 + 试卷配置) - `exam-assembly-question-pool.tsx` — 题目池子组件 - `exam-assembly-selected.tsx` — 已选题目子组件 - `exam-assembly-config.tsx` — 试卷配置子组件 目标:容器 ≤300 行,子组件各 ≤200 行。 - [ ] **Step 3: 迁移 exam-filters 到 FilterBar** 删除 `exam-filters.tsx`,调用方替换为 `` 组合。 - [ ] **Step 4: 验证** Run: `npx tsc --noEmit` + `npm run lint` Expected: 零新增错误 手动回归:组卷页、试卷列表页。 - [ ] **Step 5: Commit** ```bash git add src/modules/exams/ git commit -m "refactor(exams): 组件化重构 - 拆分组卷组件 + 迁移 FilterBar" ``` --- ### Task 15: textbooks 模块闭环 **Files:** - Modify: `src/modules/textbooks/components/textbook-reader.tsx` (>450 行) - Modify: `src/modules/textbooks/components/knowledge-graph-inner.tsx` (411 行) - Delete: `src/modules/textbooks/components/textbook-filters.tsx` - Delete: `src/modules/textbooks/components/section-error-boundary.tsx`(与 shared 同名) - [ ] **Step 1: 模块审计** Read 上述 4 个文件 + 调用方页面。参考 `docs/architecture/003_ui_refactoring_plan.md` 的拆分方案。 - [ ] **Step 2: 拆分 textbook-reader.tsx** 按 003 计划拆为: - `textbook-reader.tsx` — 容器(目录 + 内容区 + 知识点面板) - `textbook-reader-content.tsx` — 内容区子组件 - `textbook-reader-toc.tsx` — 目录子组件 目标:容器 ≤300 行,子组件各 ≤200 行。 - [ ] **Step 3: 拆分 knowledge-graph-inner.tsx** 按职责拆为: - `knowledge-graph-inner.tsx` — 容器(图渲染 + 节点交互) - `knowledge-graph-node.tsx` — 节点渲染子组件 - `knowledge-graph-controls.tsx` — 控制面板子组件 - [ ] **Step 4: 迁移 textbook-filters 到 FilterBar** 删除 `textbook-filters.tsx`,调用方替换为 `` 组合。 - [ ] **Step 5: 删除 textbooks 同名 section-error-boundary.tsx** `src/modules/textbooks/components/section-error-boundary.tsx` 与 `shared/components/section-error-boundary.tsx` 同名。删除模块层版本,调用方改为直接 import shared 层 `SectionErrorBoundary`。 - [ ] **Step 6: 验证** Run: `npx tsc --noEmit` + `npm run lint` Expected: 零新增错误 Run Grep: `textbook-filters|modules/textbooks/components/section-error-boundary` in `src/` Expected: 无匹配 手动回归:教材阅读页、知识图谱页、教材列表页。 - [ ] **Step 7: Commit** ```bash git add src/modules/textbooks/ git commit -m "refactor(textbooks): 组件化重构 - 拆分阅读器/知识图谱 + 迁移底座" ``` --- ### Task 16: 第 2 批收尾 - 文档同步 **Files:** - Modify: `docs/architecture/004_architecture_impact_map.md` - Modify: `docs/architecture/005_architecture_data.json` - Modify: `docs/troubleshooting/known-issues.md` - [ ] **Step 1: 同步 004 / 005** 更新 homework/exams/textbooks 模块的 `exports` 清单。更新 `lastUpdate`。 - [ ] **Step 2: known-issues.md 补充第 2 批章节** 记录拆分模式(编辑器/组卷类组件拆分注意事项)。 - [ ] **Step 3: Commit** ```bash git add docs/ git commit -m "docs(architecture): 同步组件化重构第 2 批(homework/exams/textbooks)" ``` --- ## Phase 2 - 第 3 批:高风险模块(lesson-preparation / ai / dashboard) ### Task 17: lesson-preparation 模块闭环 **Files:** - Modify: `src/modules/lesson-preparation/components/lesson-plan-editor.tsx` (>450 行) - Delete: `src/modules/lesson-preparation/components/lesson-plan-filters.tsx` - Delete: `src/modules/lesson-preparation/components/lesson-plan-skeleton.tsx` - Delete: `src/modules/lesson-preparation/components/lesson-plan-error-boundary.tsx` - [ ] **Step 1: 模块审计** Read 上述 4 个文件 + 调用方页面。lesson-plan-editor 是核心交互组件,需特别关注 Tiptap 编辑器集成。 - [ ] **Step 2: 拆分 lesson-plan-editor.tsx** 按三列布局拆为: - `lesson-plan-editor.tsx` — 容器(三列布局编排 + 状态管理) - `lesson-plan-sidebar.tsx` — 左侧结构树子组件 - `lesson-plan-paper.tsx` — 中间纸张区子组件(含 Tiptap 编辑器) - `lesson-plan-detail-panel.tsx` — 右侧详情面板子组件 目标:容器 ≤300 行,子组件各 ≤200 行。注意 `immediatelyRender: false` SSR 配置必须保留。 - [ ] **Step 3: 迁移 lesson-plan-filters 到 FilterBar** 删除 `lesson-plan-filters.tsx`,调用方替换为 `` 组合。 - [ ] **Step 4: 迁移 lesson-plan-skeleton 到 SkeletonCard** 调用方替换为 ``。删除 `lesson-plan-skeleton.tsx`。 - [ ] **Step 5: 迁移 lesson-plan-error-boundary 到 SectionErrorBoundary** 调用方替换为 ``。删除 `lesson-plan-error-boundary.tsx`。 - [ ] **Step 6: 验证** Run: `npx tsc --noEmit` + `npm run lint` Expected: 零新增错误 Run Grep: `lesson-plan-filters|lesson-plan-skeleton|lesson-plan-error-boundary` in `src/` Expected: 无匹配 手动回归:备课编辑器(节点展开/折叠、右键菜单、AI 辅助、保存重载)。 - [ ] **Step 7: Commit** ```bash git add src/modules/lesson-preparation/ git commit -m "refactor(lesson-preparation): 组件化重构 - 拆分编辑器 + 迁移底座" ``` --- ### Task 18: ai 模块闭环 **Files:** - Modify: `src/modules/ai/components/ai-chat-panel.tsx` (417 行) - Delete: `src/modules/ai/components/ai-skeleton.tsx` - Delete: `src/modules/ai/components/ai-error-boundary.tsx` - [ ] **Step 1: 模块审计** Read 上述 3 个文件 + 调用方页面。 - [ ] **Step 2: 拆分 ai-chat-panel.tsx** 按对话交互拆为: - `ai-chat-panel.tsx` — 容器(消息列表 + 输入框 + 状态管理) - `ai-chat-messages.tsx` — 消息列表子组件 - `ai-chat-input.tsx` — 输入框子组件(含发送、停止、附件) 目标:容器 ≤300 行,子组件各 ≤200 行。注意对话状态保持,拆分后不能破坏流式响应。 - [ ] **Step 3: 迁移 ai-skeleton 到 SkeletonCard** 调用方替换为 ``。删除 `ai-skeleton.tsx`。 - [ ] **Step 4: 迁移 ai-error-boundary 到 SectionErrorBoundary** `ai-error-boundary.tsx` 已是薄包装(委托给 SectionErrorBoundary)。直接删除,调用方改为 ``。 - [ ] **Step 5: 验证** Run: `npx tsc --noEmit` + `npm run lint` Expected: 零新增错误 Run Grep: `ai-skeleton|ai-error-boundary` in `src/` Expected: 无匹配 手动回归:AI 对话面板(发送消息、流式响应、停止、错误重试)。 - [ ] **Step 6: Commit** ```bash git add src/modules/ai/ git commit -m "refactor(ai): 组件化重构 - 拆分聊天面板 + 迁移底座" ``` --- ### Task 19: dashboard 模块闭环 **Files:** - Delete: `src/modules/dashboard/components/dashboard-loading-skeleton.tsx` - Modify: `src/modules/dashboard/components/admin-dashboard/admin-dashboard.tsx` - Modify: `src/modules/dashboard/components/parent-dashboard/parent-dashboard.tsx` - Create: `src/modules/dashboard/components/dashboard-shell.tsx`(仅布局壳抽象) - [ ] **Step 1: 模块审计** Read 4 个角色 dashboard 子组件 + dashboard-loading-skeleton。记录各角色布局差异。 - [ ] **Step 2: 抽象 DashboardShell 布局壳** 仅抽象布局壳(PageHeader + StatsGrid 区 + 内容区),业务逻辑保持各角色独立: ```tsx import type { ReactNode, ComponentType } from "react" import { PageHeader } from "@/shared/components/ui/page-header" import { StatsGrid, type StatItem } from "@/shared/components/ui/stats-grid" interface DashboardShellProps { title: string description?: string stats: StatItem[] statsLoading?: boolean children: ReactNode actions?: ReactNode } export function DashboardShell({ title, description, stats, statsLoading, children, actions, }: DashboardShellProps): ReactNode { return (
{children}
) } ``` - [ ] **Step 3: 改造 4 个角色 dashboard 使用 DashboardShell** 各角色 dashboard 子组件改为: ```tsx {/* 角色专属内容区 */} ``` - [ ] **Step 4: 迁移 dashboard-loading-skeleton 到 SkeletonCard** 调用方替换为 ``。删除 `dashboard-loading-skeleton.tsx`。 - [ ] **Step 5: 验证** Run: `npx tsc --noEmit` + `npm run lint` Expected: 零新增错误 Run Grep: `dashboard-loading-skeleton` in `src/` Expected: 无匹配 手动回归:4 个角色(admin/teacher/student/parent)分别登录,确认 dashboard 卡片渲染与数据正确。 - [ ] **Step 6: Commit** ```bash git add src/modules/dashboard/ git commit -m "refactor(dashboard): 组件化重构 - 抽象 DashboardShell + 迁移底座" ``` --- ### Task 20: 第 3 批收尾 - 文档同步 + 专项完成 **Files:** - Modify: `docs/architecture/004_architecture_impact_map.md` - Modify: `docs/architecture/005_architecture_data.json` - Modify: `docs/troubleshooting/known-issues.md` - [ ] **Step 1: 同步 004 / 005** 更新 lesson-preparation/ai/dashboard 模块的 `exports` 清单。更新 `lastUpdate`。在 004 文末"组件化重构专项"章节写完成总结。 - [ ] **Step 2: known-issues.md 补充第 3 批章节** 记录高风险模块拆分注意事项(编辑器 SSR 配置、AI 流式响应状态保持、dashboard 布局壳抽象边界)。 - [ ] **Step 3: 全量验证** Run: `npx tsc --noEmit` Expected: 零错误 Run: `npm run lint` Expected: 零新增错误 Run Grep: 验证 9 个巨型文件行数: - `src/modules/textbooks/components/textbook-reader.tsx` - `src/modules/lesson-preparation/components/lesson-plan-editor.tsx` - `src/modules/grades/components/batch-grade-entry.tsx` - `src/modules/grades/components/grade-record-list.tsx` - `src/modules/homework/components/homework-take-view.tsx` - `src/modules/homework/components/homework-grading-view.tsx` - `src/modules/exams/components/exam-assembly.tsx` - `src/modules/ai/components/ai-chat-panel.tsx` - `src/modules/textbooks/components/knowledge-graph-inner.tsx` Expected: 全部 ≤500 行 Run Grep: 验证 49 个重复组件已删除(按附录清单逐项 grep)。 - [ ] **Step 4: Commit** ```bash git add docs/ git commit -m "docs(architecture): 组件化重构专项完成 - 同步全部文档" ``` --- ## 自审清单 ### Spec 覆盖 - [x] Phase 1 底座补全:Task 1-6 覆盖 ErrorBoundary/StatsGrid/SkeletonCard - [x] Phase 1 文档同步:Task 7 - [x] 第 1 批(低风险):Task 8-12 覆盖 grades/attendance/error-book/elective - [x] 第 2 批(中风险):Task 13-16 覆盖 homework/exams/textbooks - [x] 第 3 批(高风险):Task 17-20 覆盖 lesson-preparation/ai/dashboard - [x] 9 个巨型文件拆分:Task 8/13/14/15/17/18/19 - [x] 49 个重复组件迁移:分布在各批次任务中 ### 类型一致性 - `StatItem` 在 Task 5 定义,Task 19 `DashboardShellProps.stats: StatItem[]` 一致 - `ErrorBoundaryProps` 在 Task 1 定义,Task 2/3/4 preset 使用一致 - `SkeletonCardProps.variant` 在 Task 6 定义为联合类型,Task 17/18/19 使用 `variant="form"/"chart"/"stats-grid"` 一致 ### 占位符扫描 - 无 TBD/TODO - Phase 2 各任务的拆分代码示例为策略性描述(拆分方向 + 目标行数),实际代码需在审计后编写。这是必要的,因为巨型文件的具体内容需先 Read 才能给出精确拆分代码。每个 Step 都有具体动作(Read/拆分/删除/Grep 验证)。 --- ## 执行选择 **Plan complete and saved to `docs/superpowers/plans/2026-07-06-component-refactoring.md`. Two execution options:** **1. Subagent-Driven (recommended)** - 每个 Task 派发独立 subagent,Task 间 review,快速迭代 **2. Inline Execution** - 在当前会话顺序执行,批量执行 + 检查点 review **Which approach?**