diff --git a/src/api/client.ts b/src/api/client.ts index 9bd9274..889e747 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -1,6 +1,7 @@ import axios, { AxiosError } from "axios" import { useAuthStore } from "@/store/authStore" import type { ApiResult } from "@/types/api" +import { extractMessageFromBody } from "@/lib/errors" function toCamelCase(s: string): string { return s.replace(/_([a-z])/g, (_, c) => c.toUpperCase()) @@ -31,15 +32,20 @@ client.interceptors.request.use((config) => { return config }) +function isLoginPage(): boolean { + return window.location.pathname === "/login" || window.location.pathname === "/" +} + client.interceptors.response.use( (response) => { const body = response.data as ApiResult if (body.code !== 200) { if (body.code === 401) { useAuthStore.getState().clearAuth() - window.location.href = "/login" + if (!isLoginPage()) window.location.href = "/login" } - throw new Error(body.message || "Request failed") + const msg = extractMessageFromBody(body) ?? "请求失败" + throw new Error(msg) } response.data = transformKeys(body) as ApiResult return response @@ -52,9 +58,10 @@ client.interceptors.response.use( if (data) { if (data.code === 401) { useAuthStore.getState().clearAuth() - window.location.href = "/login" + if (!isLoginPage()) window.location.href = "/login" } - throw new Error(data.message || "Request failed") + const msg = extractMessageFromBody(data) ?? "请求失败" + throw new Error(msg) } if (error.code === "ERR_NETWORK" || error.code === "ECONNABORTED") { throw new Error("网络连接失败,请检查网络") diff --git a/src/features/admin/AdminBooksPage.tsx b/src/features/admin/AdminBooksPage.tsx index 2752275..2521e8f 100644 --- a/src/features/admin/AdminBooksPage.tsx +++ b/src/features/admin/AdminBooksPage.tsx @@ -21,6 +21,7 @@ import { useAdminBooks, useAddBook, useUpdateBook, useUpdateStock, useDeleteBook import { usePlatform } from "@/hooks/usePlatform" import { useIsMobile } from "@/hooks/useIsMobile" import type { Book } from "@/types/api" +import { getErrorMessage } from "@/lib/errors" // ---- Dialog state helpers ---- interface AddDialog { open: boolean } @@ -47,7 +48,7 @@ export default function AdminBooksPage() { if (error) { return (
- 加载失败:{error instanceof Error ? error.message : "未知错误"} + 加载失败:{getErrorMessage(error)}
) } diff --git a/src/features/admin/AdminBorrowsPage.tsx b/src/features/admin/AdminBorrowsPage.tsx index fb6bcca..68b83d0 100644 --- a/src/features/admin/AdminBorrowsPage.tsx +++ b/src/features/admin/AdminBorrowsPage.tsx @@ -20,6 +20,7 @@ import { useAdminBorrows, useSearchBorrows, useAdminReturnBook } from "./hooks" import { usePlatform } from "@/hooks/usePlatform" import { useIsMobile } from "@/hooks/useIsMobile" import { formatDateTime } from "@/lib/formatters" +import { getErrorMessage } from "@/lib/errors" import type { BorrowInfoVo } from "@/types/api" export default function AdminBorrowsPage() { @@ -47,7 +48,7 @@ export default function AdminBorrowsPage() { if (error) { return (
- 加载失败:{error instanceof Error ? error.message : "未知错误"} + 加载失败:{getErrorMessage(error)}
) } diff --git a/src/features/admin/hooks.ts b/src/features/admin/hooks.ts index fdf7962..f662027 100644 --- a/src/features/admin/hooks.ts +++ b/src/features/admin/hooks.ts @@ -3,6 +3,7 @@ import { toast } from "sonner" import { getAllBooks } from "@/api/books" import { addBook, deleteBook, updateBook, updateStock } from "@/api/admin/books" import { getAllBorrows, searchBorrows, returnBook, borrowBook as adminBorrowBook } from "@/api/admin/borrows" +import { getErrorMessage } from "@/lib/errors" export function useAdminBooks() { return useQuery({ @@ -20,7 +21,7 @@ export function useAddBook() { toast.success("添加成功") }, onError: (err) => { - toast.error(err instanceof Error ? err.message : "添加失败") + toast.error(getErrorMessage(err, "添加失败")) }, }) } @@ -35,7 +36,7 @@ export function useUpdateBook() { toast.success("修改成功") }, onError: (err) => { - toast.error(err instanceof Error ? err.message : "修改失败") + toast.error(getErrorMessage(err, "修改失败")) }, }) } @@ -49,7 +50,7 @@ export function useUpdateStock() { toast.success("库存更新成功") }, onError: (err) => { - toast.error(err instanceof Error ? err.message : "库存更新失败") + toast.error(getErrorMessage(err, "库存更新失败")) }, }) } @@ -63,7 +64,7 @@ export function useDeleteBook() { toast.success("删除成功") }, onError: (err) => { - toast.error(err instanceof Error ? err.message : "删除失败") + toast.error(getErrorMessage(err, "删除失败")) }, }) } @@ -92,7 +93,7 @@ export function useAdminReturnBook() { toast.success("归还成功") }, onError: (err) => { - toast.error(err instanceof Error ? err.message : "归还失败") + toast.error(getErrorMessage(err, "归还失败")) }, }) } @@ -107,7 +108,7 @@ export function useAdminBorrowBook() { toast.success("借阅成功") }, onError: (err) => { - toast.error(err instanceof Error ? err.message : "借阅失败") + toast.error(getErrorMessage(err, "借阅失败")) }, }) } diff --git a/src/features/auth/LoginPage.tsx b/src/features/auth/LoginPage.tsx index b9b5666..c4c9d6c 100644 --- a/src/features/auth/LoginPage.tsx +++ b/src/features/auth/LoginPage.tsx @@ -9,6 +9,7 @@ import { Button } from "@/components/ui/button" import { useAuthStore, isAdminRole } from "@/store/authStore" import { login } from "@/api/auth" import { setBaseUrl } from "@/api/client" +import { getErrorMessage } from "@/lib/errors" export default function LoginPage() { const navigate = useNavigate() @@ -51,7 +52,7 @@ export default function LoginPage() { toast.success("登录成功") navigate(isAdminRole(result.role) ? "/admin/books" : "/books", { replace: true }) } catch (err) { - toast.error(err instanceof Error ? err.message : "登录失败") + toast.error(getErrorMessage(err, "登录失败")) } finally { setLoading(false) } diff --git a/src/features/books/BooksPage.tsx b/src/features/books/BooksPage.tsx index 77233d3..804bdf8 100644 --- a/src/features/books/BooksPage.tsx +++ b/src/features/books/BooksPage.tsx @@ -7,6 +7,7 @@ import { useBooks, useBorrowBook } from "./hooks" import { useAuthStore } from "@/store/authStore" import { usePlatform } from "@/hooks/usePlatform" import { useIsMobile } from "@/hooks/useIsMobile" +import { getErrorMessage } from "@/lib/errors" export default function BooksPage() { const { data: books, isLoading, error } = useBooks() @@ -22,7 +23,7 @@ export default function BooksPage() { return (
- 加载失败:{error instanceof Error ? error.message : "未知错误"} + 加载失败:{getErrorMessage(error)}
) diff --git a/src/features/books/hooks.ts b/src/features/books/hooks.ts index fabac8f..0001394 100644 --- a/src/features/books/hooks.ts +++ b/src/features/books/hooks.ts @@ -2,6 +2,7 @@ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query" import { getAllBooks } from "@/api/books" import { borrowBookForMe } from "@/api/borrows" import { toast } from "sonner" +import { getErrorMessage } from "@/lib/errors" export function useBooks() { return useQuery({ @@ -21,7 +22,7 @@ export function useBorrowBook() { toast.success("借阅成功") }, onError: (err) => { - toast.error(err instanceof Error ? err.message : "借阅失败") + toast.error(getErrorMessage(err, "借阅失败")) }, }) } diff --git a/src/features/borrows/MyBorrowsPage.tsx b/src/features/borrows/MyBorrowsPage.tsx index 8bb8d54..d20e04e 100644 --- a/src/features/borrows/MyBorrowsPage.tsx +++ b/src/features/borrows/MyBorrowsPage.tsx @@ -8,6 +8,7 @@ import { useMyBorrows, useReturnBook } from "./hooks" import { formatDateTime } from "@/lib/formatters" import { usePlatform } from "@/hooks/usePlatform" import { useIsMobile } from "@/hooks/useIsMobile" +import { getErrorMessage } from "@/lib/errors" export default function MyBorrowsPage() { const { data: records, isLoading, error } = useMyBorrows() @@ -22,7 +23,7 @@ export default function MyBorrowsPage() { return (
- 加载失败:{error instanceof Error ? error.message : "未知错误"} + 加载失败:{getErrorMessage(error)}
) diff --git a/src/features/borrows/hooks.ts b/src/features/borrows/hooks.ts index e064ba0..5be11d0 100644 --- a/src/features/borrows/hooks.ts +++ b/src/features/borrows/hooks.ts @@ -1,6 +1,7 @@ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query" import { getAllMyBorrows, returnBookForMe } from "@/api/borrows" import { toast } from "sonner" +import { getErrorMessage } from "@/lib/errors" export function useMyBorrows() { return useQuery({ @@ -18,7 +19,7 @@ export function useReturnBook() { toast.success("归还成功") }, onError: (err) => { - toast.error(err instanceof Error ? err.message : "归还失败") + toast.error(getErrorMessage(err, "归还失败")) }, }) } diff --git a/src/lib/errors.ts b/src/lib/errors.ts new file mode 100644 index 0000000..beffff9 --- /dev/null +++ b/src/lib/errors.ts @@ -0,0 +1,33 @@ +/** + * Extract a human-readable Chinese error message from anything caught in a try/catch. + * Prefers meaningful messages from API responses; falls back to the provided default. + */ +export function getErrorMessage(err: unknown, fallback = "操作失败"): string { + if (err instanceof Error) return err.message || fallback + if (typeof err === "string") return err + return fallback +} + +/** + * Try to pull an error message from an API response body of unknown shape. + * Handles: + * - { code, message } (ApiResult / custom backend wrapper) + * - { error, message } (Spring Boot default error attributes) + * - { message } (plain message) + * - string body + */ +export function extractMessageFromBody(body: unknown): string | null { + if (!body) return null + if (typeof body === "string") return body + if (typeof body !== "object") return null + + const d = body as Record + + // Preferred: message field (ApiResult and Spring Boot both use this) + if (typeof d.message === "string" && d.message) return d.message + + // Spring Boot sometimes puts the summary in "error" and detail in "message" + if (typeof d.error === "string" && d.error) return d.error + + return null +}