import { Component, type ErrorInfo, type ReactNode } from 'react'; import { Button } from '@/components/ui/button'; import { AlertTriangle, RotateCcw, Home, Bug } from 'lucide-react'; import { toast } from 'sonner'; import i18n from '@/i18n'; interface Props { children: ReactNode; fallback?: ReactNode; } interface State { hasError: boolean; error: Error | null; errorInfo: ErrorInfo | null; } function isChunkLoadError(error: Error | null) { // 发版后旧入口引用已删除 chunk 时,浏览器会抛动态 import 加载失败。 // Logan.Li const message = error?.message || ''; return message.includes('Failed to fetch dynamically imported module') || message.includes('Importing a module script failed') || message.includes('ChunkLoadError') || message.includes('Loading chunk'); } /** * ErrorBoundary - 全局错误边界组件 * * 捕获 React 组件树中的错误,防止整个应用崩溃 * 提供友好的错误提示和恢复操作 */ export class ErrorBoundary extends Component { constructor(props: Props) { super(props); this.state = { hasError: false, error: null, errorInfo: null }; } static getDerivedStateFromError(error: Error): State { return { hasError: true, error, errorInfo: null }; } override componentDidCatch(error: Error, errorInfo: ErrorInfo) { console.error(i18n.t('common.ebCaughtError'), error); console.error(i18n.t('common.ebErrorInfo'), errorInfo.componentStack); this.setState({ error, errorInfo }); } handleRetry = () => { if (isChunkLoadError(this.state.error)) { window.location.reload(); return; } this.setState({ hasError: false, error: null, errorInfo: null }); }; handleGoHome = () => { window.location.href = '/'; }; handleReport = () => { const { error, errorInfo } = this.state; const body = `## ${i18n.t('common.ebReportTitle')}\n\n**${i18n.t('common.ebReportTime')}:** ${new Date().toISOString()}\n**URL:** ${window.location.href}\n**${i18n.t('common.ebReportUA')}:** ${navigator.userAgent}\n\n### ${i18n.t('common.ebReportErrorMsg')}\n\n\`\`\`\n${error?.name}: ${error?.message}\n\`\`\`\n\n### ${i18n.t('common.ebReportStack')}\n\n\`\`\`\n${errorInfo?.componentStack}\n\`\`\`\n`; navigator.clipboard.writeText(body).then(() => { toast.success(i18n.t('common.errorCopiedToClipboard')); }).catch(() => { toast.error(i18n.t('common.copyFailedManual')); }); }; override render() { if (this.state.hasError) { if (this.props.fallback) { return this.props.fallback; } const chunkLoadError = isChunkLoadError(this.state.error); return (
{/* Error Icon */}
{/* Title */}

{i18n.t('common.ebPageError')}

{chunkLoadError ? i18n.t('common.ebChunkDescription') : i18n.t('common.ebDescription')}

{/* Error Detail (collapsible) */} {this.state.error && (
{i18n.t('common.ebViewDetails')}
{this.state.error.name}: {this.state.error.message} {this.state.errorInfo && (
                        {this.state.errorInfo.componentStack}
                      
)}
)} {/* Actions */}
); } return this.props.children; } }