diff --git a/src/api/client.ts b/src/api/client.ts index 1618ba7..9bd9274 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -2,6 +2,22 @@ import axios, { AxiosError } from "axios" import { useAuthStore } from "@/store/authStore" import type { ApiResult } from "@/types/api" +function toCamelCase(s: string): string { + return s.replace(/_([a-z])/g, (_, c) => c.toUpperCase()) +} + +function transformKeys(obj: unknown): unknown { + if (Array.isArray(obj)) return obj.map(transformKeys) + if (obj !== null && typeof obj === "object") { + const result: Record = {} + for (const [k, v] of Object.entries(obj as Record)) { + result[toCamelCase(k)] = transformKeys(v) + } + return result + } + return obj +} + const client = axios.create({ baseURL: localStorage.getItem("api_base_url") || "", timeout: 15000, @@ -25,9 +41,13 @@ client.interceptors.response.use( } throw new Error(body.message || "Request failed") } + response.data = transformKeys(body) as ApiResult return response }, (error: AxiosError>) => { + if (!navigator.onLine) { + throw new Error("网络连接失败,请检查网络") + } const data = error.response?.data if (data) { if (data.code === 401) { @@ -36,6 +56,9 @@ client.interceptors.response.use( } throw new Error(data.message || "Request failed") } + if (error.code === "ERR_NETWORK" || error.code === "ECONNABORTED") { + throw new Error("网络连接失败,请检查网络") + } throw error } ) diff --git a/src/components/layout/AdminLayout.tsx b/src/components/layout/AdminLayout.tsx index 086b191..6110724 100644 --- a/src/components/layout/AdminLayout.tsx +++ b/src/components/layout/AdminLayout.tsx @@ -3,6 +3,7 @@ import { BookOpenIcon, ListIcon, LogOutIcon } from "lucide-react" import { Button } from "@/components/ui/button" import { useAuthStore } from "@/store/authStore" import { usePlatform } from "@/hooks/usePlatform" +import { useIsMobile } from "@/hooks/useIsMobile" const navItems = [ { to: "/admin/books", label: "图书管理", icon: BookOpenIcon }, @@ -14,7 +15,8 @@ export default function AdminLayout() { const clearAuth = useAuthStore((s) => s.clearAuth) const navigate = useNavigate() const platform = usePlatform() - const isDesktop = platform === "web" || platform === "desktop" + const isMobile = useIsMobile() + const hideTitle = platform === "mobile" function handleLogout() { clearAuth() @@ -26,7 +28,7 @@ export default function AdminLayout() { isActive ? "bg-primary text-primary-foreground" : "text-muted-foreground hover:bg-muted hover:text-foreground" }` - if (isDesktop) { + if (!isMobile) { return (
) } -function MyBorrowsSkeleton() { +function MyBorrowsSkeleton({ isMobile }: { isMobile: boolean }) { + if (isMobile) { + return ( +
+ + {Array.from({ length: 5 }).map((_, i) => ( + + + + + + + + + ))} +
+ ) + } + return (
diff --git a/src/hooks/useIsMobile.ts b/src/hooks/useIsMobile.ts new file mode 100644 index 0000000..6b1d55f --- /dev/null +++ b/src/hooks/useIsMobile.ts @@ -0,0 +1,18 @@ +import { useEffect, useState } from "react" + +const BP = "(max-width: 767px)" + +export function useIsMobile(): boolean { + const [mobile, setMobile] = useState(() => window.matchMedia(BP).matches) + + useEffect(() => { + const mql = window.matchMedia(BP) + function onChange(e: MediaQueryListEvent) { + setMobile(e.matches) + } + mql.addEventListener("change", onChange) + return () => mql.removeEventListener("change", onChange) + }, []) + + return mobile +} diff --git a/src/lib/formatters.ts b/src/lib/formatters.ts index 3c2a4f8..557ab95 100644 --- a/src/lib/formatters.ts +++ b/src/lib/formatters.ts @@ -1,5 +1,6 @@ -export function formatDateTime(iso: string): string { - const d = new Date(iso) +export function formatDateTime(raw: unknown): string { + const d = toDate(raw) + if (!d) return "-" const Y = d.getFullYear() const M = String(d.getMonth() + 1).padStart(2, "0") const D = String(d.getDate()).padStart(2, "0") @@ -8,6 +9,45 @@ export function formatDateTime(iso: string): string { return `${Y}-${M}-${D} ${h}:${m}` } -export function formatDate(iso: string): string { - return new Date(iso).toLocaleDateString() +export function formatDate(raw: unknown): string { + const d = toDate(raw) + if (!d) return "-" + return d.toLocaleDateString() +} + +function toDate(raw: unknown): Date | null { + if (raw == null) return null + + if (raw instanceof Date) return isNaN(raw.getTime()) ? null : raw + + if (typeof raw === "string") { + const d = new Date(raw) + return isNaN(d.getTime()) ? null : d + } + + if (typeof raw === "number") { + const d = new Date(raw) + return isNaN(d.getTime()) ? null : d + } + + // Java LocalDateTime array: [year, month, day, hour, minute, second, nanos] + if (Array.isArray(raw) && raw.length >= 3) { + const [y, mo, d, h = 0, mi = 0, s = 0] = raw + return new Date(Number(y), Number(mo) - 1, Number(d), Number(h), Number(mi), Number(s)) + } + + if (typeof raw === "object") { + const obj = raw as Record + const y = Number(obj.year ?? 0) + const mo = Number(obj.monthValue ?? obj.month ?? 1) + const d = Number(obj.dayOfMonth ?? obj.day ?? 1) + const h = Number(obj.hour ?? 0) + const mi = Number(obj.minute ?? 0) + const s = Number(obj.second ?? 0) + if (!isNaN(y) && !isNaN(mo) && !isNaN(d)) { + return new Date(y, mo - 1, d, h, mi, s) + } + } + + return null } diff --git a/src/router/guards.tsx b/src/router/guards.tsx index f980366..2878fdc 100644 --- a/src/router/guards.tsx +++ b/src/router/guards.tsx @@ -9,7 +9,7 @@ export function ProtectedRoute() { export function AdminRoute() { const isLoggedIn = useAuthStore((s) => !!s.token) - const isAdmin = useAuthStore((s) => s.role === "ADMIN") + const isAdmin = useAuthStore((s) => s.isAdmin()) if (!isLoggedIn) return if (!isAdmin) return return diff --git a/src/store/authStore.ts b/src/store/authStore.ts index f7b0f58..b17170a 100644 --- a/src/store/authStore.ts +++ b/src/store/authStore.ts @@ -45,10 +45,19 @@ export const useAuthStore = create((set, get) => { }, isLoggedIn: () => !!get().token, - isAdmin: () => get().role === "ADMIN", + isAdmin: () => { + const r = get().role + return r != null && (r.toUpperCase() === "ADMIN" || r.toUpperCase() === "ROLE_ADMIN") + }, } }) export function getToken(): string | null { return useAuthStore.getState().token } + +export function isAdminRole(role: string | null): boolean { + if (!role) return false + const u = role.toUpperCase() + return u === "ADMIN" || u === "ROLE_ADMIN" +}