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
149 lines
4.9 KiB
TypeScript
149 lines
4.9 KiB
TypeScript
"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>
|
||
)
|
||
}
|