diff --git a/src/components/layout/AppLayout.tsx b/src/components/layout/AppLayout.tsx index e530836..9ada59a 100644 --- a/src/components/layout/AppLayout.tsx +++ b/src/components/layout/AppLayout.tsx @@ -1,8 +1,51 @@ -import { Outlet } from "react-router-dom" +import { Link, Outlet, useNavigate } from "react-router-dom" +import { useAuthStore } from "@/store/authStore" +import { Button } from "@/components/ui/button" export default function AppLayout() { + const token = useAuthStore((s) => s.token) + const username = useAuthStore((s) => s.username) + const clearAuth = useAuthStore((s) => s.clearAuth) + const navigate = useNavigate() + + function handleLogout() { + clearAuth() + navigate("/books", { replace: true }) + } + return (
+
+
+ + 图书管理系统 + + +
+ {token ? ( + <> + {username} + + + ) : ( + + )} +
+
+
) diff --git a/src/features/books/BooksPage.tsx b/src/features/books/BooksPage.tsx new file mode 100644 index 0000000..2db3223 --- /dev/null +++ b/src/features/books/BooksPage.tsx @@ -0,0 +1,139 @@ +import { Loader2Icon } from "lucide-react" +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" +import { Card, CardContent } from "@/components/ui/card" +import { Button } from "@/components/ui/button" +import { Skeleton } from "@/components/ui/skeleton" +import { useBooks, useBorrowBook } from "./hooks" +import { useAuthStore } from "@/store/authStore" +import { usePlatform } from "@/hooks/usePlatform" + +export default function BooksPage() { + const { data: books, isLoading, error } = useBooks() + const borrow = useBorrowBook() + const token = useAuthStore((s) => s.token) + const platform = usePlatform() + const isDesktop = platform === "web" || platform === "desktop" + + if (isLoading) return + + if (error) { + return ( +
+
+ 加载失败:{error instanceof Error ? error.message : "未知错误"} +
+
+ ) + } + + const list = books ?? [] + + if (list.length === 0) { + return ( +
+ 暂无书目 +
+ ) + } + + if (isDesktop) { + return ( +
+

书目列表

+ + + + 书名 + 作者 + 库存 + {token && 操作} + + + + {list.map((book) => ( + + {book.name} + {book.author} + {book.stock} + {token && ( + + + + )} + + ))} + +
+
+ ) + } + + return ( +
+

书目列表

+
+ {list.map((book) => ( + + +
+
{book.name}
+
{book.author}
+
库存:{book.stock}
+
+ {token && ( + + )} +
+
+ ))} +
+
+ ) +} + +function BooksSkeleton({ isDesktop }: { isDesktop: boolean }) { + if (!isDesktop) { + return ( +
+ + {Array.from({ length: 5 }).map((_, i) => ( + + +
+ + + +
+ +
+
+ ))} +
+ ) + } + + return ( +
+ +
+ {Array.from({ length: 8 }).map((_, i) => ( + + ))} +
+
+ ) +} diff --git a/src/features/books/hooks.ts b/src/features/books/hooks.ts new file mode 100644 index 0000000..fabac8f --- /dev/null +++ b/src/features/books/hooks.ts @@ -0,0 +1,27 @@ +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query" +import { getAllBooks } from "@/api/books" +import { borrowBookForMe } from "@/api/borrows" +import { toast } from "sonner" + +export function useBooks() { + return useQuery({ + queryKey: ["books"], + queryFn: getAllBooks, + staleTime: 30_000, + }) +} + +export function useBorrowBook() { + const qc = useQueryClient() + return useMutation({ + mutationFn: borrowBookForMe, + onSuccess: () => { + qc.invalidateQueries({ queryKey: ["books"] }) + qc.invalidateQueries({ queryKey: ["myBorrows"] }) + toast.success("借阅成功") + }, + onError: (err) => { + toast.error(err instanceof Error ? err.message : "借阅失败") + }, + }) +} diff --git a/src/features/borrows/MyBorrowsPage.tsx b/src/features/borrows/MyBorrowsPage.tsx new file mode 100644 index 0000000..614749e --- /dev/null +++ b/src/features/borrows/MyBorrowsPage.tsx @@ -0,0 +1,96 @@ +import { Loader2Icon } from "lucide-react" +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { Skeleton } from "@/components/ui/skeleton" +import { useMyBorrows, useReturnBook } from "./hooks" +import { formatDateTime } from "@/lib/formatters" + +export default function MyBorrowsPage() { + const { data: records, isLoading, error } = useMyBorrows() + const returnBook = useReturnBook() + + if (isLoading) return + + if (error) { + return ( +
+
+ 加载失败:{error instanceof Error ? error.message : "未知错误"} +
+
+ ) + } + + const list = records ?? [] + + if (list.length === 0) { + return ( +
+

暂无借阅记录

+

去书目页面借阅你感兴趣的图书吧

+
+ ) + } + + return ( +
+

我的借阅

+ + + + 书名 + 作者 + 借阅时间 + 归还时间 + 状态 + 操作 + + + + {list.map((r) => ( + + {r.bookBorrowVo.name} + {r.bookBorrowVo.author} + {formatDateTime(r.borrowTime)} + + {r.returnTime ? formatDateTime(r.returnTime) : "-"} + + + + {r.status === "BORROWED" ? "借阅中" : "已归还"} + + + + {r.status === "BORROWED" && ( + + )} + + + ))} + +
+
+ ) +} + +function MyBorrowsSkeleton() { + return ( +
+ +
+ {Array.from({ length: 5 }).map((_, i) => ( + + ))} +
+
+ ) +} diff --git a/src/features/borrows/hooks.ts b/src/features/borrows/hooks.ts new file mode 100644 index 0000000..e064ba0 --- /dev/null +++ b/src/features/borrows/hooks.ts @@ -0,0 +1,24 @@ +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query" +import { getAllMyBorrows, returnBookForMe } from "@/api/borrows" +import { toast } from "sonner" + +export function useMyBorrows() { + return useQuery({ + queryKey: ["myBorrows"], + queryFn: getAllMyBorrows, + }) +} + +export function useReturnBook() { + const qc = useQueryClient() + return useMutation({ + mutationFn: returnBookForMe, + onSuccess: () => { + qc.invalidateQueries({ queryKey: ["myBorrows"] }) + toast.success("归还成功") + }, + onError: (err) => { + toast.error(err instanceof Error ? err.message : "归还失败") + }, + }) +} diff --git a/src/lib/formatters.ts b/src/lib/formatters.ts index 6ec5085..3c2a4f8 100644 --- a/src/lib/formatters.ts +++ b/src/lib/formatters.ts @@ -1,5 +1,11 @@ export function formatDateTime(iso: string): string { - return new Date(iso).toLocaleString() + const d = new Date(iso) + const Y = d.getFullYear() + const M = String(d.getMonth() + 1).padStart(2, "0") + const D = String(d.getDate()).padStart(2, "0") + const h = String(d.getHours()).padStart(2, "0") + const m = String(d.getMinutes()).padStart(2, "0") + return `${Y}-${M}-${D} ${h}:${m}` } export function formatDate(iso: string): string { diff --git a/src/router/index.tsx b/src/router/index.tsx index b3822b3..0e6076e 100644 --- a/src/router/index.tsx +++ b/src/router/index.tsx @@ -1,14 +1,11 @@ import { createBrowserRouter, Navigate } from "react-router-dom" import { ProtectedRoute, AdminRoute } from "./guards" import LoginPage from "@/features/auth/LoginPage" +import BooksPage from "@/features/books/BooksPage" +import MyBorrowsPage from "@/features/borrows/MyBorrowsPage" +import AppLayout from "@/components/layout/AppLayout" // Placeholder components — real pages will replace these -function BooksPage() { - return
Books
-} -function MyBorrowsPage() { - return
My Borrows
-} function AdminBooksPage() { return
Admin / Books
} @@ -25,19 +22,24 @@ export const router = createBrowserRouter([ element: , }, { - path: "/", - element: , - }, - { - path: "/books", - element: , - }, - { - element: , + element: , children: [ { - path: "/my-borrows", - element: , + path: "/", + element: , + }, + { + path: "/books", + element: , + }, + { + element: , + children: [ + { + path: "/my-borrows", + element: , + }, + ], }, ], },