Selaa lähdekoodia

迁移了admin submission

yourname 6 kuukautta sitten
vanhempi
sitoutus
c26bd30f58

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

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

+ 67 - 0
client/admin/api/submission_records.ts

@@ -0,0 +1,67 @@
+import axios from 'axios';
+import type {
+  SubmissionRecord,
+  SubmissionRecordListResponse
+} from '../../share/types.ts';
+
+export const SubmissionRecordsAPI = {
+  /**
+   * 获取提交记录列表
+   */
+  getSubmissionRecords(params: {
+    page?: number;
+    pageSize?: number;
+    user_id?: string;
+    nickname?: string;
+    code?: string;
+    training_date?: string;
+  }): Promise<SubmissionRecordListResponse> {
+    return axios({
+      url: '/admin/submission-records',
+      method: 'GET',
+      params
+    });
+  },
+
+  /**
+   * 获取单个提交记录
+   */
+  getSubmissionRecord(id: number): Promise<{ data: SubmissionRecord }> {
+    return axios({
+      url: `/admin/submission-records/${id}`,
+      method: 'GET'
+    });
+  },
+
+  /**
+   * 创建提交记录
+   */
+  createSubmissionRecord(data: Omit<SubmissionRecord, 'id'>): Promise<{ data: SubmissionRecord }> {
+    return axios({
+      url: '/admin/submission-records',
+      method: 'POST',
+      data
+    });
+  },
+
+  /**
+   * 更新提交记录
+   */
+  updateSubmissionRecord(id: number, data: Partial<SubmissionRecord>): Promise<{ data: SubmissionRecord }> {
+    return axios({
+      url: `/admin/submission-records/${id}`,
+      method: 'PUT',
+      data
+    });
+  },
+
+  /**
+   * 删除提交记录
+   */
+  deleteSubmissionRecord(id: number): Promise<void> {
+    return axios({
+      url: `/admin/submission-records/${id}`,
+      method: 'DELETE'
+    });
+  }
+};

+ 410 - 0
client/admin/pages_submission_records.tsx

