Kaynağa Gözat

✨ feat(stock): 重构股票数据管理页面为现代化UI

- 从Ant Design迁移到Shadcn UI组件库【ui】
- 使用React Query替代手动数据管理【state】
- 集成Zod验证和React Hook Form【form】
- 添加sonner通知系统【ux】
- 实现骨架屏加载效果【loading】
- 优化表格展示和交互体验【table】
yourname 6 ay önce
ebeveyn
işleme
320dfae048
1 değiştirilmiş dosya ile 539 ekleme ve 295 silme
  1. 539 295
      src/client/admin/pages/StockDataPage.tsx

+ 539 - 295
src/client/admin/pages/StockDataPage.tsx

@@ -1,12 +1,57 @@
 import React, { useState, useEffect } from 'react';
-import { Table, Button, Modal, Form, Input, Space, Typography, message, Card, Tag } from 'antd';
-import { PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined, EyeOutlined } from '@ant-design/icons';
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import { format } from 'date-fns';
+import { zhCN } from 'date-fns/locale';
+import { toast } from 'sonner';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { useForm } from 'react-hook-form';
+import { z } from 'zod';
+
 import { stockDataClient } from '@/client/api';
 import type { InferResponseType, InferRequestType } from 'hono/client';
-import { App } from 'antd';
-// import ReactJson from 'react-json-view';
+import { CreateStockDataDto, UpdateStockDataDto } from '@/server/modules/stock/stock-data.schema';
+
+import {
+  Table,
+  TableBody,
+  TableCell,
+  TableHead,
+  TableHeader,
+  TableRow,
+} from '@/client/components/ui/table';
+import {
+  Card,
+  CardContent,
+  CardDescription,
+  CardHeader,
+  CardTitle,
+} from '@/client/components/ui/card';
+import { Button } from '@/client/components/ui/button';
+import { Input } from '@/client/components/ui/input';
+import { Textarea } from '@/client/components/ui/textarea';
+import { Label } from '@/client/components/ui/label';
+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 { Badge } from '@/client/components/ui/badge';
+import { Skeleton } from '@/client/components/ui/skeleton';
+import { DataTablePagination } from '@/client/admin/components/DataTablePagination';
 
-const { Title } = Typography;
+import { Plus, Edit, Trash2, Eye, Search } from 'lucide-react';
 
 // 定义类型
 type StockDataListResponse = InferResponseType<typeof stockDataClient.$get, 200>;
@@ -14,31 +59,48 @@ type StockDataItem = StockDataListResponse['data'][0];
 type CreateStockDataRequest = InferRequestType<typeof stockDataClient.$post>['json'];
 type UpdateStockDataRequest = InferRequestType<typeof stockDataClient[':id']['$put']>['json'];
 
