2025-05-22 20:42:10 +08:00

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">
&copy; {new Date().getFullYear()} . .
</p>
</div>
</footer>
</div>
)
}