|
|
@@ -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>
|
|
|
+ );
|
|
|
+};
|