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:
@@ -31,7 +31,7 @@ export default function AdminLayout() {
|
||||
if (!isMobile) {
|
||||
return (
|
||||
<div className="flex min-h-screen">
|
||||
<aside className="fixed top-0 left-0 flex h-full w-56 flex-col border-r bg-background">
|
||||
<aside className="fixed top-0 left-0 z-50 flex h-full w-56 flex-col border-r bg-background">
|
||||
<div className="flex h-12 items-center gap-2 border-b px-4">
|
||||
<span className="font-heading text-base font-semibold">管理后台</span>
|
||||
</div>
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog"
|
||||
import { useAdminBooks, useAddBook, useUpdateBook, useUpdateStock, useDeleteBook } from "./hooks"
|
||||
import { useAdminBooks, useAddBook, useUpdateBook, useUpdateStock, useDeleteBook, useAdminBorrowBook } from "./hooks"
|
||||
import { usePlatform } from "@/hooks/usePlatform"
|
||||
import { useIsMobile } from "@/hooks/useIsMobile"
|
||||
import type { Book } from "@/types/api"
|
||||
@@ -27,6 +27,7 @@ interface AddDialog { open: boolean }
|
||||
interface EditDialog { open: boolean; book: Book }
|
||||
interface StockDialog { open: boolean; book: Book }
|
||||
type DeleteDialog = { open: boolean; book: Book } | { open: false; book: null }
|
||||
interface BorrowDialog { open: boolean; book: Book }
|
||||
|
||||
const addInitial: AddDialog = { open: false }
|
||||
|
||||
@@ -40,6 +41,7 @@ export default function AdminBooksPage() {
|
||||
const [edit, setEdit] = useState<EditDialog | null>(null)
|
||||
const [stock, setStock] = useState<StockDialog | null>(null)
|
||||
const [del, setDel] = useState<DeleteDialog>({ open: false, book: null })
|
||||
const [borrow, setBorrow] = useState<BorrowDialog | null>(null)
|
||||
|
||||
if (isLoading) return <AdminBooksSkeleton isMobile={isMobile} />
|
||||
if (error) {
|
||||
@@ -93,6 +95,9 @@ export default function AdminBooksPage() {
|
||||
<Button size="xs" variant="destructive" onClick={() => setDel({ open: true, book: b })}>
|
||||
删除
|
||||
</Button>
|
||||
<Button size="xs" variant="outline" disabled={b.stock <= 0} onClick={() => setBorrow({ open: true, book: b })}>
|
||||
手动借阅
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -120,6 +125,9 @@ export default function AdminBooksPage() {
|
||||
<Button size="xs" variant="destructive" onClick={() => setDel({ open: true, book: b })}>
|
||||
删除
|
||||
</Button>
|
||||
<Button size="xs" variant="outline" disabled={b.stock <= 0} onClick={() => setBorrow({ open: true, book: b })}>
|
||||
手动借阅
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -133,6 +141,7 @@ export default function AdminBooksPage() {
|
||||
{del.book && (
|
||||
<DeleteBookDialog book={del.book} open onClose={() => setDel({ open: false, book: null })} />
|
||||
)}
|
||||
{borrow && <BorrowDialog_ book={borrow.book} open onClose={() => setBorrow(null)} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -302,6 +311,45 @@ function DeleteBookDialog({ book, open, onClose }: { book: Book; open: boolean;
|
||||
)
|
||||
}
|
||||
|
||||
// ---- Manual Borrow Dialog ----
|
||||
function BorrowDialog_({ book, open, onClose }: { book: Book; open: boolean; onClose: () => void }) {
|
||||
const mutation = useAdminBorrowBook()
|
||||
const [userId, setUserId] = useState("")
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) setUserId("")
|
||||
}, [open])
|
||||
|
||||
async function submit(e: FormEvent) {
|
||||
e.preventDefault()
|
||||
if (!userId.trim()) return
|
||||
await mutation.mutateAsync({ bookId: book.id!, userId: Number(userId) })
|
||||
onClose()
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(o) => !o && onClose()}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>手动借阅 — {book.name}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<form onSubmit={submit} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="borrow-userid">用户 ID</Label>
|
||||
<Input id="borrow-userid" type="number" min={1} required value={userId} onChange={(e) => setUserId(e.target.value)} placeholder="输入用户 ID" />
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button type="submit" disabled={mutation.isPending}>
|
||||
{mutation.isPending && <Loader2Icon className="animate-spin" />}
|
||||
确认借阅
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
// ---- Skeleton ----
|
||||
function AdminBooksSkeleton({ isMobile }: { isMobile: boolean }) {
|
||||
if (isMobile) {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
|
||||
import { toast } from "sonner"
|
||||
import { getAllBooks } from "@/api/books"
|
||||
import { addBook, deleteBook, updateBook, updateStock } from "@/api/admin/books"
|
||||
import { getAllBorrows, searchBorrows, returnBook } from "@/api/admin/borrows"
|
||||
import { getAllBorrows, searchBorrows, returnBook, borrowBook as adminBorrowBook } from "@/api/admin/borrows"
|
||||
|
||||
export function useAdminBooks() {
|
||||
return useQuery({
|
||||
@@ -96,3 +96,18 @@ export function useAdminReturnBook() {
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useAdminBorrowBook() {
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: ({ bookId, userId }: { bookId: number; userId: number }) =>
|
||||
adminBorrowBook(bookId, userId),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ["admin", "borrows"] })
|
||||
toast.success("借阅成功")
|
||||
},
|
||||
onError: (err) => {
|
||||
toast.error(err instanceof Error ? err.message : "借阅失败")
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createBrowserRouter, Navigate } from "react-router-dom"
|
||||
import { ProtectedRoute, AdminRoute } from "./guards"
|
||||
import { useAuthStore } from "@/store/authStore"
|
||||
import LoginPage from "@/features/auth/LoginPage"
|
||||
import BooksPage from "@/features/books/BooksPage"
|
||||
import MyBorrowsPage from "@/features/borrows/MyBorrowsPage"
|
||||
@@ -8,6 +9,11 @@ import AdminBorrowsPage from "@/features/admin/AdminBorrowsPage"
|
||||
import AppLayout from "@/components/layout/AppLayout"
|
||||
import AdminLayout from "@/components/layout/AdminLayout"
|
||||
|
||||
function RootRedirect() {
|
||||
const isAdmin = useAuthStore((s) => s.isAdmin())
|
||||
return <Navigate to={isAdmin ? "/admin/books" : "/books"} replace />
|
||||
}
|
||||
|
||||
export const router = createBrowserRouter([
|
||||
{
|
||||
path: "/login",
|
||||
@@ -18,7 +24,7 @@ export const router = createBrowserRouter([
|
||||
children: [
|
||||
{
|
||||
path: "/",
|
||||
element: <Navigate to="/books" replace />,
|
||||
element: <RootRedirect />,
|
||||
},
|
||||
{
|
||||
path: "/books",
|
||||
|
||||
Reference in New Issue
Block a user