mosaicmap/app/admin/blogs/create-blog-form.tsx
2025-08-18 00:03:16 +08:00

336 lines
14 KiB
TypeScript
Raw Permalink 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 { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Textarea } from "@/components/ui/textarea"
import { z } from "zod"
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"
import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import { useEffect, useState, useCallback } from "react"
import { gql, useMutation } from "@apollo/client"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { DialogClose, DialogFooter } from "@/components/ui/dialog"
import { IconLoader } from "@tabler/icons-react"
const CREATE_BLOG = gql`
mutation CreateBlog($input: CreateBlogInput!) {
createBlog(input: $input) {
id
title
slug
status
}
}
`
const schema = z.object({
title: z.string().min(1, "标题不能为空"),
slug: z.string().min(1, "链接不能为空"),
excerpt: z.string().optional(),
content: z.string().min(1, "内容不能为空"),
status: z.enum(["draft", "published", "archived"]),
metaTitle: z.string().optional(),
metaDescription: z.string().optional(),
isFeatured: z.boolean(),
isActive: z.boolean(),
})
export default function CreateBlogForm() {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [createBlog] = useMutation(CREATE_BLOG)
const form = useForm<z.infer<typeof schema>>({
resolver: zodResolver(schema),
defaultValues: {
title: "",
slug: "",
excerpt: "",
content: "",
status: "draft" as const,
metaTitle: "",
metaDescription: "",
isFeatured: false,
isActive: true,
},
})
// 生成slug
const generateSlug = useCallback((title: string) => {
return title
.toLowerCase()
.replace(/[^\w\s-]/g, '') // 移除特殊字符
.replace(/\s+/g, '-') // 空格替换为连字符
.replace(/-+/g, '-') // 多个连字符合并为一个
.trim();
}, []);
// 监听标题变化自动生成slug
const watchedTitle = form.watch("title");
useEffect(() => {
if (watchedTitle) {
const slug = generateSlug(watchedTitle);
form.setValue("slug", slug);
}
}, [watchedTitle, generateSlug, form]);
async function onSubmit(values: z.infer<typeof schema>) {
try {
setIsLoading(true);
setError(null);
await createBlog({
variables: {
input: {
title: values.title,
slug: values.slug,
content: JSON.stringify(values.content), // Convert to JSON
status: values.status,
excerpt: values.excerpt || null,
metaTitle: values.metaTitle || null,
metaDescription: values.metaDescription || null,
isFeatured: values.isFeatured,
isActive: values.isActive,
}
}
});
// 重置表单
form.reset();
} catch (err) {
setError(err instanceof Error ? err.message : '创建博客失败,请重试');
} finally {
setIsLoading(false);
}
}
return (
<Form {...form} >
<form className="space-y-4" onSubmit={form.handleSubmit(onSubmit)}>
{error && (
<div className="text-sm text-red-600 bg-red-50 p-3 rounded-md">
{error}
</div>
)}
<div className="grid gap-4">
<div className="grid gap-3">
<FormField
control={form.control}
name="title"
render={({ field }) => (
<FormItem>
<FormLabel> *</FormLabel>
<FormControl>
<Input id="title" placeholder="请输入博客标题" {...field} required />
</FormControl>
<FormMessage />
</FormItem>
)} />
</div>
<div className="grid gap-3">
<FormField
control={form.control}
name="slug"
render={({ field }) => (
<FormItem>
<FormLabel> *</FormLabel>
<FormControl>
<Input id="slug" placeholder="博客链接地址" {...field} required />
</FormControl>
<FormMessage />
</FormItem>
)} />
</div>
<div className="grid gap-3">
<FormField
control={form.control}
name="excerpt"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Textarea
id="excerpt"
placeholder="请输入博客摘要(可选)"
rows={3}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)} />
</div>
<div className="grid grid-cols-2 gap-4">
<div className="flex flex-col gap-3">
<FormField
control={form.control}
name="status"
render={({ field }) => (
<FormItem>
<FormLabel> *</FormLabel>
<FormControl>
<Select
value={field.value}
onValueChange={(value) => field.onChange(value)}
>
<SelectTrigger id="status" className="w-full">
<SelectValue placeholder="选择发布状态" />
</SelectTrigger>
<SelectContent>
<SelectItem value="draft">稿</SelectItem>
<SelectItem value="published"></SelectItem>
<SelectItem value="archived"></SelectItem>
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="flex flex-col gap-3">
<FormField
control={form.control}
name="metaTitle"
render={({ field }) => (
<FormItem>
<FormLabel>Meta </FormLabel>
<FormControl>
<Input
id="metaTitle"
placeholder="SEO 标题(可选)"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</div>
<div className="grid gap-3">
<FormField
control={form.control}
name="metaDescription"
render={({ field }) => (
<FormItem>
<FormLabel>Meta </FormLabel>
<FormControl>
<Textarea
id="metaDescription"
placeholder="SEO 描述(可选)"
rows={3}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)} />
</div>
<div className="grid grid-cols-2 gap-4">
<div className="flex flex-col gap-3">
<FormField
control={form.control}
name="isFeatured"
render={({ field }) => (
<FormItem className="flex flex-row items-center space-x-3 space-y-0">
<FormControl>
<input
type="checkbox"
checked={field.value}
onChange={field.onChange}
className="h-4 w-4"
/>
</FormControl>
<FormLabel className="text-sm font-normal"></FormLabel>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="flex flex-col gap-3">
<FormField
control={form.control}
name="isActive"
render={({ field }) => (
<FormItem className="flex flex-row items-center space-x-3 space-y-0">
<FormControl>
<input
type="checkbox"
checked={field.value}
onChange={field.onChange}
className="h-4 w-4"
/>
</FormControl>
<FormLabel className="text-sm font-normal"></FormLabel>
<FormMessage />
</FormItem>
)}
/>
</div>
</div>
<div className="grid gap-3">
<FormField
control={form.control}
name="content"
render={({ field }) => (
<FormItem>
<FormLabel> *</FormLabel>
<FormControl>
<Textarea
id="content"
placeholder="请输入博客内容(支持 Markdown"
rows={12}
{...field}
required
/>
</FormControl>
<FormMessage />
</FormItem>
)} />
</div>
</div>
<DialogFooter>
<DialogClose asChild>
<Button type="button" variant="outline">
</Button>
</DialogClose>
<Button
type="button"
variant="secondary"
onClick={() => {
form.setValue("status", "draft");
form.handleSubmit(onSubmit)();
}}
disabled={isLoading}
>
稿
</Button>
<Button type="submit" disabled={isLoading}>
{isLoading ? (
<>
<IconLoader className="mr-2 h-4 w-4 animate-spin" />
...
</>
) : (
form.watch("status") === "published" ? "发布博客" : "创建博客"
)}
</Button>
</DialogFooter>
</form>
</Form >
)
}