43 KiB
组件化重构专项实施计划
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
"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
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)不变。
"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
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() 函数)。
"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
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 三合一能力。
"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
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
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
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 配置驱动变体。
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
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 章节,新增条目:
| 组件 | 路径 | 职责 |
|------|------|------|
| 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 节点新增:
{
"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 末尾新增:
## 组件化重构专项 - Phase 1(2026-07-06)
### 底座组件使用规则
| 规则 | 正确写法 | 错误写法 |
|------|---------|---------|
| 错误边界必须用 preset | `<SectionErrorBoundary namespace="x">` | 自行实现类组件 |
| 统计卡片必须用 StatsGrid | `<StatsGrid items={[...]} />` | 手写 grid + StatCard 循环 |
| 骨架卡片必须用 SkeletonCard | `<SkeletonCard variant="table" />` | 手写 Card + Skeleton 布局 |
| 新增 *-filters.tsx 禁止 | 用 `<FilterBar>` 组合 | 新建模块专属筛选器文件 |
- Step 5: Commit
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.tsxsrc/modules/grades/components/grade-record-list.tsxsrc/modules/grades/components/grade-stats-card.tsxsrc/modules/grades/components/grade-filters.tsxsrc/modules/grades/components/grade-query-filters.tsxsrc/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 的页面中,替换为:
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 组件组合:
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
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
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
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
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
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
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
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
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
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
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
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 区 + 内容区),业务逻辑保持各角色独立:
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 子组件改为:
<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
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.tsxsrc/modules/lesson-preparation/components/lesson-plan-editor.tsxsrc/modules/grades/components/batch-grade-entry.tsxsrc/modules/grades/components/grade-record-list.tsxsrc/modules/homework/components/homework-take-view.tsxsrc/modules/homework/components/homework-grading-view.tsxsrc/modules/exams/components/exam-assembly.tsxsrc/modules/ai/components/ai-chat-panel.tsxsrc/modules/textbooks/components/knowledge-graph-inner.tsx
Expected: 全部 ≤500 行
Run Grep: 验证 49 个重复组件已删除(按附录清单逐项 grep)。
- Step 4: Commit
git add docs/
git commit -m "docs(architecture): 组件化重构专项完成 - 同步全部文档"
自审清单
Spec 覆盖
- Phase 1 底座补全:Task 1-6 覆盖 ErrorBoundary/StatsGrid/SkeletonCard
- Phase 1 文档同步:Task 7
- 第 1 批(低风险):Task 8-12 覆盖 grades/attendance/error-book/elective
- 第 2 批(中风险):Task 13-16 覆盖 homework/exams/textbooks
- 第 3 批(高风险):Task 17-20 覆盖 lesson-preparation/ai/dashboard
- 9 个巨型文件拆分:Task 8/13/14/15/17/18/19
- 49 个重复组件迁移:分布在各批次任务中
类型一致性
StatItem在 Task 5 定义,Task 19DashboardShellProps.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?