Files
carrotskin/src/components/ErrorPage.tsx

683 lines
22 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.

'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 />;
}
```
*/