Files
bookMgr-client/src/features/admin/AdminBorrowsPage.tsx
T
msksbr 10dbfa7638 {"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":"<commit>
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"}<commit>
2026-05-24 19:41:50 +08:00

223 lines
7.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useEffect } from "react"
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 { Input } from "@/components/ui/input"
import { Badge } from "@/components/ui/badge"
import { Skeleton } from "@/components/ui/skeleton"
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog"
import { useAdminBorrows, useSearchBorrows, useAdminReturnBook } from "./hooks"
import { usePlatform } from "@/hooks/usePlatform"
import { formatDateTime } from "@/lib/formatters"
import type { BorrowInfoVo } from "@/types/api"
export default function AdminBorrowsPage() {
const platform = usePlatform()
const isDesktop = platform === "web" || platform === "desktop"
const [searchText, setSearchText] = useState("")
const [debounced, setDebounced] = useState("")
useEffect(() => {
const t = setTimeout(() => setDebounced(searchText), 300)
return () => clearTimeout(t)
}, [searchText])
const isSearching = debounced.length > 0
const allQuery = useAdminBorrows()
const searchQuery = useSearchBorrows(debounced)
const { data, isLoading, error } = isSearching ? searchQuery : allQuery
const returnMutation = useAdminReturnBook()
const [returnTarget, setReturnTarget] = useState<BorrowInfoVo | null>(null)
if (isLoading) return <AdminBorrowsSkeleton isDesktop={isDesktop} />
if (error) {
return (
<div className="rounded-lg border border-destructive/50 bg-destructive/10 px-4 py-3 text-sm text-destructive">
{error instanceof Error ? error.message : "未知错误"}
</div>
)
}
const list = data ?? []
return (
<div>
<h1 className="mb-4 text-xl font-semibold"></h1>
<div className="mb-4">
<Input
placeholder="搜索借阅记录(书名/用户名)..."
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
className="max-w-sm"
/>
</div>
{list.length === 0 ? (
<p className="py-16 text-center text-muted-foreground">
{isSearching ? "无匹配记录" : "暂无借阅记录"}
</p>
) : isDesktop ? (
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[60px]">ID</TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="w-[110px]"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{list.map((r) => (
<BorrowRow
key={r.id}
record={r}
isDesktop
returning={returnMutation.isPending}
onReturn={() => setReturnTarget(r)}
/>
))}
</TableBody>
</Table>
) : (
<div className="space-y-3">
{list.map((r) => (
<Card key={r.id}>
<CardContent className="space-y-1.5 p-4">
<BorrowRow
record={r}
isDesktop={false}
returning={returnMutation.isPending}
onReturn={() => setReturnTarget(r)}
/>
</CardContent>
</Card>
))}
</div>
)}
<AlertDialog open={!!returnTarget} onOpenChange={(o) => !o && setReturnTarget(null)}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle></AlertDialogTitle>
<AlertDialogDescription>
{returnTarget?.userBorrowVo.username} {returnTarget?.bookBorrowVo.name}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={returnMutation.isPending}></AlertDialogCancel>
<AlertDialogAction
disabled={returnMutation.isPending}
onClick={() => returnMutation.mutateAsync(returnTarget!.id).then(() => setReturnTarget(null))}
>
{returnMutation.isPending && <Loader2Icon className="animate-spin" />}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
)
}
function BorrowRow({
record,
isDesktop,
returning,
onReturn,
}: {
record: BorrowInfoVo
isDesktop: boolean
returning: boolean
onReturn: () => void
}) {
const statusBadge = (
<Badge variant={record.status === "BORROWED" ? "default" : "secondary"}>
{record.status === "BORROWED" ? "借阅中" : "已归还"}
</Badge>
)
const returnButton = record.status === "BORROWED" && (
<Button size={isDesktop ? "xs" : "sm"} variant="outline" disabled={returning} onClick={onReturn}>
{returning && <Loader2Icon className="animate-spin" />}
</Button>
)
if (isDesktop) {
return (
<TableRow>
<TableCell>{record.id}</TableCell>
<TableCell className="font-medium">{record.bookBorrowVo.name}</TableCell>
<TableCell>{record.userBorrowVo.username}</TableCell>
<TableCell>{formatDateTime(record.borrowTime)}</TableCell>
<TableCell>{record.returnTime ? formatDateTime(record.returnTime) : "-"}</TableCell>
<TableCell>{statusBadge}</TableCell>
<TableCell>{returnButton}</TableCell>
</TableRow>
)
}
return (
<>
<div className="flex items-center justify-between">
<span className="text-xs text-muted-foreground">#{record.id}</span>
{statusBadge}
</div>
<div className="font-medium">{record.bookBorrowVo.name}</div>
<div className="text-sm text-muted-foreground">{record.userBorrowVo.username}</div>
<div className="text-sm text-muted-foreground">
{formatDateTime(record.borrowTime)}
{record.returnTime && ` | 归还:${formatDateTime(record.returnTime)}`}
</div>
{returnButton && <div className="pt-1">{returnButton}</div>}
</>
)
}
function AdminBorrowsSkeleton({ isDesktop }: { isDesktop: boolean }) {
if (!isDesktop) {
return (
<div className="space-y-3">
<Skeleton className="h-7 w-24" />
<Skeleton className="h-8 w-full max-w-sm rounded-lg" />
{Array.from({ length: 4 }).map((_, i) => (
<Card key={i}>
<CardContent className="space-y-2 p-4">
<Skeleton className="h-4 w-16" />
<Skeleton className="h-5 w-36" />
<Skeleton className="h-4 w-28" />
<Skeleton className="h-4 w-40" />
</CardContent>
</Card>
))}
</div>
)
}
return (
<div>
<Skeleton className="mb-4 h-7 w-24" />
<Skeleton className="mb-4 h-8 w-64 rounded-lg" />
<div className="space-y-2">
{Array.from({ length: 5 }).map((_, i) => (
<Skeleton key={i} className="h-10 w-full" />
))}
</div>
</div>
)
}