import { View } from '@tarojs/components'; import { Component, PropsWithChildren, useSyncExternalStore } from 'react'; import { CircleAlert, Copy, RefreshCw, X } from 'lucide-react-taro'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Portal } from '@/components/ui/portal'; import { ScrollArea } from '@/components/ui/scroll-area'; import { toast } from '@/components/ui/toast'; import { cn } from '@/lib/utils'; import { IS_H5_ENV } from './env'; type ErrorState = { error: Error | null; }; type ErrorBoundaryProps = PropsWithChildren; type ErrorReportOptions = { componentStack?: string; source?: string; }; type OverlayStore = { error: Error | null; report: string; source: string; visible: boolean; open: boolean; timestamp: string; }; const EMPTY_STORE: OverlayStore = { error: null, report: '', source: '', visible: false, open: false, timestamp: '', }; const ERROR_ACCENT_COLOR = 'hsl(360, 100%, 45%)'; let handlersInstalled = false; let overlayStore: OverlayStore = EMPTY_STORE; const listeners = new Set<() => void>(); const emitChange = () => { listeners.forEach(listener => listener()); }; const subscribe = (listener: () => void) => { listeners.add(listener); return () => listeners.delete(listener); }; const getSnapshot = () => overlayStore; const setOverlayStore = (nextStore: OverlayStore) => { overlayStore = nextStore; emitChange(); }; const copyText = async (text: string) => { if (typeof window === 'undefined') return false; try { if (navigator.clipboard?.writeText) { await navigator.clipboard.writeText(text); return true; } } catch (error) { console.warn('[H5ErrorBoundary] Clipboard API copy failed:', error); } try { const textarea = document.createElement('textarea'); textarea.value = text; textarea.setAttribute('readonly', 'true'); textarea.style.position = 'fixed'; textarea.style.opacity = '0'; document.body.appendChild(textarea); textarea.select(); const copied = document.execCommand('copy'); document.body.removeChild(textarea); return copied; } catch (error) { console.warn('[H5ErrorBoundary] Fallback copy failed:', error); return false; } }; const normalizeError = (value: unknown) => { if (value instanceof Error) { return value; } if (typeof value === 'string') { return new Error(value); } try { return new Error(JSON.stringify(value)); } catch { return new Error(String(value)); } }; const buildErrorReport = (error: Error, options: ErrorReportOptions = {}) => { const lines = [ '[H5 Runtime Error]', `Time: ${new Date().toISOString()}`, options.source ? `Source: ${options.source}` : '', `Name: ${error.name}`, `Message: ${error.message}`, error.stack ? `Stack:\n${error.stack}` : '', options.componentStack ? `Component Stack:\n${options.componentStack}` : '', typeof navigator !== 'undefined' ? `User Agent: ${navigator.userAgent}` : '', ].filter(Boolean); return lines.join('\n\n'); }; const setPanelOpen = (open: boolean) => { if (!overlayStore.visible) return; setOverlayStore({ ...overlayStore, open, }); }; export const showH5ErrorOverlay = ( input: unknown, options: ErrorReportOptions = {}, ) => { if (typeof window === 'undefined') { return; } const error = normalizeError(input); const report = buildErrorReport(error, options); const timestamp = new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', second: '2-digit', }); setOverlayStore({ error, report, source: options.source || 'runtime', timestamp, visible: true, open: false, }); console.error('[H5ErrorOverlay] Showing error overlay:', error, options); }; const handleWindowError = (event: ErrorEvent) => { const error = event.error || new Error(event.message || 'Unknown H5 runtime error'); showH5ErrorOverlay(error, { source: 'window.error' }); }; const handleUnhandledRejection = (event: PromiseRejectionEvent) => { showH5ErrorOverlay(event.reason, { source: 'window.unhandledrejection' }); }; export const initializeH5ErrorHandling = () => { if (!IS_H5_ENV || typeof window === 'undefined' || handlersInstalled) { return; } handlersInstalled = true; window.addEventListener('error', handleWindowError); window.addEventListener('unhandledrejection', handleUnhandledRejection); }; const H5ErrorOverlayHost = () => { const store = useSyncExternalStore(subscribe, getSnapshot, getSnapshot); if (!IS_H5_ENV || !store.visible) { return null; } const errorName = store.error?.name || 'Error'; return ( {store.open && ( Runtime Error {store.source} {errorName} Error {store.error?.name || 'Error'} Source {store.source} Full Report {store.timestamp} {store.report} )} ); }; class H5ErrorBoundaryInner extends Component { static getDerivedStateFromError(error: Error): Partial { return { error }; } state: ErrorState = { error: null, }; componentDidUpdate(prevProps: ErrorBoundaryProps) { if (this.state.error && prevProps.children !== this.props.children) { this.setState({ error: null }); } } componentDidCatch(error: Error, info: { componentStack: string }) { showH5ErrorOverlay(error, { source: 'React Error Boundary', componentStack: info.componentStack || '', }); } render() { return ( <> {this.state.error ? null : this.props.children} ); } } export const H5ErrorBoundary = ({ children }: PropsWithChildren) => { if (!IS_H5_ENV) { return <>{children}; } return {children}; };