pages_xunlian_codes.tsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376
  1. import React, { useState } from 'react';
  2. import { useQueryClient } from '@tanstack/react-query';
  3. import {
  4. Button, Table, Space,
  5. Form, Input, message, Modal,
  6. Card, Row, Col,
  7. Popconfirm, Tag, DatePicker
  8. } from 'antd';
  9. import {
  10. useQuery,
  11. } from '@tanstack/react-query';
  12. import dayjs from 'dayjs';
  13. import weekday from 'dayjs/plugin/weekday';
  14. import localeData from 'dayjs/plugin/localeData';
  15. import 'dayjs/locale/zh-cn';
  16. import type {
  17. XunlianCode,
  18. XunlianCodeListResponse
  19. } from '../share/types_stock.ts';
  20. import { getEnumOptions } from './utils.ts';
  21. import {
  22. XunlianCodeAPI
  23. } from './api/xunlian_codes.ts';
  24. // 配置 dayjs 插件
  25. dayjs.extend(weekday);
  26. dayjs.extend(localeData);
  27. // 设置 dayjs 语言
  28. dayjs.locale('zh-cn');
  29. // 训练代码管理页面组件
  30. export const XunlianCodePage = () => {
  31. const queryClient = useQueryClient();
  32. const [modalVisible, setModalVisible] = useState(false);
  33. const [formMode, setFormMode] = useState<'create' | 'edit'>('create');
  34. const [editingId, setEditingId] = useState<number | null>(null);
  35. const [form] = Form.useForm();
  36. const [searchForm] = Form.useForm();
  37. const [searchParams, setSearchParams] = useState({
  38. code: '',
  39. page: 1,
  40. limit: 10,
  41. });
  42. // 使用React Query获取训练代码列表
  43. const { data: codesData, isLoading: isListLoading, refetch } = useQuery<XunlianCodeListResponse>({
  44. queryKey: ['xunlianCodes', searchParams],
  45. queryFn: async () => {
  46. const response = await XunlianCodeAPI.getXunlianCodes({
  47. page: searchParams.page,
  48. pageSize: searchParams.limit,
  49. code: searchParams.code,
  50. });
  51. return response.data;
  52. },
  53. placeholderData: {
  54. data: [],
  55. pagination: {
  56. current: 1,
  57. pageSize: 10,
  58. total: 0,
  59. totalPages: 1
  60. }
  61. }
  62. });
  63. const codes = React.useMemo(() => (codesData as XunlianCodeListResponse)?.data || [], [codesData]);
  64. const pagination = React.useMemo(() => ({
  65. current: (codesData as XunlianCodeListResponse)?.pagination?.current || 1,
  66. pageSize: (codesData as XunlianCodeListResponse)?.pagination?.pageSize || 10,
  67. total: (codesData as XunlianCodeListResponse)?.pagination?.total || 0,
  68. totalPages: (codesData as XunlianCodeListResponse)?.pagination?.totalPages || 1
  69. }), [codesData]);
  70. // 获取单个训练代码
  71. const fetchCode = async (id: number) => {
  72. try {
  73. const response = await XunlianCodeAPI.getXunlianCode(id);
  74. return response.data;
  75. } catch (error) {
  76. message.error('获取训练代码详情失败');
  77. return null;
  78. }
  79. };
  80. // 处理表单提交
  81. const handleSubmit = async (values: Partial<XunlianCode>) => {
  82. try {
  83. // 转换交易日期格式为YYYY-MM-DD格式,确保不为null
  84. const submitValues = {
  85. ...values,
  86. ...(values.trade_date && {
  87. trade_date: dayjs(values.trade_date).format('YYYY-MM-DD')
  88. })
  89. };
  90. const response = formMode === 'create'
  91. ? await XunlianCodeAPI.createXunlianCode(submitValues)
  92. : await XunlianCodeAPI.updateXunlianCode(editingId!, submitValues);
  93. message.success(formMode === 'create' ? '创建训练代码成功' : '更新训练代码成功');
  94. setModalVisible(false);
  95. form.resetFields();
  96. refetch();
  97. } catch (error) {
  98. message.error((error as Error).message);
  99. }
  100. };
  101. // 处理编辑
  102. const handleEdit = async (id: number) => {
  103. const code = await fetchCode(id);
  104. if (code) {
  105. setFormMode('edit');
  106. setEditingId(id);
  107. // 转换交易日期格式为dayjs对象
  108. form.setFieldsValue({
  109. ...code,
  110. trade_date: code.trade_date ? dayjs(code.trade_date) : null
  111. });
  112. setModalVisible(true);
  113. }
  114. };
  115. // 处理删除
  116. const handleDelete = async (id: number) => {
  117. try {
  118. await XunlianCodeAPI.deleteXunlianCode(id);
  119. message.success('删除训练代码成功');
  120. refetch();
  121. } catch (error) {
  122. message.error((error as Error).message);
  123. }
  124. };
  125. // 处理搜索
  126. const handleSearch = async (values: any) => {
  127. try {
  128. queryClient.removeQueries({ queryKey: ['xunlianCodes'] });
  129. setSearchParams({
  130. code: values.code || '',
  131. page: 1,
  132. limit: searchParams.limit,
  133. });
  134. } catch (error) {
  135. message.error('搜索失败');
  136. }
  137. };
  138. // 处理分页
  139. const handlePageChange = (page: number, pageSize?: number) => {
  140. setSearchParams(prev => ({
  141. ...prev,
  142. page,
  143. limit: pageSize || prev.limit,
  144. }));
  145. };
  146. // 处理添加
  147. const handleAdd = () => {
  148. setFormMode('create');
  149. setEditingId(null);
  150. form.resetFields();
  151. setModalVisible(true);
  152. };
  153. // 表格列定义
  154. const columns = [
  155. {
  156. title: 'ID',
  157. dataIndex: 'id',
  158. key: 'id',
  159. width: 80,
  160. },
  161. {
  162. title: '股票代码',
  163. dataIndex: 'code',
  164. key: 'code',
  165. },
  166. {
  167. title: '股票名称',
  168. dataIndex: 'stock_name',
  169. key: 'stock_name',
  170. },
  171. {
  172. title: '案例名称',
  173. dataIndex: 'name',
  174. key: 'name',
  175. },
  176. {
  177. title: '案例类型',
  178. dataIndex: 'type',
  179. key: 'type',
  180. },
  181. {
  182. title: '交易日期',
  183. dataIndex: 'trade_date',
  184. key: 'trade_date',
  185. render: (date: string) => dayjs(date).format('YYYY-MM-DD'),
  186. },
  187. {
  188. title: '提交用户',
  189. dataIndex: 'nickname',
  190. key: 'nickname',
  191. },
  192. {
  193. title: '创建时间',
  194. dataIndex: 'created_at',
  195. key: 'created_at',
  196. render: (date: string) => dayjs(date).format('YYYY-MM-DD HH:mm:ss'),
  197. },
  198. {
  199. title: '操作',
  200. key: 'action',
  201. render: (_: any, record: XunlianCode) => (
  202. <Space size="middle">
  203. <Button type="link" onClick={() => handleEdit(record.id)}>编辑</Button>
  204. <Popconfirm
  205. title="确定要删除这条训练代码吗?"
  206. onConfirm={() => handleDelete(record.id)}
  207. okText="确定"
  208. cancelText="取消"
  209. >
  210. <Button type="link" danger>删除</Button>
  211. </Popconfirm>
  212. </Space>
  213. ),
  214. },
  215. ];
  216. return (
  217. <div>
  218. <Card title="训练代码管理" className="mb-4">
  219. <Form
  220. form={searchForm}
  221. layout="inline"
  222. onFinish={handleSearch}
  223. style={{ marginBottom: '16px' }}
  224. >
  225. <Form.Item name="code" label="股票代码">
  226. <Input placeholder="要搜索的股票代码" />
  227. </Form.Item>
  228. <Form.Item>
  229. <Space>
  230. <Button type="primary" htmlType="submit">
  231. 搜索
  232. </Button>
  233. <Button htmlType="reset" onClick={() => {
  234. searchForm.resetFields();
  235. setSearchParams({
  236. code: '',
  237. page: 1,
  238. limit: 10,
  239. });
  240. }}>
  241. 重置
  242. </Button>
  243. <Button type="primary" onClick={handleAdd}>
  244. 添加代码
  245. </Button>
  246. </Space>
  247. </Form.Item>
  248. </Form>
  249. <Table
  250. columns={columns}
  251. dataSource={codes}
  252. rowKey="id"
  253. loading={{
  254. spinning: isListLoading,
  255. tip: '正在加载数据...',
  256. }}
  257. pagination={{
  258. current: pagination.current,
  259. pageSize: pagination.pageSize,
  260. total: pagination.total,
  261. onChange: handlePageChange,
  262. showSizeChanger: true,
  263. showTotal: (total) => `共 ${total} 条`,
  264. }}
  265. />
  266. </Card>
  267. <Modal
  268. title={formMode === 'create' ? '添加训练代码' : '编辑训练代码'}
  269. open={modalVisible}
  270. onOk={() => {
  271. form.validateFields()
  272. .then(values => {
  273. handleSubmit(values);
  274. })
  275. .catch(info => {
  276. console.log('表单验证失败:', info);
  277. });
  278. }}
  279. onCancel={() => setModalVisible(false)}
  280. width={800}
  281. okText="确定"
  282. cancelText="取消"
  283. destroyOnClose
  284. >
  285. <Form
  286. form={form}
  287. layout="vertical"
  288. >
  289. <Row gutter={16}>
  290. <Col span={12}>
  291. <Form.Item
  292. name="code"
  293. label="股票代码"
  294. rules={[{ required: true, message: '请输入股票代码' }]}
  295. >
  296. <Input placeholder="请输入股票代码" />
  297. </Form.Item>
  298. </Col>
  299. <Col span={12}>
  300. <Form.Item
  301. name="stock_name"
  302. label="股票名称"
  303. rules={[{ required: true, message: '请输入股票名称' }]}
  304. >
  305. <Input placeholder="请输入股票名称" />
  306. </Form.Item>
  307. </Col>
  308. </Row>
  309. <Row gutter={16}>
  310. <Col span={12}>
  311. <Form.Item
  312. name="name"
  313. label="案例名称"
  314. rules={[{ required: true, message: '请输入案例名称' }]}
  315. >
  316. <Input placeholder="请输入案例名称" />
  317. </Form.Item>
  318. </Col>
  319. <Col span={12}>
  320. <Form.Item
  321. name="type"
  322. label="案例类型"
  323. >
  324. <Input placeholder="请输入案例类型" />
  325. </Form.Item>
  326. </Col>
  327. </Row>
  328. <Row gutter={16}>
  329. <Col span={12}>
  330. <Form.Item
  331. name="trade_date"
  332. label="交易日期"
  333. rules={[{ required: true, message: '请选择交易日期' }]}
  334. >
  335. <DatePicker style={{ width: '100%' }} format="YYYY-MM-DD" />
  336. </Form.Item>
  337. </Col>
  338. <Col span={12}>
  339. <Form.Item
  340. name="description"
  341. label="案例描述"
  342. >
  343. <Input.TextArea rows={1} placeholder="请输入案例描述" />
  344. </Form.Item>
  345. </Col>
  346. </Row>
  347. </Form>
  348. </Modal>
  349. </div>
  350. );
  351. };