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 && ( + + + + + {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" /> ); }