@@ -0,0 +1,410 @@
+import React, { useState } from 'react';
+import { useQueryClient } from '@tanstack/react-query';
+import {
+  Button, Table, Space,
+  Form, Input, Select, message, Modal,
+  Card, Row, Col,
+  Popconfirm, Tag, DatePicker
+} 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 type {
+  SubmissionRecord,
+  SubmissionRecordListResponse
+} from '../share/types.ts';
+import {
+  SubmissionRecordStatus, SubmissionRecordStatusNameMap,
+} from '../share/types.ts';
+import { getEnumOptions } from './utils.ts';
+import {
+  SubmissionRecordsAPI
+} from './api/index.ts';
+
+// 配置 dayjs 插件
+dayjs.extend(weekday);
+dayjs.extend(localeData);
+dayjs.locale('zh-cn');
+
+// 提交记录管理页面组件
+export const SubmissionRecordsPage = () => {
+  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({
+    user_id: '',
+    nickname: '',
+    code: '',
+    training_date: '',
+    training_date_end: '',
+    page: 1,
+    limit: 10,
+  });
+  
+  // 使用React Query获取提交记录列表
+  const { data: recordsData, isLoading: isListLoading, refetch } = useQuery({
+    queryKey: ['submissionRecords', searchParams],
+    queryFn: () => SubmissionRecordsAPI.getSubmissionRecords({
+      page: searchParams.page,
+      pageSize: searchParams.limit,
+      user_id: searchParams.user_id,
+      nickname: searchParams.nickname,
+      code: searchParams.code,
+      training_date: searchParams.training_date
+    }),
+    placeholderData: {
+      data: [],
+      pagination: {
+        current: 1,
+        pageSize: 10,
+        total: 0,
+        totalPages: 1
+      }
+    }
+  });
+  
+  const records = React.useMemo(() => (recordsData as SubmissionRecordListResponse)?.data || [], [recordsData]);
+  const pagination = React.useMemo(() => ({
+    current: (recordsData as SubmissionRecordListResponse)?.pagination?.current || 1,
+    pageSize: (recordsData as SubmissionRecordListResponse)?.pagination?.pageSize || 10,
+    total: (recordsData as SubmissionRecordListResponse)?.pagination?.total || 0,
+    totalPages: (recordsData as SubmissionRecordListResponse)?.pagination?.totalPages || 1
+  }), [recordsData]);
+  
+  // 获取单个提交记录
+  const fetchRecord = async (id: number) => {
+    try {
+      const response = await SubmissionRecordsAPI.getSubmissionRecord(id);
+      return response.data;
+    } catch (error) {
+      message.error('获取提交记录详情失败');
+      return null;
+    }
+  };
+  
+  // 处理表单提交
+  const handleSubmit = async (values: Omit<SubmissionRecord, 'id'>) => {
+    try {
+      const response = formMode === 'create'
+        ? await SubmissionRecordsAPI.createSubmissionRecord(values)
+        : await SubmissionRecordsAPI.updateSubmissionRecord(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 record = await fetchRecord(id);
+    if (record) {
+      setFormMode('edit');
+      setEditingId(id);
+      form.setFieldsValue({
+        ...record,
+        training_date: record.training_date ? dayjs(record.training_date) : null
+      });
+      setModalVisible(true);
+    }
+  };
+  
+  // 处理删除
+  const handleDelete = async (id: number) => {
+    try {
+      await SubmissionRecordsAPI.deleteSubmissionRecord(id);
+      message.success('删除提交记录成功');
+      refetch();
+    } catch (error) {
+      message.error((error as Error).message);
+    }
+  };
+  
+  // 处理搜索
+  const handleSearch = async (values: any) => {
+    try {
+      queryClient.removeQueries({ queryKey: ['submissionRecords'] });
+      setSearchParams({
+        user_id: values.user_id || '',
+        nickname: values.nickname || '',
+        code: values.code || '',
+        training_date: values.training_date?.[0]?.format('YYYY-MM-DD') || '',
+        training_date_end: values.training_date?.[1]?.format('YYYY-MM-DD') || '',
+        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 statusOptions = getEnumOptions(SubmissionRecordStatus, SubmissionRecordStatusNameMap);
+  
+  // 表格列定义
+  const columns = [
+    {
+      title: 'ID',
+      dataIndex: 'id',
+      key: 'id',
+      width: 80,
+    },
+    {
+      title: '用户ID',
+      dataIndex: 'user_id',
+      key: 'user_id',
+    },
+    {
+      title: '昵称',
+      dataIndex: 'nickname',
+      key: 'nickname',
+    },
+    {
+      title: '成绩',
+      dataIndex: 'score',
+      key: 'score',
+    },
+    {
+      title: '代码',
+      dataIndex: 'code',
+      key: 'code',
+    },
+    {
+      title: '训练日期',
+      dataIndex: 'training_date',
+      key: 'training_date',
+      render: (date: string) => date ? dayjs(date).format('YYYY-MM-DD') : '-',
+    },
+    {
+      title: '标记',
+      dataIndex: 'mark',
+      key: 'mark',
+    },
+    {
+      title: '状态',
+      dataIndex: 'status',
+      key: 'status',
+      render: (status: SubmissionRecordStatus) => {
+        const statusText = SubmissionRecordStatusNameMap[status];
+        const colorMap = {
+          [SubmissionRecordStatus.PENDING]: 'orange',
+          [SubmissionRecordStatus.APPROVED]: 'green',
+          [SubmissionRecordStatus.REJECTED]: 'red'
+        };
+        return statusText ? (
+          <Tag color={colorMap[status]}>{statusText}</Tag>
+        ) : null;
+      },
+    },
+    {
+      title: '操作',
+      key: 'action',
+      render: (_: any, record: SubmissionRecord) => (
+        <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="user_id" label="用户ID">
+            <Input placeholder="要搜索的用户ID" />
+          </Form.Item>
+          
+          <Form.Item name="nickname" label="昵称">
+            <Input placeholder="要搜索的用户昵称" />
+          </Form.Item>
+          
+          <Form.Item name="code" label="代码">
+            <Input placeholder="要搜索的代码" />
+          </Form.Item>
+          
+          <Form.Item name="training_date" label="训练日期">
+            <DatePicker.RangePicker />
+          </Form.Item>
+          
+          <Form.Item>
+            <Space>
+              <Button type="primary" htmlType="submit">
+                搜索
+              </Button>
+              <Button htmlType="reset" onClick={() => {
+                searchForm.resetFields();
+                setSearchParams({
+                  user_id: '',
+                  nickname: '',
+                  code: '',
+                  training_date: '',
+                  training_date_end: '',
+                  page: 1,
+                  limit: 10,
+                });
+              }}>
+                重置
+              </Button>
+              <Button type="primary" onClick={handleAdd}>
+                添加记录
+              </Button>
+            </Space>
+          </Form.Item>
+        </Form>
+        
+        <Table
+          columns={columns}
+          dataSource={records}
+          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'),
+                code: values.code || '',
+                user_id: values.user_id || 0,
+                score: values.score || 0,
+                status: values.status || SubmissionRecordStatus.PENDING
+              });
+            })
+            .catch(info => {
+              console.log('表单验证失败:', info);
+            });
+        }}
+        onCancel={() => setModalVisible(false)}
+        width={800}
+        okText="确定"
+        cancelText="取消"
+        destroyOnClose
+      >
+        <Form
+          form={form}
+          layout="vertical"
+        >
+          <Row gutter={16}>
+            <Col span={12}>
+              <Form.Item
+                name="user_id"
+                label="用户ID"
+                rules={[{ required: true, message: '请输入用户ID' }]}
+              >
+                <Input placeholder="请输入用户ID" />
+              </Form.Item>
+            </Col>
+            <Col span={12}>
+              <Form.Item
+                name="nickname"
+                label="昵称"
+              >
+                <Input placeholder="请输入用户昵称" />
+              </Form.Item>
+            </Col>
+          </Row>
+          
+          <Row gutter={16}>
+            <Col span={12}>
+              <Form.Item
+                name="score"
+                label="成绩"
+                rules={[{ required: true, message: '请输入成绩' }]}
+              >
+                <Input type="number" 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>
+          
+          <Form.Item
+            name="code"
+            label="代码"
+            rules={[{ required: true, message: '请输入代码' }]}
+          >
+            <Input.TextArea rows={4} placeholder="请输入代码" />
+          </Form.Item>
+          
+          <Form.Item
+            name="mark"
+            label="标记"
+          >
+            <Input placeholder="请输入标记" />
+          </Form.Item>
+          
+          <Form.Item
+            name="status"
+            label="状态"
+          >
+            <Select options={statusOptions} />
+          </Form.Item>
+        </Form>
+      </Modal>
+    </div>
+  );
+};

+ 58 - 0
client/share/types.ts

@@ -632,3 +632,61 @@ export interface DateNoteListResponse {
     totalPages: number;
   };
 }
+
+// 提交记录状态枚举
+export enum SubmissionRecordStatus {
+  PENDING = 0,   // 待处理
+  APPROVED = 1,  // 已通过
+  REJECTED = 2   // 已拒绝
+}
+
+// 提交记录状态中文映射
+export const SubmissionRecordStatusNameMap: Record<SubmissionRecordStatus, string> = {
+  [SubmissionRecordStatus.PENDING]: '待处理',
+  [SubmissionRecordStatus.APPROVED]: '已通过',
+  [SubmissionRecordStatus.REJECTED]: '已拒绝'
+};
+
+// 提交记录实体
+export interface SubmissionRecord {
+  /** 主键ID */
+  id: number;
+  
+  /** 用户ID */
+  user_id: number;
+  
+  /** 用户昵称 */
+  nickname: string;
+  
+  /** 成绩 */
+  score: number;
+  
+  /** 代码 */
+  code: string;
+  
+  /** 训练日期 */
+  training_date: string;
+  
+  /** 标记 */
+  mark?: string;
+  
+  /** 状态 */
+  status: SubmissionRecordStatus;
+  
+  /** 创建时间 */
+  created_at: string;
+  
+  /** 更新时间 */
+  updated_at: string;
+}
+
+// 提交记录列表响应
+export interface SubmissionRecordListResponse {
+  data: SubmissionRecord[];
+  pagination: {
+    current: number;
+    pageSize: number;
+    total: number;
+    totalPages: number;
+  };
+}