Browse Source

✨ feat(templates): 重构模板管理页面并完善功能

- 使用 react-hook-form 和 zod 进行表单验证,提升表单处理能力
- 集成 @tanstack/react-query 进行数据管理,优化数据获取和状态管理
- 添加分页、搜索和筛选功能,提升用户体验
- 实现完整的创建、编辑、删除模板操作,并添加确认对话框
- 使用骨架屏优化加载体验,添加文件选择器组件
- 更新文档说明,完善 zod v4 的使用规范和枚举处理方式

📝 docs(templates): 更新模板创建规范文档

- 修正枚举使用示例,明确 z.enum(Enum) 的正确用法
- 添加 zod v4 中 coerce 方法的类型泛型说明
- 优化文档结构和示例代码的可读性
yourname 3 months ago
parent
commit
b6ccc1733f
2 changed files with 577 additions and 279 deletions
  1. 15 20
      .roo/commands/generic-crud-创建实体验证规则.md
  2. 562 259
      src/client/admin/pages/Templates.tsx

+ 15 - 20
.roo/commands/generic-crud-创建实体验证规则.md

@@ -37,29 +37,24 @@ description: "创建已有实体的schema指令"
 4. 实体中import用到的枚举,your-entity.schema.ts 中也要import用上
     示例:
     ```typescript
-    import { 
-        DeviceStatus, 
-        NetworkStatus, 
-        packetLoss
-    } from '@/share/monitorTypes';
+    // 教室状态枚举
+    export enum ClassroomStatus {
+    CLOSED = 0,  // 关闭
+    OPEN = 1     // 开放
+    }
     // 在当前zod v4中 z.enum 代替了 z.nativeEnum;  
     z.object({
-        // z.enum(DeviceStatus) 等价于 z.nativeEnum(DeviceStatus)
-        deviceStatus: z.enum(DeviceStatus).nullable().openapi({
-            description: '设备状态 (0正常 1维护中 2故障 3下线)',
-            example: DeviceStatus.NORMAL
+        // z.enum(ClassroomStatus) 等价于 z.nativeEnum(ClassroomStatus), 不要 z.enum([ClassroomStatus.CLOSED, ClassroomStatus.OPEN]),应该是 z.enum(ClassroomStatus)
+        z.enum(ClassroomStatus).nullable().openapi({
+            description: '状态 (0关闭 1开放)',
+            example: ClassroomStatus.OPEN
         }),
-        networkStatus: z.enum(NetworkStatus).nullable().openapi({
-            description: '网络状态 (0已连接 1已断开 2不稳定)',
-            example: NetworkStatus.CONNECTED
-        }),
-        packetLoss: z.enum(PacketLossStatus).nullable().openapi({
-            description: '丢包率 (0正常 1丢包)',
-            example: PacketLossStatus.NORMAL
-        }),
-
     })
     ```
 
-5. your-entity.schema.ts中, 只创建 模板实体完整Schema,创建模板请求Schema,更新模板请求Schema 这三样就可以了, 
-   不需要创建 模板列表响应Schema 和 schema相应的类型导出
+5. 在当前 zod v4中, z.coerce.date(), z.coerce.number() 等,都要添加类型泛型指定
+    示例:
+    ```typescript
+    z.coerce.date<Date>()
+    z.coerce.number<number>()
+    ```

+ 562 - 259
src/client/admin/pages/Templates.tsx

@@ -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;