pref: ErrorPage 功能增强
ErrorPage 新增功能: - 浮动动画效果 (keyframes) - 可配置错误原因列表 (reasons prop) - 技术详情折叠面板 (techDetails prop) - 可选搜索功能 (search prop) - 更丰富的导航选项
This commit is contained in:
335
src/components/ErrorPage/README.md
Normal file
335
src/components/ErrorPage/README.md
Normal file
@@ -0,0 +1,335 @@
|
|||||||
|
# ErrorPage 通用错误页面组件
|
||||||
|
|
||||||
|
通用错误页面组件,用于显示加载失败、网络错误、404 等异常状态。
|
||||||
|
|
||||||
|
**设计风格**:黑色背景 (`#1A202C`) + 金色边框 (`#D4A574`)
|
||||||
|
|
||||||
|
## 效果预览
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────┐
|
||||||
|
│ ╭──────────╮ │
|
||||||
|
│ │ ⚠️ │ (金色圆形) │
|
||||||
|
│ ╰──────────╯ │
|
||||||
|
│ │
|
||||||
|
│ 事件走丢了 │
|
||||||
|
│ ID: ZXY-101 │
|
||||||
|
│ │
|
||||||
|
│ ┌──────────────────────────────┐ │
|
||||||
|
│ │ 抱歉,我们找不到您请求的事件... │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ 🔍 事件ID输入错误 │ │
|
||||||
|
│ │ 请检查URL中的事件ID是否正确 │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ 🗑️ 该事件已被删除或下架 │ │
|
||||||
|
│ │ 该事件可能因过期而被移除 │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ 🔄 系统暂时无法访问该事件 │ │
|
||||||
|
│ │ 请稍后重试或联系技术支持 │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ [查看技术信息 ▼] │ │
|
||||||
|
│ └──────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ [返回] [重试] │
|
||||||
|
│ │
|
||||||
|
│ 点击右下角联系客服 │
|
||||||
|
└─────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
### 基础用法
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import ErrorPage from '@/components/ErrorPage';
|
||||||
|
|
||||||
|
// 最简单的用法 - 使用所有默认配置
|
||||||
|
<ErrorPage />
|
||||||
|
|
||||||
|
// 自定义标题和描述
|
||||||
|
<ErrorPage
|
||||||
|
title="事件走丢了"
|
||||||
|
description="抱歉,我们找不到您请求的事件"
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 完整配置示例
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<ErrorPage
|
||||||
|
// 基础配置
|
||||||
|
title="事件走丢了"
|
||||||
|
subtitle="ID: ZXY-101"
|
||||||
|
description="抱歉,我们找不到您请求的事件,这可能是因为:"
|
||||||
|
|
||||||
|
// 错误原因列表
|
||||||
|
reasons={[
|
||||||
|
{
|
||||||
|
icon: '🔍',
|
||||||
|
title: '事件ID输入错误',
|
||||||
|
description: '请检查URL中的事件ID是否正确',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: '🗑️',
|
||||||
|
title: '该事件已被删除或下架',
|
||||||
|
description: '该事件可能因过期或内容调整而被移除',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: '🔄',
|
||||||
|
title: '系统暂时无法访问该事件',
|
||||||
|
description: '请稍后重试或联系技术支持',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
|
||||||
|
// 技术详情(可展开)
|
||||||
|
techDetails={{
|
||||||
|
requestUrl: 'http://localhost:3000/event-detail?id=ZXY-101',
|
||||||
|
errorType: '404 - Event Not Found',
|
||||||
|
errorMessage: 'Unexpected token...',
|
||||||
|
timestamp: '2024-01-15 14:30:22',
|
||||||
|
relatedId: '101',
|
||||||
|
}}
|
||||||
|
|
||||||
|
// 操作按钮
|
||||||
|
showBack
|
||||||
|
showRetry
|
||||||
|
onRetry={() => window.location.reload()}
|
||||||
|
showHome
|
||||||
|
homePath="/community"
|
||||||
|
|
||||||
|
// 网络状态检查
|
||||||
|
checkOffline
|
||||||
|
|
||||||
|
// 错误上报
|
||||||
|
onErrorReport={(info) => {
|
||||||
|
analytics.track('error_page_view', info);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
## API 参考
|
||||||
|
|
||||||
|
### 基础配置
|
||||||
|
|
||||||
|
| 属性 | 类型 | 默认值 | 说明 |
|
||||||
|
|------|------|--------|------|
|
||||||
|
| `title` | `string` | `'加载失败'` | 错误标题 |
|
||||||
|
| `subtitle` | `string` | - | 错误副标题(如显示错误 ID) |
|
||||||
|
| `description` | `string` | `'我们无法找到您请求的内容,这可能是因为:'` | 错误描述信息 |
|
||||||
|
| `detail` | `string` | - | 详细信息值(与 subtitle 二选一) |
|
||||||
|
| `detailLabel` | `string` | `'ID'` | 详细信息标签 |
|
||||||
|
|
||||||
|
### 错误原因配置
|
||||||
|
|
||||||
|
| 属性 | 类型 | 默认值 | 说明 |
|
||||||
|
|------|------|--------|------|
|
||||||
|
| `reasons` | `ErrorReasonItem[]` | 默认 3 条 | 错误原因列表 |
|
||||||
|
|
||||||
|
**ErrorReasonItem 结构:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface ErrorReasonItem {
|
||||||
|
icon: string | React.ReactNode; // 图标(emoji 或组件)
|
||||||
|
title: string; // 原因标题
|
||||||
|
description: string; // 原因描述
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**默认错误原因:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
[
|
||||||
|
{ icon: '🔍', title: 'ID 可能输入错误', description: '请检查 URL 中的 ID 是否正确' },
|
||||||
|
{ icon: '🗑️', title: '内容可能已被删除', description: '该内容可能因过期或调整而被下架' },
|
||||||
|
{ icon: '🔄', title: '系统暂时无法访问', description: '请稍后重试或联系技术支持' },
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 技术详情配置
|
||||||
|
|
||||||
|
| 属性 | 类型 | 默认值 | 说明 |
|
||||||
|
|------|------|--------|------|
|
||||||
|
| `techDetails` | `TechDetails` | - | 技术详情(可展开查看) |
|
||||||
|
|
||||||
|
**TechDetails 结构:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface TechDetails {
|
||||||
|
requestUrl?: string; // 请求 URL
|
||||||
|
errorType?: string; // 错误类型
|
||||||
|
errorMessage?: string; // 错误信息
|
||||||
|
timestamp?: string; // 时间戳
|
||||||
|
relatedId?: string; // 相关 ID
|
||||||
|
customFields?: Record<string, string>; // 自定义字段
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 操作按钮配置
|
||||||
|
|
||||||
|
#### 快捷配置
|
||||||
|
|
||||||
|
| 属性 | 类型 | 默认值 | 说明 |
|
||||||
|
|------|------|--------|------|
|
||||||
|
| `showBack` | `boolean` | `false` | 是否显示返回按钮 |
|
||||||
|
| `onBack` | `() => void` | `history.back()` | 返回回调 |
|
||||||
|
| `showRetry` | `boolean` | `false` | 是否显示重试按钮 |
|
||||||
|
| `onRetry` | `() => void` | - | 重试回调 |
|
||||||
|
| `showHome` | `boolean` | `false` | 是否显示返回首页按钮 |
|
||||||
|
| `homePath` | `string` | `'/'` | 首页路径 |
|
||||||
|
|
||||||
|
#### 自定义按钮
|
||||||
|
|
||||||
|
| 属性 | 类型 | 默认值 | 说明 |
|
||||||
|
|------|------|--------|------|
|
||||||
|
| `actions` | `ActionButton[]` | - | 自定义操作按钮列表(覆盖快捷配置) |
|
||||||
|
|
||||||
|
**ActionButton 结构:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface ActionButton {
|
||||||
|
label: string; // 按钮文本
|
||||||
|
icon?: string; // 按钮图标(可选)
|
||||||
|
variant?: 'primary' | 'secondary' | 'outline'; // 按钮类型
|
||||||
|
onClick?: () => void; // 点击回调
|
||||||
|
href?: string; // 跳转链接(与 onClick 二选一)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**示例:**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<ErrorPage
|
||||||
|
actions={[
|
||||||
|
{ label: '刷新页面', variant: 'primary', onClick: () => location.reload() },
|
||||||
|
{ label: '返回列表', variant: 'outline', href: '/events' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 布局配置
|
||||||
|
|
||||||
|
| 属性 | 类型 | 默认值 | 说明 |
|
||||||
|
|------|------|--------|------|
|
||||||
|
| `fullScreen` | `boolean` | `true` | 是否全屏显示 |
|
||||||
|
| `maxWidth` | `string` | `'500px'` | 最大宽度 |
|
||||||
|
|
||||||
|
### 功能增强
|
||||||
|
|
||||||
|
| 属性 | 类型 | 默认值 | 说明 |
|
||||||
|
|------|------|--------|------|
|
||||||
|
| `checkOffline` | `boolean` | `true` | 是否检查网络状态并显示离线提示 |
|
||||||
|
| `enableBuiltInReport` | `boolean` | `true` | 是否启用内置 PostHog 错误上报 |
|
||||||
|
| `onErrorReport` | `(errorInfo: Record<string, unknown>) => void` | - | 自定义错误上报回调(与内置上报同时生效) |
|
||||||
|
|
||||||
|
**内置错误上报**:
|
||||||
|
|
||||||
|
组件默认会自动上报错误到 PostHog,上报事件名为 `error_page_view`,包含以下数据:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
error_title: string; // 错误标题
|
||||||
|
error_detail: string; // 详细信息(如事件ID)
|
||||||
|
error_type: string; // 错误类型(如 "404 - Event Not Found")
|
||||||
|
error_message: string; // 错误信息
|
||||||
|
page_url: string; // 当前页面 URL
|
||||||
|
referrer: string; // 来源页面
|
||||||
|
user_agent: string; // 用户代理
|
||||||
|
event_id: string; // 相关 ID
|
||||||
|
timestamp: string; // 时间戳(自动添加)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**禁用内置上报:**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<ErrorPage enableBuiltInReport={false} />
|
||||||
|
```
|
||||||
|
|
||||||
|
**自定义上报回调示例:**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<ErrorPage
|
||||||
|
onErrorReport={(errorInfo) => {
|
||||||
|
// 自定义上报逻辑(与内置 PostHog 上报同时生效)
|
||||||
|
customAnalytics.track('custom_error_event', errorInfo);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 场景示例
|
||||||
|
|
||||||
|
### 1. 事件详情 404 页面
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<ErrorPage
|
||||||
|
title="事件走丢了"
|
||||||
|
subtitle={`ID: ${eventId}`}
|
||||||
|
description="抱歉,我们找不到您请求的事件,这可能是因为:"
|
||||||
|
reasons={[
|
||||||
|
{ icon: '🔍', title: '事件ID输入错误', description: '请检查URL中的事件ID是否正确' },
|
||||||
|
{ icon: '🗑️', title: '该事件已被删除或下架', description: '该事件可能因过期或内容调整而被移除' },
|
||||||
|
{ icon: '🔄', title: '系统暂时无法访问该事件', description: '请稍后重试或联系技术支持' },
|
||||||
|
]}
|
||||||
|
techDetails={{
|
||||||
|
requestUrl: window.location.href,
|
||||||
|
errorType: '404 - Event Not Found',
|
||||||
|
errorMessage: error.message,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
relatedId: eventId,
|
||||||
|
}}
|
||||||
|
showRetry
|
||||||
|
onRetry={() => window.location.reload()}
|
||||||
|
showBack
|
||||||
|
showHome
|
||||||
|
homePath="/community"
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 网络错误页面
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<ErrorPage
|
||||||
|
title="网络连接失败"
|
||||||
|
description="无法连接到服务器,请检查网络后重试"
|
||||||
|
reasons={[
|
||||||
|
{ icon: '📶', title: '网络连接中断', description: '请检查您的网络连接是否正常' },
|
||||||
|
{ icon: '🔧', title: '服务器维护中', description: '服务器可能正在进行维护,请稍后重试' },
|
||||||
|
]}
|
||||||
|
showRetry
|
||||||
|
onRetry={() => window.location.reload()}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 简洁模式(无原因列表)
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<ErrorPage
|
||||||
|
title="加载失败"
|
||||||
|
description="数据加载失败,请重试"
|
||||||
|
reasons={[]} // 不显示原因列表
|
||||||
|
fullScreen={false}
|
||||||
|
maxWidth="400px"
|
||||||
|
showRetry
|
||||||
|
onRetry={refetch}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 类型导出
|
||||||
|
|
||||||
|
组件导出以下 TypeScript 类型,方便外部使用:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import ErrorPage, {
|
||||||
|
ErrorPageProps,
|
||||||
|
ErrorReasonItem,
|
||||||
|
ActionButton,
|
||||||
|
TechDetails,
|
||||||
|
} from '@/components/ErrorPage';
|
||||||
|
```
|
||||||
|
|
||||||
|
## 设计说明
|
||||||
|
|
||||||
|
- **配色**:黑色背景 (`#1A202C`) + 金色边框/按钮 (`#D4A574`)
|
||||||
|
- **图标**:金色圆形背景 (50px) + 黑色感叹号
|
||||||
|
- **布局**:居中卡片式布局,最大宽度 500px
|
||||||
|
- **底部提示**:"点击右下角联系客服"(纯文本,无链接)
|
||||||
@@ -1,59 +1,270 @@
|
|||||||
/**
|
/**
|
||||||
* ErrorPage - 通用错误页面组件
|
* ErrorPage - 通用错误页面组件
|
||||||
* 用于显示加载失败、网络错误等异常状态
|
* 用于显示加载失败、网络错误、404等异常状态
|
||||||
* 设计风格:黑色背景 + 金色边框
|
* 设计风格:黑色背景 + 金色边框
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Center,
|
|
||||||
Circle,
|
|
||||||
Text,
|
Text,
|
||||||
Button,
|
Button,
|
||||||
VStack,
|
VStack,
|
||||||
HStack,
|
HStack,
|
||||||
Icon,
|
Collapse,
|
||||||
|
useDisclosure,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { WarningIcon } from '@chakra-ui/icons';
|
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 GOLD_COLOR = '#D4A574';
|
||||||
const BG_COLOR = '#1A202C'; // 与页面背景一致
|
const BG_COLOR = '#1A202C';
|
||||||
|
|
||||||
interface ErrorPageProps {
|
// 错误原因项配置
|
||||||
/** 错误标题,默认"加载失败" */
|
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;
|
title?: string;
|
||||||
|
/** 错误副标题(如显示错误 ID) */
|
||||||
|
subtitle?: string;
|
||||||
/** 错误描述信息 */
|
/** 错误描述信息 */
|
||||||
description?: string;
|
description?: string;
|
||||||
/** 详细信息(如事件ID、订单号等) */
|
|
||||||
|
// ===== 详细信息 =====
|
||||||
|
/** 详细信息值 */
|
||||||
detail?: string;
|
detail?: string;
|
||||||
/** 详细信息标签,默认"ID" */
|
/** 详细信息标签 */
|
||||||
detailLabel?: string;
|
detailLabel?: string;
|
||||||
/** 是否显示重试按钮 */
|
|
||||||
|
// ===== 错误原因列表 =====
|
||||||
|
/** 错误原因列表 */
|
||||||
|
reasons?: ErrorReasonItem[];
|
||||||
|
|
||||||
|
// ===== 技术详情 =====
|
||||||
|
/** 技术详情(可展开查看) */
|
||||||
|
techDetails?: TechDetails;
|
||||||
|
|
||||||
|
// ===== 操作按钮 =====
|
||||||
|
/** 自定义操作按钮列表 */
|
||||||
|
actions?: ActionButton[];
|
||||||
|
/** 快捷配置:是否显示重试按钮 */
|
||||||
showRetry?: boolean;
|
showRetry?: boolean;
|
||||||
/** 重试回调函数 */
|
/** 重试回调 */
|
||||||
onRetry?: () => void;
|
onRetry?: () => void;
|
||||||
/** 是否显示返回按钮 */
|
/** 快捷配置:是否显示返回按钮 */
|
||||||
showBack?: boolean;
|
showBack?: boolean;
|
||||||
/** 返回回调函数 */
|
/** 返回回调 */
|
||||||
onBack?: () => void;
|
onBack?: () => void;
|
||||||
/** 是否全屏显示,默认 true */
|
/** 快捷配置:是否显示返回首页按钮 */
|
||||||
|
showHome?: boolean;
|
||||||
|
/** 首页路径 */
|
||||||
|
homePath?: string;
|
||||||
|
|
||||||
|
// ===== 布局配置 =====
|
||||||
|
/** 是否全屏显示 */
|
||||||
fullScreen?: boolean;
|
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> = ({
|
const ErrorPage: React.FC<ErrorPageProps> = ({
|
||||||
title = '加载失败',
|
title = '加载失败',
|
||||||
description,
|
subtitle,
|
||||||
|
description = '我们无法找到您请求的内容,这可能是因为:',
|
||||||
detail,
|
detail,
|
||||||
detailLabel = 'ID',
|
detailLabel = 'ID',
|
||||||
|
reasons = DEFAULT_REASONS,
|
||||||
|
techDetails,
|
||||||
|
actions,
|
||||||
showRetry = false,
|
showRetry = false,
|
||||||
onRetry,
|
onRetry,
|
||||||
showBack = false,
|
showBack = false,
|
||||||
onBack,
|
onBack,
|
||||||
|
showHome = false,
|
||||||
|
homePath = '/',
|
||||||
fullScreen = true,
|
fullScreen = true,
|
||||||
|
maxWidth = '500px',
|
||||||
|
checkOffline = true,
|
||||||
|
enableBuiltInReport = true,
|
||||||
|
onErrorReport,
|
||||||
}) => {
|
}) => {
|
||||||
const hasButtons = (showRetry && onRetry) || (showBack && onBack);
|
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 (
|
return (
|
||||||
<Box
|
<Box
|
||||||
@@ -63,74 +274,165 @@ const ErrorPage: React.FC<ErrorPageProps> = ({
|
|||||||
alignItems="center"
|
alignItems="center"
|
||||||
justifyContent="center"
|
justifyContent="center"
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
bg={BG_COLOR}
|
bg={BG_COLOR}
|
||||||
border="1px solid"
|
border="1px solid"
|
||||||
borderColor={GOLD_COLOR}
|
borderColor={GOLD_COLOR}
|
||||||
borderRadius="lg"
|
borderRadius="lg"
|
||||||
p={8}
|
p={8}
|
||||||
maxW="400px"
|
maxW={maxWidth}
|
||||||
w="90%"
|
w="90%"
|
||||||
textAlign="center"
|
textAlign="center"
|
||||||
>
|
>
|
||||||
{/* 金色圆形图标 + 黑色感叹号 */}
|
{/* 金色圆形感叹号图标 */}
|
||||||
<Circle size="50px" bg={GOLD_COLOR} mx="auto" mb={4}>
|
<Box mx="auto" mb={4}>
|
||||||
<Icon as={WarningIcon} color={BG_COLOR} boxSize={5} />
|
<ExclamationCircleOutlined style={{ fontSize: '40px', color: GOLD_COLOR }} />
|
||||||
</Circle>
|
</Box>
|
||||||
|
|
||||||
{/* 金色标题 */}
|
{/* 金色标题 */}
|
||||||
<Text color={GOLD_COLOR} fontSize="lg" fontWeight="medium" mb={2}>
|
<Text color={GOLD_COLOR} fontSize="lg" fontWeight="medium" mb={2}>
|
||||||
{title}
|
{title}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{/* 副标题(ID 显示) */}
|
||||||
|
{(subtitle || detail) && (
|
||||||
|
<Text
|
||||||
|
color="gray.400"
|
||||||
|
fontSize="sm"
|
||||||
|
fontFamily="monospace"
|
||||||
|
mb={2}
|
||||||
|
>
|
||||||
|
{subtitle || `${detailLabel}: ${detail}`}
|
||||||
</Text>
|
</Text>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 描述信息 */}
|
{/* 离线提示 */}
|
||||||
{description && (
|
{checkOffline && isOffline && (
|
||||||
<Text color="gray.400" fontSize="sm" mb={2}>
|
<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}
|
{description}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 详情 */}
|
<VStack spacing={3} align="stretch">
|
||||||
{detail && (
|
{reasons.map((reason, index) => (
|
||||||
<Text color="gray.500" fontSize="sm" mb={4}>
|
<HStack key={index} spacing={3} align="flex-start">
|
||||||
{detailLabel}: {detail}
|
<Text fontSize="lg" flexShrink={0}>
|
||||||
</Text>
|
{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>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 按钮组 */}
|
{/* 技术详情(可展开) */}
|
||||||
{hasButtons && (
|
{techDetails && (
|
||||||
<HStack justify="center" spacing={3} mt={4}>
|
<Box mb={4}>
|
||||||
{showBack && onBack && (
|
<Button
|
||||||
<Button
|
variant="ghost"
|
||||||
variant="outline"
|
size="sm"
|
||||||
borderColor={GOLD_COLOR}
|
color="gray.500"
|
||||||
color={GOLD_COLOR}
|
rightIcon={isTechOpen ? <ChevronUpIcon /> : <ChevronDownIcon />}
|
||||||
size="sm"
|
onClick={onTechToggle}
|
||||||
px={6}
|
_hover={{ bg: 'transparent', color: 'gray.400' }}
|
||||||
_hover={{ bg: GOLD_COLOR, color: 'black' }}
|
>
|
||||||
onClick={onBack}
|
查看技术信息
|
||||||
>
|
</Button>
|
||||||
返回
|
<Collapse in={isTechOpen}>
|
||||||
</Button>
|
<Box
|
||||||
)}
|
mt={2}
|
||||||
{showRetry && onRetry && (
|
p={3}
|
||||||
<Button
|
bg="gray.800"
|
||||||
bg={GOLD_COLOR}
|
borderRadius="md"
|
||||||
color={BG_COLOR}
|
fontFamily="monospace"
|
||||||
borderColor={GOLD_COLOR}
|
fontSize="xs"
|
||||||
border="1px solid"
|
color="gray.500"
|
||||||
size="sm"
|
textAlign="left"
|
||||||
px={6}
|
overflowX="auto"
|
||||||
fontWeight="medium"
|
>
|
||||||
_hover={{ bg: '#C49A6C' }}
|
{techDetails.requestUrl && (
|
||||||
onClick={onRetry}
|
<Text>请求URL: {techDetails.requestUrl}</Text>
|
||||||
>
|
)}
|
||||||
重试
|
{techDetails.errorType && (
|
||||||
</Button>
|
<Text>错误类型: {techDetails.errorType}</Text>
|
||||||
)}
|
)}
|
||||||
</HStack>
|
{techDetails.errorMessage && (
|
||||||
)}
|
<Text>错误信息: {techDetails.errorMessage}</Text>
|
||||||
</Box>
|
)}
|
||||||
|
{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>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -66,15 +66,43 @@ const EventDetail = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 错误状态
|
// 错误状态
|
||||||
if (!error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<ErrorPage
|
<ErrorPage
|
||||||
title="页面找不到了"
|
title="事件走丢了"
|
||||||
description={error}
|
subtitle={encodedId ? `ID: ${encodedId}` : undefined}
|
||||||
|
description="抱歉,我们找不到您请求的事件,这可能是因为:"
|
||||||
detail={eventId}
|
detail={eventId}
|
||||||
detailLabel="事件ID"
|
detailLabel="事件ID"
|
||||||
|
reasons={[
|
||||||
|
{
|
||||||
|
icon: '🔍',
|
||||||
|
title: '事件ID输入错误',
|
||||||
|
description: '请检查URL中的事件ID是否正确',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: '🗑️',
|
||||||
|
title: '该事件已被删除或下架',
|
||||||
|
description: '该事件可能因过期或内容调整而被移除',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: '🔄',
|
||||||
|
title: '系统暂时无法访问该事件',
|
||||||
|
description: '请稍后重试或联系技术支持',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
techDetails={{
|
||||||
|
requestUrl: window.location.href,
|
||||||
|
errorType: '404 - Event Not Found',
|
||||||
|
errorMessage: error,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
relatedId: eventId,
|
||||||
|
}}
|
||||||
showRetry
|
showRetry
|
||||||
onRetry={() => window.location.reload()}
|
onRetry={() => window.location.reload()}
|
||||||
|
showBack
|
||||||
|
showHome
|
||||||
|
homePath="/community"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user