超管后台 - 增加错误提示
This commit is contained in:
@@ -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({
|
||||
<body className={inter.className}>
|
||||
<ThemeProvider attribute="class" defaultTheme="light" enableSystem disableTransitionOnChange>
|
||||
<ToastProvider>
|
||||
{children}
|
||||
<ErrorBoundary>
|
||||
{children}
|
||||
</ErrorBoundary>
|
||||
</ToastProvider>
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
|
||||
@@ -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() {
|
||||
</CardFooter>
|
||||
</form>
|
||||
</Card>
|
||||
|
||||
{/* 错误提示弹窗 */}
|
||||
<AlertDialog open={errorDialogOpen} onOpenChange={setErrorDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>登录失败</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{errorMessage}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogAction onClick={() => setErrorDialogOpen(false)}>
|
||||
确定
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
100
SuperAdmin/components/ErrorBoundary.tsx
Normal file
100
SuperAdmin/components/ErrorBoundary.tsx
Normal 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
|
||||
@@ -1,8 +1,7 @@
|
||||
import { getConfig } from './config';
|
||||
import { getAdminInfo, clearAdminInfo } from './utils';
|
||||
|
||||
/**
|
||||
* API响应数据结构
|
||||
* API响应接口
|
||||
*/
|
||||
export interface ApiResponse<T = any> {
|
||||
code: number;
|
||||
@@ -11,7 +10,7 @@ export interface ApiResponse<T = any> {
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用API请求函数
|
||||
* API请求函数
|
||||
* @param endpoint API端点
|
||||
* @param method HTTP方法
|
||||
* @param data 请求数据
|
||||
@@ -22,62 +21,61 @@ export async function apiRequest<T = any>(
|
||||
method: 'GET' | 'POST' | 'PUT' | 'DELETE' = 'GET',
|
||||
data?: any
|
||||
): Promise<ApiResponse<T>> {
|
||||
// 获取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<T>;
|
||||
|
||||
// 如果返回未授权错误,清除登录信息
|
||||
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;
|
||||
}
|
||||
}
|
||||
153
SuperAdmin/lib/error-handler.ts
Normal file
153
SuperAdmin/lib/error-handler.ts
Normal 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;
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user