Sfoglia il codice sorgente

✨ feat(admin): add submission records management feature

- add submission records menu item with FileTextOutlined icon
- create SubmissionRecordsPage with complete CRUD functionality
- add table display with pagination and search features
- implement add/edit/delete operations for submission records
- add form for creating and editing submission records
- add route configuration for submission records page
yourname 5 mesi fa
parent
commit
338b7f76ef

+ 8 - 0
src/client/admin/menu.tsx

@@ -8,6 +8,7 @@ import {
   TeamOutlined,
   InfoCircleOutlined,
   DatabaseOutlined,
+  FileTextOutlined,
 } from '@ant-design/icons';
 
 export interface MenuItem {
@@ -93,6 +94,13 @@ export const useMenu = () => {
       path: '/admin/classroom-data',
       permission: 'classroom:manage'
     },
+    {
+      key: 'submission-records',
+      label: '提交记录管理',
+      icon: <FileTextOutlined />,
+      path: '/admin/submission-records',
+      permission: 'submission:manage'
+    }
   ];
 
   // 用户菜单项

+ 444 - 0
src/client/admin/pages/SubmissionRecordsPage.tsx

@@ -0,0 +1,444 @@
+import React, { useState, useEffect } from 'react';
+import { Table, Button, Modal, Form, Input, DatePicker, InputNumber, Space, Typography, message } from 'antd';
+import { PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined } from '@ant-design/icons';
+import { submissionRecordsClient } from '@/client/api';
+import type { InferResponseType, InferRequestType } from 'hono/client';
+import { App } from 'antd';
+
+const { Title } = Typography;
+
+// 定义类型
+type SubmissionRecordsListResponse = InferResponseType<typeof submissionRecordsClient.$get, 200>;
+type SubmissionRecordsItem = SubmissionRecordsListResponse['data'][0];
+type CreateSubmissionRecordsRequest = InferRequestType<typeof submissionRecordsClient.$post>['json'];
+type UpdateSubmissionRecordsRequest = InferRequestType<typeof submissionRecordsClient[':id']['$put']>['json'];
+
+export const SubmissionRecordsPage: React.FC = () => {
+  const [data, setData] = useState<SubmissionRecordsItem[]>([]);
+  const [loading, setLoading] = useState<boolean>(true);
+  const [pagination, setPagination] = useState({
+    current: 1,
+    pageSize: 10,
+    total: 0,
+  });
+  const [searchText, setSearchText] = useState('');
+  const [isModalVisible, setIsModalVisible] = useState(false);
+  const [isEditing, setIsEditing] = useState(false);
+  const [currentItem, setCurrentItem] = useState<SubmissionRecordsItem | null>(null);
+  const [form] = Form.useForm();
+  const { message: antMessage } = App.useApp();
+
+  // 获取数据列表
+  const fetchData = async () => {
+    try {
+      setLoading(true);
+      const res = await submissionRecordsClient.$get({
+        query: {
+          page: pagination.current,
+          pageSize: pagination.pageSize,
+          keyword: searchText,
+        },
+      });
+      
+      if (!res.ok) {
+        throw new Error('获取数据失败');
+      }
+      
+      const result = await res.json() as SubmissionRecordsListResponse;
+      setData(result.data);
+      setPagination(prev => ({
+        ...prev,
+        total: result.pagination.total,
+      }));
+    } catch (error) {
+      console.error('获取提交记录数据失败:', error);
+      antMessage.error('获取数据失败,请重试');
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  // 初始加载和分页、搜索变化时重新获取数据
+  useEffect(() => {
+    fetchData();
+  }, [pagination.current, pagination.pageSize]);
+
+  // 搜索功能
+  const handleSearch = () => {
+    setPagination(prev => ({ ...prev, current: 1 }));
+    fetchData();
+  };
+
+  // 显示创建模态框
+  const showCreateModal = () => {
+    setIsEditing(false);
+    setCurrentItem(null);
+    form.resetFields();
+    setIsModalVisible(true);
+  };
+
+  // 显示编辑模态框
+  const showEditModal = (record: SubmissionRecordsItem) => {
+    setIsEditing(true);
+    setCurrentItem(record);
+    form.setFieldsValue({
+      classroomNo: record.classroomNo || undefined,
+      userId: record.userId || undefined,
+      nickname: record.nickname || undefined,
+      score: record.score || undefined,
+      code: record.code || undefined,
+      trainingDate: record.trainingDate ? new Date(record.trainingDate) : null,
+      mark: record.mark || undefined,
+      status: record.status || undefined,
+      holdingStock: record.holdingStock || undefined,
+      holdingCash: record.holdingCash || undefined,
+      price: record.price || undefined,
+      profitAmount: record.profitAmount || undefined,
+      profitPercent: record.profitPercent || undefined,
+      totalProfitAmount: record.totalProfitAmount || undefined,
+      totalProfitPercent: record.totalProfitPercent || undefined,
+    });
+    setIsModalVisible(true);
+  };
+
+  // 处理表单提交
+  const handleSubmit = async () => {
+    try {
+      const values = await form.validateFields();
+      
+      if (isEditing && currentItem) {
+        // 更新数据
+        const res = await submissionRecordsClient[':id'].$put({
+          param: { id: currentItem.id },
+          json: values as UpdateSubmissionRecordsRequest,
+        });
+        
+        if (!res.ok) {
+          throw new Error('更新失败');
+        }
+        antMessage.success('更新成功');
+      } else {
+        // 创建新数据
+        const res = await submissionRecordsClient.$post({
+          json: values as CreateSubmissionRecordsRequest,
+        });
+        
+        if (!res.ok) {
+          throw new Error('创建失败');
+        }
+        antMessage.success('创建成功');
+      }
+      
+      setIsModalVisible(false);
+      fetchData();
+    } catch (error) {
+      console.error('提交表单失败:', error);
+      antMessage.error(isEditing ? '更新失败,请重试' : '创建失败,请重试');
+    }
+  };
+
+  // 删除数据
+  const handleDelete = async (id: number) => {
+    try {
+      const res = await submissionRecordsClient[':id'].$delete({
+        param: { id },
+      });
+      
+      if (!res.ok) {
+        throw new Error('删除失败');
+      }
+      
+      antMessage.success('删除成功');
+      fetchData();
+    } catch (error) {
+      console.error('删除数据失败:', error);
+      antMessage.error('删除失败,请重试');
+    }
+  };
+
+  // 表格列定义
+  const columns = [
+    {
+      title: 'ID',
+      dataIndex: 'id',
+      key: 'id',
+      width: 80,
+    },
+    {
+      title: '教室号',
+      dataIndex: 'classroomNo',
+      key: 'classroomNo',
+    },
+    {
+      title: '用户ID',
+      dataIndex: 'userId',
+      key: 'userId',
+    },
+    {
+      title: '昵称',
+      dataIndex: 'nickname',
+      key: 'nickname',
+    },
+    {
+      title: '成绩',
+      dataIndex: 'score',
+      key: 'score',
+    },
+    {
+      title: '代码',
+      dataIndex: 'code',
+      key: 'code',
+    },
+    {
+      title: '训练日期',
+      dataIndex: 'trainingDate',
+      key: 'trainingDate',
+      render: (date: string) => date ? new Date(date).toLocaleString() : '-',
+    },
+    {
+      title: '状态',
+      dataIndex: 'status',
+      key: 'status',
+    },
+    {
+      title: '收益率',
+      dataIndex: 'profitPercent',
+      key: 'profitPercent',
+      render: (percent: number) => percent ? `${percent}%` : '-',
+    },
+    {
+      title: '累计收益率',
+      dataIndex: 'totalProfitPercent',
+      key: 'totalProfitPercent',
+      render: (percent: number) => percent ? `${percent}%` : '-',
+    },
+    {
+      title: '操作',
+      key: 'action',
+      render: (_: any, record: SubmissionRecordsItem) => (
+        <Space size="small">
+          <Button 
+            type="text" 
+            icon={<EditOutlined />} 
+            onClick={() => showEditModal(record)}
+          >
+            编辑
+          </Button>
+          <Button 
+            type="text" 
+            danger 
+            icon={<DeleteOutlined />} 
+            onClick={() => handleDelete(record.id)}
+          >
+            删除
+          </Button>
+        </Space>
+      ),
+    },
+  ];
+
+  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}>
+          添加记录
+        </Button>
+      </div>
+      
+      <div className="search-container" style={{ marginBottom: 16 }}>
+        <Input
+          placeholder="搜索教室号、用户ID或代码"
+          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 })}
+      />
+      
+      <Modal
+        title={isEditing ? "编辑提交记录" : "添加提交记录"}
+        open={isModalVisible}
+        onOk={handleSubmit}
+        onCancel={() => setIsModalVisible(false)}
+        destroyOnClose
+        maskClosable={false}
+        width={700}
+      >
+        <Form
+          form={form}
+          layout="vertical"
+          name="submission_records_form"
+        >
+          <Form.Item
+            name="classroomNo"
+            label="教室号"
+            rules={[{ max: 255, message: '教室号不能超过255个字符' }]}
+          >
+            <Input placeholder="请输入教室号" />
+          </Form.Item>
+          
+          <Form.Item
+            name="userId"
+            label="用户ID"
+            rules={[{ max: 255, message: '用户ID不能超过255个字符' }]}
+          >
+            <Input placeholder="请输入用户ID" />
+          </Form.Item>
+          
+          <Form.Item
+            name="nickname"
+            label="昵称"
+            rules={[{ max: 255, message: '昵称不能超过255个字符' }]}
+          >
+            <Input placeholder="请输入昵称" />
+          </Form.Item>
+          
+          <Form.Item
+            name="score"
+            label="成绩"
+          >
+            <InputNumber 
+              placeholder="请输入成绩" 
+              style={{ width: '100%' }} 
+              formatter={value => `${value}`.replace(/\B(?=(\d{3})+(?!\d))/g, ',')}
+              parser={value => value!.replace(/\$\s?|(,*)/g, '')}
+              precision={2}
+            />
+          </Form.Item>
+          
+          <Form.Item
+            name="code"
+            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="mark"
+            label="标记"
+            rules={[{ max: 255, message: '标记不能超过255个字符' }]}
+          >
+            <Input placeholder="请输入标记" />
+          </Form.Item>
+          
+          <Form.Item
+            name="status"
+            label="状态"
+          >
+            <InputNumber placeholder="请输入状态" style={{ width: '100%' }} />
+          </Form.Item>
+          
+          <div style={{ display: 'flex', gap: 16, marginBottom: 16 }}>
+            <Form.Item
+              name="holdingStock"
+              label="持股"
+              rules={[{ max: 255, message: '持股信息不能超过255个字符' }]}
+              style={{ flex: 1 }}
+            >
+              <Input placeholder="请输入持股信息" />
+            </Form.Item>
+            
+            <Form.Item
+              name="holdingCash"
+              label="持币"
+              rules={[{ max: 255, message: '持币信息不能超过255个字符' }]}
+              style={{ flex: 1 }}
+            >
+              <Input placeholder="请输入持币信息" />
+            </Form.Item>
+          </div>
+          
+          <div style={{ display: 'flex', gap: 16, marginBottom: 16 }}>
+            <Form.Item
+              name="price"
+              label="价格"
+              style={{ flex: 1 }}
+            >
+              <InputNumber 
+                placeholder="请输入价格" 
+                style={{ width: '100%' }} 
+                precision={2}
+              />
+            </Form.Item>
+            
+            <Form.Item
+              name="profitAmount"
+              label="收益金额"
+              style={{ flex: 1 }}
+            >
+              <InputNumber 
+                placeholder="请输入收益金额" 
+                style={{ width: '100%' }} 
+                precision={2}
+              />
+            </Form.Item>
+          </div>
+          
+          <div style={{ display: 'flex', gap: 16 }}>
+            <Form.Item
+              name="profitPercent"
+              label="收益率(%)"
+              style={{ flex: 1 }}
+            >
+              <InputNumber 
+                placeholder="请输入收益率" 
+                style={{ width: '100%' }} 
+                precision={2}
+              />
+            </Form.Item>
+            
+            <Form.Item
+              name="totalProfitPercent"
+              label="累计收益率(%)"
+              style={{ flex: 1 }}
+            >
+              <InputNumber 
+                placeholder="请输入累计收益率" 
+                style={{ width: '100%' }} 
+                precision={2}
+              />
+            </Form.Item>
+            
+            <Form.Item
+              name="totalProfitAmount"
+              label="累计收益金额"
+              style={{ flex: 1 }}
+            >
+              <InputNumber 
+                placeholder="请输入累计收益金额" 
+                style={{ width: '100%' }} 
+                precision={2}
+              />
+            </Form.Item>
+          </div>
+        </Form>
+      </Modal>
+    </div>
+  );
+};
+
+export default SubmissionRecordsPage;

+ 6 - 0
src/client/admin/routes.tsx

@@ -7,6 +7,7 @@ import { NotFoundPage } from './components/NotFoundPage';
 import { DashboardPage } from './pages/Dashboard';
 import { UsersPage } from './pages/Users';
 import { ClassroomDataPage } from './pages/ClassroomDataPage';
+import { SubmissionRecordsPage } from './pages/SubmissionRecordsPage';
 import { LoginPage } from './pages/Login';
 
 export const router = createBrowserRouter([
@@ -45,6 +46,11 @@ export const router = createBrowserRouter([
         element: <ClassroomDataPage />,
         errorElement: <ErrorPage />
       },
+      {
+        path: 'submission-records',
+        element: <SubmissionRecordsPage />,
+        errorElement: <ErrorPage />
+      },
       {
         path: '*',
         element: <NotFoundPage />,