2
0
Просмотр исходного кода

♻️ refactor(classroom): 重构教室数据管理页面从 Antd 迁移到 Shadcn UI

- 替换 Antd 组件为 Shadcn UI 组件(Button, Card, Table, Dialog等)
- 集成 React Hook Form 和 Zod 进行表单验证
- 使用 TanStack Query 替代手动数据获取和状态管理
- 优化加载状态显示,添加骨架屏
- 实现删除确认对话框增强用户体验
- 更新日期处理从 dayjs 改为 date-fns
- 统一状态管理和表单处理逻辑
yourname 6 месяцев назад
Родитель
Сommit
55ad5350a7
2 измененных файлов с 714 добавлено и 337 удалено
  1. 713 336
      src/client/admin/pages/ClassroomDataPage.tsx
  2. 1 1
      src/server/api/classroom-data/index.ts

+ 713 - 336
src/client/admin/pages/ClassroomDataPage.tsx

@@ -1,154 +1,197 @@
-import React, { useState, useEffect } from 'react';
-import { Table, Button, Modal, Form, Input, DatePicker, Space, Typography, message, Select } from 'antd';
-import { PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined, CopyOutlined } from '@ant-design/icons';
+import React, { useState } from 'react';
+import { useQuery } from '@tanstack/react-query';
+import { format } from 'date-fns';
+import { zhCN } from 'date-fns/locale';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { useForm } from 'react-hook-form';
+import { toast } from 'sonner';
+import { Plus, Search, Edit, Trash2, Copy, Calendar } from 'lucide-react';
+
 import { classroomDataClient } from '@/client/api';
 import type { InferResponseType, InferRequestType } from 'hono/client';
-import { App } from 'antd';
-import dayjs from 'dayjs';
+import { ClassroomStatus, ClassroomStatusNameMap, CreateClassroomDataDto, UpdateClassroomDataDto } from '@/server/modules/classroom/classroom-data.schema';
 
-const { Option } = Select;
+import { Button } from '@/client/components/ui/button';
+import { Input } from '@/client/components/ui/input';
+import { Badge } from '@/client/components/ui/badge';
+import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/client/components/ui/card';
+import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/client/components/ui/table';
+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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/client/components/ui/select';
+import { Skeleton } from '@/client/components/ui/skeleton';
+import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from '@/client/components/ui/alert-dialog';
 
-const { Title } = Typography;
+import { DataTablePagination } from '@/client/admin/components/DataTablePagination';
 
-// 定义类型
 type ClassroomDataListResponse = InferResponseType<typeof classroomDataClient.$get, 200>;
 type ClassroomDataItem = ClassroomDataListResponse['data'][0];
 type CreateClassroomDataRequest = InferRequestType<typeof classroomDataClient.$post>['json'];
 type UpdateClassroomDataRequest = InferRequestType<typeof classroomDataClient[':id']['$put']>['json'];
 