-export const StockDataPage: React.FC = () => {
-  const [data, setData] = useState<StockDataItem[]>([]);
-  const [loading, setLoading] = useState<boolean>(true);
-  const [pagination, setPagination] = useState({
-    current: 1,
-    pageSize: 10,
-    total: 0,
+// 表单Schema直接使用后端定义
+const createFormSchema = CreateStockDataDto;
+const updateFormSchema = UpdateStockDataDto;
+
+export const StockDataPage = () => {
+  const queryClient = useQueryClient();
+  
+  // 状态管理
+  const [searchParams, setSearchParams] = useState({
+    page: 1,
+    limit: 10,
+    search: '',
   });
-  const [searchText, setSearchText] = useState('');
-  const [isModalVisible, setIsModalVisible] = useState(false);
-  const [isViewModalVisible, setIsViewModalVisible] = useState(false);
-  const [isEditing, setIsEditing] = useState(false);
-  const [currentItem, setCurrentItem] = useState<StockDataItem | null>(null);
-  const [form] = Form.useForm();
-  const { message: antMessage } = App.useApp();
-
-  // 获取数据列表
-  const fetchData = async () => {
-    try {
-      setLoading(true);
+  const [isModalOpen, setIsModalOpen] = useState(false);
+  const [isViewModalOpen, setIsViewModalOpen] = useState(false);
+  const [isCreateForm, setIsCreateForm] = useState(true);
+  const [editingStockData, setEditingStockData] = useState<StockDataItem | null>(null);
+  const [stockDataToDelete, setStockDataToDelete] = useState<number | null>(null);
+  const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
+
+  // 表单实例
+  const createForm = useForm<CreateStockDataRequest>({
+    resolver: zodResolver(createFormSchema),
+    defaultValues: {
+      code: '',
+      data: [],
+    },
+  });
+
+  const updateForm = useForm<UpdateStockDataRequest>({
+    resolver: zodResolver(updateFormSchema),
+  });
+
+  // 数据查询
+  const { data, isLoading, refetch } = useQuery({
+    queryKey: ['stock-data', searchParams],
+    queryFn: async () => {
       const res = await stockDataClient.$get({
         query: {
-          page: pagination.current,
-          pageSize: pagination.pageSize,
-          keyword: searchText,
+          page: searchParams.page,
+          pageSize: searchParams.limit,
+          keyword: searchParams.search,
         },
       });
       
@@ -46,303 +108,485 @@ export const StockDataPage: React.FC = () => {
         throw new Error('获取数据失败');
       }
       
-      const result = await res.json() as StockDataListResponse;
-      setData(result.data);
-      setPagination(prev => ({
-        ...prev,
-        total: result.pagination.total,
-      }));
-    } catch (error) {
-      console.error('获取股票数据失败:', error);
-      antMessage.error('获取数据失败,请重试');
-    } finally {
-      setLoading(false);
-    }
-  };
+      return await res.json() as StockDataListResponse;
+    },
+  });
 
-  // 初始加载和分页、搜索变化时重新获取数据
-  useEffect(() => {
-    fetchData();
-  }, [pagination.current, pagination.pageSize]);
+  // 创建数据
+  const createMutation = useMutation({
+    mutationFn: async (data: CreateStockDataRequest) => {
+      const res = await stockDataClient.$post({ json: data });
+      if (!res.ok) {
+        const error = await res.json();
+        throw new Error((error as any).message || '创建失败');
+      }
+      return res.json();
+    },
+    onSuccess: () => {
+      toast.success('创建成功');
+      setIsModalOpen(false);
+      refetch();
+    },
+    onError: (error) => {
+      toast.error(error instanceof Error ? error.message : '创建失败');
+    },
+  });
 
-  // 搜索功能
+  // 更新数据
+  const updateMutation = useMutation({
+    mutationFn: async ({ id, data }: { id: number; data: UpdateStockDataRequest }) => {
+      const res = await stockDataClient[':id']['$put']({
+        param: { id: id.toString() },
+        json: data,
+      });
+      if (!res.ok) {
+        const error = await res.json();
+        throw new Error((error as any).message || '更新失败');
+      }
+      return res.json();
+    },
+    onSuccess: () => {
+      toast.success('更新成功');
+      setIsModalOpen(false);
+      refetch();
+    },
+    onError: (error) => {
+      toast.error(error instanceof Error ? error.message : '更新失败');
+    },
+  });
+
+  // 删除数据
+  const deleteMutation = useMutation({
+    mutationFn: async (id: number) => {
+      const res = await stockDataClient[':id']['$delete']({
+        param: { id: id.toString() },
+      });
+      if (!res.ok) {
+        const error = await res.json();
+        throw new Error((error as any).message || '删除失败');
+      }
+      return res.json();
+    },
+    onSuccess: () => {
+      toast.success('删除成功');
+      refetch();
+    },
+    onError: (error) => {
+      toast.error(error instanceof Error ? error.message : '删除失败');
+    },
+  });
+
+  // 处理搜索
   const handleSearch = () => {
-    setPagination(prev => ({ ...prev, current: 1 }));
-    fetchData();
+    setSearchParams(prev => ({ ...prev, page: 1 }));
   };
 
-  // 显示创建模态框
-  const showCreateModal = () => {
-    setIsEditing(false);
-    setCurrentItem(null);
-    form.resetFields();
-    setIsModalVisible(true);
+  // 处理创建
+  const handleCreate = () => {
+    setIsCreateForm(true);
+    setEditingStockData(null);
+    createForm.reset();
+    setIsModalOpen(true);
   };
 
-  // 显示编辑模态框
-  const showEditModal = (record: StockDataItem) => {
-    setIsEditing(true);
-    setCurrentItem(record);
-    form.setFieldsValue({
+  // 处理编辑
+  const handleEdit = (record: StockDataItem) => {
+    setIsCreateForm(false);
+    setEditingStockData(record);
+    updateForm.reset({
       code: record.code,
-      data: JSON.stringify(record.data, null, 2 ),
+      data: record.data,
     });
-    setIsModalVisible(true);
+    setIsModalOpen(true);
+  };
+
+  // 处理查看
+  const handleView = (record: StockDataItem) => {
+    setEditingStockData(record);
+    setIsViewModalOpen(true);
   };
 
-  // 显示查看模态框
-  const showViewModal = (record: StockDataItem) => {
-    setCurrentItem(record);
-    setIsViewModalVisible(true);
+  // 处理删除
+  const handleDelete = (id: number) => {
+    setStockDataToDelete(id);
+    setDeleteDialogOpen(true);
+  };
+
+  const confirmDelete = async () => {
+    if (stockDataToDelete) {
+      await deleteMutation.mutateAsync(stockDataToDelete);
+      setDeleteDialogOpen(false);
+      setStockDataToDelete(null);
+    }
   };
 
   // 处理表单提交
-  const handleSubmit = async () => {
+  const handleSubmit = async (values: CreateStockDataRequest | UpdateStockDataRequest) => {
+    if (isCreateForm) {
+      await createMutation.mutateAsync(values as CreateStockDataRequest);
+    } else if (editingStockData) {
+      await updateMutation.mutateAsync({
+        id: editingStockData.id,
+        data: values as UpdateStockDataRequest,
+      });
+    }
+  };
+
+  // 格式化JSON数据
+  const formatJsonData = (data: any) => {
     try {
-      const values = await form.validateFields();
-      
-      if (isEditing && currentItem) {
-        // 更新数据
-        const res = await stockDataClient[':id'].$put({
-          param: { id: currentItem.id },
-          json: values as UpdateStockDataRequest,
-        });
-        
-        if (!res.ok) {
-          throw new Error('更新失败');
-        }
-        antMessage.success('更新成功');
-      } else {
-        // 创建新数据
-        const res = await stockDataClient.$post({
-          json: values as CreateStockDataRequest,
-        });
-        
-        if (!res.ok) {
-          throw new Error('创建失败');
-        }
-        antMessage.success('创建成功');
-      }
-      
-      setIsModalVisible(false);
-      fetchData();
-    } catch (error) {
-      console.error('提交表单失败:', error);
-      antMessage.error(isEditing ? '更新失败,请重试' : '创建失败,请重试');
+      return JSON.stringify(data, null, 2);
+    } catch {
+      return String(data);
     }
   };
 
-  // 删除数据
-  const handleDelete = async (id: number) => {
+  // 解析JSON数据
+  const parseJsonData = (jsonString: string) => {
     try {
-      const res = await stockDataClient[':id'].$delete({
-        param: { id },
-      });
-      
-      if (!res.ok) {
-        throw new Error('删除失败');
-      }
-      
-      antMessage.success('删除成功');
-      fetchData();
-    } catch (error) {
-      console.error('删除数据失败:', error);
-      antMessage.error('删除失败,请重试');
+      return JSON.parse(jsonString);
+    } catch {
+      return [];
     }
   };
 
-  // 表格列定义
-  const columns = [
-    {
-      title: 'ID',
-      dataIndex: 'id',
-      key: 'id',
-      width: 80,
-    },
-    {
-      title: '股票代码',
-      dataIndex: 'code',
-      key: 'code',
-      filters: [
-        ...Array.from(new Set(data.map(item => item.code))).map(code => ({
-          text: code,
-          value: code,
-        }))
-      ],
-      onFilter: (value: string, record: StockDataItem) => record.code === value,
-    },
-    {
-      title: '数据摘要',
-      key: 'dataSummary',
-      render: (_: any, record: StockDataItem) => (
-        <div>
-          {record.data.date && <div>日期: {record.data.date}</div>}
-          {record.data.close && <div>收盘价: {record.data.close}</div>}
-          {record.data.volume && <div>成交量: {record.data.volume.toLocaleString()}</div>}
+  // 骨架屏
+  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>
-      ),
-    },
-    {
-      title: '创建时间',
-      dataIndex: 'createdAt',
-      key: 'createdAt',
-      render: (date: string) => new Date(date).toLocaleString(),
-    },
-    {
-      title: '状态',
-      key: 'status',
-      render: (_: any, record: StockDataItem) => (
-        <Tag color={new Date(record.updatedAt) > new Date(record.createdAt) ? 'blue' : 'green'}>
-          {new Date(record.updatedAt) > new Date(record.createdAt) ? '已更新' : '原始数据'}
-        </Tag>
-      ),
-    },
-    {
-      title: '操作',
-      key: 'action',
-      render: (_: any, record: StockDataItem) => (
-        <Space size="small">
-          <Button 
-            type="text" 
-            icon={<EyeOutlined />} 
-            onClick={() => showViewModal(record)}
-          >
-            查看
-          </Button>
-          <Button 
-            type="text" 
-            icon={<EditOutlined />} 
-            onClick={() => showEditModal(record)}
-          >
-            编辑
-          </Button>
-          <Button 
-            type="text" 
-            danger 
-            icon={<DeleteOutlined />} 
-            onClick={() => handleDelete(record.id)}
-          >
-            删除
-          </Button>
-        </Space>
-      ),
-    },
-  ];
+        
+        <Card>
+          <CardContent className="pt-6">
+            <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={handleCreate}>
+          <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 })}
-        rowKey="id"
-        bordered
-        scroll={{ x: 'max-content' }}
-        headerCellStyle={{ backgroundColor: '#f9fafb' }}
-        rowClassName={(record, index) => index % 2 === 0 ? 'bg-white' : 'bg-gray-50'}
-      />
-      
-      {/* 添加/编辑模态框 */}
-      <Modal
-        title={isEditing ? "编辑股票数据" : "添加股票数据"}
-        open={isModalVisible}
-        onOk={handleSubmit}
-        onCancel={() => setIsModalVisible(false)}
-        destroyOnClose
-        maskClosable={false}
-        width={700}
-      >
-        <Form
-          form={form}
-          layout="vertical"
-          name="stock_data_form"
-        >
-          <Form.Item
-            name="code"
-            label="股票代码"
-            rules={[
-              { required: true, message: '请输入股票代码' },
-              { max: 255, message: '股票代码不能超过255个字符' }
-            ]}
-          >
-            <Input placeholder="请输入股票代码" />
-          </Form.Item>
-          
-          <Form.Item
-            name="data"
-            label="股票数据 (JSON格式)"
-            rules={[
-              { required: true, message: '请输入股票数据' }
-            ]}
-            getValueFromEvent={(e) => {
-              try {
-                return JSON.stringify(JSON.parse(e.target.value || '{}'), null, 2);
-              } catch (err) {
-                return e.target.value;
-              }
-            }}
-          >
-            <Input.TextArea
-              placeholder='请输入JSON格式的股票数据,例如: {"date": "2025-05-21", "open": 15.68, "close": 16.25, "high": 16.50, "low": 15.50, "volume": 1250000}'
-              rows={8}
-            />
-          </Form.Item>
-        </Form>
-      </Modal>
-      
-      {/* 查看模态框 */}
-      <Modal
-        title={`股票数据详情 - ${currentItem?.code}`}
-        open={isViewModalVisible}
-        onCancel={() => setIsViewModalVisible(false)}
-        destroyOnClose
-        maskClosable={false}
-        width={800}
-        footer={null}
-      >
-        {currentItem && (
-          <div>
-            <Card title="基本信息" style={{ marginBottom: 16 }}>
-              <p><strong>ID:</strong> {currentItem.id}</p>
-              <p><strong>股票代码:</strong> {currentItem.code}</p>
-              <p><strong>创建时间:</strong> {new Date(currentItem.createdAt).toLocaleString()}</p>
-              <p><strong>更新时间:</strong> {new Date(currentItem.updatedAt).toLocaleString()}</p>
-            </Card>
-            
-            <Card title="股票数据">
-              <pre style={{ whiteSpace: 'pre-wrap', wordWrap: 'break-word', maxHeight: '400px', overflow: 'auto', padding: '10px', backgroundColor: '#f5f5f5', borderRadius: '4px' }}>
-                {JSON.stringify(currentItem.data, null, 2)}
-              </pre>
-            </Card>
+
+      {/* 搜索区域 */}
+      <Card>
+        <CardHeader>
+          <CardTitle>搜索</CardTitle>
+          <CardDescription>搜索股票数据</CardDescription>
+        </CardHeader>
+        <CardContent>
+          <div 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 }))}
+                onKeyPress={(e) => e.key === 'Enter' && handleSearch()}
+                className="pl-8"
+              />
+            </div>
+            <Button onClick={handleSearch}>搜索</Button>
           </div>
-        )}
-      </Modal>
+        </CardContent>
+      </Card>
+
+      {/* 数据表格 */}
+      <Card>
+        <CardHeader>
+          <CardTitle>股票数据列表</CardTitle>
+          <CardDescription>
+            共 {data?.pagination.total || 0} 条记录
+          </CardDescription>
+        </CardHeader>
+        <CardContent>
+          <div className="rounded-md border">
+            <Table>
+              <TableHeader>
+                <TableRow>
+                  <TableHead>ID</TableHead>
+                  <TableHead>股票代码</TableHead>
+                  <TableHead>数据摘要</TableHead>
+                  <TableHead>创建时间</TableHead>
+                  <TableHead>状态</TableHead>
+                  <TableHead className="text-right">操作</TableHead>
+                </TableRow>
+              </TableHeader>
+              <TableBody>
+                {data?.data.map((item) => (
+                  <TableRow key={item.id}>
+                    <TableCell>{item.id}</TableCell>
+                    <TableCell>{item.code}</TableCell>
+                    <TableCell>
+                      <div className="max-w-xs truncate">
+                        {Array.isArray(item.data) && item.data.length > 0 ? (
+                          <div>
+                            <div className="text-sm text-muted-foreground">
+                              包含 {item.data.length} 条记录
+                            </div>
+                            {item.data[0]?.date && (
+                              <div className="text-xs text-muted-foreground">
+                                最新日期: {item.data[0].date}
+                              </div>
+                            )}
+                          </div>
+                        ) : (
+                          <span className="text-muted-foreground">无数据</span>
+                        )}
+                      </div>
+                    </TableCell>
+                    <TableCell>
+                      {format(new Date(item.createdAt), 'yyyy-MM-dd HH:mm', { locale: zhCN })}
+                    </TableCell>
+                    <TableCell>
+                      <Badge 
+                        variant={new Date(item.updatedAt) > new Date(item.createdAt) ? 'default' : 'secondary'}
+                      >
+                        {new Date(item.updatedAt) > new Date(item.createdAt) ? '已更新' : '原始数据'}
+                      </Badge>
+                    </TableCell>
+                    <TableCell className="text-right">
+                      <div className="flex justify-end gap-2">
+                        <Button
+                          variant="ghost"
+                          size="icon"
+                          onClick={() => handleView(item)}
+                        >
+                          <Eye className="h-4 w-4" />
+                        </Button>
+                        <Button
+                          variant="ghost"
+                          size="icon"
+                          onClick={() => handleEdit(item)}
+                        >
+                          <Edit className="h-4 w-4" />
+                        </Button>
+                        <Button
+                          variant="ghost"
+                          size="icon"
+                          onClick={() => handleDelete(item.id)}
+                        >
+                          <Trash2 className="h-4 w-4" />
+                        </Button>
+                      </div>
+                    </TableCell>
+                  </TableRow>
+                ))}
+              </TableBody>
+            </Table>
+          </div>
+
+          {data?.data.length === 0 && (
+            <div className="text-center py-8">
+              <p className="text-muted-foreground">暂无数据</p>
+            </div>
+          )}
+
+          <DataTablePagination
+            currentPage={searchParams.page}
+            pageSize={searchParams.limit}
+            totalCount={data?.pagination.total || 0}
+            onPageChange={(page, limit) => setSearchParams(prev => ({ ...prev, page, limit }))}
+          />
+        </CardContent>
+      </Card>
+
+      {/* 创建/编辑模态框 */}
+      <Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
+        <DialogContent className="sm:max-w-[700px] max-h-[90vh] overflow-y-auto">
+          <DialogHeader>
+            <DialogTitle>{isCreateForm ? '添加股票数据' : '编辑股票数据'}</DialogTitle>
+            <DialogDescription>
+              {isCreateForm ? '创建一个新的股票数据记录' : '编辑现有股票数据信息'}
+            </DialogDescription>
+          </DialogHeader>
+
+          {isCreateForm ? (
+            <Form {...createForm}>
+              <form onSubmit={createForm.handleSubmit(handleSubmit)} className="space-y-4">
+                <FormField
+                  control={createForm.control}
+                  name="code"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>股票代码</FormLabel>
+                      <FormControl>
+                        <Input placeholder="请输入股票代码" {...field} />
+                      </FormControl>
+                      <FormDescription>例如:000001</FormDescription>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={createForm.control}
+                  name="data"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>股票数据 (JSON格式)</FormLabel>
+                      <FormControl>
+                        <Textarea
+                          placeholder='请输入JSON格式的股票数据,例如: [{"date": "2025-05-21", "open": 15.68, "close": 16.25, "high": 16.50, "low": 15.50, "volume": 1250000}]'
+                          rows={8}
+                          value={typeof field.value === 'string' ? field.value : formatJsonData(field.value)}
+                          onChange={(e) => field.onChange(parseJsonData(e.target.value))}
+                        />
+                      </FormControl>
+                      <FormDescription>请确保数据格式正确</FormDescription>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <DialogFooter>
+                  <Button type="button" variant="outline" onClick={() => setIsModalOpen(false)}>
+                    取消
+                  </Button>
+                  <Button type="submit" disabled={createMutation.isPending}>
+                    {createMutation.isPending ? '创建中...' : '创建'}
+                  </Button>
+                </DialogFooter>
+              </form>
+            </Form>
+          ) : (
+            <Form {...updateForm}>
+              <form onSubmit={updateForm.handleSubmit(handleSubmit)} className="space-y-4">
+                <FormField
+                  control={updateForm.control}
+                  name="code"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>股票代码</FormLabel>
+                      <FormControl>
+                        <Input placeholder="请输入股票代码" {...field} />
+                      </FormControl>
+                      <FormDescription>例如:000001</FormDescription>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={updateForm.control}
+                  name="data"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>股票数据 (JSON格式)</FormLabel>
+                      <FormControl>
+                        <Textarea
+                          placeholder='请输入JSON格式的股票数据'
+                          rows={8}
+                          value={typeof field.value === 'string' ? field.value : formatJsonData(field.value)}
+                          onChange={(e) => field.onChange(parseJsonData(e.target.value))}
+                        />
+                      </FormControl>
+                      <FormDescription>请确保数据格式正确</FormDescription>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <DialogFooter>
+                  <Button type="button" variant="outline" onClick={() => setIsModalOpen(false)}>
+                    取消
+                  </Button>
+                  <Button type="submit" disabled={updateMutation.isPending}>
+                    {updateMutation.isPending ? '更新中...' : '更新'}
+                  </Button>
+                </DialogFooter>
+              </form>
+            </Form>
+          )}
+        </DialogContent>
+      </Dialog>
+
+      {/* 查看详情模态框 */}
+      <Dialog open={isViewModalOpen} onOpenChange={setIsViewModalOpen}>
+        <DialogContent className="sm:max-w-[800px] max-h-[90vh] overflow-y-auto">
+          <DialogHeader>
+            <DialogTitle>股票数据详情</DialogTitle>
+            <DialogDescription>
+              {editingStockData?.code} 的详细信息
+            </DialogDescription>
+          </DialogHeader>
+
+          {editingStockData && (
+            <div className="space-y-4">
+              <Card>
+                <CardHeader>
+                  <CardTitle>基本信息</CardTitle>
+                </CardHeader>
+                <CardContent className="space-y-2">
+                  <div><strong>ID:</strong> {editingStockData.id}</div>
+                  <div><strong>股票代码:</strong> {editingStockData.code}</div>
+                  <div><strong>创建时间:</strong> {format(new Date(editingStockData.createdAt), 'yyyy-MM-dd HH:mm:ss', { locale: zhCN })}</div>
+                  <div><strong>更新时间:</strong> {format(new Date(editingStockData.updatedAt), 'yyyy-MM-dd HH:mm:ss', { locale: zhCN })}</div>
+                </CardContent>
+              </Card>
+
+              <Card>
+                <CardHeader>
+                  <CardTitle>股票数据</CardTitle>
+                </CardHeader>
+                <CardContent>
+                  <pre className="whitespace-pre-wrap break-words max-h-96 overflow-auto p-4 bg-muted rounded-md text-sm">
+                    {formatJsonData(editingStockData.data)}
+                  </pre>
+                </CardContent>
+              </Card>
+            </div>
+          )}
+
+          <DialogFooter>
+            <Button onClick={() => setIsViewModalOpen(false)}>关闭</Button>
+          </DialogFooter>
+        </DialogContent>
+      </Dialog>
+
+      {/* 删除确认对话框 */}
+      <Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
+        <DialogContent>
+          <DialogHeader>
+            <DialogTitle>确认删除</DialogTitle>
+            <DialogDescription>
+              确定要删除这条股票数据吗?此操作无法撤销。
+            </DialogDescription>
+          </DialogHeader>
+          <DialogFooter>
+            <Button variant="outline" onClick={() => setDeleteDialogOpen(false)}>
+              取消
+            </Button>
+            <Button
+              variant="destructive"
+              onClick={confirmDelete}
+              disabled={deleteMutation.isPending}
+            >
+              {deleteMutation.isPending ? '删除中...' : '删除'}
+            </Button>
+          </DialogFooter>
+        </DialogContent>
+      </Dialog>
     </div>
   );
 };