iloom-flatten/src/components/CrashReporter.tsx

235 lines
8.2 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.

import React, { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { AlertTriangle, X, Send, Bug, CheckCircle } from 'lucide-react';
import { supabase } from '../supabase/client';
interface CrashReport {
error: Error;
errorInfo: React.ErrorInfo;
timestamp: string;
userAgent: string;
url: string;
}
export function CrashReporter() {
const [crash, setCrash] = useState<CrashReport | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const [isSubmitted, setIsSubmitted] = useState(false);
const [userDescription, setUserDescription] = useState('');
useEffect(() => {
// 监听全局错误
const handleError = (event: ErrorEvent) => {
const crashData: CrashReport = {
error: event.error || new Error(event.message),
errorInfo: { componentStack: '' },
timestamp: new Date().toISOString(),
userAgent: navigator.userAgent,
url: window.location.href,
};
setCrash(crashData);
};
window.addEventListener('error', handleError);
return () => window.removeEventListener('error', handleError);
}, []);
const handleSubmit = async () => {
if (!crash) return;
setIsSubmitting(true);
try {
// 获取当前用户信息
const { data: { user } } = await supabase.auth.getUser();
// 提交崩溃报告
await supabase.from('crash_reports').insert({
error_message: crash.error.message,
error_stack: crash.error.stack,
component_stack: crash.errorInfo.componentStack,
user_description: userDescription.trim() || null,
user_agent: crash.userAgent,
url: crash.url,
user_id: user?.id || null,
status: 'pending',
});
setIsSubmitted(true);
setTimeout(() => {
setCrash(null);
setIsSubmitted(false);
setUserDescription('');
}, 3000);
} catch (error) {
console.error('提交崩溃报告失败:', error);
// 即使失败也关闭弹窗
setIsSubmitted(true);
} finally {
setIsSubmitting(false);
}
};
const handleClose = () => {
setCrash(null);
setUserDescription('');
};
if (!crash) return null;
return (
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-[100] p-4">
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
className="bg-white rounded-2xl w-full max-w-md shadow-2xl overflow-hidden"
>
{/* 头部 */}
<div className="bg-red-500 p-4 text-white">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-white/20 rounded-xl flex items-center justify-center">
<AlertTriangle className="w-5 h-5" />
</div>
<div>
<h2 className="text-lg font-semibold"></h2>
<p className="text-xs text-white/80"></p>
</div>
</div>
<button
onClick={handleClose}
className="p-2 hover:bg-white/20 rounded-lg transition-colors"
>
<X className="w-5 h-5" />
</button>
</div>
</div>
{/* 内容 */}
<div className="p-4 space-y-4">
{isSubmitted ? (
<div className="text-center py-8">
<div className="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4">
<CheckCircle className="w-8 h-8 text-green-600" />
</div>
<h3 className="text-lg font-semibold text-gray-900 mb-2"></h3>
<p className="text-sm text-gray-500"></p>
</div>
) : (
<>
{/* 错误信息 */}
<div className="bg-red-50 border border-red-100 rounded-xl p-3">
<div className="flex items-center gap-2 mb-2">
<Bug className="w-4 h-4 text-red-500" />
<span className="text-sm font-medium text-red-700"></span>
</div>
<p className="text-xs text-red-600 font-mono break-all">
{crash.error.message}
</p>
</div>
{/* 用户描述 */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
</label>
<textarea
value={userDescription}
onChange={(e) => setUserDescription(e.target.value)}
placeholder="例如:我正在创建计划,点击保存后出现这个错误..."
rows={3}
className="w-full px-3 py-2 border border-gray-200 rounded-xl text-sm focus:ring-2 focus:ring-red-500/20 focus:border-red-500 resize-none"
/>
</div>
{/* 隐私说明 */}
<p className="text-xs text-gray-500">
</p>
{/* 按钮 */}
<div className="flex gap-3">
<button
onClick={handleClose}
className="flex-1 py-2.5 border border-gray-300 rounded-xl text-gray-700 hover:bg-gray-50 transition-colors text-sm font-medium"
>
</button>
<button
onClick={handleSubmit}
disabled={isSubmitting}
className="flex-1 py-2.5 bg-red-500 hover:bg-red-600 text-white rounded-xl transition-colors text-sm font-medium flex items-center justify-center gap-2"
>
{isSubmitting ? (
<>
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
...
</>
) : (
<>
<Send className="w-4 h-4" />
</>
)}
</button>
</div>
</>
)}
</div>
</motion.div>
</div>
);
}
// 错误边界组件
export class ErrorBoundary extends React.Component<
{ children: React.ReactNode },
{ hasError: boolean; error: Error | null; errorInfo: React.ErrorInfo | null }
> {
constructor(props: { children: React.ReactNode }) {
super(props);
this.state = { hasError: false, error: null, errorInfo: null };
}
static getDerivedStateFromError(error: Error) {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
this.setState({ error, errorInfo });
console.error('Error caught by boundary:', error, errorInfo);
// 触发崩溃报告
window.dispatchEvent(new CustomEvent('app-crash', {
detail: { error, errorInfo }
}));
}
render() {
if (this.state.hasError) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-4">
<div className="bg-white rounded-2xl p-8 max-w-md w-full text-center shadow-xl">
<div className="w-20 h-20 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-4">
<AlertTriangle className="w-10 h-10 text-red-500" />
</div>
<h2 className="text-xl font-bold text-gray-900 mb-2"></h2>
<p className="text-sm text-gray-500 mb-6">
</p>
<button
onClick={() => window.location.reload()}
className="w-full py-3 bg-blue-600 hover:bg-blue-700 text-white rounded-xl transition-colors font-medium"
>
</button>
</div>
</div>
);
}
return this.props.children;
}
}