超管后台 - 增加错误提示
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
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 { 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
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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