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

452 lines
17 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.

"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 { Textarea } from "@/components/ui/textarea"
import { useToast } from "@/components/ui/use-toast"
import { useAuth } from "@/context/auth-context"
import { zodResolver } from "@hookform/resolvers/zod"
import { format } from "date-fns"
import { ArrowLeft, BookOpen, CalendarIcon, ImageIcon, Loader2, Plus, Save, X } from "lucide-react"
import { useRouter } from "next/navigation"
import { useEffect, useState } from "react"
import { useFieldArray, useForm } from "react-hook-form"
import * as z from "zod"
import { fetchWithAuth } from "@/lib/api"
interface Publisher {
publisherId: number
name: string
address: 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 AddBookPage() {
const { user } = useAuth()
const router = useRouter()
const { toast } = useToast()
const [publishers, setPublishers] = useState<Publisher[]>([])
const [loading, setLoading] = 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: "服务器连接错误,请稍后再试",
})
}
}
fetchPublishers()
}, [user, router, toast])
const onSubmit = async (values: z.infer<typeof formSchema>) => {
setLoading(true)
try {
const bookData = {
...values,
authors: values.authors.map((author) => author.name),
}
const response = await fetchWithAuth("book/admin/add", {
method: "POST",
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 {
setLoading(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 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>
<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={loading}>
{loading ? (
<>
<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>
)
}