336 lines
14 KiB
TypeScript
336 lines
14 KiB
TypeScript
"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 >
|
||
)
|
||
} |