iloom-flatten/src/components/CrashReporter.tsx

235 lines
8.2 KiB
TypeScript
Raw Normal View History

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;
}
}