diff --git a/docs/superpowers/plans/2026-07-06-component-refactoring.md b/docs/superpowers/plans/2026-07-06-component-refactoring.md new file mode 100644 index 0000000..86cab0e --- /dev/null +++ b/docs/superpowers/plans/2026-07-06-component-refactoring.md @@ -0,0 +1,1438 @@ +# 组件化重构专项实施计划 + +> **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?**