Files
Edu/apps/portal-shell/src/shared/components/section-error-boundary.tsx
SpecialX 9cedf0c437 feat(portal-shell): v2.0 P0 shadcn standardization + security + streaming + error handling
- shadcn/ui 标准化:废弃纸感令牌,统一 bg-background/text-foreground 等
- Tailwind v4 + @theme inline,移除 tailwind.config.js
- React 19 use() + Suspense 流式渲染,首屏骨架秒出
- 三级错误边界:Route → Section → Widget 层层兜底
- 错误上报:useErrorReport → sendBeacon → /api/log mock 端点
- 三层安全边界:L1 角色门禁 / L2 权限点门禁 / L3 数据范围
- 权限位图 base36 压缩:67 权限点 → ~14 字符,JWT 体积减少 ≥ 99%
- notify 统一 Toast 封装,禁止业务直接 import sonner
- PluginBoundary 替代 PluginLoader(错误边界 + Suspense + Skeleton 三件套)

验证:typecheck 0 错误 / lint 0 错误 / build 6 路由生成成功
2026-07-17 16:10:05 +08:00

95 lines
2.8 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 { Component, type ErrorInfo, type ReactNode } from "react";
import { AlertCircle, RefreshCw } from "lucide-react";
import { Button } from "@/shared/components/ui/button";
import { cn } from "@/shared/lib/utils";
/**
* SectionErrorBoundary - 区块级错误边界(用于 DashboardSection 内)
*
* 职责:隔离单个区块(如统计卡片组、图表区、列表区)的渲染错误,
* 不影响其他区块和整个页面。
*
* 与 RouteErrorBoundary 的区别:
* - RouteErrorBoundary整页崩溃兜底由 Next.js error.tsx 触发
* - SectionErrorBoundary区块崩溃隔离由 DashboardSection 内部挂载
*
* 与 PluginBoundary 的区别:
* - PluginBoundary单个插件崩溃隔离含 Suspense + Skeleton
* - SectionErrorBoundary区块级可能含多个插件无 Suspense
*
* 关联portal-shell README v2.0 §5.4 三级错误处理L2 区块级)
*/
export interface SectionErrorBoundaryProps {
children: ReactNode;
/** 区块标题(用于错误 UI 显示,如 "统计概览" */
title?: string;
/** 自定义错误降级 UI */
fallback?: (error: Error, reset: () => void) => ReactNode;
/** 错误回调(上报) */
onError?: (error: Error, info: ErrorInfo) => void;
/** 自定义类名 */
className?: string;
}
interface SectionErrorBoundaryState {
error: Error | null;
}
export class SectionErrorBoundary extends Component<
SectionErrorBoundaryProps,
SectionErrorBoundaryState
> {
override state: SectionErrorBoundaryState = { error: null };
static getDerivedStateFromError(error: Error): SectionErrorBoundaryState {
return { error };
}
override componentDidCatch(error: Error, info: ErrorInfo): void {
this.props.onError?.(error, info);
}
reset = (): void => {
this.setState({ error: null });
};
override render(): ReactNode {
const { error } = this.state;
const { children, fallback, title, className } = this.props;
if (error) {
if (fallback) {
return fallback(error, this.reset);
}
return (
<div
role="alert"
aria-live="assertive"
className={cn(
"flex min-h-[200px] flex-col items-center justify-center gap-3 rounded-lg border border-destructive/30 bg-destructive/5 p-6",
className,
)}
>
<AlertCircle className="size-8 text-destructive" />
<div className="text-center">
<p className="text-sm font-medium">{title ?? "区块加载失败"}</p>
<p className="mt-1 text-xs text-muted-foreground">
{error.message}
</p>
</div>
<Button onClick={this.reset} variant="outline" size="sm">
<RefreshCw className="size-4" />
</Button>
</div>
);
}
return children;
}
}