diff --git a/frontend/src/components/ErrorBoundary.tsx b/frontend/src/components/ErrorBoundary.tsx new file mode 100644 index 0000000..cdd1870 --- /dev/null +++ b/frontend/src/components/ErrorBoundary.tsx @@ -0,0 +1,104 @@ +import React from 'react'; + +interface State { + hasError: boolean; + error: Error | null; + errorInfo: React.ErrorInfo | null; +} + +/** + * #962: 全局 ErrorBoundary — 隔离任何组件渲染错误 + * - 防止单个组件抛错导致整个 React 树崩溃(root 空白) + * - 显示错误信息便于诊断 + * - 提供 reset 按钮,方便恢复 + */ +export default class ErrorBoundary extends React.Component<{ children: React.ReactNode }, State> { + constructor(props: { children: React.ReactNode }) { + super(props); + this.state = { hasError: false, error: null, errorInfo: null }; + } + + static getDerivedStateFromError(error: Error): Partial { + return { hasError: true, error }; + } + + componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { + console.error('[qihu-error-boundary]', error, errorInfo); + this.setState({ errorInfo }); + } + + reset = () => { + this.setState({ hasError: false, error: null, errorInfo: null }); + // Hard reload to clear any bad state + if (typeof window !== 'undefined') { + window.location.href = window.location.pathname + window.location.search; + } + }; + + render() { + if (this.state.hasError && this.state.error) { + const { error, errorInfo } = this.state; + return ( +
+

❌ React 组件渲染错误

+

+ ErrorBoundary 捕获了渲染错误,防止整个应用崩溃。 +

+

错误信息

+
+            {error.name}: {error.message}
+          
+ {errorInfo && errorInfo.componentStack && ( + <> +

组件堆栈

+
+                {errorInfo.componentStack}
+              
+ + )} + +
+ ); + } + return this.props.children; + } +} + diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 5e90fa4..05c2b0f 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -2,34 +2,26 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; import { BrowserRouter } from 'react-router-dom'; import App from './App'; +import ErrorBoundary from './components/ErrorBoundary'; import './styles/global.css'; -// #961: 全局错误处理 + 防止 hydration mismatch window.addEventListener('error', (e) => { - const root = document.getElementById('root'); - if (root && e.error) { - console.error('[qihu-global-error]', e.error); - } + console.error('[qihu-global-error]', e.error); }); window.addEventListener('unhandledrejection', (e) => { console.error('[qihu-unhandled-rejection]', e.reason); }); const rootEl = document.getElementById('root')!; -// 清空避免 hydration mismatch rootEl.innerHTML = ''; -try { - ReactDOM.createRoot(rootEl).render( - +ReactDOM.createRoot(rootEl).render( + + - - ); -} catch (err) { - console.error('[qihu-mount-error]', err); - rootEl.innerHTML = '
React mount failed: ' + String(err) + '
'; -} - + +
+);