超管后台 - 增加错误提示

This commit is contained in:
柳清爽
2025-04-10 17:49:42 +08:00
parent 15ef17b79f
commit 4607bb1d4a
5 changed files with 326 additions and 50 deletions

View File

@@ -4,6 +4,7 @@ import { Inter } from "next/font/google"
import "./globals.css" import "./globals.css"
import { ThemeProvider } from "@/components/theme-provider" import { ThemeProvider } from "@/components/theme-provider"
import { ToastProvider } from "@/components/ui/use-toast" import { ToastProvider } from "@/components/ui/use-toast"
import ErrorBoundary from "@/components/ErrorBoundary"
const inter = Inter({ subsets: ["latin"] }) const inter = Inter({ subsets: ["latin"] })
@@ -23,7 +24,9 @@ export default function RootLayout({
<body className={inter.className}> <body className={inter.className}>
<ThemeProvider attribute="class" defaultTheme="light" enableSystem disableTransitionOnChange> <ThemeProvider attribute="class" defaultTheme="light" enableSystem disableTransitionOnChange>
<ToastProvider> <ToastProvider>
{children} <ErrorBoundary>
{children}
</ErrorBoundary>
</ToastProvider> </ToastProvider>
</ThemeProvider> </ThemeProvider>
</body> </body>

View File

@@ -11,11 +11,22 @@ import { Label } from "@/components/ui/label"
import { md5, saveAdminInfo } from "@/lib/utils" import { md5, saveAdminInfo } from "@/lib/utils"
import { login } from "@/lib/admin-api" import { login } from "@/lib/admin-api"
import { useToast } from "@/components/ui/use-toast" import { useToast } from "@/components/ui/use-toast"
import {
AlertDialog,
AlertDialogAction,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog"
export default function LoginPage() { export default function LoginPage() {
const [account, setAccount] = useState("") const [account, setAccount] = useState("")
const [password, setPassword] = useState("") const [password, setPassword] = useState("")
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const [errorDialogOpen, setErrorDialogOpen] = useState(false)
const [errorMessage, setErrorMessage] = useState("")
const router = useRouter() const router = useRouter()
const { toast } = useToast() const { toast } = useToast()
@@ -44,22 +55,16 @@ export default function LoginPage() {
// 跳转到仪表盘 // 跳转到仪表盘
router.push("/dashboard") router.push("/dashboard")
} else { } else {
// 显示错误提示 // 显示错误弹窗
toast({ setErrorMessage(result.msg || "账号或密码错误")
title: "登录失败", setErrorDialogOpen(true)
description: result.msg || "账号或密码错误",
variant: "destructive",
})
} }
} catch (err) { } catch (err: any) {
console.error("登录失败:", err) console.error("登录失败:", err)
// 显示错误提示 // 显示错误弹窗
toast({ setErrorMessage(err.msg || "网络错误,请稍后再试")
title: "登录失败", setErrorDialogOpen(true)
description: "网络错误,请稍后再试",
variant: "destructive",
})
} finally { } finally {
setIsLoading(false) setIsLoading(false)
} }
@@ -103,6 +108,23 @@ export default function LoginPage() {
</CardFooter> </CardFooter>
</form> </form>
</Card> </Card>
{/* 错误提示弹窗 */}
<AlertDialog open={errorDialogOpen} onOpenChange={setErrorDialogOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle></AlertDialogTitle>
<AlertDialogDescription>
{errorMessage}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogAction onClick={() => setErrorDialogOpen(false)}>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div> </div>
) )
} }

View File

