f28dc98f5e
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>
162 lines
5.8 KiB
TypeScript
162 lines
5.8 KiB
TypeScript
import { Loader2Icon } from "lucide-react"
|
||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||
import { Card, CardContent } from "@/components/ui/card"
|
||
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"
|
||
import { usePlatform } from "@/hooks/usePlatform"
|
||
import { useIsMobile } from "@/hooks/useIsMobile"
|
||
|
||
export default function MyBorrowsPage() {
|
||
const { data: records, isLoading, error } = useMyBorrows()
|
||
const returnBook = useReturnBook()
|
||
const platform = usePlatform()
|
||
const isMobile = useIsMobile()
|
||
const sel = platform === "web" ? ({ "data-selectable": true } as const) : ({} as const)
|
||
|
||
if (isLoading) return <MyBorrowsSkeleton isMobile={isMobile} />
|
||
|
||
if (error) {
|
||
return (
|
||
<div className="mx-auto max-w-[1200px] px-4 py-8">
|
||
<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>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
const list = records ?? []
|
||
|
||
if (list.length === 0) {
|
||
return (
|
||
<div className="mx-auto max-w-[1200px] px-4 py-16 text-center text-muted-foreground">
|
||
<p className="text-lg">暂无借阅记录</p>
|
||
<p className="mt-1 text-sm">去书目页面借阅你感兴趣的图书吧</p>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
if (!isMobile) {
|
||
return (
|
||
<div className="mx-auto max-w-[1200px] px-4 py-8">
|
||
<h1 className="mb-4 text-xl font-semibold">我的借阅</h1>
|
||
<Table>
|
||
<TableHeader>
|
||
<TableRow>
|
||
<TableHead>书名</TableHead>
|
||
<TableHead>作者</TableHead>
|
||
<TableHead>借阅时间</TableHead>
|
||
<TableHead>归还时间</TableHead>
|
||
<TableHead>状态</TableHead>
|
||
<TableHead className="w-[100px]">操作</TableHead>
|
||
</TableRow>
|
||
</TableHeader>
|
||
<TableBody>
|
||
{list.map((r) => (
|
||
<TableRow key={r.id}>
|
||
<TableCell className="font-medium" {...sel}>{r.bookBorrowVo?.name ?? "-"}</TableCell>
|
||
<TableCell {...sel}>{r.bookBorrowVo?.author ?? "-"}</TableCell>
|
||
<TableCell>{formatDateTime(r.borrowTime)}</TableCell>
|
||
<TableCell>
|
||
{r.returnTime ? formatDateTime(r.returnTime) : "-"}
|
||
</TableCell>
|
||
<TableCell>
|
||
<Badge variant={r.status === "BORROWED" ? "default" : "secondary"}>
|
||
{r.status === "BORROWED" ? "借阅中" : "已归还"}
|
||
</Badge>
|
||
</TableCell>
|
||
<TableCell>
|
||
{r.status === "BORROWED" && (
|
||
<Button
|
||
size="sm"
|
||
variant="outline"
|
||
disabled={returnBook.isPending}
|
||
onClick={() => returnBook.mutate(r.id)}
|
||
>
|
||
{returnBook.isPending && <Loader2Icon className="animate-spin" />}
|
||
归还
|
||
</Button>
|
||
)}
|
||
</TableCell>
|
||
</TableRow>
|
||
))}
|
||
</TableBody>
|
||
</Table>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<div className="px-4 py-4">
|
||
<h1 className="mb-4 text-xl font-semibold">我的借阅</h1>
|
||
<div className="space-y-3">
|
||
{list.map((r) => (
|
||
<Card key={r.id}>
|
||
<CardContent className="space-y-1.5 p-4">
|
||
<div className="flex items-center justify-between">
|
||
<span className="text-xs text-muted-foreground">#{r.id}</span>
|
||
<Badge variant={r.status === "BORROWED" ? "default" : "secondary"}>
|
||
{r.status === "BORROWED" ? "借阅中" : "已归还"}
|
||
</Badge>
|
||
</div>
|
||
<div className="font-medium" {...sel}>{r.bookBorrowVo?.name ?? "-"}</div>
|
||
<div className="text-sm text-muted-foreground" {...sel}>{r.bookBorrowVo?.author ?? "-"}</div>
|
||
<div className="text-sm text-muted-foreground">
|
||
借阅:{formatDateTime(r.borrowTime)}
|
||
{r.returnTime && ` | 归还:${formatDateTime(r.returnTime)}`}
|
||
</div>
|
||
{r.status === "BORROWED" && (
|
||
<div className="pt-1">
|
||
<Button
|
||
size="sm"
|
||
variant="outline"
|
||
disabled={returnBook.isPending}
|
||
onClick={() => returnBook.mutate(r.id)}
|
||
>
|
||
{returnBook.isPending && <Loader2Icon className="animate-spin" />}
|
||
归还
|
||
</Button>
|
||
</div>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
function MyBorrowsSkeleton({ isMobile }: { isMobile: boolean }) {
|
||
if (isMobile) {
|
||
return (
|
||
<div className="space-y-3 px-4 py-4">
|
||
<Skeleton className="h-7 w-24" />
|
||
{Array.from({ length: 5 }).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 className="mx-auto max-w-[1200px] px-4 py-8">
|
||
<Skeleton className="mb-4 h-7 w-24" />
|
||
<div className="space-y-2">
|
||
{Array.from({ length: 5 }).map((_, i) => (
|
||
<Skeleton key={i} className="h-10 w-full" />
|
||
))}
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|