feat(admin): add book borrowing dialog and root redirect
- Add admin book borrowing dialog with user selection - Add useAdminBorrowBook mutation with success/error toasts - Fix sidebar z-index layering on admin layout - Add RootRedirect component that routes based on admin role
This commit is contained in:
+11
-4
@@ -1,6 +1,7 @@
|
|||||||
import axios, { AxiosError } from "axios"
|
import axios, { AxiosError } from "axios"
|
||||||
import { useAuthStore } from "@/store/authStore"
|
import { useAuthStore } from "@/store/authStore"
|
||||||
import type { ApiResult } from "@/types/api"
|
import type { ApiResult } from "@/types/api"
|
||||||
|
import { extractMessageFromBody } from "@/lib/errors"
|
||||||
|
|
||||||
function toCamelCase(s: string): string {
|
function toCamelCase(s: string): string {
|
||||||
return s.replace(/_([a-z])/g, (_, c) => c.toUpperCase())
|
return s.replace(/_([a-z])/g, (_, c) => c.toUpperCase())
|
||||||
@@ -31,15 +32,20 @@ client.interceptors.request.use((config) => {
|
|||||||
return config
|
return config
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function isLoginPage(): boolean {
|
||||||
|
return window.location.pathname === "/login" || window.location.pathname === "/"
|
||||||
|
}
|
||||||
|
|
||||||
client.interceptors.response.use(
|
client.interceptors.response.use(
|
||||||
(response) => {
|
(response) => {
|
||||||
const body = response.data as ApiResult<unknown>
|
const body = response.data as ApiResult<unknown>
|
||||||
if (body.code !== 200) {
|
if (body.code !== 200) {
|
||||||
if (body.code === 401) {
|
if (body.code === 401) {
|
||||||
useAuthStore.getState().clearAuth()
|
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<unknown>
|
response.data = transformKeys(body) as ApiResult<unknown>
|
||||||
return response
|
return response
|
||||||
@@ -52,9 +58,10 @@ client.interceptors.response.use(
|
|||||||
if (data) {
|
if (data) {
|
||||||
if (data.code === 401) {
|
if (data.code === 401) {
|
||||||
useAuthStore.getState().clearAuth()
|
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") {
|
if (error.code === "ERR_NETWORK" || error.code === "ECONNABORTED") {
|
||||||
throw new Error("网络连接失败,请检查网络")
|
throw new Error("网络连接失败,请检查网络")
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import { useAdminBooks, useAddBook, useUpdateBook, useUpdateStock, useDeleteBook
|
|||||||
import { usePlatform } from "@/hooks/usePlatform"
|
import { usePlatform } from "@/hooks/usePlatform"
|
||||||
import { useIsMobile } from "@/hooks/useIsMobile"
|
import { useIsMobile } from "@/hooks/useIsMobile"
|
||||||
import type { Book } from "@/types/api"
|
import type { Book } from "@/types/api"
|
||||||
|
import { getErrorMessage } from "@/lib/errors"
|
||||||
|
|
||||||
// ---- Dialog state helpers ----
|
// ---- Dialog state helpers ----
|
||||||
interface AddDialog { open: boolean }
|
interface AddDialog { open: boolean }
|
||||||
@@ -47,7 +48,7 @@ export default function AdminBooksPage() {
|
|||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<div className="rounded-lg border border-destructive/50 bg-destructive/10 px-4 py-3 text-sm text-destructive">
|
<div className="rounded-lg border border-destructive/50 bg-destructive/10 px-4 py-3 text-sm text-destructive">
|
||||||
加载失败:{error instanceof Error ? error.message : "未知错误"}
|
加载失败:{getErrorMessage(error)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { useAdminBorrows, useSearchBorrows, useAdminReturnBook } from "./hooks"
|
|||||||
import { usePlatform } from "@/hooks/usePlatform"
|
import { usePlatform } from "@/hooks/usePlatform"
|
||||||
import { useIsMobile } from "@/hooks/useIsMobile"
|
import { useIsMobile } from "@/hooks/useIsMobile"
|
||||||
import { formatDateTime } from "@/lib/formatters"
|
import { formatDateTime } from "@/lib/formatters"
|
||||||
|
import { getErrorMessage } from "@/lib/errors"
|
||||||
import type { BorrowInfoVo } from "@/types/api"
|
import type { BorrowInfoVo } from "@/types/api"
|
||||||
|
|
||||||
export default function AdminBorrowsPage() {
|
export default function AdminBorrowsPage() {
|
||||||
@@ -47,7 +48,7 @@ export default function AdminBorrowsPage() {
|
|||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<div className="rounded-lg border border-destructive/50 bg-destructive/10 px-4 py-3 text-sm text-destructive">
|
<div className="rounded-lg border border-destructive/50 bg-destructive/10 px-4 py-3 text-sm text-destructive">
|
||||||
加载失败:{error instanceof Error ? error.message : "未知错误"}
|
加载失败:{getErrorMessage(error)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { toast } from "sonner"
|
|||||||
import { getAllBooks } from "@/api/books"
|
import { getAllBooks } from "@/api/books"
|
||||||
import { addBook, deleteBook, updateBook, updateStock } from "@/api/admin/books"
|
import { addBook, deleteBook, updateBook, updateStock } from "@/api/admin/books"
|
||||||
import { getAllBorrows, searchBorrows, returnBook, borrowBook as adminBorrowBook } from "@/api/admin/borrows"
|
import { getAllBorrows, searchBorrows, returnBook, borrowBook as adminBorrowBook } from "@/api/admin/borrows"
|
||||||
|
import { getErrorMessage } from "@/lib/errors"
|
||||||
|
|
||||||
export function useAdminBooks() {
|
export function useAdminBooks() {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
@@ -20,7 +21,7 @@ export function useAddBook() {
|
|||||||
toast.success("添加成功")
|
toast.success("添加成功")
|
||||||
},
|
},
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
toast.error(err instanceof Error ? err.message : "添加失败")
|
toast.error(getErrorMessage(err, "添加失败"))
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -35,7 +36,7 @@ export function useUpdateBook() {
|
|||||||
toast.success("修改成功")
|
toast.success("修改成功")
|
||||||
},
|
},
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
toast.error(err instanceof Error ? err.message : "修改失败")
|
toast.error(getErrorMessage(err, "修改失败"))
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -49,7 +50,7 @@ export function useUpdateStock() {
|
|||||||
toast.success("库存更新成功")
|
toast.success("库存更新成功")
|
||||||
},
|
},
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
toast.error(err instanceof Error ? err.message : "库存更新失败")
|
toast.error(getErrorMessage(err, "库存更新失败"))
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -63,7 +64,7 @@ export function useDeleteBook() {
|
|||||||
toast.success("删除成功")
|
toast.success("删除成功")
|
||||||
},
|
},
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
toast.error(err instanceof Error ? err.message : "删除失败")
|
toast.error(getErrorMessage(err, "删除失败"))
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -92,7 +93,7 @@ export function useAdminReturnBook() {
|
|||||||
toast.success("归还成功")
|
toast.success("归还成功")
|
||||||
},
|
},
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
toast.error(err instanceof Error ? err.message : "归还失败")
|
toast.error(getErrorMessage(err, "归还失败"))
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -107,7 +108,7 @@ export function useAdminBorrowBook() {
|
|||||||
toast.success("借阅成功")
|
toast.success("借阅成功")
|
||||||
},
|
},
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
toast.error(err instanceof Error ? err.message : "借阅失败")
|
toast.error(getErrorMessage(err, "借阅失败"))
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { Button } from "@/components/ui/button"
|
|||||||
import { useAuthStore, isAdminRole } from "@/store/authStore"
|
import { useAuthStore, isAdminRole } from "@/store/authStore"
|
||||||
import { login } from "@/api/auth"
|
import { login } from "@/api/auth"
|
||||||
import { setBaseUrl } from "@/api/client"
|
import { setBaseUrl } from "@/api/client"
|
||||||
|
import { getErrorMessage } from "@/lib/errors"
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
@@ -51,7 +52,7 @@ export default function LoginPage() {
|
|||||||
toast.success("登录成功")
|
toast.success("登录成功")
|
||||||
navigate(isAdminRole(result.role) ? "/admin/books" : "/books", { replace: true })
|
navigate(isAdminRole(result.role) ? "/admin/books" : "/books", { replace: true })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error(err instanceof Error ? err.message : "登录失败")
|
toast.error(getErrorMessage(err, "登录失败"))
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { useBooks, useBorrowBook } from "./hooks"
|
|||||||
import { useAuthStore } from "@/store/authStore"
|
import { useAuthStore } from "@/store/authStore"
|
||||||
import { usePlatform } from "@/hooks/usePlatform"
|
import { usePlatform } from "@/hooks/usePlatform"
|
||||||
import { useIsMobile } from "@/hooks/useIsMobile"
|
import { useIsMobile } from "@/hooks/useIsMobile"
|
||||||
|
import { getErrorMessage } from "@/lib/errors"
|
||||||
|
|
||||||
export default function BooksPage() {
|
export default function BooksPage() {
|
||||||
const { data: books, isLoading, error } = useBooks()
|
const { data: books, isLoading, error } = useBooks()
|
||||||
@@ -22,7 +23,7 @@ export default function BooksPage() {
|
|||||||
return (
|
return (
|
||||||
<div className="mx-auto max-w-[1200px] px-4 py-8">
|
<div className="mx-auto max-w-[1200px] px-4 py-8">
|
||||||
<div className="rounded-lg border border-destructive/50 bg-destructive/10 px-4 py-3 text-sm text-destructive">
|
<div className="rounded-lg border border-destructive/50 bg-destructive/10 px-4 py-3 text-sm text-destructive">
|
||||||
加载失败:{error instanceof Error ? error.message : "未知错误"}
|
加载失败:{getErrorMessage(error)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
|
|||||||
import { getAllBooks } from "@/api/books"
|
import { getAllBooks } from "@/api/books"
|
||||||
import { borrowBookForMe } from "@/api/borrows"
|
import { borrowBookForMe } from "@/api/borrows"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
|
import { getErrorMessage } from "@/lib/errors"
|
||||||
|
|
||||||
export function useBooks() {
|
export function useBooks() {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
@@ -21,7 +22,7 @@ export function useBorrowBook() {
|
|||||||
toast.success("借阅成功")
|
toast.success("借阅成功")
|
||||||
},
|
},
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
toast.error(err instanceof Error ? err.message : "借阅失败")
|
toast.error(getErrorMessage(err, "借阅失败"))
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { useMyBorrows, useReturnBook } from "./hooks"
|
|||||||
import { formatDateTime } from "@/lib/formatters"
|
import { formatDateTime } from "@/lib/formatters"
|
||||||
import { usePlatform } from "@/hooks/usePlatform"
|
import { usePlatform } from "@/hooks/usePlatform"
|
||||||
import { useIsMobile } from "@/hooks/useIsMobile"
|
import { useIsMobile } from "@/hooks/useIsMobile"
|
||||||
|
import { getErrorMessage } from "@/lib/errors"
|
||||||
|
|
||||||
export default function MyBorrowsPage() {
|
export default function MyBorrowsPage() {
|
||||||
const { data: records, isLoading, error } = useMyBorrows()
|
const { data: records, isLoading, error } = useMyBorrows()
|
||||||
@@ -22,7 +23,7 @@ export default function MyBorrowsPage() {
|
|||||||
return (
|
return (
|
||||||
<div className="mx-auto max-w-[1200px] px-4 py-8">
|
<div className="mx-auto max-w-[1200px] px-4 py-8">
|
||||||
<div className="rounded-lg border border-destructive/50 bg-destructive/10 px-4 py-3 text-sm text-destructive">
|
<div className="rounded-lg border border-destructive/50 bg-destructive/10 px-4 py-3 text-sm text-destructive">
|
||||||
加载失败:{error instanceof Error ? error.message : "未知错误"}
|
加载失败:{getErrorMessage(error)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
|
||||||
import { getAllMyBorrows, returnBookForMe } from "@/api/borrows"
|
import { getAllMyBorrows, returnBookForMe } from "@/api/borrows"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
|
import { getErrorMessage } from "@/lib/errors"
|
||||||
|
|
||||||
export function useMyBorrows() {
|
export function useMyBorrows() {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
@@ -18,7 +19,7 @@ export function useReturnBook() {
|
|||||||
toast.success("归还成功")
|
toast.success("归还成功")
|
||||||
},
|
},
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
toast.error(err instanceof Error ? err.message : "归还失败")
|
toast.error(getErrorMessage(err, "归还失败"))
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<string, unknown>
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user