|
@@ -0,0 +1,317 @@
|
|
|
|
|
+import React, { useState } from 'react';
|
|
|
|
|
+import { useQueryClient } from '@tanstack/react-query';
|
|
|
|
|
+import {
|
|
|
|
|
+ Button, Table, Space,
|
|
|
|
|
+ Form, Input, message, Modal,
|
|
|
|
|
+ Card, Row, Col,
|
|
|
|
|
+ Popconfirm, Tag
|
|
|
|
|
+} 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 {
|
|
|
|
|
+ XunlianCode,
|
|
|
|
|
+ XunlianCodeListResponse
|
|
|
|
|
+} from '../share/types.ts';
|
|
|
|
|
+
|
|
|
|
|
+import { getEnumOptions } from './utils.ts';
|
|
|
|
|
+
|
|
|
|
|
+import {
|
|
|
|
|
+ XunlianCodeAPI
|
|
|
|
|
+} from './api/xunlian_codes.ts';
|
|
|
|
|
+
|
|
|
|
|
+// 配置 dayjs 插件
|
|
|
|
|
+dayjs.extend(weekday);
|
|
|
|
|
+dayjs.extend(localeData);
|
|
|
|
|
+
|
|
|
|
|
+// 设置 dayjs 语言
|
|
|
|
|
+dayjs.locale('zh-cn');
|
|
|
|
|
+
|
|
|
|
|
+// 训练代码管理页面组件
|
|
|
|
|
+export const XunlianCodePage = () => {
|
|
|
|
|
+ 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({
|
|
|
|
|
+ code: '',
|
|
|
|
|
+ page: 1,
|
|
|
|
|
+ limit: 10,
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 使用React Query获取训练代码列表
|
|
|
|
|
+ const { data: codesData, isLoading: isListLoading, refetch } = useQuery<XunlianCodeListResponse>({
|
|
|
|
|
+ queryKey: ['xunlianCodes', searchParams],
|
|
|
|
|
+ queryFn: async () => {
|
|
|
|
|
+ const response = await XunlianCodeAPI.getXunlianCodes({
|
|
|
|
|
+ page: searchParams.page,
|
|
|
|
|
+ pageSize: searchParams.limit,
|
|
|
|
|
+ code: searchParams.code,
|
|
|
|
|
+ });
|
|
|
|
|
+ return response.data;
|
|
|
|
|
+ },
|
|
|
|
|
+ placeholderData: {
|
|
|
|
|
+ data: [],
|
|
|
|
|
+ pagination: {
|
|
|
|
|
+ current: 1,
|
|
|
|
|
+ pageSize: 10,
|
|
|
|
|
+ total: 0,
|
|
|
|
|
+ totalPages: 1
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const codes = React.useMemo(() => (codesData as XunlianCodeListResponse)?.data || [], [codesData]);
|
|
|
|
|
+ const pagination = React.useMemo(() => ({
|
|
|
|
|
+ current: (codesData as XunlianCodeListResponse)?.pagination?.current || 1,
|
|
|
|
|
+ pageSize: (codesData as XunlianCodeListResponse)?.pagination?.pageSize || 10,
|
|
|
|
|
+ total: (codesData as XunlianCodeListResponse)?.pagination?.total || 0,
|
|
|
|
|
+ totalPages: (codesData as XunlianCodeListResponse)?.pagination?.totalPages || 1
|
|
|
|
|
+ }), [codesData]);
|
|
|
|
|
+
|
|
|
|
|
+ // 获取单个训练代码
|
|
|
|
|
+ const fetchCode = async (id: number) => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const response = await XunlianCodeAPI.getXunlianCode(id);
|
|
|
|
|
+ return response.data;
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ message.error('获取训练代码详情失败');
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 处理表单提交
|
|
|
|
|
+ const handleSubmit = async (values: Partial<XunlianCode>) => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const response = formMode === 'create'
|
|
|
|
|
+ ? await XunlianCodeAPI.createXunlianCode(values)
|
|
|
|
|
+ : await XunlianCodeAPI.updateXunlianCode(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 code = await fetchCode(id);
|
|
|
|
|
+ if (code) {
|
|
|
|
|
+ setFormMode('edit');
|
|
|
|
|
+ setEditingId(id);
|
|
|
|
|
+ form.setFieldsValue(code);
|
|
|
|
|
+ setModalVisible(true);
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 处理删除
|
|
|
|
|
+ const handleDelete = async (id: number) => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ await XunlianCodeAPI.deleteXunlianCode(id);
|
|
|
|
|
+
|
|
|
|
|
+ message.success('删除训练代码成功');
|
|
|
|
|
+ refetch();
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ message.error((error as Error).message);
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 处理搜索
|
|
|
|
|
+ const handleSearch = async (values: any) => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ queryClient.removeQueries({ queryKey: ['xunlianCodes'] });
|
|
|
|
|
+ setSearchParams({
|
|
|
|
|
+ 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 columns = [
|
|
|
|
|
+ {
|
|
|
|
|
+ title: 'ID',
|
|
|
|
|
+ dataIndex: 'id',
|
|
|
|
|
+ key: 'id',
|
|
|
|
|
+ width: 80,
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ title: '股票代码',
|
|
|
|
|
+ dataIndex: 'code',
|
|
|
|
|
+ key: 'code',
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ title: '训练日期',
|
|
|
|
|
+ dataIndex: 'training_date',
|
|
|
|
|
+ key: 'training_date',
|
|
|
|
|
+ render: (date: string) => dayjs(date).format('YYYY-MM-DD'),
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ title: '提交用户',
|
|
|
|
|
+ dataIndex: 'nickname',
|
|
|
|
|
+ key: 'nickname',
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ title: '创建时间',
|
|
|
|
|
+ dataIndex: 'created_at',
|
|
|
|
|
+ key: 'created_at',
|
|
|
|
|
+ render: (date: string) => dayjs(date).format('YYYY-MM-DD HH:mm:ss'),
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ title: '操作',
|
|
|
|
|
+ key: 'action',
|
|
|
|
|
+ render: (_: any, record: XunlianCode) => (
|
|
|
|
|
+ <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="code" label="股票代码">
|
|
|
|
|
+ <Input placeholder="要搜索的股票代码" />
|
|
|
|
|
+ </Form.Item>
|
|
|
|
|
+
|
|
|
|
|
+ <Form.Item>
|
|
|
|
|
+ <Space>
|
|
|
|
|
+ <Button type="primary" htmlType="submit">
|
|
|
|
|
+ 搜索
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ <Button htmlType="reset" onClick={() => {
|
|
|
|
|
+ searchForm.resetFields();
|
|
|
|
|
+ setSearchParams({
|
|
|
|
|
+ code: '',
|
|
|
|
|
+ page: 1,
|
|
|
|
|
+ limit: 10,
|
|
|
|
|
+ });
|
|
|
|
|
+ }}>
|
|
|
|
|
+ 重置
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ <Button type="primary" onClick={handleAdd}>
|
|
|
|
|
+ 添加代码
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </Space>
|
|
|
|
|
+ </Form.Item>
|
|
|
|
|
+ </Form>
|
|
|
|
|
+
|
|
|
|
|
+ <Table
|
|
|
|
|
+ columns={columns}
|
|
|
|
|
+ dataSource={codes}
|
|
|
|
|
+ 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);
|
|
|
|
|
+ })
|
|
|
|
|
+ .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="code"
|
|
|
|
|
+ label="股票代码"
|
|
|
|
|
+ rules={[{ required: true, message: '请输入股票代码' }]}
|
|
|
|
|
+ >
|
|
|
|
|
+ <Input placeholder="请输入股票代码" />
|
|
|
|
|
+ </Form.Item>
|
|
|
|
|
+ </Col>
|
|
|
|
|
+ <Col span={12}>
|
|
|
|
|
+ <Form.Item
|
|
|
|
|
+ name="training_date"
|
|
|
|
|
+ label="训练日期"
|
|
|
|
|
+ rules={[{ required: true, message: '请选择训练日期' }]}
|
|
|
|
|
+ >
|
|
|
|
|
+ <Input placeholder="请输入训练日期,格式:YYYY-MM-DD" />
|
|
|
|
|
+ </Form.Item>
|
|
|
|
|
+ </Col>
|
|
|
|
|
+ </Row>
|
|
|
|
|
+
|
|
|
|
|
+ <Form.Item
|
|
|
|
|
+ name="content"
|
|
|
|
|
+ label="代码内容"
|
|
|
|
|
+ rules={[{ required: true, message: '请输入代码内容' }]}
|
|
|
|
|
+ >
|
|
|
|
|
+ <Input.TextArea rows={15} placeholder="请输入代码内容" />
|
|
|
|
|
+ </Form.Item>
|
|
|
|
|
+
|
|
|
|
|
+ </Form>
|
|
|
|
|
+ </Modal>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ );
|
|
|
|
|
+};
|