|
|
@@ -1,321 +1,624 @@
|
|
|
-import React, { useState, useEffect } from 'react';
|
|
|
+import React, { useState } from 'react';
|
|
|
+import { useQuery } from '@tanstack/react-query';
|
|
|
+import { format } from 'date-fns';
|
|
|
+import { Plus, Search, Edit, Trash2, Download, Eye } from 'lucide-react';
|
|
|
import { templateClient } from '@/client/api';
|
|
|
-import { DataTable } from '@/client/admin/components/DataTable';
|
|
|
+import type { InferRequestType, InferResponseType } from 'hono/client';
|
|
|
import { Button } from '@/client/components/ui/button';
|
|
|
import { Input } from '@/client/components/ui/input';
|
|
|
-import { Label } from '@/client/components/ui/label';
|
|
|
-import { Textarea } from '@/client/components/ui/textarea';
|
|
|
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/client/components/ui/card';
|
|
|
+import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/client/components/ui/table';
|
|
|
import { Badge } from '@/client/components/ui/badge';
|
|
|
-import {
|
|
|
- Dialog,
|
|
|
- DialogContent,
|
|
|
- DialogDescription,
|
|
|
- DialogHeader,
|
|
|
- DialogTitle,
|
|
|
- DialogTrigger,
|
|
|
-} from '@/client/components/ui/dialog';
|
|
|
-import {
|
|
|
- Select,
|
|
|
- SelectContent,
|
|
|
- SelectItem,
|
|
|
- SelectTrigger,
|
|
|
- SelectValue,
|
|
|
-} from '@/client/components/ui/select';
|
|
|
+import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/client/components/ui/dialog';
|
|
|
+import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/client/components/ui/form';
|
|
|
+import { DataTablePagination } from '@/client/admin/components/DataTablePagination';
|
|
|
+import { useForm } from 'react-hook-form';
|
|
|
+import { zodResolver } from '@hookform/resolvers/zod';
|
|
|
+import { toast } from 'sonner';
|
|
|
+import { Skeleton } from '@/client/components/ui/skeleton';
|
|
|
import { Switch } from '@/client/components/ui/switch';
|
|
|
-import { toast } from 'react-toastify';
|
|
|
-import { Plus, Edit, Trash2, Eye, Download } from 'lucide-react';
|
|
|
-import { MinioUploader } from '@/client/admin/components/MinioUploader';
|
|
|
-
|
|
|
-interface Template {
|
|
|
- id: number;
|
|
|
- title: string;
|
|
|
- description: string | null;
|
|
|
- category: string;
|
|
|
- isFree: number;
|
|
|
- isDisabled: number;
|
|
|
- downloadCount: number;
|
|
|
- file: {
|
|
|
- id: number;
|
|
|
- name: string;
|
|
|
- fullUrl: string;
|
|
|
- } | null;
|
|
|
-}
|
|
|
-
|
|
|
-const Templates: React.FC = () => {
|
|
|
- const [templates, setTemplates] = useState<Template[]>([]);
|
|
|
- const [loading, setLoading] = useState(true);
|
|
|
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/client/components/ui/select';
|
|
|
+import { DisabledStatus, DeleteStatus } from '@/share/types';
|
|
|
+import { CreateTemplateDto, UpdateTemplateDto } from '@/server/modules/templates/template.schema';
|
|
|
+import FileSelector from '@/client/admin/components/FileSelector';
|
|
|
+
|
|
|
+// 使用RPC方式提取类型
|
|
|
+type CreateTemplateRequest = InferRequestType<typeof templateClient.$post>['json'];
|
|
|
+type UpdateTemplateRequest = InferRequestType<typeof templateClient[':id']['$put']>['json'];
|
|
|
+type TemplateResponse = InferResponseType<typeof templateClient.$get, 200>['data'][0];
|
|
|
+
|
|
|
+// 直接使用后端定义的 schema
|
|
|
+const createTemplateFormSchema = CreateTemplateDto;
|
|
|
+const updateTemplateFormSchema = UpdateTemplateDto;
|
|
|
+
|
|
|
+type CreateTemplateFormData = CreateTemplateRequest;
|
|
|
+type UpdateTemplateFormData = UpdateTemplateRequest;
|
|
|
+
|
|
|
+export const TemplatesPage = () => {
|
|
|
+ const [searchParams, setSearchParams] = useState({
|
|
|
+ page: 1,
|
|
|
+ limit: 10,
|
|
|
+ search: ''
|
|
|
+ });
|
|
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
|
- const [editingTemplate, setEditingTemplate] = useState<Template | null>(null);
|
|
|
- const [formData, setFormData] = useState({
|
|
|
- title: '',
|
|
|
- description: '',
|
|
|
- category: '',
|
|
|
- isFree: false,
|
|
|
- isDisabled: false,
|
|
|
- fileId: null as number | null,
|
|
|
+ const [editingTemplate, setEditingTemplate] = useState<any>(null);
|
|
|
+ const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
|
|
+ const [templateToDelete, setTemplateToDelete] = useState<number | null>(null);
|
|
|
+ const [isCreateForm, setIsCreateForm] = useState(true);
|
|
|
+
|
|
|
+ const createForm = useForm<CreateTemplateFormData>({
|
|
|
+ resolver: zodResolver(createTemplateFormSchema),
|
|
|
+ defaultValues: {
|
|
|
+ title: '',
|
|
|
+ description: null,
|
|
|
+ fileId: undefined,
|
|
|
+ category: '',
|
|
|
+ isFree: 0,
|
|
|
+ },
|
|
|
});
|
|
|
|
|
|
- useEffect(() => {
|
|
|
- fetchTemplates();
|
|
|
- }, []);
|
|
|
+ const updateForm = useForm<UpdateTemplateFormData>({
|
|
|
+ resolver: zodResolver(updateTemplateFormSchema),
|
|
|
+ defaultValues: {
|
|
|
+ title: undefined,
|
|
|
+ description: null,
|
|
|
+ fileId: undefined,
|
|
|
+ category: undefined,
|
|
|
+ isFree: undefined,
|
|
|
+ isDisabled: undefined,
|
|
|
+ },
|
|
|
+ });
|
|
|
|
|
|
- const fetchTemplates = async () => {
|
|
|
- try {
|
|
|
- setLoading(true);
|
|
|
- const response = await templateClient.$get();
|
|
|
- if (response.ok) {
|
|
|
- const data = await response.json();
|
|
|
- setTemplates(data.data);
|
|
|
+ const { data: templatesData, isLoading, refetch } = useQuery({
|
|
|
+ queryKey: ['templates', searchParams],
|
|
|
+ queryFn: async () => {
|
|
|
+ const res = await templateClient.$get({
|
|
|
+ query: {
|
|
|
+ page: searchParams.page,
|
|
|
+ pageSize: searchParams.limit,
|
|
|
+ keyword: searchParams.search
|
|
|
+ }
|
|
|
+ });
|
|
|
+ if (res.status !== 200) {
|
|
|
+ throw new Error('获取模板列表失败');
|
|
|
}
|
|
|
- } catch (error) {
|
|
|
- console.error('Failed to fetch templates:', error);
|
|
|
- toast.error('获取模板列表失败');
|
|
|
- } finally {
|
|
|
- setLoading(false);
|
|
|
+ return await res.json();
|
|
|
}
|
|
|
+ });
|
|
|
+
|
|
|
+ const templates = templatesData?.data || [];
|
|
|
+ const totalCount = templatesData?.pagination?.total || 0;
|
|
|
+
|
|
|
+ // 处理搜索
|
|
|
+ const handleSearch = (e: React.FormEvent) => {
|
|
|
+ e.preventDefault();
|
|
|
+ setSearchParams(prev => ({ ...prev, page: 1 }));
|
|
|
};
|
|
|
|
|
|
- const handleSubmit = async () => {
|
|
|
- try {
|
|
|
- const data = {
|
|
|
- ...formData,
|
|
|
- isFree: formData.isFree ? 1 : 0,
|
|
|
- isDisabled: formData.isDisabled ? 1 : 0,
|
|
|
- };
|
|
|
-
|
|
|
- let response;
|
|
|
- if (editingTemplate) {
|
|
|
- response = await templateClient[':id'].$put({
|
|
|
- param: { id: editingTemplate.id.toString() },
|
|
|
- json: data
|
|
|
- });
|
|
|
- } else {
|
|
|
- response = await templateClient.$post({
|
|
|
- json: data
|
|
|
- });
|
|
|
- }
|
|
|
+ // 处理分页
|
|
|
+ const handlePageChange = (page: number, limit: number) => {
|
|
|
+ setSearchParams(prev => ({ ...prev, page, limit }));
|
|
|
+ };
|
|
|
|
|
|
- if (response.ok) {
|
|
|
- toast.success(editingTemplate ? '更新成功' : '创建成功');
|
|
|
- setIsModalOpen(false);
|
|
|
- resetForm();
|
|
|
- fetchTemplates();
|
|
|
+ // 打开创建模板对话框
|
|
|
+ const handleCreateTemplate = () => {
|
|
|
+ setEditingTemplate(null);
|
|
|
+ setIsCreateForm(true);
|
|
|
+ createForm.reset({
|
|
|
+ title: '',
|
|
|
+ description: null,
|
|
|
+ fileId: undefined,
|
|
|
+ category: '',
|
|
|
+ isFree: 0,
|
|
|
+ });
|
|
|
+ setIsModalOpen(true);
|
|
|
+ };
|
|
|
+
|
|
|
+ // 打开编辑模板对话框
|
|
|
+ const handleEditTemplate = (template: TemplateResponse) => {
|
|
|
+ setEditingTemplate(template);
|
|
|
+ setIsCreateForm(false);
|
|
|
+ updateForm.reset({
|
|
|
+ title: template.title,
|
|
|
+ description: template.description,
|
|
|
+ fileId: template.fileId,
|
|
|
+ category: template.category,
|
|
|
+ isFree: template.isFree,
|
|
|
+ isDisabled: template.isDisabled,
|
|
|
+ });
|
|
|
+ setIsModalOpen(true);
|
|
|
+ };
|
|
|
+
|
|
|
+ // 处理创建表单提交
|
|
|
+ const handleCreateSubmit = async (data: CreateTemplateFormData) => {
|
|
|
+ try {
|
|
|
+ const res = await templateClient.$post({
|
|
|
+ json: data
|
|
|
+ });
|
|
|
+ if (res.status !== 201) {
|
|
|
+ throw new Error('创建模板失败');
|
|
|
}
|
|
|
+ toast.success('模板创建成功');
|
|
|
+ setIsModalOpen(false);
|
|
|
+ refetch();
|
|
|
} catch (error) {
|
|
|
- console.error('Failed to save template:', error);
|
|
|
- toast.error('保存失败');
|
|
|
+ console.error('创建模板失败:', error);
|
|
|
+ toast.error('创建失败,请重试');
|
|
|
}
|
|
|
};
|
|
|
|
|
|
- const handleDelete = async (id: number) => {
|
|
|
- if (!window.confirm('确定要删除该模板吗?')) return;
|
|
|
-
|
|
|
+ // 处理更新表单提交
|
|
|
+ const handleUpdateSubmit = async (data: UpdateTemplateFormData) => {
|
|
|
+ if (!editingTemplate) return;
|
|
|
+
|
|
|
try {
|
|
|
- const response = await templateClient[':id'].$delete({
|
|
|
- param: { id: id.toString() }
|
|
|
+ const res = await templateClient[':id']['$put']({
|
|
|
+ param: { id: editingTemplate.id },
|
|
|
+ json: data
|
|
|
});
|
|
|
-
|
|
|
- if (response.ok) {
|
|
|
- toast.success('删除成功');
|
|
|
- fetchTemplates();
|
|
|
+ if (res.status !== 200) {
|
|
|
+ throw new Error('更新模板失败');
|
|
|
}
|
|
|
+ toast.success('模板更新成功');
|
|
|
+ setIsModalOpen(false);
|
|
|
+ refetch();
|
|
|
} catch (error) {
|
|
|
- console.error('Failed to delete template:', error);
|
|
|
- toast.error('删除失败');
|
|
|
+ console.error('更新模板失败:', error);
|
|
|
+ toast.error('更新失败,请重试');
|
|
|
}
|
|
|
};
|
|
|
|
|
|
- const resetForm = () => {
|
|
|
- setFormData({
|
|
|
- title: '',
|
|
|
- description: '',
|
|
|
- category: '',
|
|
|
- isFree: false,
|
|
|
- isDisabled: false,
|
|
|
- fileId: null,
|
|
|
- });
|
|
|
- setEditingTemplate(null);
|
|
|
+ // 处理删除模板
|
|
|
+ const handleDeleteTemplate = (id: number) => {
|
|
|
+ setTemplateToDelete(id);
|
|
|
+ setDeleteDialogOpen(true);
|
|
|
};
|
|
|
|
|
|
- const openModal = (template?: Template) => {
|
|
|
- if (template) {
|
|
|
- setEditingTemplate(template);
|
|
|
- setFormData({
|
|
|
- title: template.title,
|
|
|
- description: template.description || '',
|
|
|
- category: template.category,
|
|
|
- isFree: template.isFree === 1,
|
|
|
- isDisabled: template.isDisabled === 1,
|
|
|
- fileId: template.file?.id || null,
|
|
|
+ const confirmDelete = async () => {
|
|
|
+ if (!templateToDelete) return;
|
|
|
+
|
|
|
+ try {
|
|
|
+ const res = await templateClient[':id']['$delete']({
|
|
|
+ param: { id: templateToDelete }
|
|
|
});
|
|
|
- } else {
|
|
|
- resetForm();
|
|
|
+ if (res.status !== 204) {
|
|
|
+ throw new Error('删除模板失败');
|
|
|
+ }
|
|
|
+ toast.success('模板删除成功');
|
|
|
+ refetch();
|
|
|
+ } catch (error) {
|
|
|
+ console.error('删除模板失败:', error);
|
|
|
+ toast.error('删除失败,请重试');
|
|
|
+ } finally {
|
|
|
+ setDeleteDialogOpen(false);
|
|
|
+ setTemplateToDelete(null);
|
|
|
}
|
|
|
- setIsModalOpen(true);
|
|
|
};
|
|
|
|
|
|
- const columns = [
|
|
|
- {
|
|
|
- accessorKey: 'title',
|
|
|
- header: '标题',
|
|
|
- },
|
|
|
- {
|
|
|
- accessorKey: 'category',
|
|
|
- header: '分类',
|
|
|
- cell: ({ row }) => (
|
|
|
- <Badge variant="outline">{row.getValue('category')}</Badge>
|
|
|
- ),
|
|
|
- },
|
|
|
- {
|
|
|
- accessorKey: 'isFree',
|
|
|
- header: '类型',
|
|
|
- cell: ({ row }) => (
|
|
|
- <Badge variant={row.getValue('isFree') ? "default" : "secondary"}>
|
|
|
- {row.getValue('isFree') ? '免费' : '会员'}
|
|
|
- </Badge>
|
|
|
- ),
|
|
|
- },
|
|
|
- {
|
|
|
- accessorKey: 'downloadCount',
|
|
|
- header: '下载次数',
|
|
|
- },
|
|
|
- {
|
|
|
- accessorKey: 'isDisabled',
|
|
|
- header: '状态',
|
|
|
- cell: ({ row }) => (
|
|
|
- <Badge variant={row.getValue('isDisabled') ? "destructive" : "default"}>
|
|
|
- {row.getValue('isDisabled') ? '禁用' : '启用'}
|
|
|
- </Badge>
|
|
|
- ),
|
|
|
- },
|
|
|
- {
|
|
|
- id: 'actions',
|
|
|
- header: '操作',
|
|
|
- cell: ({ row }) => {
|
|
|
- const template = row.original as Template;
|
|
|
- return (
|
|
|
- <div className="flex gap-2">
|
|
|
- <Button
|
|
|
- variant="ghost"
|
|
|
- size="sm"
|
|
|
- onClick={() => window.open(template.file?.fullUrl, '_blank')}
|
|
|
- >
|
|
|
- <Eye className="w-4 h-4" />
|
|
|
- </Button>
|
|
|
- <Button
|
|
|
- variant="ghost"
|
|
|
- size="sm"
|
|
|
- onClick={() => openModal(template)}
|
|
|
- >
|
|
|
- <Edit className="w-4 h-4" />
|
|
|
- </Button>
|
|
|
- <Button
|
|
|
- variant="ghost"
|
|
|
- size="sm"
|
|
|
- onClick={() => handleDelete(template.id)}
|
|
|
- >
|
|
|
- <Trash2 className="w-4 h-4" />
|
|
|
- </Button>
|
|
|
- </div>
|
|
|
- );
|
|
|
- },
|
|
|
- },
|
|
|
- ];
|
|
|
+ // 渲染加载骨架
|
|
|
+ if (isLoading) {
|
|
|
+ return (
|
|
|
+ <div className="space-y-4">
|
|
|
+ <div className="flex justify-between items-center">
|
|
|
+ <h1 className="text-2xl font-bold">模板管理</h1>
|
|
|
+ <Button disabled>
|
|
|
+ <Plus className="mr-2 h-4 w-4" />
|
|
|
+ 创建模板
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <Card>
|
|
|
+ <CardHeader>
|
|
|
+ <Skeleton className="h-6 w-1/4" />
|
|
|
+ </CardHeader>
|
|
|
+ <CardContent>
|
|
|
+ <div className="space-y-2">
|
|
|
+ <Skeleton className="h-4 w-full" />
|
|
|
+ <Skeleton className="h-4 w-full" />
|
|
|
+ <Skeleton className="h-4 w-full" />
|
|
|
+ </div>
|
|
|
+ </CardContent>
|
|
|
+ </Card>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ }
|
|
|
|
|
|
return (
|
|
|
- <div className="container mx-auto py-8">
|
|
|
- <div className="flex justify-between items-center mb-6">
|
|
|
+ <div className="space-y-4">
|
|
|
+ <div className="flex justify-between items-center">
|
|
|
<h1 className="text-2xl font-bold">模板管理</h1>
|
|
|
- <Button onClick={() => openModal()}>
|
|
|
- <Plus className="w-4 h-4 mr-2" />
|
|
|
- 新建模板
|
|
|
+ <Button onClick={handleCreateTemplate}>
|
|
|
+ <Plus className="mr-2 h-4 w-4" />
|
|
|
+ 创建模板
|
|
|
</Button>
|
|
|
</div>
|
|
|
|
|
|
- <DataTable
|
|
|
- columns={columns}
|
|
|
- data={templates}
|
|
|
- loading={loading}
|
|
|
- searchPlaceholder="搜索模板..."
|
|
|
- />
|
|
|
+ <Card>
|
|
|
+ <CardHeader>
|
|
|
+ <CardTitle>模板列表</CardTitle>
|
|
|
+ <CardDescription>
|
|
|
+ 管理系统中的所有模板,共 {totalCount} 个模板
|
|
|
+ </CardDescription>
|
|
|
+ </CardHeader>
|
|
|
+ <CardContent>
|
|
|
+ <div className="mb-4">
|
|
|
+ <form onSubmit={handleSearch} className="flex gap-2">
|
|
|
+ <div className="relative flex-1 max-w-sm">
|
|
|
+ <Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
|
|
+ <Input
|
|
|
+ placeholder="搜索模板标题、描述或分类..."
|
|
|
+ value={searchParams.search}
|
|
|
+ onChange={(e) => setSearchParams(prev => ({ ...prev, search: e.target.value }))}
|
|
|
+ className="pl-8"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <Button type="submit" variant="outline">
|
|
|
+ 搜索
|
|
|
+ </Button>
|
|
|
+ </form>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="rounded-md border">
|
|
|
+ <Table>
|
|
|
+ <TableHeader>
|
|
|
+ <TableRow>
|
|
|
+ <TableHead>模板标题</TableHead>
|
|
|
+ <TableHead>分类</TableHead>
|
|
|
+ <TableHead>文件</TableHead>
|
|
|
+ <TableHead>免费状态</TableHead>
|
|
|
+ <TableHead>下载次数</TableHead>
|
|
|
+ <TableHead>状态</TableHead>
|
|
|
+ <TableHead>创建时间</TableHead>
|
|
|
+ <TableHead className="text-right">操作</TableHead>
|
|
|
+ </TableRow>
|
|
|
+ </TableHeader>
|
|
|
+ <TableBody>
|
|
|
+ {templates.map((template) => (
|
|
|
+ <TableRow key={template.id}>
|
|
|
+ <TableCell className="font-medium">{template.title}</TableCell>
|
|
|
+ <TableCell>
|
|
|
+ <Badge variant="outline">{template.category}</Badge>
|
|
|
+ </TableCell>
|
|
|
+ <TableCell>
|
|
|
+ {template.file?.name ? (
|
|
|
+ <span className="text-sm text-muted-foreground">
|
|
|
+ {template.file.name}
|
|
|
+ </span>
|
|
|
+ ) : (
|
|
|
+ '-'
|
|
|
+ )}
|
|
|
+ </TableCell>
|
|
|
+ <TableCell>
|
|
|
+ <Badge
|
|
|
+ variant={template.isFree === 1 ? 'default' : 'secondary'}
|
|
|
+ >
|
|
|
+ {template.isFree === 1 ? '免费' : '收费'}
|
|
|
+ </Badge>
|
|
|
+ </TableCell>
|
|
|
+ <TableCell>{template.downloadCount}</TableCell>
|
|
|
+ <TableCell>
|
|
|
+ <Badge
|
|
|
+ variant={template.isDisabled === 1 ? 'secondary' : 'default'}
|
|
|
+ >
|
|
|
+ {template.isDisabled === 1 ? '禁用' : '启用'}
|
|
|
+ </Badge>
|
|
|
+ </TableCell>
|
|
|
+ <TableCell>
|
|
|
+ {format(new Date(template.createdAt), 'yyyy-MM-dd HH:mm')}
|
|
|
+ </TableCell>
|
|
|
+ <TableCell className="text-right">
|
|
|
+ <div className="flex justify-end gap-2">
|
|
|
+ <Button
|
|
|
+ variant="ghost"
|
|
|
+ size="icon"
|
|
|
+ onClick={() => handleEditTemplate(template)}
|
|
|
+ >
|
|
|
+ <Edit className="h-4 w-4" />
|
|
|
+ </Button>
|
|
|
+ <Button
|
|
|
+ variant="ghost"
|
|
|
+ size="icon"
|
|
|
+ onClick={() => handleDeleteTemplate(template.id)}
|
|
|
+ >
|
|
|
+ <Trash2 className="h-4 w-4" />
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
+ </TableCell>
|
|
|
+ </TableRow>
|
|
|
+ ))}
|
|
|
+ </TableBody>
|
|
|
+ </Table>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {templates.length === 0 && !isLoading && (
|
|
|
+ <div className="text-center py-8">
|
|
|
+ <p className="text-muted-foreground">暂无模板数据</p>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+
|
|
|
+ <DataTablePagination
|
|
|
+ currentPage={searchParams.page}
|
|
|
+ totalCount={totalCount}
|
|
|
+ pageSize={searchParams.limit}
|
|
|
+ onPageChange={handlePageChange}
|
|
|
+ />
|
|
|
+ </CardContent>
|
|
|
+ </Card>
|
|
|
|
|
|
+ {/* 创建/编辑模板对话框 */}
|
|
|
<Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
|
|
|
- <DialogContent className="max-w-2xl">
|
|
|
+ <DialogContent className="sm:max-w-[500px] max-h-[90vh] overflow-y-auto">
|
|
|
<DialogHeader>
|
|
|
<DialogTitle>
|
|
|
- {editingTemplate ? '编辑模板' : '新建模板'}
|
|
|
+ {editingTemplate ? '编辑模板' : '创建模板'}
|
|
|
</DialogTitle>
|
|
|
<DialogDescription>
|
|
|
- {editingTemplate ? '修改模板信息' : '创建新的模板'}
|
|
|
+ {editingTemplate ? '编辑现有模板信息' : '创建一个新的模板'}
|
|
|
</DialogDescription>
|
|
|
</DialogHeader>
|
|
|
+
|
|
|
+ {isCreateForm ? (
|
|
|
+ <Form {...createForm}>
|
|
|
+ <form onSubmit={createForm.handleSubmit(handleCreateSubmit)} className="space-y-4">
|
|
|
+ <FormField
|
|
|
+ control={createForm.control}
|
|
|
+ name="title"
|
|
|
+ render={({ field }) => (
|
|
|
+ <FormItem>
|
|
|
+ <FormLabel className="flex items-center">
|
|
|
+ 模板标题
|
|
|
+ <span className="text-red-500 ml-1">*</span>
|
|
|
+ </FormLabel>
|
|
|
+ <FormControl>
|
|
|
+ <Input placeholder="请输入模板标题" {...field} />
|
|
|
+ </FormControl>
|
|
|
+ <FormMessage />
|
|
|
+ </FormItem>
|
|
|
+ )}
|
|
|
+ />
|
|
|
|
|
|
- <div className="space-y-4">
|
|
|
- <div>
|
|
|
- <Label>标题</Label>
|
|
|
- <Input
|
|
|
- value={formData.title}
|
|
|
- onChange={(e) => setFormData({ ...formData, title: e.target.value })}
|
|
|
- placeholder="请输入模板标题"
|
|
|
- />
|
|
|
- </div>
|
|
|
+ <FormField
|
|
|
+ control={createForm.control}
|
|
|
+ name="description"
|
|
|
+ render={({ field }) => (
|
|
|
+ <FormItem>
|
|
|
+ <FormLabel>模板描述</FormLabel>
|
|
|
+ <FormControl>
|
|
|
+ <Input placeholder="请输入模板描述" {...field} />
|
|
|
+ </FormControl>
|
|
|
+ <FormMessage />
|
|
|
+ </FormItem>
|
|
|
+ )}
|
|
|
+ />
|
|
|
|
|
|
- <div>
|
|
|
- <Label>描述</Label>
|
|
|
- <Textarea
|
|
|
- value={formData.description}
|
|
|
- onChange={(e) => setFormData({ ...formData, description: e.target.value })}
|
|
|
- placeholder="请输入模板描述"
|
|
|
- rows={3}
|
|
|
- />
|
|
|
- </div>
|
|
|
+ <FormField
|
|
|
+ control={createForm.control}
|
|
|
+ name="fileId"
|
|
|
+ render={({ field }) => (
|
|
|
+ <FormItem>
|
|
|
+ <FormLabel className="flex items-center">
|
|
|
+ 关联文件
|
|
|
+ <span className="text-red-500 ml-1">*</span>
|
|
|
+ </FormLabel>
|
|
|
+ <FormControl>
|
|
|
+ <FileSelector
|
|
|
+ value={field.value || undefined}
|
|
|
+ onChange={(value) => field.onChange(value)}
|
|
|
+ maxSize={50}
|
|
|
+ uploadPath="/templates"
|
|
|
+ uploadButtonText="上传模板文件"
|
|
|
+ previewSize="medium"
|
|
|
+ placeholder="选择模板文件"
|
|
|
+ title="选择模板文件"
|
|
|
+ description="上传新文件或从已有文件中选择"
|
|
|
+ filterType="all"
|
|
|
+ />
|
|
|
+ </FormControl>
|
|
|
+ <FormDescription>请选择或上传模板文件</FormDescription>
|
|
|
+ <FormMessage />
|
|
|
+ </FormItem>
|
|
|
+ )}
|
|
|
+ />
|
|
|
|
|
|
- <div>
|
|
|
- <Label>分类</Label>
|
|
|
- <Input
|
|
|
- value={formData.category}
|
|
|
- onChange={(e) => setFormData({ ...formData, category: e.target.value })}
|
|
|
- placeholder="请输入分类"
|
|
|
- />
|
|
|
- </div>
|
|
|
+ <FormField
|
|
|
+ control={createForm.control}
|
|
|
+ name="category"
|
|
|
+ render={({ field }) => (
|
|
|
+ <FormItem>
|
|
|
+ <FormLabel className="flex items-center">
|
|
|
+ 分类
|
|
|
+ <span className="text-red-500 ml-1">*</span>
|
|
|
+ </FormLabel>
|
|
|
+ <FormControl>
|
|
|
+ <Input placeholder="请输入分类名称" {...field} />
|
|
|
+ </FormControl>
|
|
|
+ <FormMessage />
|
|
|
+ </FormItem>
|
|
|
+ )}
|
|
|
+ />
|
|
|
|
|
|
- <div>
|
|
|
- <Label>文件</Label>
|
|
|
- <MinioUploader
|
|
|
- onUploadSuccess={(file) => setFormData({ ...formData, fileId: file.id })}
|
|
|
- accept=".doc,.docx,.pdf,.xls,.xlsx,.ppt,.pptx"
|
|
|
- />
|
|
|
- </div>
|
|
|
+ <FormField
|
|
|
+ control={createForm.control}
|
|
|
+ name="isFree"
|
|
|
+ render={({ field }) => (
|
|
|
+ <FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
|
|
|
+ <div className="space-y-0.5">
|
|
|
+ <FormLabel className="text-base">免费状态</FormLabel>
|
|
|
+ <FormDescription>
|
|
|
+ 设置为免费后用户无需付费即可下载
|
|
|
+ </FormDescription>
|
|
|
+ </div>
|
|
|
+ <FormControl>
|
|
|
+ <Switch
|
|
|
+ checked={field.value === 1}
|
|
|
+ onCheckedChange={(checked) => field.onChange(checked ? 1 : 0)}
|
|
|
+ />
|
|
|
+ </FormControl>
|
|
|
+ </FormItem>
|
|
|
+ )}
|
|
|
+ />
|
|
|
|
|
|
- <div className="flex items-center justify-between">
|
|
|
- <Label>免费下载</Label>
|
|
|
- <Switch
|
|
|
- checked={formData.isFree}
|
|
|
- onCheckedChange={(checked) => setFormData({ ...formData, isFree: checked })}
|
|
|
- />
|
|
|
- </div>
|
|
|
+ <DialogFooter>
|
|
|
+ <Button type="button" variant="outline" onClick={() => setIsModalOpen(false)}>
|
|
|
+ 取消
|
|
|
+ </Button>
|
|
|
+ <Button type="submit">
|
|
|
+ 创建模板
|
|
|
+ </Button>
|
|
|
+ </DialogFooter>
|
|
|
+ </form>
|
|
|
+ </Form>
|
|
|
+ ) : (
|
|
|
+ <Form {...updateForm}>
|
|
|
+ <form onSubmit={updateForm.handleSubmit(handleUpdateSubmit)} className="space-y-4">
|
|
|
+ <FormField
|
|
|
+ control={updateForm.control}
|
|
|
+ name="title"
|
|
|
+ render={({ field }) => (
|
|
|
+ <FormItem>
|
|
|
+ <FormLabel className="flex items-center">
|
|
|
+ 模板标题
|
|
|
+ <span className="text-red-500 ml-1">*</span>
|
|
|
+ </FormLabel>
|
|
|
+ <FormControl>
|
|
|
+ <Input placeholder="请输入模板标题" {...field} />
|
|
|
+ </FormControl>
|
|
|
+ <FormMessage />
|
|
|
+ </FormItem>
|
|
|
+ )}
|
|
|
+ />
|
|
|
|
|
|
- <div className="flex items-center justify-between">
|
|
|
- <Label>禁用</Label>
|
|
|
- <Switch
|
|
|
- checked={formData.isDisabled}
|
|
|
- onCheckedChange={(checked) => setFormData({ ...formData, isDisabled: checked })}
|
|
|
- />
|
|
|
- </div>
|
|
|
- </div>
|
|
|
+ <FormField
|
|
|
+ control={updateForm.control}
|
|
|
+ name="description"
|
|
|
+ render={({ field }) => (
|
|
|
+ <FormItem>
|
|
|
+ <FormLabel>模板描述</FormLabel>
|
|
|
+ <FormControl>
|
|
|
+ <Input placeholder="请输入模板描述" {...field} />
|
|
|
+ </FormControl>
|
|
|
+ <FormMessage />
|
|
|
+ </FormItem>
|
|
|
+ )}
|
|
|
+ />
|
|
|
+
|
|
|
+ <FormField
|
|
|
+ control={updateForm.control}
|
|
|
+ name="fileId"
|
|
|
+ render={({ field }) => (
|
|
|
+ <FormItem>
|
|
|
+ <FormLabel>关联文件</FormLabel>
|
|
|
+ <FormControl>
|
|
|
+ <FileSelector
|
|
|
+ value={field.value || undefined}
|
|
|
+ onChange={(value) => field.onChange(value)}
|
|
|
+ maxSize={50}
|
|
|
+ uploadPath="/templates"
|
|
|
+ uploadButtonText="上传模板文件"
|
|
|
+ previewSize="medium"
|
|
|
+ placeholder="选择模板文件"
|
|
|
+ title="选择模板文件"
|
|
|
+ description="上传新文件或从已有文件中选择"
|
|
|
+ filterType="all"
|
|
|
+ />
|
|
|
+ </FormControl>
|
|
|
+ <FormDescription>修改关联的模板文件</FormDescription>
|
|
|
+ <FormMessage />
|
|
|
+ </FormItem>
|
|
|
+ )}
|
|
|
+ />
|
|
|
+
|
|
|
+ <FormField
|
|
|
+ control={updateForm.control}
|
|
|
+ name="category"
|
|
|
+ render={({ field }) => (
|
|
|
+ <FormItem>
|
|
|
+ <FormLabel>分类</FormLabel>
|
|
|
+ <FormControl>
|
|
|
+ <Input placeholder="请输入分类名称" {...field} />
|
|
|
+ </FormControl>
|
|
|
+ <FormMessage />
|
|
|
+ </FormItem>
|
|
|
+ )}
|
|
|
+ />
|
|
|
|
|
|
- <div className="flex justify-end gap-2 mt-6">
|
|
|
- <Button variant="outline" onClick={() => setIsModalOpen(false)}>
|
|
|
+ <FormField
|
|
|
+ control={updateForm.control}
|
|
|
+ name="isFree"
|
|
|
+ render={({ field }) => (
|
|
|
+ <FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
|
|
|
+ <div className="space-y-0.5">
|
|
|
+ <FormLabel className="text-base">免费状态</FormLabel>
|
|
|
+ <FormDescription>
|
|
|
+ 设置为免费后用户无需付费即可下载
|
|
|
+ </FormDescription>
|
|
|
+ </div>
|
|
|
+ <FormControl>
|
|
|
+ <Switch
|
|
|
+ checked={field.value === 1}
|
|
|
+ onCheckedChange={(checked) => field.onChange(checked ? 1 : 0)}
|
|
|
+ />
|
|
|
+ </FormControl>
|
|
|
+ </FormItem>
|
|
|
+ )}
|
|
|
+ />
|
|
|
+
|
|
|
+ <FormField
|
|
|
+ control={updateForm.control}
|
|
|
+ name="isDisabled"
|
|
|
+ render={({ field }) => (
|
|
|
+ <FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
|
|
|
+ <div className="space-y-0.5">
|
|
|
+ <FormLabel className="text-base">模板状态</FormLabel>
|
|
|
+ <FormDescription>
|
|
|
+ 禁用后模板将不会在模板广场显示
|
|
|
+ </FormDescription>
|
|
|
+ </div>
|
|
|
+ <FormControl>
|
|
|
+ <Switch
|
|
|
+ checked={field.value === 1}
|
|
|
+ onCheckedChange={(checked) => field.onChange(checked ? 1 : 0)}
|
|
|
+ />
|
|
|
+ </FormControl>
|
|
|
+ </FormItem>
|
|
|
+ )}
|
|
|
+ />
|
|
|
+
|
|
|
+ <DialogFooter>
|
|
|
+ <Button type="button" variant="outline" onClick={() => setIsModalOpen(false)}>
|
|
|
+ 取消
|
|
|
+ </Button>
|
|
|
+ <Button type="submit">
|
|
|
+ 更新模板
|
|
|
+ </Button>
|
|
|
+ </DialogFooter>
|
|
|
+ </form>
|
|
|
+ </Form>
|
|
|
+ )}
|
|
|
+ </DialogContent>
|
|
|
+ </Dialog>
|
|
|
+
|
|
|
+ {/* 删除确认对话框 */}
|
|
|
+ <Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
|
|
+ <DialogContent>
|
|
|
+ <DialogHeader>
|
|
|
+ <DialogTitle>确认删除</DialogTitle>
|
|
|
+ <DialogDescription>
|
|
|
+ 确定要删除这个模板吗?此操作无法撤销。
|
|
|
+ </DialogDescription>
|
|
|
+ </DialogHeader>
|
|
|
+ <DialogFooter>
|
|
|
+ <Button variant="outline" onClick={() => setDeleteDialogOpen(false)}>
|
|
|
取消
|
|
|
</Button>
|
|
|
- <Button onClick={handleSubmit}>
|
|
|
- {editingTemplate ? '更新' : '创建'}
|
|
|
+ <Button variant="destructive" onClick={confirmDelete}>
|
|
|
+ 删除
|
|
|
</Button>
|
|
|
- </div>
|
|
|
+ </DialogFooter>
|
|
|
</DialogContent>
|
|
|
</Dialog>
|
|
|
</div>
|
|
|
);
|
|
|
};
|
|
|
|
|
|
-export default Templates;
|
|
|
+export default TemplatesPage;
|