@@ -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<ErrorBoundaryProps, ErrorBoundaryState> {
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 <ErrorScreen error={this.state.error} onReset={() => 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 (
<div className="w-full h-[calc(100vh-200px)] flex items-center justify-center p-4">
<Card className="w-full max-w-md">
<CardContent className="pt-6">
<div className="flex flex-col gap-4">
<div className="bg-red-50 text-red-600 p-4 rounded-md">
<h2 className="text-xl font-semibold mb-2"></h2>
<p className="text-sm text-red-700 mb-4">
</p>
{error && (
<div className="bg-white/50 p-2 rounded text-xs font-mono overflow-auto max-h-24">
{error.message}
</div>
)}
</div>
<p className="text-sm text-gray-500">
</p>
</div>
</CardContent>
<CardFooter className="flex justify-end gap-2">
<Button variant="outline" onClick={goHome}>
</Button>
<Button onClick={refreshPage}></Button>
</CardFooter>
</Card>
</div>
)
}
export default ErrorBoundary

View File

@@ -1,8 +1,7 @@
import { getConfig } from './config'; import { getConfig } from './config';
import { getAdminInfo, clearAdminInfo } from './utils';
/** /**
* API响应数据结构 * API响应接口
*/ */
export interface ApiResponse<T = any> { export interface ApiResponse<T = any> {
code: number; code: number;
@@ -11,7 +10,7 @@ export interface ApiResponse<T = any> {
} }
/** /**
* 通用API请求函数 * API请求函数
* @param endpoint API端点 * @param endpoint API端点
* @param method HTTP方法 * @param method HTTP方法
* @param data 请求数据 * @param data 请求数据
@@ -22,62 +21,61 @@ export async function apiRequest<T = any>(
method: 'GET' | 'POST' | 'PUT' | 'DELETE' = 'GET', method: 'GET' | 'POST' | 'PUT' | 'DELETE' = 'GET',
data?: any data?: any
): Promise<ApiResponse<T>> { ): Promise<ApiResponse<T>> {
// 获取API基础URL
const { apiBaseUrl } = getConfig(); const { apiBaseUrl } = getConfig();
const url = `${apiBaseUrl}${endpoint}`; const url = `${apiBaseUrl}${endpoint}`;
// 获取认证信息 // 构建请求头
const adminInfo = getAdminInfo();
// 请求头
const headers: HeadersInit = { const headers: HeadersInit = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}; };
// 如果有认证信息添加Cookie头 // 添加认证信息(如果有)
if (adminInfo?.token) { if (typeof window !== 'undefined') {
// 添加认证令牌作为Cookie发送 const token = localStorage.getItem('admin_token');
document.cookie = `admin_id=${adminInfo.id}; path=/`; if (token) {
document.cookie = `admin_token=${adminInfo.token}; path=/`; // 设置Cookie中的认证信息
document.cookie = `admin_token=${token}; path=/`;
}
} }
// 请求配置 // 构建请求选项
const config: RequestInit = { const options: RequestInit = {
method, method,
headers, headers,
credentials: 'include', // 包含跨域请求的Cookie credentials: 'include', // 包含跨域请求的Cookie
}; };
// 如果有请求数据转换为JSON // 添加请求体针对POST、PUT请求
if (data && method !== 'GET') { if (method !== 'GET' && data) {
config.body = JSON.stringify(data); options.body = JSON.stringify(data);
} }
try { 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<T>; // 如果响应状态码不是2xx或者接口返回的code不是200抛出错误
if (!response.ok || (result && result.code !== 200)) {
// 如果返回未授权错误,清除登录信息 // 如果是认证错误,清除登录信息
if (result.code === 401) { if (result.code === 401) {
clearAdminInfo(); if (typeof window !== 'undefined') {
// 如果在浏览器环境,跳转到登录页 localStorage.removeItem('admin_id');
if (typeof window !== 'undefined') { localStorage.removeItem('admin_name');
window.location.href = '/login'; localStorage.removeItem('admin_account');
localStorage.removeItem('admin_token');
}
} }
throw result; // 抛出响应结果作为错误
} }
return result; return result;
} catch (error) { } catch (error) {
console.error('API请求错误:', error); // 直接抛出错误,由调用方处理
throw error;
return {
code: 500,
msg: error instanceof Error ? error.message : '未知错误',
data: null
};
} }
} }

View File

@@ -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<ErrorType, ErrorConfig> = {
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<T, Args extends any[]>(
apiFn: (...args: Args) => Promise<T>,
errorMessage?: string,
onError?: (error: any) => void
) {
return async (...args: Args): Promise<T | null> => {
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;
}
};
}