+// 表单schema
+const createFormSchema = CreateClassroomDataDto;
+const updateFormSchema = UpdateClassroomDataDto;
+
 export const ClassroomDataPage: React.FC = () => {
-  const [data, setData] = useState<ClassroomDataItem[]>([]);
-  const [loading, setLoading] = useState<boolean>(true);
-  const [pagination, setPagination] = useState({
-    current: 1,
-    pageSize: 10,
-    total: 0,
+  const [searchParams, setSearchParams] = useState({
+    page: 1,
+    limit: 10,
+    search: ''
+  });
+  const [isModalOpen, setIsModalOpen] = useState(false);
+  const [isCreateForm, setIsCreateForm] = useState(true);
+  const [editingItem, setEditingItem] = useState<ClassroomDataItem | null>(null);
+  const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
+  const [itemToDelete, setItemToDelete] = useState<number | null>(null);
+
+  // 创建表单
+  const createForm = useForm<CreateClassroomDataRequest>({
+    resolver: zodResolver(createFormSchema),
+    defaultValues: {
+      classroomNo: null,
+      trainingDate: null,
+      holdingStock: null,
+      holdingCash: null,
+      price: null,
+      code: null,
+      status: null,
+      spare: null,
+      submitUser: null
+    }
+  });
+
+  // 更新表单
+  const updateForm = useForm<UpdateClassroomDataRequest>({
+    resolver: zodResolver(updateFormSchema),
+    defaultValues: {
+      classroomNo: null,
+      trainingDate: null,
+      holdingStock: null,
+      holdingCash: null,
+      price: null,
+      code: null,
+      status: null,
+      spare: null,
+      submitUser: null
+    }
   });
-  const [searchText, setSearchText] = useState('');
-  const [isModalVisible, setIsModalVisible] = useState(false);
-  const [isEditing, setIsEditing] = useState(false);
-  const [currentItem, setCurrentItem] = useState<ClassroomDataItem | null>(null);
-  const [form] = Form.useForm();
-  const { message: antMessage } = App.useApp();
 
   // 获取数据列表
-  const fetchData = async () => {
-    try {
-      setLoading(true);
+  const { data: classroomData, isLoading, refetch } = useQuery({
+    queryKey: ['classroom-data', searchParams],
+    queryFn: async () => {
       const res = await classroomDataClient.$get({
         query: {
-          page: pagination.current,
-          pageSize: pagination.pageSize,
-          keyword: searchText,
-        },
+          page: searchParams.page,
+          pageSize: searchParams.limit,
+          keyword: searchParams.search
+        }
       });
-      
-      if (!res.ok) {
-        throw new Error('获取数据失败');
+      if (res.status !== 200) {
+        throw new Error('获取教室数据失败');
       }
-      
-      const result = await res.json() as ClassroomDataListResponse;
-      setData(result.data);
-      setPagination(prev => ({
-        ...prev,
-        total: result.pagination.total,
-      }));
-    } catch (error) {
-      console.error('获取教室数据失败:', error);
-      antMessage.error('获取数据失败,请重试');
-    } finally {
-      setLoading(false);
+      return await res.json();
     }
-  };
+  });
 
-  // 初始加载和分页、搜索变化时重新获取数据
-  useEffect(() => {
-    fetchData();
-  }, [pagination.current, pagination.pageSize]);
+  const data = classroomData?.data || [];
+  const totalCount = classroomData?.pagination?.total || 0;
 
-  // 搜索功能
-  const handleSearch = () => {
-    setPagination(prev => ({ ...prev, current: 1 }));
-    fetchData();
+  // 处理搜索
+  const handleSearch = (e: React.FormEvent) => {
+    e.preventDefault();
+    setSearchParams(prev => ({ ...prev, page: 1 }));
   };
 
-  // 显示创建模态
+  // 打开创建对话
   const showCreateModal = () => {
-    setIsEditing(false);
-    setCurrentItem(null);
-    form.resetFields();
-    setIsModalVisible(true);
+    setIsCreateForm(true);
+    setEditingItem(null);
+    createForm.reset();
+    setIsModalOpen(true);
   };
 
-  // 显示编辑模态框
-  const showEditModal = (record: ClassroomDataItem) => {
-    setIsEditing(true);
-    setCurrentItem(record);
-    form.setFieldsValue({
-      classroomNo: record.classroomNo || undefined,
-      trainingDate: record.trainingDate ? dayjs(record.trainingDate) : null,
-      holdingStock: record.holdingStock || undefined,
-      holdingCash: record.holdingCash || undefined,
-      price: record.price || undefined,
-      code: record.code || undefined,
-      status: record.status !== null ? record.status : undefined,
-      spare: record.spare || undefined,
+  // 打开编辑对话框
+  const showEditModal = (item: ClassroomDataItem) => {
+    setIsCreateForm(false);
+    setEditingItem(item);
+    updateForm.reset({
+      classroomNo: item.classroomNo || null,
+      trainingDate: item.trainingDate ? new Date(item.trainingDate) : null,
+      holdingStock: item.holdingStock || null,
+      holdingCash: item.holdingCash || null,
+      price: item.price || null,
+      code: item.code || null,
+      status: item.status,
+      spare: item.spare || null,
+      submitUser: item.submitUser || null
     });
-    setIsModalVisible(true);
+    setIsModalOpen(true);
   };
 
-  // 处理表单提交
-  const handleSubmit = async () => {
+  // 处理创建提交
+  const handleCreateSubmit = async (formData: CreateClassroomDataRequest) => {
     try {
-      const values = await form.validateFields();
-      
-      if (isEditing && currentItem) {
-        // 更新数据
-        const res = await classroomDataClient[':id'].$put({
-          param: { id: currentItem.id },
-          json: values as UpdateClassroomDataRequest,
-        });
-        
-        if (!res.ok) {
-          throw new Error('更新失败');
-        }
-        antMessage.success('更新成功');
-      } else {
-        // 创建新数据
-        const res = await classroomDataClient.$post({
-          json: values as CreateClassroomDataRequest,
-        });
-        
-        if (!res.ok) {
-          throw new Error('创建失败');
-        }
-        antMessage.success('创建成功');
+      const res = await classroomDataClient.$post({ json: formData });
+      if (res.status !== 201) {
+        const error = await res.json();
+        throw new Error(error.message || '创建教室数据失败');
       }
-      
-      setIsModalVisible(false);
-      fetchData();
+      toast.success('教室数据创建成功');
+      setIsModalOpen(false);
+      createForm.reset();
+      refetch();
     } catch (error) {
-      console.error('提交表单失败:', error);
-      antMessage.error(isEditing ? '更新失败,请重试' : '创建失败,请重试');
+      toast.error(error instanceof Error ? error.message : '创建失败,请重试');
     }
   };
 
-  // 删除数据
-  const handleDelete = async (id: number) => {
+  // 处理更新提交
+  const handleUpdateSubmit = async (formData: UpdateClassroomDataRequest) => {
+    if (!editingItem) return;
+    
     try {
-      const res = await classroomDataClient[':id'].$delete({
-        param: { id },
+      const res = await classroomDataClient[':id']['$put']({
+        param: { id: editingItem.id.toString() },
+        json: formData
       });
-      
-      if (!res.ok) {
-        throw new Error('删除失败');
+      if (res.status !== 200) {
+        const error = await res.json();
+        throw new Error(error.message || '更新教室数据失败');
       }
-      
-      antMessage.success('删除成功');
-      fetchData();
+      toast.success('教室数据更新成功');
+      setIsModalOpen(false);
+      updateForm.reset();
+      refetch();
     } catch (error) {
-      console.error('删除数据失败:', error);
-      antMessage.error('删除失败,请重试');
+      toast.error(error instanceof Error ? error.message : '更新失败,请重试');
+    }
+  };
+
+  // 打开删除确认对话框
+  const showDeleteDialog = (id: number) => {
+    setItemToDelete(id);
+    setDeleteDialogOpen(true);
+  };
+
+  // 处理删除
+  const handleDelete = async () => {
+    if (!itemToDelete) return;
+    
+    try {
+      const res = await classroomDataClient[':id']['$delete']({
+        param: { id: itemToDelete.toString() }
+      });
+      if (res.status !== 204) {
+        throw new Error('删除教室数据失败');
+      }
+      toast.success('教室数据删除成功');
+      setDeleteDialogOpen(false);
+      setItemToDelete(null);
+      refetch();
+    } catch (error) {
+      toast.error('删除失败,请重试');
     }
   };
 
@@ -174,239 +217,573 @@ export const ClassroomDataPage: React.FC = () => {
     }
 
     navigator.clipboard.writeText(url).then(() => {
-      antMessage.success(successMsg);
+      toast.success(successMsg);
     }).catch(() => {
-      antMessage.error('复制失败,请手动复制');
+      toast.error('复制失败,请手动复制');
     });
   };
 
-  // 表格列定义
-  const columns = [
-    {
-      title: 'ID',
-      dataIndex: 'id',
-      key: 'id',
-      width: 80,
-    },
-    {
-      title: '教室号',
-      dataIndex: 'classroomNo',
-      key: 'classroomNo',
-    },
-    {
-      title: '训练日期',
-      dataIndex: 'trainingDate',
-      key: 'trainingDate',
-      render: (date: string) => date ? dayjs(date).format('YYYY-MM-DD HH:mm:ss') : '-',
-    },
-    {
-      title: '持股',
-      dataIndex: 'holdingStock',
-      key: 'holdingStock',
-    },
-    {
-      title: '持币',
-      dataIndex: 'holdingCash',
-      key: 'holdingCash',
-    },
-    {
-      title: '价格',
-      dataIndex: 'price',
-      key: 'price',
-    },
-    {
-      title: '代码',
-      dataIndex: 'code',
-      key: 'code',
-    },
-    {
-      title: '状态',
-      dataIndex: 'status',
-      key: 'status',
-      render: (status: number) => {
-        const statusMap = {
-          0: '关闭',
-          1: '开放'
-        };
-        return statusMap[status as keyof typeof statusMap] || '-';
-      },
-    },
-    {
-      title: '链接',
-      key: 'links',
-      render: (_: any, record: ClassroomDataItem) => (
-        <Space direction="vertical" size={4}>
-          <Button
-            type="link"
-            size="small"
-            icon={<CopyOutlined />}
-            onClick={() => copyLink('stock', record.classroomNo || '', record.code || '')}
-          >
-            复制股票训练链接
-          </Button>
-          <Button
-            type="link"
-            size="small"
-            icon={<CopyOutlined />}
-            onClick={() => copyLink('exam', record.classroomNo || '', record.code || '')}
-          >
-            复制答题卡链接
-          </Button>
-          <Button
-            type="link"
-            size="small"
-            icon={<CopyOutlined />}
-            onClick={() => copyLink('admin', record.classroomNo || '', record.code || '')}
-          >
-            复制管理员链接
-          </Button>
-        </Space>
-      ),
-    },
-    {
-      title: '操作',
-      key: 'action',
-      render: (_: any, record: ClassroomDataItem) => (
-        <Space size="small">
-          <Button
-            type="text"
-            icon={<EditOutlined />}
-            onClick={() => showEditModal(record)}
-          >
-            编辑
-          </Button>
-          <Button
-            type="text"
-            danger
-            icon={<DeleteOutlined />}
-            onClick={() => handleDelete(record.id)}
-          >
-            删除
-          </Button>
-        </Space>
-      ),
-    },
-  ];
+  // 加载状态
+  if (isLoading) {
+    return (
+      <div className="space-y-4">
+        <div className="flex justify-between items-center">
+          <Skeleton className="h-8 w-48" />
+          <Skeleton className="h-10 w-32" />
+        </div>
+        
+        <Card>
+          <CardHeader>
+            <Skeleton className="h-6 w-1/4" />
+          </CardHeader>
+          <CardContent>
+            <div className="space-y-3">
+              {[...Array(5)].map((_, i) => (
+                <div key={i} className="flex gap-4">
+                  <Skeleton className="h-10 flex-1" />
+                  <Skeleton className="h-10 flex-1" />
+                  <Skeleton className="h-10 flex-1" />
+                  <Skeleton className="h-10 w-20" />
+                </div>
+              ))}
+            </div>
+          </CardContent>
+        </Card>
+      </div>
+    );
+  }
 
   return (
-    <div className="page-container">
-      <div className="page-header" style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
-        <Title level={2}>教室数据管理</Title>
-        <Button type="primary" icon={<PlusOutlined />} onClick={showCreateModal}>
+    <div className="space-y-4">
+      {/* 页面标题 */}
+      <div className="flex justify-between items-center">
+        <h1 className="text-2xl font-bold">教室数据管理</h1>
+        <Button onClick={showCreateModal}>
+          <Plus className="mr-2 h-4 w-4" />
           添加数据
         </Button>
       </div>
-      
-      <div className="search-container" style={{ marginBottom: 16 }}>
-        <Input
-          placeholder="搜索教室号、代码或提交用户"
-          value={searchText}
-          onChange={(e) => setSearchText(e.target.value)}
-          onPressEnter={handleSearch}
-          style={{ width: 300 }}
-          suffix={<SearchOutlined onClick={handleSearch} />}
-        />
-      </div>
-      
-      <Table
-        columns={columns}
-        dataSource={data.map(item => ({ ...item, key: item.id }))}
-        loading={loading}
-        pagination={{
-          current: pagination.current,
-          pageSize: pagination.pageSize,
-          total: pagination.total,
-          showSizeChanger: true,
-          showTotal: (total) => `共 ${total} 条记录`,
-        }}
-        onChange={(p) => setPagination({ ...pagination, current: p.current || 1, pageSize: p.pageSize || 10 })}
-        bordered
-        scroll={{ x: 'max-content' }}
-        className="ant-table-striped"
-        rowClassName={(record, index) => index % 2 === 0 ? 'bg-white' : 'bg-gray-50'}
-        rowKey="id"
-      />
-      
-      <Modal
-        title={isEditing ? "编辑教室数据" : "添加教室数据"}
-        open={isModalVisible}
-        onOk={handleSubmit}
-        onCancel={() => setIsModalVisible(false)}
-        destroyOnClose
-        maskClosable={false}
-      >
-        <Form
-          form={form}
-          layout="vertical"
-          name="classroom_data_form"
-        >
-          <Form.Item
-            name="classroomNo"
-            label="教室号"
-            rules={[{ max: 255, message: '教室号不能超过255个字符' }]}
-          >
-            <Input placeholder="请输入教室号" />
-          </Form.Item>
-          
-          <Form.Item
-            name="trainingDate"
-            label="训练日期"
-          >
-            <DatePicker showTime placeholder="请选择训练日期" style={{ width: '100%' }} />
-          </Form.Item>
-          
-          <Form.Item
-            name="holdingStock"
-            label="持股"
-            rules={[{ max: 255, message: '持股信息不能超过255个字符' }]}
-          >
-            <Input placeholder="请输入持股信息" />
-          </Form.Item>
-          
-          <Form.Item
-            name="holdingCash"
-            label="持币"
-            rules={[{ max: 255, message: '持币信息不能超过255个字符' }]}
-          >
-            <Input placeholder="请输入持币信息" />
-          </Form.Item>
-          
-          <Form.Item
-            name="price"
-            label="价格"
-            rules={[{ max: 255, message: '价格不能超过255个字符' }]}
-          >
-            <Input placeholder="请输入价格" />
-          </Form.Item>
-          
-          <Form.Item
-            name="code"
-            label="代码"
-            rules={[{ max: 255, message: '代码不能超过255个字符' }]}
-          >
-            <Input placeholder="请输入代码" />
-          </Form.Item>
-          
-          <Form.Item
-            name="status"
-            label="状态"
-            rules={[{ required: true, message: '请选择状态' }]}
-          >
-            <Select placeholder="请选择状态">
-              <Option value={0}>关闭</Option>
-              <Option value={1}>开放</Option>
-            </Select>
-          </Form.Item>
-          
-          <Form.Item
-            name="spare"
-            label="备用"
-            rules={[{ max: 255, message: '备用信息不能超过255个字符' }]}
-          >
-            <Input placeholder="请输入备用信息" />
-          </Form.Item>
-        </Form>
-      </Modal>
+
+      {/* 搜索区域 */}
+      <Card>
+        <CardHeader>
+          <CardTitle>教室数据列表</CardTitle>
+          <CardDescription>管理教室训练数据</CardDescription>
+        </CardHeader>
+        <CardContent>
+          <form onSubmit={handleSearch} className="flex gap-2 mb-4">
+            <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 className="rounded-md border">
+            <Table>
+              <TableHeader>
+                <TableRow>
+                  <TableHead>ID</TableHead>
+                  <TableHead>教室号</TableHead>
+                  <TableHead>训练日期</TableHead>
+                  <TableHead>持股</TableHead>
+                  <TableHead>持币</TableHead>
+                  <TableHead>价格</TableHead>
+                  <TableHead>代码</TableHead>
+                  <TableHead>状态</TableHead>
+                  <TableHead>链接</TableHead>
+                  <TableHead className="text-right">操作</TableHead>
+                </TableRow>
+              </TableHeader>
+              <TableBody>
+                {data.map((item) => (
+                  <TableRow key={item.id}>
+                    <TableCell className="font-medium">{item.id}</TableCell>
+                    <TableCell>{item.classroomNo || '-'}</TableCell>
+                    <TableCell>
+                      {item.trainingDate ? format(new Date(item.trainingDate), 'yyyy-MM-dd HH:mm:ss', { locale: zhCN }) : '-'}
+                    </TableCell>
+                    <TableCell>{item.holdingStock || '-'}</TableCell>
+                    <TableCell>{item.holdingCash || '-'}</TableCell>
+                    <TableCell>{item.price || '-'}</TableCell>
+                    <TableCell>{item.code || '-'}</TableCell>
+                    <TableCell>
+                      <Badge 
+                        variant={item.status === ClassroomStatus.OPEN ? 'default' : 'secondary'}
+                        className={item.status === ClassroomStatus.OPEN ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'}
+                      >
+                        {item.status !== null ? ClassroomStatusNameMap[item.status] : '-'}
+                      </Badge>
+                    </TableCell>
+                    <TableCell>
+                      <div className="flex flex-col gap-1">
+                        <Button
+                          variant="link"
+                          size="sm"
+                          onClick={() => copyLink('stock', item.classroomNo || '', item.code || '')}
+                          className="h-6 p-0 text-xs"
+                        >
+                          <Copy className="mr-1 h-3 w-3" />
+                          股票训练
+                        </Button>
+                        <Button
+                          variant="link"
+                          size="sm"
+                          onClick={() => copyLink('exam', item.classroomNo || '', item.code || '')}
+                          className="h-6 p-0 text-xs"
+                        >
+                          <Copy className="mr-1 h-3 w-3" />
+                          答题卡
+                        </Button>
+                        <Button
+                          variant="link"
+                          size="sm"
+                          onClick={() => copyLink('admin', item.classroomNo || '', item.code || '')}
+                          className="h-6 p-0 text-xs"
+                        >
+                          <Copy className="mr-1 h-3 w-3" />
+                          管理员
+                        </Button>
+                      </div>
+                    </TableCell>
+                    <TableCell className="text-right">
+                      <div className="flex justify-end gap-2">
+                        <Button
+                          variant="ghost"
+                          size="sm"
+                          onClick={() => showEditModal(item)}
+                        >
+                          <Edit className="h-4 w-4" />
+                        </Button>
+                        <Button
+                          variant="ghost"
+                          size="sm"
+                          onClick={() => showDeleteDialog(item.id)}
+                          className="text-destructive hover:text-destructive"
+                        >
+                          <Trash2 className="h-4 w-4" />
+                        </Button>
+                      </div>
+                    </TableCell>
+                  </TableRow>
+                ))}
+              </TableBody>
+            </Table>
+          </div>
+
+          {data.length === 0 && (
+            <div className="text-center py-8">
+              <p className="text-muted-foreground">暂无数据</p>
+            </div>
+          )}
+
+          {/* 分页 */}
+          <DataTablePagination
+            currentPage={searchParams.page}
+            pageSize={searchParams.limit}
+            totalCount={totalCount}
+            onPageChange={(page, limit) => setSearchParams(prev => ({ ...prev, page, limit }))}
+          />
+        </CardContent>
+      </Card>
+
+      {/* 创建/编辑对话框 */}
+      <Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
+        <DialogContent className="sm:max-w-[500px] max-h-[90vh] overflow-y-auto">
+          <DialogHeader>
+            <DialogTitle>{isCreateForm ? '创建教室数据' : '编辑教室数据'}</DialogTitle>
+            <DialogDescription>
+              {isCreateForm ? '创建新的教室训练数据' : '编辑现有教室数据信息'}
+            </DialogDescription>
+          </DialogHeader>
+
+          {isCreateForm ? (
+            <Form {...createForm}>
+              <form onSubmit={createForm.handleSubmit(handleCreateSubmit)} className="space-y-4">
+                <FormField
+                  control={createForm.control}
+                  name="classroomNo"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>教室号</FormLabel>
+                      <FormControl>
+                        <Input 
+                          placeholder="请输入教室号" 
+                          value={field.value || ''}
+                          onChange={(e) => field.onChange(e.target.value || null)}
+                        />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={createForm.control}
+                  name="trainingDate"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>训练日期</FormLabel>
+                      <FormControl>
+                        <Input
+                          type="datetime-local"
+                          value={field.value ? format(new Date(field.value), "yyyy-MM-dd'T'HH:mm") : ''}
+                          onChange={(e) => field.onChange(e.target.value ? new Date(e.target.value) : null)}
+                        />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={createForm.control}
+                  name="holdingStock"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>持股</FormLabel>
+                      <FormControl>
+                        <Input 
+                          placeholder="请输入持股信息" 
+                          value={field.value || ''}
+                          onChange={(e) => field.onChange(e.target.value || null)}
+                        />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={createForm.control}
+                  name="holdingCash"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>持币</FormLabel>
+                      <FormControl>
+                        <Input 
+                          placeholder="请输入持币信息" 
+                          value={field.value || ''}
+                          onChange={(e) => field.onChange(e.target.value || null)}
+                        />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={createForm.control}
+                  name="price"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>价格</FormLabel>
+                      <FormControl>
+                        <Input 
+                          placeholder="请输入价格" 
+                          value={field.value || ''}
+                          onChange={(e) => field.onChange(e.target.value || null)}
+                        />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={createForm.control}
+                  name="code"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>代码</FormLabel>
+                      <FormControl>
+                        <Input 
+                          placeholder="请输入代码" 
+                          value={field.value || ''}
+                          onChange={(e) => field.onChange(e.target.value || null)}
+                        />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={createForm.control}
+                  name="status"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>状态</FormLabel>
+                      <Select
+                        onValueChange={(value) => field.onChange(value ? parseInt(value) : null)}
+                        value={field.value?.toString() || ''}
+                      >
+                        <FormControl>
+                          <SelectTrigger>
+                            <SelectValue placeholder="请选择状态" />
+                          </SelectTrigger>
+                        </FormControl>
+                        <SelectContent>
+                          <SelectItem value={ClassroomStatus.OPEN.toString()}>开放</SelectItem>
+                          <SelectItem value={ClassroomStatus.CLOSED.toString()}>关闭</SelectItem>
+                        </SelectContent>
+                      </Select>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={createForm.control}
+                  name="spare"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>备用</FormLabel>
+                      <FormControl>
+                        <Input
+                          placeholder="请输入备用信息"
+                          value={field.value || ''}
+                          onChange={(e) => field.onChange(e.target.value || null)}
+                        />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={createForm.control}
+                  name="submitUser"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>提交用户</FormLabel>
+                      <FormControl>
+                        <Input
+                          placeholder="请输入提交用户"
+                          value={field.value || ''}
+                          onChange={(e) => field.onChange(e.target.value || null)}
+                        />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <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="classroomNo"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>教室号</FormLabel>
+                      <FormControl>
+                        <Input
+                          placeholder="请输入教室号"
+                          value={field.value || ''}
+                          onChange={(e) => field.onChange(e.target.value || null)}
+                        />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={updateForm.control}
+                  name="trainingDate"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>训练日期</FormLabel>
+                      <FormControl>
+                        <Input
+                          type="datetime-local"
+                          value={field.value ? format(new Date(field.value), "yyyy-MM-dd'T'HH:mm") : ''}
+                          onChange={(e) => field.onChange(e.target.value ? new Date(e.target.value) : null)}
+                        />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={updateForm.control}
+                  name="holdingStock"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>持股</FormLabel>
+                      <FormControl>
+                        <Input
+                          placeholder="请输入持股信息"
+                          value={field.value || ''}
+                          onChange={(e) => field.onChange(e.target.value || null)}
+                        />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={updateForm.control}
+                  name="holdingCash"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>持币</FormLabel>
+                      <FormControl>
+                        <Input
+                          placeholder="请输入持币信息"
+                          value={field.value || ''}
+                          onChange={(e) => field.onChange(e.target.value || null)}
+                        />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={updateForm.control}
+                  name="price"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>价格</FormLabel>
+                      <FormControl>
+                        <Input
+                          placeholder="请输入价格"
+                          value={field.value || ''}
+                          onChange={(e) => field.onChange(e.target.value || null)}
+                        />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={updateForm.control}
+                  name="code"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>代码</FormLabel>
+                      <FormControl>
+                        <Input
+                          placeholder="请输入代码"
+                          value={field.value || ''}
+                          onChange={(e) => field.onChange(e.target.value || null)}
+                        />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={updateForm.control}
+                  name="status"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>状态</FormLabel>
+                      <Select
+                        onValueChange={(value) => field.onChange(value ? parseInt(value) : null)}
+                        value={field.value?.toString() || ''}
+                      >
+                        <FormControl>
+                          <SelectTrigger>
+                            <SelectValue placeholder="请选择状态" />
+                          </SelectTrigger>
+                        </FormControl>
+                        <SelectContent>
+                          <SelectItem value={ClassroomStatus.OPEN.toString()}>开放</SelectItem>
+                          <SelectItem value={ClassroomStatus.CLOSED.toString()}>关闭</SelectItem>
+                        </SelectContent>
+                      </Select>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={updateForm.control}
+                  name="spare"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>备用</FormLabel>
+                      <FormControl>
+                        <Input
+                          placeholder="请输入备用信息"
+                          value={field.value || ''}
+                          onChange={(e) => field.onChange(e.target.value || null)}
+                        />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={updateForm.control}
+                  name="submitUser"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>提交用户</FormLabel>
+                      <FormControl>
+                        <Input
+                          placeholder="请输入提交用户"
+                          value={field.value || ''}
+                          onChange={(e) => field.onChange(e.target.value || null)}
+                        />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <DialogFooter>
+                  <Button type="button" variant="outline" onClick={() => setIsModalOpen(false)}>
+                    取消
+                  </Button>
+                  <Button type="submit">更新</Button>
+                </DialogFooter>
+              </form>
+            </Form>
+          )}
+        </DialogContent>
+      </Dialog>
+
+      {/* 删除确认对话框 */}
+      <AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
+        <AlertDialogContent>
+          <AlertDialogHeader>
+            <AlertDialogTitle>确认删除</AlertDialogTitle>
+            <AlertDialogDescription>
+              确定要删除这个教室数据吗?此操作无法撤销。
+            </AlertDialogDescription>
+          </AlertDialogHeader>
+          <AlertDialogFooter>
+            <AlertDialogCancel>取消</AlertDialogCancel>
+            <AlertDialogAction onClick={handleDelete}>删除</AlertDialogAction>
+          </AlertDialogFooter>
+        </AlertDialogContent>
+      </AlertDialog>
     </div>
   );
 };

+ 1 - 1
src/server/api/classroom-data/index.ts

@@ -1,6 +1,6 @@
 import { createCrudRoutes } from '@/server/utils/generic-crud.routes';
 import { ClassroomData } from '@/server/modules/classroom/classroom-data.entity';
-import { ClassroomDataSchema, CreateClassroomDataDto, UpdateClassroomDataDto } from '@/server/modules/classroom/classroom-data.entity';
+import { ClassroomDataSchema, CreateClassroomDataDto, UpdateClassroomDataDto } from '@/server/modules/classroom/classroom-data.schema';
 import { authMiddleware } from '@/server/middleware/auth.middleware';
 
 const classroomDataRoutes = createCrudRoutes({