Files
NextEdu/src/shared/components/section-error-boundary.tsx
SpecialX 0e63c24ed9
Some checks failed
CI / scheduled-backup (push) Has been skipped
CI / backup-verify (push) Has been skipped
CI / weekly-dr-drill (push) Failing after 0s
CI / build-deploy (push) Has been cancelled
CI / security-scan (push) Has been cancelled
feat(shared,tests): add error boundaries, lib utils, i18n messages, and integration tests
shared:

- Add class-filter, error-state, route-error, section-error-boundary, widget-boundary components

- Add ui/alert component

- Add constants directory

- Add breached-password, export-utils, permission-bitmap, rate-limit, resolve-action-error, route-permissions, route-resolver, type-guards lib

- Add i18n messages (en, zh-CN) for invitation-codes, parent, questions, rbac

tests:

- Add integration tests for elective

- Add tests/setup/empty-stub

scripts:

- Add update-md.cjs, tmp_append_en.ps1, tmp_merge_en.ps1 utilities
2026-07-03 10:26:38 +08:00

149 lines
4.9 KiB
TypeScript
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.
"use client"
import type { JSX } from "react"
import { Component, type ErrorInfo, type ReactNode } from "react"
import { useTranslations } from "next-intl"
import { AlertCircle, RefreshCw } from "lucide-react"
import { Button } from "@/shared/components/ui/button"
interface BoundaryProps {
children: ReactNode
/** 自定义降级 UI函数形式优先级最高。若提供则忽略 title/description/retryLabel */
fallback?: (error: Error, reset: () => void) => ReactNode
/** 渲染失败时的标题(已国际化,默认取 {namespace}.error.boundaryTitle */
title?: string
/** 渲染失败时的描述(已国际化,默认取 {namespace}.error.boundaryDescription */
description?: string
/** 重试按钮文案(已国际化,默认取 {namespace}.error.retry */
retryLabel?: string
/** 错误回调(用于埋点/监控) */
onError?: (error: Error, info: ErrorInfo) => void
}
interface State {
hasError: boolean
error: Error | null
}
/**
* 组件级 Error Boundary类组件实现承载错误捕获能力
* 不直接使用,请使用默认导出的函数式 `SectionErrorBoundary` 包装器以获得 i18n 支持。
*
* 支持两种降级形式:
* 1. `fallback`(函数 `(error, reset) => ReactNode`)— 完全自定义降级 UI优先级最高
* 2. `title` / `description` / `retryLabel` — 默认降级 UI + i18n 文案
*/
class SectionErrorBoundaryBase extends Component<BoundaryProps, State> {
constructor(props: BoundaryProps) {
super(props)
this.state = { hasError: false, error: null }
}
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error }
}
componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
console.error("[SectionErrorBoundary]", error, errorInfo)
this.props.onError?.(error, errorInfo)
}
private handleRetry = (): void => {
this.setState({ hasError: false, error: null })
}
render(): ReactNode {
if (this.state.hasError && this.state.error) {
// 优先使用自定义 fallback
if (this.props.fallback) {
return this.props.fallback(this.state.error, this.handleRetry)
}
// 默认降级 UI
return (
<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">
{this.props.title ?? "加载失败"}
</p>
<p className="text-xs text-muted-foreground">
{this.props.description ?? "数据加载时发生错误,请重试。"}
</p>
</div>
<Button
variant="outline"
size="sm"
onClick={this.handleRetry}
aria-label={this.props.retryLabel ?? "重试"}
>
<RefreshCw className="mr-1.5 h-3.5 w-3.5" aria-hidden="true" />
{this.props.retryLabel ?? "重试"}
</Button>
</div>
)
}
return this.props.children
}
}
interface SectionErrorBoundaryProps {
children: ReactNode
/** i18n namespace默认 "common"。需保证该 namespace 下存在 error.boundaryTitle / error.boundaryDescription / error.retry 键 */
namespace?: string
/** 自定义降级 UI函数形式优先级最高。若提供则忽略 title/description/retryLabel */
fallback?: (error: Error, reset: () => void) => ReactNode
/** 显式覆盖标题(优先级高于 i18n 默认) */
title?: string
/** 显式覆盖描述 */
description?: string
/** 显式覆盖重试按钮文案 */
retryLabel?: string
/** 错误回调(用于埋点/监控) */
onError?: (error: Error, info: ErrorInfo) => void
}
/**
* 组件级 Error Boundary包裹独立数据区块防止单区块错误导致整页崩溃。
* 自动使用 next-intl 提供本地化文案;如需完全自定义可传入 `fallback` 函数。
*
* @example
* ```tsx
* // 默认 i18n 文案
* <SectionErrorBoundary namespace="coursePlans">
* <CoursePlanProgress plan={plan} />
* </SectionErrorBoundary>
*
* // 完全自定义降级 UI
* <SectionErrorBoundary fallback={(error, reset) => <CustomUI error={error} onRetry={reset} />}>
* <CoursePlanProgress plan={plan} />
* </SectionErrorBoundary>
* ```
*/
export function SectionErrorBoundary({
children,
namespace = "common",
fallback,
title,
description,
retryLabel,
onError,
}: SectionErrorBoundaryProps): JSX.Element {
const t = useTranslations(namespace)
return (
<SectionErrorBoundaryBase
fallback={fallback}
title={title ?? t("error.boundaryTitle")}
description={description ?? t("error.boundaryDescription")}
retryLabel={retryLabel ?? t("error.retry")}
onError={onError}
>
{children}
</SectionErrorBoundaryBase>
)
}