Browse Source

迁移了admin classroom_data

yourname 6 months ago
parent
commit
0330bc44bc

+ 85 - 0
client/admin/api/classroom_data.ts

@@ -0,0 +1,85 @@
+import axios from 'axios';
+import type {
+  ClassroomData,
+  ClassroomDataListResponse,
+  ClassroomStatus
+} from '../../share/types.ts';
+
+interface ClassroomDataResponse {
+  data: ClassroomData;
+  message?: string;
+}
+
+interface ClassroomDataCreateResponse {
+  message: string;
+  data: ClassroomData;
+}
+
+interface ClassroomDataUpdateResponse {
+  message: string;
+  data: ClassroomData;
+}
+
+interface ClassroomDataDeleteResponse {
+  message: string;
+  id: number;
+}
+
+// 教室数据API
+export const ClassroomDataAPI = {
+  // 获取教室数据列表
+  getClassroomDatas: async (params?: {
+    page?: number;
+    pageSize?: number;
+    classroom_no?: string;
+    training_date?: string;
+    status?: ClassroomStatus;
+  }): Promise<ClassroomDataListResponse> => {
+    try {
+      const response = await axios.get('/classroom-datas', { params });
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  // 获取单个教室数据
+  getClassroomData: async (id: number): Promise<ClassroomDataResponse> => {
+    try {
+      const response = await axios.get(`/classroom-datas/${id}`);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  // 创建教室数据
+  createClassroomData: async (data: Partial<ClassroomData>): Promise<ClassroomDataCreateResponse> => {
+    try {
+      const response = await axios.post('/classroom-datas', data);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  // 更新教室数据
+  updateClassroomData: async (id: number, data: Partial<ClassroomData>): Promise<ClassroomDataUpdateResponse> => {
+    try {
+      const response = await axios.put(`/classroom-datas/${id}`, data);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  // 删除教室数据
+  deleteClassroomData: async (id: number): Promise<ClassroomDataDeleteResponse> => {
+    try {
+      const response = await axios.delete(`/classroom-datas/${id}`);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  }
+};

+ 2 - 1
client/admin/api/index.ts

@@ -22,4 +22,5 @@ export * from './charts.ts';
 export * from './messages.ts';
 export * from './sys.ts';
 export * from './know_info.ts';
-export * from './maps.ts';
+export * from './maps.ts';
+export * from './classroom_data.ts';

+ 454 - 0
client/admin/pages_classroom_data.tsx

@@ -0,0 +1,454 @@
+import React, { useState } from 'react';
+import { useQueryClient } from '@tanstack/react-query';
+import {
+  Button, Table, Space,
+  Form, Input, message, Modal,
+  Card, Row, Col, Popconfirm, Tag,
+  DatePicker, Select
+} from 'antd';
+import { useQuery } from '@tanstack/react-query';
+import dayjs from 'dayjs';
+import weekday from 'dayjs/plugin/weekday';
+import localeData from 'dayjs/plugin/localeData';
+import 'dayjs/locale/zh-cn';
+
+import {
+  ClassroomData,
+  ClassroomStatus,
+  ClassroomStatusNameMap
+} from '../share/types.ts';
+import { getEnumOptions } from './utils.ts';
+import { ClassroomDataAPI } from './api/index.ts';
+
+// 配置 dayjs 插件
+dayjs.extend(weekday);
+dayjs.extend(localeData);
+dayjs.locale('zh-cn');
+
+export const ClassroomDataPage = () => {
+  const queryClient = useQueryClient();
+  const [modalVisible, setModalVisible] = useState(false);
+  const [formMode, setFormMode] = useState<'create' | 'edit'>('create');
+  const [editingId, setEditingId] = useState<number | null>(null);
+  const [form] = Form.useForm();
+  const [searchForm] = Form.useForm();
+  const [searchParams, setSearchParams] = useState({
+    classroom_no: '',
+    code: '',
+    page: 1,
+    limit: 10,
+  });
+
+  // 使用React Query获取教室数据列表
+  const { data: classroomData, isLoading: isListLoading, refetch } = useQuery({
+    queryKey: ['classroomData', searchParams],
+    queryFn: () => ClassroomDataAPI.getClassroomDatas({
+      page: searchParams.page,
+      pageSize: searchParams.limit,
+      classroom_no: searchParams.classroom_no,
+      training_date: '',
+      status: undefined
+    }),
+    placeholderData: {
+      data: [],
+      pagination: {
+        current: 1,
+        pageSize: 10,
+        total: 0,
+        totalPages: 1
+      }
+    }
+  });
+
+  const classrooms = React.useMemo(() => classroomData?.data || [], [classroomData]);
+  const pagination = React.useMemo(() => ({
+    current: classroomData?.pagination?.current || 1,
+    pageSize: classroomData?.pagination?.pageSize || 10,
+    total: classroomData?.pagination?.total || 0,
+    totalPages: classroomData?.pagination?.totalPages || 1
+  }), [classroomData]);
+
+  // 获取单个教室数据
+  const fetchClassroom = async (id: number) => {
+    try {
+      const response = await ClassroomDataAPI.getClassroomData(id);
+      return response.data;
+    } catch (error) {
+      message.error('获取教室数据详情失败');
+      return null;
+    }
+  };
+
+  // 处理表单提交
+  const handleSubmit = async (values: Partial<ClassroomData>) => {
+    try {
+      const response = formMode === 'create'
+        ? await ClassroomDataAPI.createClassroomData(values)
+        : await ClassroomDataAPI.updateClassroomData(editingId!, values);
+      
+      message.success(formMode === 'create' ? '创建教室数据成功' : '更新教室数据成功');
+      setModalVisible(false);
+      form.resetFields();
+      refetch();
+    } catch (error) {
+      message.error((error as Error).message);
+    }
+  };
+
+  // 处理编辑
+  const handleEdit = async (id: number) => {
+    const classroom = await fetchClassroom(id);
+    if (classroom) {
+      setFormMode('edit');
+      setEditingId(id);
+      form.setFieldsValue({
+        ...classroom,
+        training_date: dayjs(classroom.training_date)
+      });
+      setModalVisible(true);
+    }
+  };
+
+  // 处理删除
+  const handleDelete = async (id: number) => {
+    try {
+      await ClassroomDataAPI.deleteClassroomData(id);
+      message.success('删除教室数据成功');
+      refetch();
+    } catch (error) {
+      message.error((error as Error).message);
+    }
+  };
+
+  // 处理搜索
+  const handleSearch = async (values: any) => {
+    try {
+      queryClient.removeQueries({ queryKey: ['classroomData'] });
+      setSearchParams({
+        classroom_no: values.classroom_no || '',
+        code: values.code || '',
+        page: 1,
+        limit: searchParams.limit,
+      });
+    } catch (error) {
+      message.error('搜索失败');
+    }
+  };
+
+  // 处理分页
+  const handlePageChange = (page: number, pageSize?: number) => {
+    setSearchParams(prev => ({
+      ...prev,
+      page,
+      limit: pageSize || prev.limit,
+    }));
+  };
+
+  // 处理添加
+  const handleAdd = () => {
+    setFormMode('create');
+    setEditingId(null);
+    form.resetFields();
+    setModalVisible(true);
+  };
+
+  // 复制链接到剪贴板
+  const copyLink = (type: 'exam' | 'stock' | 'admin', classroom_no: string) => {
+    const baseUrl = window.location.origin;
+    let url = '';
+    let successMsg = '';
+
+    switch(type) {
+      case 'exam':
+        url = `${baseUrl}/exam?classroom=${classroom_no}`;
+        successMsg = '答题卡链接已复制';
+        break;
+      case 'stock':
+        url = `${baseUrl}/stock?classroom=${classroom_no}`;
+        successMsg = '股票训练链接已复制';
+        break;
+      case 'admin':
+        url = `${baseUrl}/exam/admin?classroom=${classroom_no}`;
+        successMsg = '管理员链接已复制';
+        break;
+    }
+
+    navigator.clipboard.writeText(url).then(() => {
+      message.success(successMsg);
+    }).catch(() => {
+      message.error('复制失败,请手动复制');
+    });
+  };
+
+  // 教室状态选项
+  const statusOptions = getEnumOptions(ClassroomStatus, ClassroomStatusNameMap);
+
+  // 表格列定义
+  const columns = [
+    {
+      title: 'ID',
+      dataIndex: 'id',
+      key: 'id',
+      width: 80,
+    },
+    {
+      title: '教室号',
+      dataIndex: 'classroom_no',
+      key: 'classroom_no',
+    },
+    {
+      title: '训练日期',
+      dataIndex: 'training_date',
+      key: 'training_date',
+      render: (date: string) => dayjs(date).format('YYYY-MM-DD'),
+    },
+    {
+      title: '持股',
+      dataIndex: 'holding_stock',
+      key: 'holding_stock',
+    },
+    {
+      title: '持币',
+      dataIndex: 'holding_cash',
+      key: 'holding_cash',
+    },
+    {
+      title: '价格',
+      dataIndex: 'price',
+      key: 'price',
+    },
+    {
+      title: '代码',
+      dataIndex: 'code',
+      key: 'code',
+    },
+    {
+      title: '状态',
+      dataIndex: 'status',
+      key: 'status',
+      render: (status: ClassroomStatus) => {
+        const color = status === ClassroomStatus.OPEN ? 'green' : 'red';
+        const text = ClassroomStatusNameMap[status];
+        return <Tag color={color}>{text}</Tag>;
+      },
+    },
+    {
+      title: '链接',
+      key: 'links',
+      render: (_: any, record: ClassroomData) => (
+        <Space direction="vertical" size={4}>
+          <div>
+            <Button 
+              type="link" 
+              size="small"
+              onClick={() => copyLink('stock', record.classroom_no)}
+            >
+              复制股票训练链接
+            </Button>
+          </div>
+          <div>
+            <Button 
+              type="link" 
+              size="small"
+              onClick={() => copyLink('exam', record.classroom_no)}
+            >
+              复制答题卡链接
+            </Button>
+          </div>
+          <div>
+            <Button 
+              type="link" 
+              size="small"
+              onClick={() => copyLink('admin', record.classroom_no)}
+            >
+              复制管理员链接
+            </Button>
+          </div>
+        </Space>
+      ),
+    },
+    {
+      title: '操作',
+      key: 'action',
+      render: (_: any, record: ClassroomData) => (
+        <Space size="middle">
+          <Button type="link" onClick={() => handleEdit(record.id)}>编辑</Button>
+          <Popconfirm
+            title="确定要删除这条教室数据吗?"
+            onConfirm={() => handleDelete(record.id)}
+            okText="确定"
+            cancelText="取消"
+          >
+            <Button type="link" danger>删除</Button>
+          </Popconfirm>
+        </Space>
+      ),
+    },
+  ];
+
+  return (
+    <div>
+      <Card title="教室数据管理" className="mb-4">
+        <Form
+          form={searchForm}
+          layout="inline"
+          onFinish={handleSearch}
+          style={{ marginBottom: '16px' }}
+        >
+          <Form.Item name="classroom_no" label="教室号">
+            <Input placeholder="要搜索的教室号" />
+          </Form.Item>
+          
+          <Form.Item name="code" label="代码">
+            <Input placeholder="要搜索的代码" />
+          </Form.Item>
+          
+          <Form.Item>
+            <Space>
+              <Button type="primary" htmlType="submit">
+                搜索
+              </Button>
+              <Button htmlType="reset" onClick={() => {
+                searchForm.resetFields();
+                setSearchParams({
+                  classroom_no: '',
+                  code: '',
+                  page: 1,
+                  limit: 10,
+                });
+              }}>
+                重置
+              </Button>
+              <Button type="primary" onClick={handleAdd}>
+                添加教室数据
+              </Button>
+            </Space>
+          </Form.Item>
+        </Form>
+        
+        <Table
+          columns={columns}
+          dataSource={classrooms}
+          rowKey="id"
+          loading={{
+            spinning: isListLoading,
+            tip: '正在加载数据...',
+          }}
+          pagination={{
+            current: pagination.current,
+            pageSize: pagination.pageSize,
+            total: pagination.total,
+            onChange: handlePageChange,
+            showSizeChanger: true,
+            showTotal: (total) => `共 ${total} 条`,
+          }}
+        />
+      </Card>
+      
+      <Modal
+        title={formMode === 'create' ? '添加教室数据' : '编辑教室数据'}
+        open={modalVisible}
+        onOk={() => {
+          form.validateFields()
+            .then(values => {
+              handleSubmit({
+                ...values,
+                training_date: values.training_date.format('YYYY-MM-DD')
+              });
+            })
+            .catch(info => {
+              console.log('表单验证失败:', info);
+            });
+        }}
+        onCancel={() => setModalVisible(false)}
+        width={800}
+        okText="确定"
+        cancelText="取消"
+        destroyOnClose
+      >
+        <Form
+          form={form}
+          layout="vertical"
+          initialValues={{
+            status: ClassroomStatus.OPEN,
+          }}
+        >
+          <Row gutter={16}>
+            <Col span={12}>
+              <Form.Item
+                name="classroom_no"
+                label="教室号"
+                rules={[{ required: true, message: '请输入教室号' }]}
+              >
+                <Input placeholder="请输入教室号" />
+              </Form.Item>
+            </Col>
+            <Col span={12}>
+              <Form.Item
+                name="training_date"
+                label="训练日期"
+                rules={[{ required: true, message: '请选择训练日期' }]}
+              >
+                <DatePicker style={{ width: '100%' }} />
+              </Form.Item>
+            </Col>
+          </Row>
+          
+          <Row gutter={16}>
+            <Col span={8}>
+              <Form.Item
+                name="holding_stock"
+                label="持股"
+              >
+                <Input placeholder="请输入持股" />
+              </Form.Item>
+            </Col>
+            <Col span={8}>
+              <Form.Item
+                name="holding_cash"
+                label="持币"
+              >
+                <Input placeholder="请输入持币" />
+              </Form.Item>
+            </Col>
+            <Col span={8}>
+              <Form.Item
+                name="price"
+                label="价格"
+              >
+                <Input placeholder="请输入价格" />
+              </Form.Item>
+            </Col>
+          </Row>
+          
+          <Row gutter={16}>
+            <Col span={12}>
+              <Form.Item
+                name="code"
+                label="代码"
+              >
+                <Input placeholder="请输入代码" />
+              </Form.Item>
+            </Col>
+            <Col span={12}>
+              <Form.Item
+                name="status"
+                label="状态"
+                rules={[{ required: true, message: '请选择状态' }]}
+              >
+                <Select options={statusOptions} />
+              </Form.Item>
+            </Col>
+          </Row>
+          
+          <Form.Item
+            name="spare"
+            label="备用字段"
+          >
+            <Input placeholder="请输入备用字段" />
+          </Form.Item>
+        </Form>
+      </Modal>
+    </div>
+  );
+};

+ 62 - 0
client/share/types.ts

@@ -538,3 +538,65 @@ export interface UserMessage {
   message?: Message;
   sender?: User;
 }
+
+// 教室状态枚举
+export enum ClassroomStatus {
+  CLOSED = 0,  // 关闭
+  OPEN = 1     // 开放
+}
+
+// 教室状态中文映射
+export const ClassroomStatusNameMap: Record<ClassroomStatus, string> = {
+  [ClassroomStatus.CLOSED]: '关闭',
+  [ClassroomStatus.OPEN]: '开放'
+};
+
+// 教室数据接口
+export interface ClassroomData {
+  /** 主键ID */
+  id: number;
+  
+  /** 教室号 */
+  classroom_no: string;
+  
+  /** 训练日期 */
+  training_date: string;
+  
+  /** 持股 */
+  holding_stock?: string;
+  
+  /** 持币 */
+  holding_cash?: string;
+  
+  /** 价格 */
+  price?: string;
+  
+  /** 代码 */
+  code?: string;
+  
+  /** 状态 */
+  status: ClassroomStatus;
+  
+  /** 备用字段 */
+  spare?: string;
+  
+  /** 提交用户ID */
+  submit_user?: number;
+  
+  /** 创建时间 */
+  created_at: string;
+  
+  /** 更新时间 */
+  updated_at: string;
+}
+
+// 教室数据列表响应
+export interface ClassroomDataListResponse {
+  data: ClassroomData[];
+  pagination: {
+    current: number;
+    pageSize: number;
+    total: number;
+    totalPages: number;
+  };
+}