555 lines
21 KiB
TypeScript
555 lines
21 KiB
TypeScript
"use client"
|
||
|
||
import { AdminNav } from "@/components/admin-nav"
|
||
import { ThemeToggle } from "@/components/theme-toggle"
|
||
import { Button } from "@/components/ui/button"
|
||
import { Calendar } from "@/components/ui/calendar"
|
||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
|
||
import { Combobox } from "@/components/ui/combobox"
|
||
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"
|
||
import { Input } from "@/components/ui/input"
|
||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
|
||
import { Skeleton } from "@/components/ui/skeleton"
|
||
import { Textarea } from "@/components/ui/textarea"
|
||
import { useToast } from "@/components/ui/use-toast"
|
||
import { useAuth } from "@/context/auth-context"
|
||
import { fetchWithAuth } from "@/lib/api"
|
||
import { zodResolver } from "@hookform/resolvers/zod"
|
||
import { format } from "date-fns"
|
||
import { ArrowLeft, BookOpen, CalendarIcon, ImageIcon, Loader2, Save, X } from "lucide-react"
|
||
import { useRouter } from "next/navigation"
|
||
import { useEffect, useState, use } from "react"
|
||
import { useFieldArray, useForm } from "react-hook-form"
|
||
import * as z from "zod"
|
||
import { Plus } from "lucide-react"
|
||
|
||
interface Publisher {
|
||
publisherId: number
|
||
name: string
|
||
address: string
|
||
}
|
||
|
||
interface BookDetail {
|
||
bookId: number
|
||
title: string
|
||
isbn: string
|
||
price: number
|
||
stock: number
|
||
publishDate: string
|
||
publisherId: number
|
||
publisherName: string
|
||
description: string
|
||
coverImage: string
|
||
author: string[]
|
||
}
|
||
|
||
const formSchema = z.object({
|
||
title: z.string().min(1, "标题不能为空"),
|
||
isbn: z.string().min(1, "ISBN不能为空"),
|
||
price: z.coerce.number().min(0, "价格不能为负数"),
|
||
stock: z.coerce.number().min(0, "库存不能为负数").int("库存必须是整数"),
|
||
publishDate: z.date(),
|
||
publisherId: z.coerce.number(),
|
||
description: z.string().optional(),
|
||
coverImage: z.string().optional(),
|
||
authors: z
|
||
.array(
|
||
z.object({
|
||
name: z.string().min(1, "作者名不能为空"),
|
||
}),
|
||
)
|
||
.min(1, "至少需要一位作者"),
|
||
})
|
||
|
||
export default function EditBookPage({ params }: { params: Promise<{ id: string }> | { id: string } }) {
|
||
// 使用 React.use() 解包 params
|
||
const resolvedParams = "then" in params ? use(params) : params
|
||
const bookId = resolvedParams.id
|
||
|
||
const { user } = useAuth()
|
||
const router = useRouter()
|
||
const { toast } = useToast()
|
||
const [publishers, setPublishers] = useState<Publisher[]>([])
|
||
const [book, setBook] = useState<BookDetail | null>(null)
|
||
const [loading, setLoading] = useState(true)
|
||
const [submitting, setSubmitting] = useState(false)
|
||
const [coverPreview, setCoverPreview] = useState<string | null>(null)
|
||
|
||
const form = useForm<z.infer<typeof formSchema>>({
|
||
resolver: zodResolver(formSchema),
|
||
defaultValues: {
|
||
title: "",
|
||
isbn: "",
|
||
price: 0,
|
||
stock: 0,
|
||
publishDate: new Date(),
|
||
publisherId: 0,
|
||
description: "",
|
||
coverImage: "",
|
||
authors: [{ name: "" }],
|
||
},
|
||
})
|
||
|
||
const { fields, append, remove } = useFieldArray({
|
||
control: form.control,
|
||
name: "authors",
|
||
})
|
||
|
||
// 监听封面图片URL变化,更新预览
|
||
const coverImageValue = form.watch("coverImage")
|
||
useEffect(() => {
|
||
if (coverImageValue) {
|
||
setCoverPreview(coverImageValue)
|
||
}
|
||
}, [coverImageValue])
|
||
|
||
useEffect(() => {
|
||
// 检查用户是否登录且是管理员
|
||
if (!user) {
|
||
toast({
|
||
title: "请先登录",
|
||
description: "您需要登录后才能访问管理页面",
|
||
variant: "destructive",
|
||
})
|
||
router.push("/login")
|
||
return
|
||
}
|
||
|
||
if (!user.isAdmin) {
|
||
toast({
|
||
title: "权限不足",
|
||
description: "您没有管理员权限",
|
||
variant: "destructive",
|
||
})
|
||
router.push("/")
|
||
return
|
||
}
|
||
|
||
// 获取出版社列表
|
||
const fetchPublishers = async () => {
|
||
try {
|
||
const response = await fetchWithAuth("publisher/all")
|
||
const result = await response.json()
|
||
|
||
if (result.code === 0) {
|
||
setPublishers(result.data)
|
||
} else {
|
||
toast({
|
||
variant: "destructive",
|
||
title: "获取出版社失败",
|
||
description: result.msg || "无法获取出版社信息",
|
||
})
|
||
}
|
||
} catch (error) {
|
||
toast({
|
||
variant: "destructive",
|
||
title: "获取出版社失败",
|
||
description: "服务器连接错误,请稍后再试",
|
||
})
|
||
}
|
||
}
|
||
|
||
// 获取图书详情
|
||
const fetchBookDetail = async () => {
|
||
try {
|
||
const response = await fetchWithAuth(`book/${bookId}`)
|
||
const result = await response.json()
|
||
|
||
if (result.code === 0) {
|
||
setBook(result.data)
|
||
setCoverPreview(result.data.coverImage)
|
||
|
||
// 设置表单默认值
|
||
form.reset({
|
||
title: result.data.title,
|
||
isbn: result.data.isbn,
|
||
price: result.data.price,
|
||
stock: result.data.stock,
|
||
publishDate: new Date(result.data.publishDate),
|
||
publisherId: result.data.publisherId,
|
||
description: result.data.description || "",
|
||
coverImage: result.data.coverImage || "",
|
||
authors: result.data.author.map((name: string) => ({ name })),
|
||
})
|
||
} else {
|
||
toast({
|
||
variant: "destructive",
|
||
title: "获取图书失败",
|
||
description: result.msg || "无法获取图书信息",
|
||
})
|
||
router.push("/admin/books")
|
||
}
|
||
} catch (error) {
|
||
toast({
|
||
variant: "destructive",
|
||
title: "获取图书失败",
|
||
description: "服务器连接错误,请稍后再试",
|
||
})
|
||
router.push("/admin/books")
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
Promise.all([fetchPublishers(), fetchBookDetail()])
|
||
}, [user, router, toast, bookId, form])
|
||
|
||
const onSubmit = async (values: z.infer<typeof formSchema>) => {
|
||
if (!book) return
|
||
|
||
setSubmitting(true)
|
||
try {
|
||
const bookData = {
|
||
...values,
|
||
authors: values.authors.map((author) => author.name),
|
||
}
|
||
|
||
const response = await fetchWithAuth(`book/admin/update/${book.bookId}`, {
|
||
method: "PUT",
|
||
headers: {
|
||
"Content-Type": "application/json",
|
||
},
|
||
body: JSON.stringify(bookData),
|
||
})
|
||
|
||
const result = await response.json()
|
||
|
||
if (result.code === 0) {
|
||
toast({
|
||
title: "更新成功",
|
||
description: "图书信息已成功更新",
|
||
})
|
||
router.push("/admin/books")
|
||
} else {
|
||
toast({
|
||
variant: "destructive",
|
||
title: "更新失败",
|
||
description: result.msg || "无法更新图书信息",
|
||
})
|
||
}
|
||
} catch (error) {
|
||
toast({
|
||
variant: "destructive",
|
||
title: "更新失败",
|
||
description: "服务器连接错误,请稍后再试",
|
||
})
|
||
} finally {
|
||
setSubmitting(false)
|
||
}
|
||
}
|
||
|
||
if (!user || !user.isAdmin) {
|
||
return null
|
||
}
|
||
|
||
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">
|
||
<div className="flex items-center gap-2 font-semibold">
|
||
<BookOpen className="h-6 w-6" />
|
||
<span>图书管理系统 - 管理后台</span>
|
||
</div>
|
||
<div className="flex items-center gap-4">
|
||
<ThemeToggle />
|
||
<Button variant="ghost" onClick={() => router.push("/")}>
|
||
返回前台
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
<div className="flex flex-1">
|
||
<AdminNav />
|
||
<main className="flex-1 p-6">
|
||
<div className="space-y-6">
|
||
<div className="flex items-center justify-between">
|
||
<div className="flex items-center gap-2">
|
||
<Button variant="outline" size="icon" onClick={() => router.push("/admin/books")}>
|
||
<ArrowLeft className="h-4 w-4" />
|
||
</Button>
|
||
<h1 className="text-3xl font-bold">编辑图书</h1>
|
||
</div>
|
||
{!loading && book && (
|
||
<div className="flex items-center gap-2">
|
||
<span className="text-sm text-muted-foreground">图书ID: {book.bookId}</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{loading ? (
|
||
<div className="space-y-6">
|
||
<Card>
|
||
<CardHeader>
|
||
<Skeleton className="h-8 w-1/3" />
|
||
<Skeleton className="h-4 w-1/2" />
|
||
</CardHeader>
|
||
<CardContent className="space-y-6">
|
||
<div className="grid gap-6 md:grid-cols-2">
|
||
{Array.from({ length: 6 }).map((_, i) => (
|
||
<div key={i} className="space-y-2">
|
||
<Skeleton className="h-4 w-20" />
|
||
<Skeleton className="h-10 w-full" />
|
||
</div>
|
||
))}
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Skeleton className="h-4 w-20" />
|
||
<Skeleton className="h-32 w-full" />
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
) : (
|
||
<Form {...form}>
|
||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
||
<div className="grid gap-6 md:grid-cols-2">
|
||
<Card className="md:col-span-1">
|
||
<CardHeader>
|
||
<CardTitle>基本信息</CardTitle>
|
||
<CardDescription>编辑图书的基本信息</CardDescription>
|
||
</CardHeader>
|
||
<CardContent className="space-y-4">
|
||
<FormField
|
||
control={form.control}
|
||
name="title"
|
||
render={({ field }) => (
|
||
<FormItem>
|
||
<FormLabel>标题</FormLabel>
|
||
<FormControl>
|
||
<Input placeholder="输入图书标题" {...field} />
|
||
</FormControl>
|
||
<FormMessage />
|
||
</FormItem>
|
||
)}
|
||
/>
|
||
|
||
<FormField
|
||
control={form.control}
|
||
name="isbn"
|
||
render={({ field }) => (
|
||
<FormItem>
|
||
<FormLabel>ISBN</FormLabel>
|
||
<FormControl>
|
||
<Input placeholder="输入ISBN" {...field} />
|
||
</FormControl>
|
||
<FormMessage />
|
||
</FormItem>
|
||
)}
|
||
/>
|
||
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<FormField
|
||
control={form.control}
|
||
name="price"
|
||
render={({ field }) => (
|
||
<FormItem>
|
||
<FormLabel>价格</FormLabel>
|
||
<FormControl>
|
||
<Input type="number" step="0.01" placeholder="输入价格" {...field} />
|
||
</FormControl>
|
||
<FormMessage />
|
||
</FormItem>
|
||
)}
|
||
/>
|
||
|
||
<FormField
|
||
control={form.control}
|
||
name="stock"
|
||
render={({ field }) => (
|
||
<FormItem>
|
||
<FormLabel>库存</FormLabel>
|
||
<FormControl>
|
||
<Input type="number" placeholder="输入库存" {...field} />
|
||
</FormControl>
|
||
<FormMessage />
|
||
</FormItem>
|
||
)}
|
||
/>
|
||
</div>
|
||
|
||
<FormField
|
||
control={form.control}
|
||
name="publishDate"
|
||
render={({ field }) => (
|
||
<FormItem className="flex flex-col">
|
||
<FormLabel>出版日期</FormLabel>
|
||
<Popover>
|
||
<PopoverTrigger asChild>
|
||
<FormControl>
|
||
<Button variant={"outline"} className="w-full pl-3 text-left font-normal">
|
||
{field.value ? format(field.value, "yyyy-MM-dd") : <span>选择日期</span>}
|
||
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
|
||
</Button>
|
||
</FormControl>
|
||
</PopoverTrigger>
|
||
<PopoverContent className="w-auto p-0" align="start">
|
||
<Calendar
|
||
mode="single"
|
||
selected={field.value}
|
||
onSelect={field.onChange}
|
||
initialFocus
|
||
/>
|
||
</PopoverContent>
|
||
</Popover>
|
||
<FormMessage />
|
||
</FormItem>
|
||
)}
|
||
/>
|
||
|
||
<FormField
|
||
control={form.control}
|
||
name="publisherId"
|
||
render={({ field }) => (
|
||
<FormItem className="flex flex-col">
|
||
<FormLabel>出版社</FormLabel>
|
||
<Combobox
|
||
items={publishers.map((publisher) => ({
|
||
label: publisher.name,
|
||
value: publisher.publisherId.toString(),
|
||
}))}
|
||
value={field.value.toString()}
|
||
onChange={(value) => field.onChange(Number.parseInt(value))}
|
||
placeholder="选择出版社"
|
||
/>
|
||
<FormMessage />
|
||
</FormItem>
|
||
)}
|
||
/>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
<Card className="md:col-span-1">
|
||
<CardHeader>
|
||
<CardTitle>封面与描述</CardTitle>
|
||
<CardDescription>编辑图书的封面和描述信息</CardDescription>
|
||
</CardHeader>
|
||
<CardContent className="space-y-4">
|
||
<FormField
|
||
control={form.control}
|
||
name="coverImage"
|
||
render={({ field }) => (
|
||
<FormItem>
|
||
<FormLabel>封面图片URL</FormLabel>
|
||
<FormControl>
|
||
<Input placeholder="输入封面图片URL" {...field} />
|
||
</FormControl>
|
||
<FormDescription>输入图书封面的URL地址</FormDescription>
|
||
<FormMessage />
|
||
</FormItem>
|
||
)}
|
||
/>
|
||
|
||
<div className="mt-4">
|
||
<p className="text-sm font-medium mb-2">封面预览</p>
|
||
<div className="border rounded-md p-4 flex justify-center bg-muted/30">
|
||
{coverPreview ? (
|
||
<img
|
||
src={coverPreview || "/placeholder.svg"}
|
||
alt="封面预览"
|
||
className="max-h-[200px] object-contain rounded-md"
|
||
onError={() => setCoverPreview(null)}
|
||
/>
|
||
) : (
|
||
<div className="flex flex-col items-center justify-center h-[200px] w-full text-muted-foreground">
|
||
<ImageIcon className="h-12 w-12 mb-2" />
|
||
<p className="text-sm">无封面图片</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
<FormField
|
||
control={form.control}
|
||
name="description"
|
||
render={({ field }) => (
|
||
<FormItem>
|
||
<FormLabel>描述</FormLabel>
|
||
<FormControl>
|
||
<Textarea placeholder="输入图书描述" className="min-h-[120px]" {...field} />
|
||
</FormControl>
|
||
<FormMessage />
|
||
</FormItem>
|
||
)}
|
||
/>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle>作者信息</CardTitle>
|
||
<CardDescription>编辑图书的作者信息</CardDescription>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="space-y-4">
|
||
{fields.map((field, index) => (
|
||
<div key={field.id} className="flex items-center gap-2">
|
||
<FormField
|
||
control={form.control}
|
||
name={`authors.${index}.name`}
|
||
render={({ field }) => (
|
||
<FormItem className="flex-1">
|
||
<FormLabel className={index !== 0 ? "sr-only" : ""}>
|
||
{index === 0 ? "作者" : `作者 ${index + 1}`}
|
||
</FormLabel>
|
||
<FormControl>
|
||
<Input placeholder="作者姓名" {...field} />
|
||
</FormControl>
|
||
<FormMessage />
|
||
</FormItem>
|
||
)}
|
||
/>
|
||
{index > 0 && (
|
||
<Button
|
||
type="button"
|
||
variant="destructive"
|
||
size="icon"
|
||
className="mt-7"
|
||
onClick={() => remove(index)}
|
||
>
|
||
<X className="h-4 w-4" />
|
||
</Button>
|
||
)}
|
||
</div>
|
||
))}
|
||
<Button
|
||
type="button"
|
||
variant="outline"
|
||
size="sm"
|
||
className="mt-2"
|
||
onClick={() => append({ name: "" })}
|
||
>
|
||
<Plus className="h-4 w-4 mr-2" />
|
||
添加作者
|
||
</Button>
|
||
</div>
|
||
</CardContent>
|
||
<CardFooter className="flex justify-between border-t px-6 py-4">
|
||
<Button variant="outline" type="button" onClick={() => router.push("/admin/books")}>
|
||
取消
|
||
</Button>
|
||
<Button type="submit" disabled={submitting}>
|
||
{submitting ? (
|
||
<>
|
||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||
保存中...
|
||
</>
|
||
) : (
|
||
<>
|
||
<Save className="mr-2 h-4 w-4" />
|
||
保存修改
|
||
</>
|
||
)}
|
||
</Button>
|
||
</CardFooter>
|
||
</Card>
|
||
</form>
|
||
</Form>
|
||
)}
|
||
</div>
|
||
</main>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|