ErrorPage 新增功能: - 浮动动画效果 (keyframes) - 可配置错误原因列表 (reasons prop) - 技术详情折叠面板 (techDetails prop) - 可选搜索功能 (search prop) - 更丰富的导航选项
441 lines
12 KiB
TypeScript
441 lines
12 KiB
TypeScript
/**
|
||
* ErrorPage - 通用错误页面组件
|
||
* 用于显示加载失败、网络错误、404等异常状态
|
||
* 设计风格:黑色背景 + 金色边框
|
||
*/
|
||
|
||
import React from 'react';
|
||
import {
|
||
Box,
|
||
Text,
|
||
Button,
|
||
VStack,
|
||
HStack,
|
||
Collapse,
|
||
useDisclosure,
|
||
} from '@chakra-ui/react';
|
||
import { ChevronDownIcon, ChevronUpIcon } from '@chakra-ui/icons';
|
||
import { ExclamationCircleOutlined } from '@ant-design/icons';
|
||
import { useNavigate } from 'react-router-dom';
|
||
import { trackEventAsync } from '@/lib/posthog';
|
||
|
||
// 主题色(保持原来的配色)
|
||
const GOLD_COLOR = '#D4A574';
|
||
const BG_COLOR = '#1A202C';
|
||
|
||
// 错误原因项配置
|
||
export interface ErrorReasonItem {
|
||
/** 图标(emoji 或自定义组件) */
|
||
icon: string | React.ReactNode;
|
||
/** 原因标题 */
|
||
title: string;
|
||
/** 原因描述 */
|
||
description: string;
|
||
}
|
||
|
||
// 操作按钮配置
|
||
export interface ActionButton {
|
||
/** 按钮文本 */
|
||
label: string;
|
||
/** 按钮图标(可选,放在文本前) */
|
||
icon?: string;
|
||
/** 按钮类型:primary(主要)、secondary(次要)、outline(轮廓) */
|
||
variant?: 'primary' | 'secondary' | 'outline';
|
||
/** 点击回调 */
|
||
onClick?: () => void;
|
||
/** 跳转链接(与 onClick 二选一) */
|
||
href?: string;
|
||
}
|
||
|
||
// 技术详情配置
|
||
export interface TechDetails {
|
||
/** 请求 URL */
|
||
requestUrl?: string;
|
||
/** 错误类型 */
|
||
errorType?: string;
|
||
/** 错误信息 */
|
||
errorMessage?: string;
|
||
/** 时间戳 */
|
||
timestamp?: string;
|
||
/** 相关 ID */
|
||
relatedId?: string;
|
||
/** 自定义字段 */
|
||
customFields?: Record<string, string>;
|
||
}
|
||
|
||
// 完整的 ErrorPage 配置
|
||
export interface ErrorPageProps {
|
||
// ===== 基础配置 =====
|
||
/** 错误标题 */
|
||
title?: string;
|
||
/** 错误副标题(如显示错误 ID) */
|
||
subtitle?: string;
|
||
/** 错误描述信息 */
|
||
description?: string;
|
||
|
||
// ===== 详细信息 =====
|
||
/** 详细信息值 */
|
||
detail?: string;
|
||
/** 详细信息标签 */
|
||
detailLabel?: string;
|
||
|
||
// ===== 错误原因列表 =====
|
||
/** 错误原因列表 */
|
||
reasons?: ErrorReasonItem[];
|
||
|
||
// ===== 技术详情 =====
|
||
/** 技术详情(可展开查看) */
|
||
techDetails?: TechDetails;
|
||
|
||
// ===== 操作按钮 =====
|
||
/** 自定义操作按钮列表 */
|
||
actions?: ActionButton[];
|
||
/** 快捷配置:是否显示重试按钮 */
|
||
showRetry?: boolean;
|
||
/** 重试回调 */
|
||
onRetry?: () => void;
|
||
/** 快捷配置:是否显示返回按钮 */
|
||
showBack?: boolean;
|
||
/** 返回回调 */
|
||
onBack?: () => void;
|
||
/** 快捷配置:是否显示返回首页按钮 */
|
||
showHome?: boolean;
|
||
/** 首页路径 */
|
||
homePath?: string;
|
||
|
||
// ===== 布局配置 =====
|
||
/** 是否全屏显示 */
|
||
fullScreen?: boolean;
|
||
/** 最大宽度 */
|
||
maxWidth?: string;
|
||
|
||
// ===== 网络状态 =====
|
||
/** 是否检查网络状态并显示离线提示 */
|
||
checkOffline?: boolean;
|
||
|
||
// ===== 错误上报 =====
|
||
/** 是否启用内置 PostHog 错误上报(默认 true) */
|
||
enableBuiltInReport?: boolean;
|
||
/** 自定义错误上报回调(可选,与内置上报同时生效) */
|
||
onErrorReport?: (errorInfo: Record<string, unknown>) => void;
|
||
}
|
||
|
||
// 默认错误原因
|
||
const DEFAULT_REASONS: ErrorReasonItem[] = [
|
||
{
|
||
icon: '🔍',
|
||
title: 'ID 可能输入错误',
|
||
description: '请检查 URL 中的 ID 是否正确',
|
||
},
|
||
{
|
||
icon: '🗑️',
|
||
title: '内容可能已被删除',
|
||
description: '该内容可能因过期或调整而被下架',
|
||
},
|
||
{
|
||
icon: '🔄',
|
||
title: '系统暂时无法访问',
|
||
description: '请稍后重试或联系技术支持',
|
||
},
|
||
];
|
||
|
||
const ErrorPage: React.FC<ErrorPageProps> = ({
|
||
title = '加载失败',
|
||
subtitle,
|
||
description = '我们无法找到您请求的内容,这可能是因为:',
|
||
detail,
|
||
detailLabel = 'ID',
|
||
reasons = DEFAULT_REASONS,
|
||
techDetails,
|
||
actions,
|
||
showRetry = false,
|
||
onRetry,
|
||
showBack = false,
|
||
onBack,
|
||
showHome = false,
|
||
homePath = '/',
|
||
fullScreen = true,
|
||
maxWidth = '500px',
|
||
checkOffline = true,
|
||
enableBuiltInReport = true,
|
||
onErrorReport,
|
||
}) => {
|
||
const navigate = useNavigate();
|
||
const { isOpen: isTechOpen, onToggle: onTechToggle } = useDisclosure();
|
||
const [isOffline, setIsOffline] = React.useState(!navigator.onLine);
|
||
|
||
// 监听网络状态
|
||
React.useEffect(() => {
|
||
if (!checkOffline) return;
|
||
|
||
const handleOnline = () => setIsOffline(false);
|
||
const handleOffline = () => setIsOffline(true);
|
||
|
||
window.addEventListener('online', handleOnline);
|
||
window.addEventListener('offline', handleOffline);
|
||
|
||
return () => {
|
||
window.removeEventListener('online', handleOnline);
|
||
window.removeEventListener('offline', handleOffline);
|
||
};
|
||
}, [checkOffline]);
|
||
|
||
// 错误上报
|
||
React.useEffect(() => {
|
||
const errorInfo = {
|
||
error_title: title,
|
||
error_detail: detail,
|
||
error_type: techDetails?.errorType,
|
||
error_message: techDetails?.errorMessage,
|
||
page_url: window.location.href,
|
||
referrer: document.referrer,
|
||
user_agent: navigator.userAgent,
|
||
event_id: techDetails?.relatedId,
|
||
};
|
||
|
||
// 内置 PostHog 上报(异步,不阻塞渲染)
|
||
if (enableBuiltInReport) {
|
||
trackEventAsync('error_page_view', errorInfo);
|
||
}
|
||
|
||
// 自定义上报回调(保持兼容)
|
||
if (onErrorReport) {
|
||
onErrorReport({
|
||
...errorInfo,
|
||
timestamp: new Date().toISOString(),
|
||
...techDetails,
|
||
});
|
||
}
|
||
}, [enableBuiltInReport, onErrorReport, title, detail, techDetails]);
|
||
|
||
// 构建操作按钮列表
|
||
const buildActionButtons = (): ActionButton[] => {
|
||
if (actions) return actions;
|
||
|
||
const buttons: ActionButton[] = [];
|
||
|
||
if (showBack) {
|
||
buttons.push({
|
||
label: '返回',
|
||
variant: 'outline',
|
||
onClick: onBack || (() => window.history.back()),
|
||
});
|
||
}
|
||
|
||
if (showRetry && onRetry) {
|
||
buttons.push({
|
||
label: '重试',
|
||
variant: 'primary',
|
||
onClick: onRetry,
|
||
});
|
||
}
|
||
|
||
if (showHome) {
|
||
buttons.push({
|
||
label: '返回首页',
|
||
variant: 'outline',
|
||
onClick: () => navigate(homePath),
|
||
});
|
||
}
|
||
|
||
return buttons;
|
||
};
|
||
|
||
// 获取按钮样式(保持原来的金色风格)
|
||
const getButtonStyle = (variant: ActionButton['variant']) => {
|
||
switch (variant) {
|
||
case 'primary':
|
||
return {
|
||
bg: GOLD_COLOR,
|
||
color: BG_COLOR,
|
||
border: '1px solid',
|
||
borderColor: GOLD_COLOR,
|
||
_hover: { bg: '#C49A6C' },
|
||
};
|
||
case 'outline':
|
||
default:
|
||
return {
|
||
variant: 'outline' as const,
|
||
borderColor: GOLD_COLOR,
|
||
color: GOLD_COLOR,
|
||
_hover: { bg: GOLD_COLOR, color: 'black' },
|
||
};
|
||
}
|
||
};
|
||
|
||
const actionButtons = buildActionButtons();
|
||
const hasButtons = actionButtons.length > 0;
|
||
|
||
return (
|
||
<Box
|
||
h={fullScreen ? '100vh' : '60vh'}
|
||
w="100%"
|
||
display="flex"
|
||
alignItems="center"
|
||
justifyContent="center"
|
||
>
|
||
<Box
|
||
bg={BG_COLOR}
|
||
border="1px solid"
|
||
borderColor={GOLD_COLOR}
|
||
borderRadius="lg"
|
||
p={8}
|
||
maxW={maxWidth}
|
||
w="90%"
|
||
textAlign="center"
|
||
>
|
||
{/* 金色圆形感叹号图标 */}
|
||
<Box mx="auto" mb={4}>
|
||
<ExclamationCircleOutlined style={{ fontSize: '40px', color: GOLD_COLOR }} />
|
||
</Box>
|
||
|
||
{/* 金色标题 */}
|
||
<Text color={GOLD_COLOR} fontSize="lg" fontWeight="medium" mb={2}>
|
||
{title}
|
||
</Text>
|
||
|
||
{/* 副标题(ID 显示) */}
|
||
{(subtitle || detail) && (
|
||
<Text
|
||
color="gray.400"
|
||
fontSize="sm"
|
||
fontFamily="monospace"
|
||
mb={2}
|
||
>
|
||
{subtitle || `${detailLabel}: ${detail}`}
|
||
</Text>
|
||
)}
|
||
|
||
{/* 离线提示 */}
|
||
{checkOffline && isOffline && (
|
||
<Box
|
||
bg="orange.900"
|
||
border="1px solid"
|
||
borderColor="orange.600"
|
||
borderRadius="md"
|
||
p={2}
|
||
mb={4}
|
||
>
|
||
<Text color="orange.300" fontSize="sm">
|
||
当前处于离线状态,请检查网络连接
|
||
</Text>
|
||
</Box>
|
||
)}
|
||
|
||
{/* 错误原因列表 */}
|
||
{reasons.length > 0 && (
|
||
<Box
|
||
bg="gray.800"
|
||
borderRadius="md"
|
||
p={4}
|
||
mb={4}
|
||
textAlign="left"
|
||
>
|
||
<Text color="gray.400" fontSize="sm" mb={3}>
|
||
{description}
|
||
</Text>
|
||
|
||
<VStack spacing={3} align="stretch">
|
||
{reasons.map((reason, index) => (
|
||
<HStack key={index} spacing={3} align="flex-start">
|
||
<Text fontSize="lg" flexShrink={0}>
|
||
{typeof reason.icon === 'string' ? reason.icon : reason.icon}
|
||
</Text>
|
||
<Box>
|
||
<Text fontWeight="500" color="gray.300" fontSize="sm">
|
||
{reason.title}
|
||
</Text>
|
||
<Text fontSize="xs" color="gray.500">
|
||
{reason.description}
|
||
</Text>
|
||
</Box>
|
||
</HStack>
|
||
))}
|
||
</VStack>
|
||
</Box>
|
||
)}
|
||
|
||
{/* 技术详情(可展开) */}
|
||
{techDetails && (
|
||
<Box mb={4}>
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
color="gray.500"
|
||
rightIcon={isTechOpen ? <ChevronUpIcon /> : <ChevronDownIcon />}
|
||
onClick={onTechToggle}
|
||
_hover={{ bg: 'transparent', color: 'gray.400' }}
|
||
>
|
||
查看技术信息
|
||
</Button>
|
||
<Collapse in={isTechOpen}>
|
||
<Box
|
||
mt={2}
|
||
p={3}
|
||
bg="gray.800"
|
||
borderRadius="md"
|
||
fontFamily="monospace"
|
||
fontSize="xs"
|
||
color="gray.500"
|
||
textAlign="left"
|
||
overflowX="auto"
|
||
>
|
||
{techDetails.requestUrl && (
|
||
<Text>请求URL: {techDetails.requestUrl}</Text>
|
||
)}
|
||
{techDetails.errorType && (
|
||
<Text>错误类型: {techDetails.errorType}</Text>
|
||
)}
|
||
{techDetails.errorMessage && (
|
||
<Text>错误信息: {techDetails.errorMessage}</Text>
|
||
)}
|
||
{techDetails.timestamp && (
|
||
<Text>时间戳: {techDetails.timestamp}</Text>
|
||
)}
|
||
{techDetails.relatedId && (
|
||
<Text>相关ID: {techDetails.relatedId}</Text>
|
||
)}
|
||
{techDetails.customFields &&
|
||
Object.entries(techDetails.customFields).map(([key, value]) => (
|
||
<Text key={key}>
|
||
{key}: {value}
|
||
</Text>
|
||
))}
|
||
</Box>
|
||
</Collapse>
|
||
</Box>
|
||
)}
|
||
|
||
{/* 按钮组 */}
|
||
{hasButtons && (
|
||
<HStack justify="center" spacing={3} mt={4}>
|
||
{actionButtons.map((btn, index) => (
|
||
<Button
|
||
key={index}
|
||
size="sm"
|
||
px={6}
|
||
fontWeight="medium"
|
||
{...getButtonStyle(btn.variant)}
|
||
onClick={btn.href ? () => navigate(btn.href!) : btn.onClick}
|
||
>
|
||
{btn.icon && <Text as="span" mr={2}>{btn.icon}</Text>}
|
||
{btn.label}
|
||
</Button>
|
||
))}
|
||
</HStack>
|
||
)}
|
||
|
||
{/* 底部帮助提示 */}
|
||
<Text fontSize="xs" color="gray.500" mt={6}>
|
||
点击右下角
|
||
<Text as="span" color={GOLD_COLOR} fontWeight="medium">
|
||
联系客服
|
||
</Text>
|
||
</Text>
|
||
</Box>
|
||
</Box>
|
||
);
|
||
};
|
||
|
||
export default ErrorPage;
|