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
This commit is contained in:
148
src/shared/components/section-error-boundary.tsx
Normal file
148
src/shared/components/section-error-boundary.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
"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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user