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