diff --git a/src/components/home/enhanced-traffic-stats.tsx b/src/components/home/enhanced-traffic-stats.tsx index 39d68a850..dd5c5ebd4 100644 --- a/src/components/home/enhanced-traffic-stats.tsx +++ b/src/components/home/enhanced-traffic-stats.tsx @@ -3,10 +3,14 @@ import { ArrowUpwardRounded, CloudDownloadRounded, CloudUploadRounded, + ErrorOutlineRounded, LinkRounded, MemoryRounded, } from '@mui/icons-material' import { + Alert, + Box, + Button, Grid, PaletteColor, Paper, @@ -15,9 +19,9 @@ import { useTheme, } from '@mui/material' import { ReactNode, memo, useMemo, useRef } from 'react' +import { ErrorBoundary, FallbackProps } from 'react-error-boundary' import { useTranslation } from 'react-i18next' -import { TrafficErrorBoundary } from '@/components/shared/traffic-error-boundary' import { useConnectionSummaryData } from '@/hooks/use-connection-data' import { useMemoryData } from '@/hooks/use-memory-data' import { useTrafficData } from '@/hooks/use-traffic-data' @@ -127,6 +131,50 @@ const CompactStatCard = memo( // 添加显示名称 CompactStatCard.displayName = 'CompactStatCard' +const fallbackSx = { + p: 2, + minHeight: 200, + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + border: '1px dashed', + borderColor: 'error.main', + borderRadius: 2, + bgcolor: 'error.light', +} + +const reloadPage = () => window.location.reload() + +const TrafficFallback = ({ error, resetErrorBoundary }: FallbackProps) => { + const { t } = useTranslation() + + return ( + + + + {t('shared.feedback.errors.trafficStats')} + + + {t('shared.feedback.errors.trafficStatsDescription')} + + + {error instanceof Error + ? error.message + : t('shared.feedback.errors.unknown')} + + + + + + + ) +} + export const EnhancedTrafficStats = () => { const { t } = useTranslation() const theme = useTheme() @@ -254,10 +302,11 @@ export const EnhancedTrafficStats = () => { }, [t, parsedData, displayMemory]) return ( - { - console.error('[EnhancedTrafficStats] 组件错误:', error, errorInfo) - }} + + console.error('[EnhancedTrafficStats]', error, errorInfo) + } > {trafficGraph && ( @@ -273,6 +322,6 @@ export const EnhancedTrafficStats = () => { ))} - + ) } diff --git a/src/components/layout/layout-traffic.tsx b/src/components/layout/layout-traffic.tsx index b7d50e5d6..9c6564edc 100644 --- a/src/components/layout/layout-traffic.tsx +++ b/src/components/layout/layout-traffic.tsx @@ -1,14 +1,15 @@ import { ArrowDownwardRounded, ArrowUpwardRounded, + ErrorOutlineRounded, MemoryRounded, } from '@mui/icons-material' import type { BoxProps, SvgIconProps, TypographyProps } from '@mui/material' import { Box, Typography } from '@mui/material' import { useEffect, useRef } from 'react' +import { ErrorBoundary } from 'react-error-boundary' import { useTranslation } from 'react-i18next' -import { LightweightTrafficErrorBoundary } from '@/components/shared/traffic-error-boundary' import { useMemoryData } from '@/hooks/use-memory-data' import { useTrafficData } from '@/hooks/use-traffic-data' import { useVerge } from '@/hooks/use-verge' @@ -17,6 +18,30 @@ import parseTraffic from '@/utils/parse-traffic' import { TrafficGraph, type TrafficRef } from './traffic-graph' +const unavailableSx = { + p: 1, + minHeight: 60, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + bgcolor: 'error.light', + borderRadius: 1, + color: 'error.contrastText', +} + +const TrafficUnavailableFallback = () => { + const { t } = useTranslation() + + return ( + + + + {t('shared.feedback.errors.trafficUnavailable')} + + + ) +} + export const LayoutTraffic = () => { const { t } = useTranslation() const { verge } = useVerge() @@ -75,7 +100,12 @@ export const LayoutTraffic = () => { } return ( - + + console.error('[LayoutTraffic]', error, errorInfo) + } + > {trafficGraph && pageVisible && (
{ )} - + ) } diff --git a/src/components/shared/traffic-error-boundary.tsx b/src/components/shared/traffic-error-boundary.tsx deleted file mode 100644 index 75d77b110..000000000 --- a/src/components/shared/traffic-error-boundary.tsx +++ /dev/null @@ -1,301 +0,0 @@ -import { - ErrorOutlineRounded, - RefreshRounded, - BugReportRounded, -} from '@mui/icons-material' -import { Box, Typography, Button, Alert, Collapse } from '@mui/material' -import React, { Component, ErrorInfo, ReactNode } from 'react' -import { useTranslation } from 'react-i18next' - -interface Props { - children: ReactNode - fallbackComponent?: ReactNode - onError?: (error: Error, errorInfo: ErrorInfo) => void -} - -interface State { - hasError: boolean - error: Error | null - errorInfo: ErrorInfo | null - showDetails: boolean -} - -export class TrafficErrorBoundary extends Component { - private retryCount = 0 - private maxRetries = 3 - - constructor(props: Props) { - super(props) - this.state = { - hasError: false, - error: null, - errorInfo: null, - showDetails: false, - } - } - - static getDerivedStateFromError(error: Error): Partial { - return { hasError: true, error } - } - - componentDidCatch(error: Error, errorInfo: ErrorInfo) { - console.error('[TrafficErrorBoundary] 捕获到组件错误:', error, errorInfo) - - this.setState({ - error, - errorInfo, - }) - - if (this.props.onError) { - this.props.onError(error, errorInfo) - } - - this.reportError(error, errorInfo) - } - - private reportError = (error: Error, errorInfo: ErrorInfo) => { - const errorReport = { - message: error.message, - stack: error.stack, - componentStack: errorInfo.componentStack, - timestamp: new Date().toISOString(), - userAgent: navigator.userAgent, - url: window.location.href, - } - - console.error('[TrafficErrorBoundary] 错误报告:', errorReport) - } - - private handleRetry = () => { - if (this.retryCount < this.maxRetries) { - this.retryCount++ - console.log( - `[TrafficErrorBoundary] 尝试重试 (${this.retryCount}/${this.maxRetries})`, - ) - - this.setState({ - hasError: false, - error: null, - errorInfo: null, - showDetails: false, - }) - } else { - console.warn('[TrafficErrorBoundary] 已达到最大重试次数') - } - } - - private handleRefresh = () => { - window.location.reload() - } - - private toggleDetails = () => { - this.setState((prev) => ({ showDetails: !prev.showDetails })) - } - - render() { - if (this.state.hasError) { - if (this.props.fallbackComponent) { - return this.props.fallbackComponent - } - - return ( - - ) - } - - return this.props.children - } -} - -interface TrafficErrorFallbackProps { - error: Error | null - errorInfo: ErrorInfo | null - showDetails: boolean - canRetry: boolean - retryCount: number - maxRetries: number - onRetry: () => void - onRefresh: () => void - onToggleDetails: () => void -} - -const TrafficErrorFallback: React.FC = ({ - error, - errorInfo, - showDetails, - canRetry, - retryCount, - maxRetries, - onRetry, - onRefresh, - onToggleDetails, -}) => { - const { t } = useTranslation() - - return ( - - - - - {t('shared.feedback.errors.trafficStats')} - - - - {t('shared.feedback.errors.trafficStatsDescription')} - - - - - {t('shared.feedback.errors.label')}:{' '} - {error instanceof Error - ? error.message - : t('shared.feedback.errors.unknown')} - - {retryCount > 0 && ( - - {t('shared.labels.retryAttempts')}: {retryCount}/{maxRetries} - - )} - - - - {canRetry && ( - - )} - - - - - - - - - - {t('shared.feedback.errors.details')}: - - - {error?.stack} - - - {errorInfo?.componentStack && ( - <> - - {t('shared.feedback.errors.componentStack')}: - - - {errorInfo.componentStack} - - - )} - - - - ) -} - -export const LightweightTrafficErrorBoundary: React.FC<{ - children: ReactNode -}> = ({ children }) => { - const { t } = useTranslation() - - return ( - - - - {t('shared.feedback.errors.trafficUnavailable')} - - - } - > - {children} - - ) -}