1439 lines
43 KiB
Markdown
1439 lines
43 KiB
Markdown
# 组件化重构专项实施计划
|
||
|
||
> **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": "基础 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 | `<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 派发独立 subagent,Task 间 review,快速迭代
|
||
|
||
**2. Inline Execution** - 在当前会话顺序执行,批量执行 + 检查点 review
|
||
|
||
**Which approach?**
|