333 lines
12 KiB
TypeScript
333 lines
12 KiB
TypeScript
"use client"
|
|
|
|
import { MainNav } from "@/components/main-nav"
|
|
import { SearchBar } from "@/components/search-bar"
|
|
import { ThemeToggle } from "@/components/theme-toggle"
|
|
import { ShoppingCart } from "@/components/shopping-cart"
|
|
import { UserNav } from "@/components/user-nav"
|
|
import { Button } from "@/components/ui/button"
|
|
import { Card, CardContent } from "@/components/ui/card"
|
|
import { Separator } from "@/components/ui/separator"
|
|
import { useToast } from "@/components/ui/use-toast"
|
|
import { useCart } from "@/context/cart-context"
|
|
import {
|
|
BookOpen,
|
|
Calendar,
|
|
DollarSign,
|
|
MinusCircle,
|
|
PlusCircle,
|
|
ShoppingCartIcon as CartIcon,
|
|
Heart,
|
|
} from "lucide-react"
|
|
import { useEffect, useState, use } from "react"
|
|
import { fetchWithAuth } from "@/lib/api"
|
|
import { motion } from "framer-motion"
|
|
import { notify } from "@/lib/event-bus"
|
|
import { Badge } from "@/components/ui/badge"
|
|
|
|
interface BookDetail {
|
|
bookId: number
|
|
title: string
|
|
isbn: string
|
|
price: number
|
|
stock: number
|
|
publishDate: string
|
|
publisherName: string
|
|
description: string
|
|
coverImage: string
|
|
author: string[]
|
|
}
|
|
|
|
export default function BookDetailPage({ params }: { params: Promise<{ id: string }> | { id: string } }) {
|
|
// 使用 React.use() 解包 params
|
|
const resolvedParams = "then" in params ? use(params) : params
|
|
const bookId = resolvedParams.id
|
|
|
|
const [book, setBook] = useState<BookDetail | null>(null)
|
|
const [loading, setLoading] = useState(true)
|
|
const [quantity, setQuantity] = useState(1)
|
|
const [isFavorite, setIsFavorite] = useState(false)
|
|
const { toast } = useToast()
|
|
const { addToCart } = useCart()
|
|
|
|
useEffect(() => {
|
|
const fetchBook = async () => {
|
|
try {
|
|
const response = await fetchWithAuth(`book/${bookId}`)
|
|
const result = await response.json()
|
|
|
|
if (result.code === 0) {
|
|
setBook(result.data)
|
|
} else {
|
|
toast({
|
|
variant: "destructive",
|
|
title: "获取图书失败",
|
|
description: result.msg || "无法获取图书信息",
|
|
})
|
|
}
|
|
} catch (error) {
|
|
toast({
|
|
variant: "destructive",
|
|
title: "获取图书失败",
|
|
description: "服务器连接错误,请稍后再试",
|
|
})
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
fetchBook()
|
|
}, [bookId, toast])
|
|
|
|
const handleAddToCart = () => {
|
|
if (book) {
|
|
addToCart({
|
|
id: book.bookId,
|
|
title: book.title,
|
|
price: book.price,
|
|
quantity,
|
|
coverImage: book.coverImage || "/placeholder.svg?height=100&width=80",
|
|
})
|
|
|
|
notify({
|
|
title: "已添加到购物车",
|
|
message: `${book.title} x ${quantity}`,
|
|
type: "success",
|
|
duration: 3000,
|
|
})
|
|
}
|
|
}
|
|
|
|
const increaseQuantity = () => {
|
|
if (book && quantity < book.stock) {
|
|
setQuantity(quantity + 1)
|
|
}
|
|
}
|
|
|
|
const decreaseQuantity = () => {
|
|
if (quantity > 1) {
|
|
setQuantity(quantity - 1)
|
|
}
|
|
}
|
|
|
|
const toggleFavorite = () => {
|
|
setIsFavorite(!isFavorite)
|
|
|
|
notify({
|
|
title: isFavorite ? "已移除收藏" : "已添加收藏",
|
|
message: book?.title || "",
|
|
type: "info",
|
|
duration: 2000,
|
|
})
|
|
}
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="flex min-h-screen flex-col">
|
|
<header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
|
<div className="container flex h-16 items-center justify-between">
|
|
<MainNav />
|
|
<div className="flex items-center gap-4">
|
|
<SearchBar />
|
|
<ShoppingCart />
|
|
<ThemeToggle />
|
|
<UserNav />
|
|
</div>
|
|
</div>
|
|
</header>
|
|
<main className="flex-1 container py-8">
|
|
<div className="grid gap-8 md:grid-cols-2">
|
|
<div className="flex justify-center">
|
|
<div className="aspect-[3/4] w-full max-w-[350px] rounded-lg loading-shimmer" />
|
|
</div>
|
|
<div className="space-y-6">
|
|
<div className="h-8 w-3/4 loading-shimmer rounded" />
|
|
<div className="h-4 w-1/2 loading-shimmer rounded" />
|
|
<Separator />
|
|
<div className="space-y-4">
|
|
<div className="h-6 w-1/4 loading-shimmer rounded" />
|
|
<div className="h-4 w-1/3 loading-shimmer rounded" />
|
|
<div className="h-4 w-1/2 loading-shimmer rounded" />
|
|
</div>
|
|
<Separator />
|
|
<div className="h-10 w-full loading-shimmer rounded" />
|
|
</div>
|
|
</div>
|
|
<div className="mt-12">
|
|
<div className="h-6 w-1/4 loading-shimmer rounded mb-4" />
|
|
<div className="h-40 w-full loading-shimmer rounded" />
|
|
</div>
|
|
</main>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (!book) {
|
|
return (
|
|
<div className="flex min-h-screen flex-col">
|
|
<header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
|
<div className="container flex h-16 items-center justify-between">
|
|
<MainNav />
|
|
<div className="flex items-center gap-4">
|
|
<SearchBar />
|
|
<ShoppingCart />
|
|
<ThemeToggle />
|
|
<UserNav />
|
|
</div>
|
|
</div>
|
|
</header>
|
|
<main className="flex-1 container py-8">
|
|
<div className="flex flex-col justify-center items-center h-[60vh]">
|
|
<BookOpen className="h-16 w-16 text-muted-foreground mb-4" />
|
|
<h2 className="text-2xl font-semibold">图书不存在</h2>
|
|
<p className="text-muted-foreground mt-2">该图书可能已被删除或移动</p>
|
|
<Button className="mt-6" asChild>
|
|
<a href="/books">浏览所有图书</a>
|
|
</Button>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="flex min-h-screen flex-col">
|
|
<header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
|
<div className="container flex h-16 items-center justify-between">
|
|
<MainNav />
|
|
<div className="flex items-center gap-4">
|
|
<SearchBar />
|
|
<ShoppingCart />
|
|
<ThemeToggle />
|
|
<UserNav />
|
|
</div>
|
|
</div>
|
|
</header>
|
|
<main className="flex-1">
|
|
<div className="container py-8">
|
|
<motion.div
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
transition={{ duration: 0.5 }}
|
|
className="grid gap-8 md:grid-cols-2"
|
|
>
|
|
<motion.div
|
|
initial={{ opacity: 0, x: -20 }}
|
|
animate={{ opacity: 1, x: 0 }}
|
|
transition={{ duration: 0.5, delay: 0.2 }}
|
|
className="flex justify-center"
|
|
>
|
|
<img
|
|
src={book.coverImage || "/placeholder.svg?height=500&width=350"}
|
|
alt={book.title}
|
|
className="rounded-lg object-cover max-h-[500px] w-auto book-detail-image"
|
|
/>
|
|
</motion.div>
|
|
<motion.div
|
|
initial={{ opacity: 0, x: 20 }}
|
|
animate={{ opacity: 1, x: 0 }}
|
|
transition={{ duration: 0.5, delay: 0.2 }}
|
|
className="space-y-6"
|
|
>
|
|
<div>
|
|
<div className="flex justify-between items-start">
|
|
<h1 className="text-3xl font-bold">{book.title}</h1>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={toggleFavorite}
|
|
className={isFavorite ? "text-red-500" : "text-muted-foreground"}
|
|
>
|
|
<Heart className={`h-5 w-5 ${isFavorite ? "fill-current" : ""}`} />
|
|
</Button>
|
|
</div>
|
|
<div className="mt-2 flex items-center">
|
|
<p className="text-muted-foreground">作者: {book.author.join(", ")}</p>
|
|
</div>
|
|
</div>
|
|
<Separator />
|
|
<div className="space-y-4">
|
|
<div className="flex items-center gap-2">
|
|
<DollarSign className="h-5 w-5 text-primary" />
|
|
<span className="text-2xl font-bold price-tag">¥{book.price.toFixed(2)}</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<BookOpen className="h-5 w-5 text-muted-foreground" />
|
|
<span>ISBN: {book.isbn}</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<Calendar className="h-5 w-5 text-muted-foreground" />
|
|
<span>出版日期: {new Date(book.publishDate).toLocaleDateString()}</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<span>出版社: </span>
|
|
<Badge variant="outline">{book.publisherName}</Badge>
|
|
</div>
|
|
<div>
|
|
<span
|
|
className={book.stock > 0 ? "text-green-600 dark:text-green-400" : "text-red-600 dark:text-red-400"}
|
|
>
|
|
{book.stock > 0 ? `库存: ${book.stock}` : "缺货"}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<Separator />
|
|
<div className="space-y-4">
|
|
<div className="flex items-center">
|
|
<Button
|
|
variant="outline"
|
|
size="icon"
|
|
onClick={decreaseQuantity}
|
|
disabled={quantity <= 1}
|
|
className="rounded-full h-10 w-10"
|
|
>
|
|
<MinusCircle className="h-4 w-4" />
|
|
</Button>
|
|
<span className="w-12 text-center font-medium text-lg">{quantity}</span>
|
|
<Button
|
|
variant="outline"
|
|
size="icon"
|
|
onClick={increaseQuantity}
|
|
disabled={book.stock <= quantity}
|
|
className="rounded-full h-10 w-10"
|
|
>
|
|
<PlusCircle className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
<Button
|
|
className="w-full h-12 text-base btn-hover-effect"
|
|
onClick={handleAddToCart}
|
|
disabled={book.stock <= 0}
|
|
>
|
|
<CartIcon className="mr-2 h-5 w-5" />
|
|
添加到购物车
|
|
</Button>
|
|
</div>
|
|
</motion.div>
|
|
</motion.div>
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.5, delay: 0.4 }}
|
|
className="mt-12"
|
|
>
|
|
<h2 className="text-2xl font-bold mb-4">图书简介</h2>
|
|
<Card className="overflow-hidden">
|
|
<CardContent className="p-6">
|
|
<p className="leading-7">{book.description || "暂无简介"}</p>
|
|
</CardContent>
|
|
</Card>
|
|
</motion.div>
|
|
</div>
|
|
</main>
|
|
<footer className="border-t py-6 md:py-0">
|
|
<div className="container flex flex-col items-center justify-between gap-4 md:h-24 md:flex-row">
|
|
<p className="text-center text-sm leading-loose text-muted-foreground md:text-left">
|
|
© {new Date().getFullYear()} 图书管理系统. 保留所有权利.
|
|
</p>
|
|
</div>
|
|
</footer>
|
|
</div>
|
|
)
|
|
}
|