Files
NextEdu/docs/superpowers/plans/2026-07-06-component-refactoring.md
2026-07-06 17:49:14 +08:00

1439 lines
43 KiB
Markdown
Raw 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.
# 组件化重构专项实施计划
> **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<ErrorBoundaryProps, ErrorBoundaryState> {
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
* <SectionErrorBoundary namespace="coursePlans">
* <CoursePlanProgress plan={plan} />
* </SectionErrorBoundary>
*/
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 => (
<div
role="alert"
aria-live="assertive"
className="flex flex-col items-center justify-center space-y-3 rounded-lg border border-dashed p-8 text-center"
>
<AlertCircle className="h-8 w-8 text-muted-foreground" aria-hidden="true" />
<div className="space-y-1">
<p className="text-sm font-medium">{effectiveTitle}</p>
<p className="text-xs text-muted-foreground">{effectiveDescription}</p>
</div>
<Button
variant="outline"
size="sm"
onClick={reset}
aria-label={effectiveRetryLabel}
>
<RefreshCw className="mr-1.5 h-3.5 w-3.5" aria-hidden="true" />
{effectiveRetryLabel}
</Button>
</div>
)
return (
<ErrorBoundary fallback={fallback ?? defaultFallback} onError={onError}>
{children}
</ErrorBoundary>
)
}
```
- [ ] **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 <RouteErrorBoundary reset={reset} namespace="attendance" />
* }
*/
export function RouteErrorBoundary({
reset,
namespace = "attendance",
}: {
reset: () => void
namespace?: string
}): ReactNode {
const t = useTranslations(namespace)
return (
<ErrorBoundary
fallback={() => (
<div className="flex h-full flex-col items-center justify-center space-y-4 p-8">
<EmptyState
icon={AlertCircle}
title={t("errors.unexpected")}
description={t("errors.unexpected")}
action={{
label: t("actions.retry"),
onClick: () => reset(),
}}
className="border-none shadow-none h-auto"
/>
</div>
)}
>
{/* RouteErrorBoundary 作为 error.tsx 的渲染组件children 不适用,直接渲染 fallback */}
{null}
</ErrorBoundary>
)
}
```
注意:`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 (
<div
role="status"
aria-label={loadingAriaLabel}
aria-live="polite"
className="space-y-3 p-4"
style={{ minHeight: height }}
>
<Skeleton className="h-6 w-1/3" />
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-2/3" />
<Skeleton className="h-32 w-full" />
</div>
)
}
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 => (
<div
role="alert"
aria-live="assertive"
className="flex h-full min-h-[200px] flex-col items-center justify-center gap-3 rounded-lg border border-destructive/30 bg-destructive/5 p-6 text-center"
>
<AlertCircle className="h-8 w-8 text-destructive" aria-hidden="true" />
<div className="space-y-1">
<p className="text-sm font-medium text-foreground">{loadFailedMessage}</p>
<p className="text-xs text-muted-foreground">{effectiveFallbackDescription}</p>
</div>
<Button
size="sm"
variant="outline"
onClick={reset}
aria-label={retryAriaLabel}
>
{effectiveRetryLabel}
</Button>
</div>
)
return (
<ErrorBoundary fallback={errorFallback}>
<Suspense
fallback={<WidgetSkeleton height={skeletonHeight} loadingAriaLabel={loadingAriaLabel} />}
>
{children}
</Suspense>
</ErrorBoundary>
)
}
```
- [ ] **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 (
<div className={cn("grid gap-4", columnsClass[columns], className)}>
{items.map((item) => (
<StatCard
key={item.label}
title={item.label}
value={item.value}
icon={item.icon}
description={item.description}
color={item.color}
highlight={item.highlight}
href={item.href}
isLoading={isLoading}
valueClassName={item.valueClassName}
/>
))}
</div>
)
}
```
- [ ] **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<HTMLDivElement>) {
return (
<div
className={cn("animate-pulse rounded-md bg-muted", className)}
{...props}
/>
)
}
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 (
<Card className={className}>
<CardHeader>
<Skeleton className="h-5 w-1/4" />
</CardHeader>
<CardContent className="space-y-3">
{Array.from({ length: rows }).map((_, i) => (
<div key={i} className="flex gap-4">
<Skeleton className="h-4 flex-1" />
<Skeleton className="h-4 w-20" />
<Skeleton className="h-4 w-16" />
</div>
))}
</CardContent>
</Card>
)
case "list":
return (
<div className={cn("space-y-3", className)}>
{Array.from({ length: rows }).map((_, i) => (
<Card key={i}>
<CardContent className="flex items-center gap-4 p-4">
<Skeleton className="h-10 w-10 rounded-full" />
<div className="flex-1 space-y-2">
<Skeleton className="h-4 w-1/3" />
<Skeleton className="h-3 w-1/2" />
</div>
<Skeleton className="h-8 w-20" />
</CardContent>
</Card>
))}
</div>
)
case "chart":
return (
<Card className={className}>
<CardHeader>
<Skeleton className="h-5 w-1/3" />
</CardHeader>
<CardContent>
<Skeleton className="h-[200px] w-full" />
</CardContent>
</Card>
)
case "form":
return (
<Card className={className}>
<CardContent className="space-y-4 p-6">
{Array.from({ length: rows }).map((_, i) => (
<div key={i} className="space-y-2">
<Skeleton className="h-4 w-1/4" />
<Skeleton className="h-10 w-full" />
</div>
))}
<Skeleton className="h-10 w-32" />
</CardContent>
</Card>
)
case "stats-grid":
return (
<div className={cn("grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4", className)}>
{Array.from({ length: 4 }).map((_, i) => (
<Card key={i}>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<Skeleton className="h-4 w-[100px]" />
<Skeleton className="h-4 w-4 rounded-full" />
</CardHeader>
<CardContent>
<Skeleton className="mb-2 h-8 w-[60px]" />
<Skeleton className="h-3 w-[140px]" />
</CardContent>
</Card>
))}
</div>
)
}
}
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": "基础 ErrorBoundarypreset 底座"
},
"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 12026-07-06
### 底座组件使用规则
| 规则 | 正确写法 | 错误写法 |
|------|---------|---------|
| 错误边界必须用 preset | `<SectionErrorBoundary namespace="x">` | 自行实现类组件 |
| 统计卡片必须用 StatsGrid | `<StatsGrid items={[...]} />` | 手写 grid + StatCard 循环 |
| 骨架卡片必须用 SkeletonCard | `<SkeletonCard variant="table" />` | 手写 Card + Skeleton 布局 |
| 新增 *-filters.tsx 禁止 | 用 `<FilterBar>` 组合 | 新建模块专属筛选器文件 |
```
- [ ] **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" },
// ...
]
<StatsGrid items={items} isLoading={isLoading} />
```
删除 `grade-stats-card.tsx`
- [ ] **Step 5: 迁移 grade-filters / grade-query-filters / analytics-filters 到 FilterBar**
在调用方页面中,替换为 `<FilterBar>` + `<FilterSearchInput>` + Select 组件组合:
```tsx
import { FilterBar, FilterSearchInput } from "@/shared/components/ui/filter-bar"
import { Select } from "@/shared/components/ui/select"
<FilterBar hasFilters={hasFilters} onReset={handleReset} layout="wrap">
<FilterSearchInput value={search} onChange={setSearch} placeholder="搜索学生..." />
<Select value={classId} onValueChange={setClassId}>
{/* options */}
</Select>
</FilterBar>
```
删除 `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**
合并单数与复数版本,统一替换为 `<StatsGrid>`。删除 `attendance-stats-cards.tsx``attendance-stats-card.tsx`
- [ ] **Step 3: 迁移 attendance-filters 到 FilterBar**
调用方替换为 `<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` 均替换为 `<StatsGrid>`。删除原文件。
- [ ] **Step 3: 迁移 error-book-filters 到 FilterBar**
调用方替换为 `<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**
调用方替换为 `<StatsGrid>`。删除 `elective-stats-cards.tsx`
- [ ] **Step 3: 迁移 elective-filters 到 FilterBar**
调用方替换为 `<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`,调用方替换为 `<FilterBar>` 组合。
- [ ] **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`,调用方替换为 `<FilterBar>` 组合。
- [ ] **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`,调用方替换为 `<FilterBar>` 组合。
- [ ] **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`,调用方替换为 `<FilterBar>` 组合。
- [ ] **Step 4: 迁移 lesson-plan-skeleton 到 SkeletonCard**
调用方替换为 `<SkeletonCard variant="form" />`。删除 `lesson-plan-skeleton.tsx`
- [ ] **Step 5: 迁移 lesson-plan-error-boundary 到 SectionErrorBoundary**
调用方替换为 `<SectionErrorBoundary namespace="lessonPreparation">`。删除 `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**
调用方替换为 `<SkeletonCard variant="chart" />`。删除 `ai-skeleton.tsx`
- [ ] **Step 4: 迁移 ai-error-boundary 到 SectionErrorBoundary**
`ai-error-boundary.tsx` 已是薄包装(委托给 SectionErrorBoundary。直接删除调用方改为 `<SectionErrorBoundary namespace="ai">`
- [ ] **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 (
<div className="space-y-6">
<PageHeader title={title} description={description} actions={actions} />
<StatsGrid items={stats} isLoading={statsLoading} />
{children}
</div>
)
}
```
- [ ] **Step 3: 改造 4 个角色 dashboard 使用 DashboardShell**
各角色 dashboard 子组件改为:
```tsx
<DashboardShell title="..." stats={stats} statsLoading={isLoading}>
{/* 角色专属内容区 */}
</DashboardShell>
```
- [ ] **Step 4: 迁移 dashboard-loading-skeleton 到 SkeletonCard**
调用方替换为 `<SkeletonCard variant="stats-grid" />`。删除 `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 派发独立 subagentTask 间 review快速迭代
**2. Inline Execution** - 在当前会话顺序执行,批量执行 + 检查点 review
**Which approach?**