geo/frontend/components/ErrorBoundary.tsx

131 lines
4.4 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";
/**
* 全局错误边界组件
*
* - 捕获子树中的 React 渲染错误,防止整个页面白屏
* - 在开发环境输出详细错误栈到 console
* - 预留 Sentry 集成点(搜索 TODO:SENTRY
* - 提供可重置的友好错误 UI
*/
import React, { Component, ErrorInfo, ReactNode } from "react";
interface Props {
children: ReactNode;
/** 自定义 fallback UI不传则使用内置样式 */
fallback?: ReactNode;
}
interface State {
hasError: boolean;
error: Error | null;
errorInfo: ErrorInfo | null;
}
export class ErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false, error: null, errorInfo: null };
}
static getDerivedStateFromError(error: Error): Partial<State> {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
this.setState({ errorInfo });
// 开发环境:完整错误栈
if (process.env.NODE_ENV !== "production") {
console.group("[ErrorBoundary] 捕获到未处理的渲染错误");
console.error("Error:", error);
console.error("Component Stack:", errorInfo.componentStack);
console.groupEnd();
} else {
console.error("[ErrorBoundary]", error.message);
}
// TODO:SENTRY — 生产环境错误上报
// import * as Sentry from "@sentry/nextjs";
// Sentry.captureException(error, { extra: { componentStack: errorInfo.componentStack } });
}
handleReset = (): void => {
this.setState({ hasError: false, error: null, errorInfo: null });
};
render(): ReactNode {
if (!this.state.hasError) {
return this.props.children;
}
// 使用自定义 fallback
if (this.props.fallback) {
return this.props.fallback;
}
// 内置友好错误页面
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50 px-4">
<div className="max-w-md w-full bg-white rounded-2xl shadow-lg p-8 text-center">
{/* 图标 */}
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-red-50">
<svg
className="h-8 w-8 text-red-500"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z"
/>
</svg>
</div>
<h1 className="text-xl font-semibold text-gray-900 mb-2">
</h1>
<p className="text-sm text-gray-500 mb-6">
</p>
{/* 开发环境:展示错误摘要 */}
{process.env.NODE_ENV !== "production" && this.state.error && (
<details className="mb-6 text-left rounded-lg bg-red-50 p-4 text-xs text-red-700">
<summary className="cursor-pointer font-medium select-none mb-1">
</summary>
<pre className="whitespace-pre-wrap break-all mt-2 opacity-80">
{this.state.error.message}
{this.state.errorInfo?.componentStack}
</pre>
</details>
)}
<div className="flex flex-col gap-3">
<button
onClick={this.handleReset}
className="w-full rounded-lg bg-blue-600 px-4 py-2.5 text-sm font-medium text-white hover:bg-blue-700 transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
>
</button>
<button
onClick={() => window.location.reload()}
className="w-full rounded-lg border border-gray-200 px-4 py-2.5 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2"
>
</button>
</div>
</div>
</div>
);
}
}
export default ErrorBoundary;