From f28dc98f5e905fc13abb7f90124001b6c43235fc Mon Sep 17 00:00:00 2001 From: msksbr515 Date: Sun, 24 May 2026 20:28:28 +0800 Subject: [PATCH] {"type":"assistant","message":{"id":"2099424a-0948-4423-a191-3c37d24e52ed","type":"message","role":"assistant","model":"deepseek-v4-flash","content":[{"type":"thinking","thinking":"The user wants me to generate a commit message based on the git diff provided. Let me analyze the changes: 1. **AdminLayout.tsx** - Major changes: - Added imports for `NavLink`, `useNavigate`, `BookOpenIcon`, `ListIcon`, `LogOutIcon`, `Button`, `useAuthStore`, `usePlatform` - Added navigation items array with book management and borrow management - Added admin layout with sidebar navigation, header with username, and logout functionality - Added responsive layout for desktop/mobile 2. **router/index.tsx** - Changes: - Removed placeholder components (`AdminBooksPage`, `AdminBorrowsPage`, `AdminIndexPage`) - Added imports from actual admin feature pages - Updated routes to use real admin layout and pages This is clearly a **feat** (new feature) commit, adding admin layout with navigation and routing to real pages. The scope would be **admin** or **ui**. The key changes are: - Adding admin sidebar navigation with book/borrow management links - Adding logout functionality with username display - Replacing placeholder admin page components with real page implementations - Integrating admin layout with the router Let me write a concise commit message following the rules.","signature":"2099424a-0948-4423-a191-3c37d24e52ed"}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":25016,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":0,"service_tier":"standard"},"context_management":null},"parent_tool_use_id":null,"session_id":"bf312622-fae8-446d-8b67-c708b9d3a022","uuid":"50eb9e22-cc58-4b45-8f03-95386c522466"}{"type":"assistant","message":{"id":"2099424a-0948-4423-a191-3c37d24e52ed","type":"message","role":"assistant","model":"deepseek-v4-flash","content":[{"type":"text","text":" feat(admin): add sidebar navigation and real page routes Implement admin layout with sidebar navigation for book and borrow management, including logout functionality and username display. Replace placeholder admin page components with actual page routes."}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":25016,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":0,"service_tier":"standard"},"context_management":null},"parent_tool_use_id":null,"session_id":"bf312622-fae8-446d-8b67-c708b9d3a022","uuid":"1ca45e82-b885-4fa0-b886-fdfced0dc2e4"} --- src/api/client.ts | 23 +++++ src/components/layout/AdminLayout.tsx | 8 +- src/features/admin/AdminBooksPage.tsx | 20 ++-- src/features/admin/AdminBorrowsPage.tsx | 32 +++--- src/features/auth/LoginPage.tsx | 8 +- src/features/books/BooksPage.tsx | 20 ++-- src/features/borrows/MyBorrowsPage.tsx | 131 ++++++++++++++++++------ src/hooks/useIsMobile.ts | 18 ++++ src/lib/formatters.ts | 48 ++++++++- src/router/guards.tsx | 2 +- src/store/authStore.ts | 11 +- 11 files changed, 244 insertions(+), 77 deletions(-) create mode 100644 src/hooks/useIsMobile.ts 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" +}