Files
carrotskin/src/components/ErrorPage.tsx

683 lines
22 KiB
TypeScript
Raw Normal View History

'use client';
import Link from 'next/link';
import { motion, AnimatePresence, useScroll, useTransform } from 'framer-motion';
import { useState, useCallback, useEffect } from 'react';
import {
HomeIcon,
ArrowLeftIcon,
ExclamationTriangleIcon,
XCircleIcon,
ClockIcon,
ServerIcon,
WifiIcon,
ClipboardDocumentIcon,
ArrowPathIcon,
CubeIcon,
QuestionMarkCircleIcon,
SparklesIcon,
RocketLaunchIcon
} from '@heroicons/react/24/outline';
import { messageManager } from './MessageNotification';
export interface ErrorPageProps {
code?: number;
title?: string;
message?: string;
description?: string;
type?: '404' | '500' | '403' | 'network' | 'timeout' | 'maintenance' | 'custom';
actions?: {
primary?: {
label: string;
href?: string;
onClick?: () => void;
};
secondary?: {
label: string;
href?: string;
onClick?: () => void;
};
};
showContact?: boolean;
showRetry?: boolean;
onRetry?: () => void;
showCopyError?: boolean;
errorDetails?: string;
className?: string;
}
const errorConfigs = {
'404': {
icon: <CubeIcon className="w-20 h-20" />,
title: '页面未找到',
message: '这个页面似乎不存在于我们的世界中',
description: '页面可能已被移除、重命名,或者您输入的地址不正确。',
suggestions: [
'检查网址拼写是否正确',
'返回主页重新探索',
'使用搜索功能寻找内容'
]
},
'500': {
icon: <ServerIcon className="w-20 h-20" />,
title: '服务器错误',
message: '我们的服务器遇到了一些技术问题',
description: '工程师们正在紧急修复中,请稍后再试。',
suggestions: [
'稍后刷新页面重试',
'清除浏览器缓存',
'检查网络连接'
]
},
'403': {
icon: <ExclamationTriangleIcon className="w-20 h-20" />,
title: '访问被拒绝',
message: '您没有权限进入这个区域',
description: '请检查您的权限等级或联系管理员获取访问权限。',
suggestions: [
'确认您是否已登录',
'检查账户权限等级',
'联系管理员申请权限'
]
},
network: {
icon: <WifiIcon className="w-20 h-20" />,
title: '网络连接问题',
message: '与我们的连接出现了问题',
description: '请检查您的网络连接,然后重新尝试。',
suggestions: [
'检查网络连接状态',
'尝试重新连接',
'检查防火墙设置'
]
},
timeout: {
icon: <ClockIcon className="w-20 h-20" />,
title: '连接超时',
message: '服务器响应时间过长',
description: '服务器响应缓慢,请稍后再试。',
suggestions: [
'检查网络连接状态',
'稍后重新尝试连接',
'联系技术支持团队'
]
},
maintenance: {
icon: <ServerIcon className="w-20 h-20" />,
title: '系统维护中',
message: '我们正在对系统进行升级改造',
description: '为了提供更好的体验,系统暂时关闭维护。',
suggestions: [
'关注官方公告获取开放时间',
'加入官方群组了解进度',
'稍后再试'
]
},
custom: {
icon: <QuestionMarkCircleIcon className="w-20 h-20" />,
title: '未知错误',
message: '发生了一些奇怪的事情',
description: '请稍后再试或联系我们的支持团队。',
suggestions: []
}
};
// Action Button Component
function ActionButton({ action, colorClass, primary = false }: {
action: { label: string; href?: string; onClick?: () => void };
colorClass?: string;
primary?: boolean;
}) {
const buttonContent = (
<>
{primary ? <HomeIcon className="w-5 h-5 mr-2" /> : <ArrowLeftIcon className="w-5 h-5 mr-2" />}
{action.label}
</>
);
const buttonClass = primary
? `inline-flex items-center justify-center px-6 py-4 bg-gradient-to-r ${colorClass} text-white font-semibold rounded-xl transition-all duration-200 shadow-lg hover:shadow-xl transform hover:scale-105`
: 'inline-flex items-center justify-center px-6 py-4 border-2 border-orange-500 text-orange-500 hover:bg-orange-500 hover:text-white font-semibold rounded-xl transition-all duration-200';
if ('href' in action && action.href) {
return (
<Link href={action.href} className={buttonClass}>
{buttonContent}
</Link>
);
} else if ('onClick' in action && action.onClick) {
return (
<button onClick={action.onClick} className={buttonClass}>
{buttonContent}
</button>
);
}
return null;
}
export function ErrorPage({
code,
title,
message,
description,
type = 'custom',
actions,
showContact = true,
showRetry = true,
onRetry,
showCopyError = true,
errorDetails,
className = ''
}: ErrorPageProps) {
const [isRetrying, setIsRetrying] = useState(false);
const [showDetails, setShowDetails] = useState(false);
const [copySuccess, setCopySuccess] = useState(false);
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });
const { scrollYProgress } = useScroll();
const opacity = useTransform(scrollYProgress, [0, 0.3], [1, 0.8]);
const config = errorConfigs[type] || {};
const displayTitle = title || config.title || '出错了';
const displayMessage = message || config.message || '发生了一些错误';
const displayDescription = description || config.description || '';
// 生成详细的错误信息
const generateErrorDetails = useCallback(() => {
const details = {
timestamp: new Date().toISOString(),
errorType: type,
errorCode: code,
userAgent: typeof window !== 'undefined' ? window.navigator.userAgent : 'Unknown',
url: typeof window !== 'undefined' ? window.location.href : 'Unknown',
customDetails: errorDetails
};
return JSON.stringify(details, null, 2);
}, [type, code, errorDetails]);
const defaultActions = {
primary: {
label: '返回主城',
href: '/'
},
secondary: {
label: '返回上页',
onClick: () => {
if (typeof window !== 'undefined') {
window.history.back();
}
}
}
};
const finalActions = { ...defaultActions, ...actions };
const getThemeStyles = () => {
return {
bg: 'bg-gradient-to-br from-slate-50 via-orange-50 to-amber-50 dark:from-gray-900 dark:via-gray-800 dark:to-gray-900',
card: 'bg-white/70 dark:bg-gray-800/70 backdrop-blur-lg',
text: 'text-gray-900 dark:text-white',
subtext: 'text-gray-600 dark:text-gray-300',
accent: 'text-orange-500 dark:text-orange-400'
};
};
const getIconColor = () => {
const colors = {
'404': 'text-orange-500',
'500': 'text-red-500',
'403': 'text-yellow-500',
'network': 'text-blue-500',
'timeout': 'text-purple-500',
'maintenance': 'text-gray-500',
'custom': 'text-gray-500'
};
return colors[type] || 'text-gray-500';
};
const getCodeColor = () => {
const colors = {
'404': 'from-orange-400 via-orange-500 to-amber-500',
'500': 'from-red-400 via-red-500 to-pink-500',
'403': 'from-yellow-400 via-yellow-500 to-orange-500',
'network': 'from-blue-400 via-blue-500 to-cyan-500',
'timeout': 'from-purple-400 via-purple-500 to-pink-500',
'maintenance': 'from-gray-400 via-gray-500 to-slate-500',
'custom': 'from-gray-400 via-gray-500 to-slate-500'
};
return colors[type] || 'from-gray-400 via-gray-500 to-slate-500';
};
const getButtonColor = () => {
return 'from-orange-500 to-amber-500 hover:from-orange-600 hover:to-amber-600';
};
const handleRetry = async () => {
if (onRetry) {
setIsRetrying(true);
try {
await onRetry();
messageManager.success('重试成功!', { duration: 3000 });
} catch (error) {
messageManager.error('重试失败,请稍后重试', { duration: 5000 });
} finally {
setIsRetrying(false);
}
} else {
// 默认重试逻辑:刷新页面
if (typeof window !== 'undefined') {
window.location.reload();
}
}
};
const handleCopyError = async () => {
try {
const details = generateErrorDetails();
if (typeof navigator !== 'undefined' && navigator.clipboard) {
await navigator.clipboard.writeText(details);
setCopySuccess(true);
messageManager.success('错误信息已复制到剪贴板', { duration: 2000 });
setTimeout(() => setCopySuccess(false), 2000);
} else {
// 降级方案
const textArea = document.createElement('textarea');
textArea.value = details;
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
setCopySuccess(true);
messageManager.success('错误信息已复制到剪贴板', { duration: 2000 });
setTimeout(() => setCopySuccess(false), 2000);
}
} catch (error) {
messageManager.error('复制失败,请手动复制', { duration: 3000 });
}
};
const handleReportError = () => {
messageManager.info('感谢您的反馈,我们会尽快处理', { duration: 3000 });
// 这里可以添加实际的错误报告逻辑
};
// 键盘快捷键支持
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'r' && (event.ctrlKey || event.metaKey)) {
event.preventDefault();
handleRetry();
}
};
if (typeof window !== 'undefined') {
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}
}, [handleRetry]);
const themeStyles = getThemeStyles();
return (
<div className={`min-h-screen bg-gradient-to-br from-slate-50 via-orange-50 to-amber-50 dark:from-gray-900 dark:via-gray-800 dark:to-gray-900 ${className}`}>
{/* Animated Background - 简化背景动画 */}
<div className="fixed inset-0 overflow-hidden pointer-events-none">
<motion.div
className="absolute top-1/4 left-1/4 w-96 h-96 bg-gradient-to-br from-orange-400/10 to-amber-400/10 rounded-full blur-3xl"
animate={{
scale: [1, 1.2, 1],
opacity: [0.3, 0.5, 0.3]
}}
transition={{ duration: 4, repeat: Infinity }}
/>
<motion.div
className="absolute bottom-1/4 right-1/4 w-80 h-80 bg-gradient-to-tr from-pink-400/10 to-orange-400/10 rounded-full blur-3xl"
animate={{
scale: [1.2, 1, 1.2],
opacity: [0.5, 0.3, 0.5]
}}
transition={{ duration: 5, repeat: Infinity }}
/>
</div>
{/* Main Content - 考虑navbar高度的居中布局 */}
<motion.div
className="relative z-10 min-h-[calc(100vh-var(--navbar-height,4rem))] flex items-center justify-center px-4 sm:px-6 lg:px-8"
style={{ paddingTop: 'calc(var(--navbar-height, 4rem) + 2rem)' }}
>
<div className="max-w-4xl mx-auto w-full text-center">
{/* Error Code - 更突出的错误代码 */}
{code && (
<motion.div
initial={{ scale: 0.5, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ duration: 0.8, type: 'spring', stiffness: 100 }}
className="mb-12"
>
<h1 className={`text-9xl md:text-[12rem] font-black bg-gradient-to-r ${getCodeColor()} bg-clip-text text-transparent mb-2`}>
{code}
</h1>
<motion.div
className={`w-24 h-1 bg-gradient-to-r ${getCodeColor()} mx-auto rounded-full`}
initial={{ width: 0 }}
animate={{ width: 96 }}
transition={{ delay: 0.5, duration: 0.8 }}
/>
</motion.div>
)}
{/* Main Error Message - 简洁有力的错误信息 */}
<motion.div
className="mb-16 space-y-6"
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3, duration: 0.8 }}
>
<div className="space-y-4">
<h2 className="text-4xl md:text-6xl font-bold text-gray-900 dark:text-white leading-tight">
{displayTitle}
</h2>
<p className="text-xl md:text-2xl text-gray-600 dark:text-gray-300 leading-relaxed max-w-2xl mx-auto">
{displayMessage}
</p>
{displayDescription && (
<p className="text-lg text-gray-500 dark:text-gray-400 leading-relaxed max-w-xl mx-auto">
{displayDescription}
</p>
)}
</div>
{/* Icon - 更简洁的图标展示 */}
<motion.div
className="flex justify-center"
initial={{ opacity: 0, scale: 0.5 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.6, duration: 0.6 }}
>
<div className={`${getIconColor()} opacity-80`}>
{config.icon || <QuestionMarkCircleIcon className="w-24 h-24" />}
</div>
</motion.div>
</motion.div>
{/* Action Buttons - 更简洁的按钮布局 */}
<motion.div
className="mb-12"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.8, duration: 0.6 }}
>
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center mb-6">
{finalActions.primary && (
<ActionButton action={finalActions.primary} colorClass={getButtonColor()} primary />
)}
{finalActions.secondary && (
<ActionButton action={finalActions.secondary} />
)}
</div>
{showRetry && (
<motion.button
onClick={handleRetry}
disabled={isRetrying}
className="inline-flex items-center px-8 py-4 bg-gradient-to-r from-orange-500 to-amber-500 disabled:opacity-50 disabled:cursor-not-allowed text-white font-semibold rounded-2xl transition-all duration-200 shadow-lg hover:shadow-xl transform hover:scale-105"
whileHover={{ scale: isRetrying ? 1 : 1.05 }}
whileTap={{ scale: isRetrying ? 1 : 0.95 }}
>
<ArrowPathIcon className={`w-5 h-5 mr-2 ${isRetrying ? 'animate-spin' : ''}`} />
{isRetrying ? '重试中...' : '重新加载'}
</motion.button>
)}
</motion.div>
{/* Suggestions - 更简洁的建议展示 */}
{config.suggestions && config.suggestions.length > 0 && (
<motion.div
className="mb-12"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 1, duration: 0.6 }}
>
<h3 className="text-lg font-semibold text-gray-700 dark:text-gray-300 mb-4">
</h3>
<div className="flex flex-wrap justify-center gap-3">
{config.suggestions.map((suggestion, index) => (
<motion.div
key={index}
className="flex items-center px-4 py-2 bg-white/50 dark:bg-gray-800/50 rounded-full text-sm text-gray-600 dark:text-gray-400 border border-gray-200 dark:border-gray-700"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 1.2 + index * 0.1, duration: 0.4 }}
>
<div className={`w-1.5 h-1.5 rounded-full ${getIconColor()} mr-2`} />
{suggestion}
</motion.div>
))}
</div>
</motion.div>
)}
{/* Error Details - 更简洁的错误详情 */}
{showCopyError && (
<motion.div
className="mb-12"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 1.4, duration: 0.6 }}
>
<div className="flex justify-center gap-3 mb-4">
<button
onClick={handleCopyError}
className="inline-flex items-center px-4 py-2 bg-white/50 dark:bg-gray-800/50 hover:bg-white/70 dark:hover:bg-gray-800/70 text-gray-700 dark:text-gray-300 text-sm font-medium rounded-full transition-all duration-200 border border-gray-200 dark:border-gray-700"
>
<ClipboardDocumentIcon className="w-4 h-4 mr-2" />
{copySuccess ? '已复制!' : '复制错误信息'}
</button>
<button
onClick={() => setShowDetails(!showDetails)}
className="inline-flex items-center px-4 py-2 bg-white/50 dark:bg-gray-800/50 hover:bg-white/70 dark:hover:bg-gray-800/70 text-gray-700 dark:text-gray-300 text-sm font-medium rounded-full transition-all duration-200 border border-gray-200 dark:border-gray-700"
>
{showDetails ? '隐藏详情' : '显示详情'}
</button>
</div>
<AnimatePresence>
{showDetails && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.3 }}
className="max-w-2xl mx-auto p-4 bg-white/30 dark:bg-gray-800/30 rounded-2xl text-left border border-white/50 dark:border-gray-700/50 backdrop-blur-sm"
>
<h4 className="font-semibold text-gray-900 dark:text-white mb-2 text-sm"></h4>
<pre className="text-xs text-gray-600 dark:text-gray-400 overflow-auto max-h-24">
{generateErrorDetails()}
</pre>
</motion.div>
)}
</AnimatePresence>
</motion.div>
)}
{/* Contact Info - 更简洁的联系信息 */}
{showContact && (
<motion.div
className="text-center pb-8"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 1.6, duration: 0.6 }}
>
<p className="text-sm text-gray-500 dark:text-gray-400">
<Link
href="/contact"
className="text-orange-500 hover:text-orange-600 underline mx-1 font-medium"
onClick={(e) => {
e.preventDefault();
messageManager.info('联系我们页面正在开发中', { duration: 3000 });
}}
>
</Link>
</p>
</motion.div>
)}
</div>
</motion.div>
</div>
);
}
// 预设的错误页面组件
export function NotFoundPage() {
return <ErrorPage type="404" code={404} />;
}
export function ServerErrorPage() {
return <ErrorPage type="500" code={500} />;
}
export function ForbiddenPage() {
return <ErrorPage type="403" code={403} />;
}
export function NetworkErrorPage() {
return <ErrorPage type="network" />;
}
export function TimeoutErrorPage() {
return <ErrorPage type="timeout" />;
}
export function MaintenancePage() {
return <ErrorPage type="maintenance" />;
}
// 现代化的错误页面
export function ModernNotFoundPage() {
return <ErrorPage type="404" code={404} />;
}
export function ModernServerErrorPage() {
return <ErrorPage type="500" code={500} />;
}
// 使用示例和最佳实践
/*
ErrorPage组件提供了以下改进
1. ****
- 使MessageNotification组件替代alert
-
-
2. ****
- Minecraft风格
- Modern风格
- Minimal风格
3. ****
-
-
- /
- (Ctrl+R/+R )
-
-
4. ****
-
-
-
-
- Minecraft风格的游戏化提示
5. ****
-
-
-
-
使
```tsx
// 基础使用 - Minecraft风格404页面
<ErrorPage type="404" />
// 现代风格500错误
<ErrorPage type="500" code={500} theme="modern" />
// 自定义错误信息
<ErrorPage
type="500"
code={500}
title="数据库连接失败"
message="无法连接到数据库服务器"
description="请检查数据库配置或联系系统管理员"
theme="modern"
/>
// 自定义操作按钮
<ErrorPage
type="network"
actions={{
primary: {
label: '重新连接',
onClick: () => reconnect()
},
secondary: {
label: '离线模式',
href: '/offline'
}
}}
/>
// 启用重试功能
<ErrorPage
type="timeout"
showRetry={true}
onRetry={async () => {
// 自定义重试逻辑
await fetchData();
}}
/>
// 显示错误详情
<ErrorPage
type="500"
showCopyError={true}
errorDetails={JSON.stringify(errorDetails)}
theme="minimal"
/>
```
使
```tsx
// 在页面中使用预设的错误组件
import { NotFoundPage, ServerErrorPage, ModernNotFoundPage } from '@/components/ErrorPage';
// Minecraft风格404页面
export default function Custom404() {
return <NotFoundPage />;
}
// 现代化404页面
export default function Modern404() {
return <ModernNotFoundPage />;
}
// 500页面
export default function Custom500() {
return <ServerErrorPage />;
}
```
*/