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 (
)
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: ,
+ },
+ ],
},
],
},