Explorar o código

✨ feat(admin): add classroom data management page

- add ClassroomDataPage component with CRUD functionality for classroom data
- implement data table with pagination, search, and sorting features
- add create/edit modal forms for classroom data management
- integrate API client for classroom data operations

✨ feat(api): add new API clients

- add classroomDataClient for classroom data API operations
- add submissionRecordsClient, stockDataClient, stockXunlianCodesClient and dateNotesClient for other API endpoints
yourname hai 5 meses
pai
achega
e8eebc6aeb
Modificáronse 2 ficheiros con 373 adicións e 1 borrados
  1. 350 0
      src/client/admin/pages/ClassroomDataPage.tsx
  2. 23 1
      src/client/api.ts

+ 350 - 0
src/client/admin/pages/ClassroomDataPage.tsx

@@ -0,0 +1,350 @@
+import React, { useState, useEffect } from 'react';
+import { Table, Button, Modal, Form, Input, DatePicker, Space, Typography, message } from 'antd';
+import { PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined } from '@ant-design/icons';
+import { classroomDataClient } from '@/client/api';
+import type { InferResponseType, InferRequestType } from 'hono/client';
+import { App } from 'antd';
+
+const { Title } = Typography;
+
+// 定义类型
+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'];
+
+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 [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 res = await classroomDataClient.$get({
+        query: {
+          page: pagination.current,
+          pageSize: pagination.pageSize,
+          keyword: searchText,
+        },
+      });
+      
+      if (!res.ok) {
+        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);
+    }
+  };
+
+  // 初始加载和分页、搜索变化时重新获取数据
+  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: ClassroomDataItem) => {
+    setIsEditing(true);
+    setCurrentItem(record);
+    form.setFieldsValue({
+      classroomNo: record.classroomNo || undefined,
+      trainingDate: record.trainingDate ? new Date(record.trainingDate) : null,
+      holdingStock: record.holdingStock || undefined,
+      holdingCash: record.holdingCash || undefined,
+      price: record.price || undefined,
+      code: record.code || undefined,
+      status: record.status || undefined,
+      spare: record.spare || undefined,
+      submitUser: record.submitUser || undefined,
+    });
+    setIsModalVisible(true);
+  };
+
+  // 处理表单提交
+  const handleSubmit = async () => {
+    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('创建成功');
+      }
+      
+      setIsModalVisible(false);
+      fetchData();
+    } catch (error) {
+      console.error('提交表单失败:', error);
+      antMessage.error(isEditing ? '更新失败,请重试' : '创建失败,请重试');
+    }
+  };
+
+  // 删除数据
+  const handleDelete = async (id: number) => {
+    try {
+      const res = await classroomDataClient[':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: '训练日期',
+      dataIndex: 'trainingDate',
+      key: 'trainingDate',
+      render: (date: string) => date ? new Date(date).toLocaleString() : '-',
+    },
+    {
+      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',
+    },
+    {
+      title: '提交用户',
+      dataIndex: 'submitUser',
+      key: 'submitUser',
+    },
+    {
+      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>
+      ),
+    },
+  ];
+
+  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="搜索教室号、代码或提交用户"
+          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}
+      >
+        <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={[{ max: 255, message: '状态不能超过255个字符' }]}
+          >
+            <Input placeholder="请输入状态" />
+          </Form.Item>
+          
+          <Form.Item
+            name="spare"
+            label="备用"
+            rules={[{ max: 255, message: '备用信息不能超过255个字符' }]}
+          >
+            <Input placeholder="请输入备用信息" />
+          </Form.Item>
+          
+          <Form.Item
+            name="submitUser"
+            label="提交用户"
+            rules={[{ max: 255, message: '提交用户不能超过255个字符' }]}
+          >
+            <Input placeholder="请输入提交用户" />
+          </Form.Item>
+        </Form>
+      </Modal>
+    </div>
+  );
+};
+
+export default ClassroomDataPage;

+ 23 - 1
src/client/api.ts

@@ -1,7 +1,9 @@
 import axios, { isAxiosError } from 'axios';
 import { hc } from 'hono/client'
 import type {
-  AuthRoutes, UserRoutes, RoleRoutes
+  AuthRoutes, UserRoutes, RoleRoutes,
+  ClassroomDataRoutes, SubmissionRecordsRoutes,
+  StockDataRoutes, StockXunlianCodesRoutes, DateNotesRoutes
 } from '@/server/api';
 
 // 创建 axios 适配器
@@ -64,3 +66,23 @@ export const userClient = hc<UserRoutes>('/', {
 export const roleClient = hc<RoleRoutes>('/', {
   fetch: axiosFetch,
 }).api.v1.roles;
+
+export const classroomDataClient = hc<ClassroomDataRoutes>('/', {
+  fetch: axiosFetch,
+}).api.v1['classroom-data'];
+
+export const submissionRecordsClient = hc<SubmissionRecordsRoutes>('/', {
+  fetch: axiosFetch,
+}).api.v1['submission-records'];
+
+export const stockDataClient = hc<StockDataRoutes>('/', {
+  fetch: axiosFetch,
+}).api.v1['stock-data'];
+
+export const stockXunlianCodesClient = hc<StockXunlianCodesRoutes>('/', {
+  fetch: axiosFetch,
+}).api.v1['stock-xunlian-codes'];
+
+export const dateNotesClient = hc<DateNotesRoutes>('/', {
+  fetch: axiosFetch,
+}).api.v1['date-notes'];