From 4607bb1d4a1a31bcfe21e2532b0694725902c69b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9F=B3=E6=B8=85=E7=88=BD?= Date: Thu, 10 Apr 2025 17:49:42 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B6=85=E7=AE=A1=E5=90=8E=E5=8F=B0=20-=20?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E9=94=99=E8=AF=AF=E6=8F=90=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SuperAdmin/app/layout.tsx | 5 +- SuperAdmin/app/login/page.tsx | 48 ++++++-- SuperAdmin/components/ErrorBoundary.tsx | 100 ++++++++++++++++ SuperAdmin/lib/api-utils.ts | 70 ++++++----- SuperAdmin/lib/error-handler.ts | 153 ++++++++++++++++++++++++ 5 files changed, 326 insertions(+), 50 deletions(-) create mode 100644 SuperAdmin/components/ErrorBoundary.tsx create mode 100644 SuperAdmin/lib/error-handler.ts diff --git a/SuperAdmin/app/layout.tsx b/SuperAdmin/app/layout.tsx index cf84e55d..298e2c51 100644 --- a/SuperAdmin/app/layout.tsx +++ b/SuperAdmin/app/layout.tsx @@ -4,6 +4,7 @@ import { Inter } from "next/font/google" import "./globals.css" import { ThemeProvider } from "@/components/theme-provider" import { ToastProvider } from "@/components/ui/use-toast" +import ErrorBoundary from "@/components/ErrorBoundary" const inter = Inter({ subsets: ["latin"] }) @@ -23,7 +24,9 @@ export default function RootLayout({ - {children} + + {children} + diff --git a/SuperAdmin/app/login/page.tsx b/SuperAdmin/app/login/page.tsx index 5188527b..47dc57fe 100644 --- a/SuperAdmin/app/login/page.tsx +++ b/SuperAdmin/app/login/page.tsx @@ -11,11 +11,22 @@ import { Label } from "@/components/ui/label" import { md5, saveAdminInfo } from "@/lib/utils" import { login } from "@/lib/admin-api" import { useToast } from "@/components/ui/use-toast" +import { + AlertDialog, + AlertDialogAction, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog" export default function LoginPage() { const [account, setAccount] = useState("") const [password, setPassword] = useState("") const [isLoading, setIsLoading] = useState(false) + const [errorDialogOpen, setErrorDialogOpen] = useState(false) + const [errorMessage, setErrorMessage] = useState("") const router = useRouter() const { toast } = useToast() @@ -44,22 +55,16 @@ export default function LoginPage() { // 跳转到仪表盘 router.push("/dashboard") } else { - // 显示错误提示 - toast({ - title: "登录失败", - description: result.msg || "账号或密码错误", - variant: "destructive", - }) + // 显示错误弹窗 + setErrorMessage(result.msg || "账号或密码错误") + setErrorDialogOpen(true) } - } catch (err) { + } catch (err: any) { console.error("登录失败:", err) - // 显示错误提示 - toast({ - title: "登录失败", - description: "网络错误,请稍后再试", - variant: "destructive", - }) + // 显示错误弹窗 + setErrorMessage(err.msg || "网络错误,请稍后再试") + setErrorDialogOpen(true) } finally { setIsLoading(false) } @@ -103,6 +108,23 @@ export default function LoginPage() { + + {/* 错误提示弹窗 */} + + + + 登录失败 + + {errorMessage} + + + + setErrorDialogOpen(false)}> + 确定 + + + + ) } diff --git a/SuperAdmin/components/ErrorBoundary.tsx b/SuperAdmin/components/ErrorBoundary.tsx new file mode 100644 index 00000000..d2f86cf4 --- /dev/null +++ b/SuperAdmin/components/ErrorBoundary.tsx @@ -0,0 +1,100 @@ +"use client" + +import React, { type ErrorInfo } from "react" +import { Card, CardContent, CardFooter } from "@/components/ui/card" +import { Button } from "@/components/ui/button" +import { useRouter } from "next/navigation" +import { useToast } from "@/components/ui/use-toast" + +interface ErrorBoundaryProps { + children: React.ReactNode +} + +interface ErrorBoundaryState { + hasError: boolean + error?: Error +} + +class ErrorBoundary extends React.Component { + constructor(props: ErrorBoundaryProps) { + super(props) + this.state = { hasError: false } + } + + static getDerivedStateFromError(error: Error): ErrorBoundaryState { + // 更新state使下一次渲染可以显示错误界面 + return { hasError: true, error } + } + + componentDidCatch(error: Error, errorInfo: ErrorInfo) { + // 记录错误信息 + console.error("错误边界捕获到错误:", error, errorInfo) + } + + render() { + if (this.state.hasError) { + return this.setState({ hasError: false })} /> + } + + return this.props.children + } +} + +// 错误显示界面 +function ErrorScreen({ error, onReset }: { error?: Error; onReset: () => void }) { + const router = useRouter() + const { toast } = useToast() + + // 导航到主页 + const goHome = () => { + router.push("/dashboard") + onReset() + } + + // 刷新当前页面 + const refreshPage = () => { + if (typeof window !== "undefined") { + toast({ + title: "正在刷新页面", + description: "正在重新加载页面内容...", + variant: "default", + }) + setTimeout(() => { + window.location.reload() + }, 500) + } + } + + return ( +
+ + +
+
+

页面出错了

+

+ 很抱歉,页面加载过程中遇到了问题。 +

+ {error && ( +
+ {error.message} +
+ )} +
+

+ 您可以尝试刷新页面或返回首页。如果问题持续存在,请联系系统管理员。 +

+
+
+ + + + +
+
+ ) +} + +export default ErrorBoundary \ No newline at end of file diff --git a/SuperAdmin/lib/api-utils.ts b/SuperAdmin/lib/api-utils.ts index 8ddd25d9..087707d0 100644 --- a/SuperAdmin/lib/api-utils.ts +++ b/SuperAdmin/lib/api-utils.ts @@ -1,8 +1,7 @@ import { getConfig } from './config'; -import { getAdminInfo, clearAdminInfo } from './utils'; /** - * API响应数据结构 + * API响应接口 */ export interface ApiResponse { code: number; @@ -11,7 +10,7 @@ export interface ApiResponse { } /** - * 通用API请求函数 + * API请求函数 * @param endpoint API端点 * @param method HTTP方法 * @param data 请求数据 @@ -22,62 +21,61 @@ export async function apiRequest( method: 'GET' | 'POST' | 'PUT' | 'DELETE' = 'GET', data?: any ): Promise> { + // 获取API基础URL const { apiBaseUrl } = getConfig(); const url = `${apiBaseUrl}${endpoint}`; - // 获取认证信息 - const adminInfo = getAdminInfo(); - - // 请求头 + // 构建请求头 const headers: HeadersInit = { 'Content-Type': 'application/json', }; - // 如果有认证信息,添加Cookie头 - if (adminInfo?.token) { - // 添加认证令牌,作为Cookie发送 - document.cookie = `admin_id=${adminInfo.id}; path=/`; - document.cookie = `admin_token=${adminInfo.token}; path=/`; + // 添加认证信息(如果有) + if (typeof window !== 'undefined') { + const token = localStorage.getItem('admin_token'); + if (token) { + // 设置Cookie中的认证信息 + document.cookie = `admin_token=${token}; path=/`; + } } - // 请求配置 - const config: RequestInit = { + // 构建请求选项 + const options: RequestInit = { method, headers, credentials: 'include', // 包含跨域请求的Cookie }; - // 如果有请求数据,转换为JSON - if (data && method !== 'GET') { - config.body = JSON.stringify(data); + // 添加请求体(针对POST、PUT请求) + if (method !== 'GET' && data) { + options.body = JSON.stringify(data); } try { - const response = await fetch(url, config); + // 发送请求 + const response = await fetch(url, options); - if (!response.ok) { - throw new Error(`请求失败: ${response.status} ${response.statusText}`); - } + // 解析响应 + const result = await response.json(); - const result = await response.json() as ApiResponse; - - // 如果返回未授权错误,清除登录信息 - if (result.code === 401) { - clearAdminInfo(); - // 如果在浏览器环境,跳转到登录页 - if (typeof window !== 'undefined') { - window.location.href = '/login'; + // 如果响应状态码不是2xx,或者接口返回的code不是200,抛出错误 + if (!response.ok || (result && result.code !== 200)) { + // 如果是认证错误,清除登录信息 + if (result.code === 401) { + if (typeof window !== 'undefined') { + localStorage.removeItem('admin_id'); + localStorage.removeItem('admin_name'); + localStorage.removeItem('admin_account'); + localStorage.removeItem('admin_token'); + } } + + throw result; // 抛出响应结果作为错误 } return result; } catch (error) { - console.error('API请求错误:', error); - - return { - code: 500, - msg: error instanceof Error ? error.message : '未知错误', - data: null - }; + // 直接抛出错误,由调用方处理 + throw error; } } \ No newline at end of file diff --git a/SuperAdmin/lib/error-handler.ts b/SuperAdmin/lib/error-handler.ts new file mode 100644 index 00000000..0778c298 --- /dev/null +++ b/SuperAdmin/lib/error-handler.ts @@ -0,0 +1,153 @@ +"use client" + +import { useToast } from "@/components/ui/use-toast" + +// 错误类型 +type ErrorType = 'api' | 'auth' | 'network' | 'validation' | 'unknown'; + +// 错误处理配置 +interface ErrorConfig { + title: string; + variant: "default" | "destructive" | "success"; + defaultMessage: string; +} + +// 不同类型错误的配置 +const errorConfigs: Record = { + api: { + title: '接口错误', + variant: 'destructive', + defaultMessage: '服务器处理请求失败,请稍后再试', + }, + auth: { + title: '认证错误', + variant: 'destructive', + defaultMessage: '您的登录状态已失效,请重新登录', + }, + network: { + title: '网络错误', + variant: 'destructive', + defaultMessage: '网络连接失败,请检查您的网络状态', + }, + validation: { + title: '数据验证错误', + variant: 'destructive', + defaultMessage: '输入数据不正确,请检查后重试', + }, + unknown: { + title: '未知错误', + variant: 'destructive', + defaultMessage: '发生未知错误,请刷新页面后重试', + }, +}; + +/** + * 全局错误处理工具,使用React Hook方式调用 + */ +export function useErrorHandler() { + const { toast } = useToast(); + + /** + * 处理API响应错误 + * @param error 错误对象 + * @param customMessage 自定义错误消息 + * @param errorType 错误类型 + */ + const handleError = ( + error: any, + customMessage?: string, + errorType: ErrorType = 'api' + ) => { + let message = customMessage; + let type = errorType; + + // 如果是API错误响应 + if (error && error.code !== undefined) { + switch (error.code) { + case 401: + type = 'auth'; + message = error.msg || errorConfigs.auth.defaultMessage; + + // 清除登录信息并跳转到登录页 + if (typeof window !== 'undefined') { + localStorage.removeItem('admin_id'); + localStorage.removeItem('admin_name'); + localStorage.removeItem('admin_account'); + localStorage.removeItem('admin_token'); + + // 延迟跳转,确保用户能看到错误提示 + setTimeout(() => { + window.location.href = '/login'; + }, 1500); + } + break; + case 400: + type = 'validation'; + message = error.msg || errorConfigs.validation.defaultMessage; + break; + case 500: + message = error.msg || errorConfigs.api.defaultMessage; + break; + default: + message = error.msg || message || errorConfigs[type].defaultMessage; + } + } else if (error instanceof Error) { + // 如果是普通Error对象 + if (error.message.includes('network') || error.message.includes('fetch')) { + type = 'network'; + message = errorConfigs.network.defaultMessage; + } else { + message = error.message || errorConfigs.unknown.defaultMessage; + } + } + + // 使用Toast显示错误 + toast({ + title: errorConfigs[type].title, + description: message || errorConfigs[type].defaultMessage, + variant: errorConfigs[type].variant, + }); + + // 将错误信息记录到控制台,方便调试 + console.error('Error:', error); + }; + + return { handleError }; +} + +/** + * 封装错误处理的高阶函数,用于API请求 + * @param apiFn API函数 + * @param errorMessage 自定义错误消息 + * @param onError 错误发生时的回调函数 + */ +export function withErrorHandling( + apiFn: (...args: Args) => Promise, + errorMessage?: string, + onError?: (error: any) => void +) { + return async (...args: Args): Promise => { + try { + return await apiFn(...args); + } catch (error) { + if (typeof window !== 'undefined') { + // 创建一个临时div来获取toast函数 + const div = document.createElement('div'); + div.style.display = 'none'; + document.body.appendChild(div); + + const { handleError } = useErrorHandler(); + handleError(error, errorMessage); + + document.body.removeChild(div); + } + + // 调用外部错误处理函数 + if (onError) { + onError(error); + } + + return null; + } + }; +} \ No newline at end of file