From 302acbafe35ca8aa8d4103c2f15eb1a4ea796260 Mon Sep 17 00:00:00 2001
From: zdl <3489966805@qq.com>
Date: Fri, 5 Dec 2025 14:34:03 +0800
Subject: [PATCH] =?UTF-8?q?pref:=20ErrorPage=20=E5=8A=9F=E8=83=BD=E5=A2=9E?=
=?UTF-8?q?=E5=BC=BA=20ErrorPage=20=E6=96=B0=E5=A2=9E=E5=8A=9F=E8=83=BD:?=
=?UTF-8?q?=20=20-=20=E6=B5=AE=E5=8A=A8=E5=8A=A8=E7=94=BB=E6=95=88?=
=?UTF-8?q?=E6=9E=9C=20(keyframes)=20=20-=20=E5=8F=AF=E9=85=8D=E7=BD=AE?=
=?UTF-8?q?=E9=94=99=E8=AF=AF=E5=8E=9F=E5=9B=A0=E5=88=97=E8=A1=A8=20(reaso?=
=?UTF-8?q?ns=20prop)=20=20-=20=E6=8A=80=E6=9C=AF=E8=AF=A6=E6=83=85?=
=?UTF-8?q?=E6=8A=98=E5=8F=A0=E9=9D=A2=E6=9D=BF=20(techDetails=20prop)=20?=
=?UTF-8?q?=20-=20=E5=8F=AF=E9=80=89=E6=90=9C=E7=B4=A2=E5=8A=9F=E8=83=BD?=
=?UTF-8?q?=20(search=20prop)=20=20-=20=E6=9B=B4=E4=B8=B0=E5=AF=8C?=
=?UTF-8?q?=E7=9A=84=E5=AF=BC=E8=88=AA=E9=80=89=E9=A1=B9?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/components/ErrorPage/README.md | 335 +++++++++++++++++++++
src/components/ErrorPage/index.tsx | 460 ++++++++++++++++++++++++-----
src/views/EventDetail/index.js | 34 ++-
3 files changed, 747 insertions(+), 82 deletions(-)
create mode 100644 src/components/ErrorPage/README.md
diff --git a/src/components/ErrorPage/README.md b/src/components/ErrorPage/README.md
new file mode 100644
index 00000000..197ab2a2
--- /dev/null
+++ b/src/components/ErrorPage/README.md
@@ -0,0 +1,335 @@
+# ErrorPage 通用错误页面组件
+
+通用错误页面组件,用于显示加载失败、网络错误、404 等异常状态。
+
+**设计风格**:黑色背景 (`#1A202C`) + 金色边框 (`#D4A574`)
+
+## 效果预览
+
+```
+┌─────────────────────────────────────┐
+│ ╭──────────╮ │
+│ │ ⚠️ │ (金色圆形) │
+│ ╰──────────╯ │
+│ │
+│ 事件走丢了 │
+│ ID: ZXY-101 │
+│ │
+│ ┌──────────────────────────────┐ │
+│ │ 抱歉,我们找不到您请求的事件... │ │
+│ │ │ │
+│ │ 🔍 事件ID输入错误 │ │
+│ │ 请检查URL中的事件ID是否正确 │ │
+│ │ │ │
+│ │ 🗑️ 该事件已被删除或下架 │ │
+│ │ 该事件可能因过期而被移除 │ │
+│ │ │ │
+│ │ 🔄 系统暂时无法访问该事件 │ │
+│ │ 请稍后重试或联系技术支持 │ │
+│ │ │ │
+│ │ [查看技术信息 ▼] │ │
+│ └──────────────────────────────┘ │
+│ │
+│ [返回] [重试] │
+│ │
+│ 点击右下角联系客服 │
+└─────────────────────────────────────┘
+```
+
+## 快速开始
+
+### 基础用法
+
+```tsx
+import ErrorPage from '@/components/ErrorPage';
+
+// 最简单的用法 - 使用所有默认配置
+
+
+// 自定义标题和描述
+
+```
+
+### 完整配置示例
+
+```tsx
+ 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; // 自定义字段
+}
+```
+
+### 操作按钮配置
+
+#### 快捷配置
+
+| 属性 | 类型 | 默认值 | 说明 |
+|------|------|--------|------|
+| `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
+ location.reload() },
+ { label: '返回列表', variant: 'outline', href: '/events' },
+ ]}
+/>
+```
+
+### 布局配置
+
+| 属性 | 类型 | 默认值 | 说明 |
+|------|------|--------|------|
+| `fullScreen` | `boolean` | `true` | 是否全屏显示 |
+| `maxWidth` | `string` | `'500px'` | 最大宽度 |
+
+### 功能增强
+
+| 属性 | 类型 | 默认值 | 说明 |
+|------|------|--------|------|
+| `checkOffline` | `boolean` | `true` | 是否检查网络状态并显示离线提示 |
+| `enableBuiltInReport` | `boolean` | `true` | 是否启用内置 PostHog 错误上报 |
+| `onErrorReport` | `(errorInfo: Record) => 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
+
+```
+
+**自定义上报回调示例:**
+
+```tsx
+ {
+ // 自定义上报逻辑(与内置 PostHog 上报同时生效)
+ customAnalytics.track('custom_error_event', errorInfo);
+ }}
+/>
+```
+
+## 场景示例
+
+### 1. 事件详情 404 页面
+
+```tsx
+ window.location.reload()}
+ showBack
+ showHome
+ homePath="/community"
+/>
+```
+
+### 2. 网络错误页面
+
+```tsx
+ window.location.reload()}
+/>
+```
+
+### 3. 简洁模式(无原因列表)
+
+```tsx
+
+```
+
+## 类型导出
+
+组件导出以下 TypeScript 类型,方便外部使用:
+
+```typescript
+import ErrorPage, {
+ ErrorPageProps,
+ ErrorReasonItem,
+ ActionButton,
+ TechDetails,
+} from '@/components/ErrorPage';
+```
+
+## 设计说明
+
+- **配色**:黑色背景 (`#1A202C`) + 金色边框/按钮 (`#D4A574`)
+- **图标**:金色圆形背景 (50px) + 黑色感叹号
+- **布局**:居中卡片式布局,最大宽度 500px
+- **底部提示**:"点击右下角联系客服"(纯文本,无链接)
diff --git a/src/components/ErrorPage/index.tsx b/src/components/ErrorPage/index.tsx
index 14edb446..568b4c81 100644
--- a/src/components/ErrorPage/index.tsx
+++ b/src/components/ErrorPage/index.tsx
@@ -1,59 +1,270 @@
/**
* ErrorPage - 通用错误页面组件
- * 用于显示加载失败、网络错误等异常状态
+ * 用于显示加载失败、网络错误、404等异常状态
* 设计风格:黑色背景 + 金色边框
*/
import React from 'react';
import {
Box,
- Center,
- Circle,
Text,
Button,
VStack,
HStack,
- Icon,
+ Collapse,
+ useDisclosure,
} 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 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;
+}
+
+// 完整的 ErrorPage 配置
+export interface ErrorPageProps {
+ // ===== 基础配置 =====
+ /** 错误标题 */
title?: string;
+ /** 错误副标题(如显示错误 ID) */
+ subtitle?: string;
/** 错误描述信息 */
description?: string;
- /** 详细信息(如事件ID、订单号等) */
+
+ // ===== 详细信息 =====
+ /** 详细信息值 */
detail?: string;
- /** 详细信息标签,默认"ID" */
+ /** 详细信息标签 */
detailLabel?: string;
- /** 是否显示重试按钮 */
+
+ // ===== 错误原因列表 =====
+ /** 错误原因列表 */
+ reasons?: ErrorReasonItem[];
+
+ // ===== 技术详情 =====
+ /** 技术详情(可展开查看) */
+ techDetails?: TechDetails;
+
+ // ===== 操作按钮 =====
+ /** 自定义操作按钮列表 */
+ actions?: ActionButton[];
+ /** 快捷配置:是否显示重试按钮 */
showRetry?: boolean;
- /** 重试回调函数 */
+ /** 重试回调 */
onRetry?: () => void;
- /** 是否显示返回按钮 */
+ /** 快捷配置:是否显示返回按钮 */
showBack?: boolean;
- /** 返回回调函数 */
+ /** 返回回调 */
onBack?: () => void;
- /** 是否全屏显示,默认 true */
+ /** 快捷配置:是否显示返回首页按钮 */
+ showHome?: boolean;
+ /** 首页路径 */
+ homePath?: string;
+
+ // ===== 布局配置 =====
+ /** 是否全屏显示 */
fullScreen?: boolean;
+ /** 最大宽度 */
+ maxWidth?: string;
+
+ // ===== 网络状态 =====
+ /** 是否检查网络状态并显示离线提示 */
+ checkOffline?: boolean;
+
+ // ===== 错误上报 =====
+ /** 是否启用内置 PostHog 错误上报(默认 true) */
+ enableBuiltInReport?: boolean;
+ /** 自定义错误上报回调(可选,与内置上报同时生效) */
+ onErrorReport?: (errorInfo: Record) => void;
}
+// 默认错误原因
+const DEFAULT_REASONS: ErrorReasonItem[] = [
+ {
+ icon: '🔍',
+ title: 'ID 可能输入错误',
+ description: '请检查 URL 中的 ID 是否正确',
+ },
+ {
+ icon: '🗑️',
+ title: '内容可能已被删除',
+ description: '该内容可能因过期或调整而被下架',
+ },
+ {
+ icon: '🔄',
+ title: '系统暂时无法访问',
+ description: '请稍后重试或联系技术支持',
+ },
+];
+
const ErrorPage: React.FC = ({
title = '加载失败',
- description,
+ 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 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 (
= ({
alignItems="center"
justifyContent="center"
>
-
- {/* 金色圆形图标 + 黑色感叹号 */}
-
-
-
+
+ {/* 金色圆形感叹号图标 */}
+
+
+
- {/* 金色标题 */}
-
- {title}
+ {/* 金色标题 */}
+
+ {title}
+
+
+ {/* 副标题(ID 显示) */}
+ {(subtitle || detail) && (
+
+ {subtitle || `${detailLabel}: ${detail}`}
+ )}
- {/* 描述信息 */}
- {description && (
-
+ {/* 离线提示 */}
+ {checkOffline && isOffline && (
+
+
+ 当前处于离线状态,请检查网络连接
+
+
+ )}
+
+ {/* 错误原因列表 */}
+ {reasons.length > 0 && (
+
+
{description}
- )}
- {/* 详情 */}
- {detail && (
-
- {detailLabel}: {detail}
-
- )}
+
+ {reasons.map((reason, index) => (
+
+
+ {typeof reason.icon === 'string' ? reason.icon : reason.icon}
+
+
+
+ {reason.title}
+
+
+ {reason.description}
+
+
+
+ ))}
+
+
+ )}
- {/* 按钮组 */}
- {hasButtons && (
-
- {showBack && onBack && (
-
- )}
- {showRetry && onRetry && (
-
- )}
-
- )}
-
+ {/* 技术详情(可展开) */}
+ {techDetails && (
+
+ : }
+ onClick={onTechToggle}
+ _hover={{ bg: 'transparent', color: 'gray.400' }}
+ >
+ 查看技术信息
+
+
+
+ {techDetails.requestUrl && (
+ 请求URL: {techDetails.requestUrl}
+ )}
+ {techDetails.errorType && (
+ 错误类型: {techDetails.errorType}
+ )}
+ {techDetails.errorMessage && (
+ 错误信息: {techDetails.errorMessage}
+ )}
+ {techDetails.timestamp && (
+ 时间戳: {techDetails.timestamp}
+ )}
+ {techDetails.relatedId && (
+ 相关ID: {techDetails.relatedId}
+ )}
+ {techDetails.customFields &&
+ Object.entries(techDetails.customFields).map(([key, value]) => (
+
+ {key}: {value}
+
+ ))}
+
+
+
+ )}
+
+ {/* 按钮组 */}
+ {hasButtons && (
+
+ {actionButtons.map((btn, index) => (
+
+ ))}
+
+ )}
+
+ {/* 底部帮助提示 */}
+
+ 点击右下角
+
+ 联系客服
+
+
+
);
};
diff --git a/src/views/EventDetail/index.js b/src/views/EventDetail/index.js
index c6ac3ab5..0fc6ce8a 100644
--- a/src/views/EventDetail/index.js
+++ b/src/views/EventDetail/index.js
@@ -66,15 +66,43 @@ const EventDetail = () => {
}
// 错误状态
- if (!error) {
+ if (error) {
return (
window.location.reload()}
+ showBack
+ showHome
+ homePath="/community"
/>
);
}