pages_classroom_data.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455
  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, Popconfirm, Tag,
  7. DatePicker, Select
  8. } from 'antd';
  9. import { useQuery } from '@tanstack/react-query';
  10. import dayjs from 'dayjs';
  11. import weekday from 'dayjs/plugin/weekday';
  12. import localeData from 'dayjs/plugin/localeData';
  13. import 'dayjs/locale/zh-cn';
  14. import {
  15. ClassroomData,
  16. ClassroomStatus,
  17. ClassroomStatusNameMap
  18. } from '../share/types_stock.ts';
  19. import { getEnumOptions } from './utils.ts';
  20. import { ClassroomDataAPI } from './api/index.ts';
  21. // 配置 dayjs 插件
  22. dayjs.extend(weekday);
  23. dayjs.extend(localeData);
  24. dayjs.locale('zh-cn');
  25. export const ClassroomDataPage = () => {
  26. const queryClient = useQueryClient();
  27. const [modalVisible, setModalVisible] = useState(false);
  28. const [formMode, setFormMode] = useState<'create' | 'edit'>('create');
  29. const [editingId, setEditingId] = useState<number | null>(null);
  30. const [form] = Form.useForm();
  31. const [searchForm] = Form.useForm();
  32. const [searchParams, setSearchParams] = useState({
  33. classroom_no: '',
  34. code: '',
  35. page: 1,
  36. limit: 10,
  37. });
  38. // 使用React Query获取教室数据列表
  39. const { data: classroomData, isLoading: isListLoading, refetch } = useQuery({
  40. queryKey: ['classroomData', searchParams],
  41. queryFn: () => ClassroomDataAPI.getClassroomDatas({
  42. page: searchParams.page,
  43. pageSize: searchParams.limit,
  44. classroom_no: searchParams.classroom_no,
  45. training_date: '',
  46. status: undefined
  47. }),
  48. placeholderData: {
  49. data: [],
  50. pagination: {
  51. current: 1,
  52. pageSize: 10,
  53. total: 0,
  54. totalPages: 1
  55. }
  56. }
  57. });
  58. const classrooms = React.useMemo(() => classroomData?.data || [], [classroomData]);
  59. const pagination = React.useMemo(() => ({
  60. current: classroomData?.pagination?.current || 1,
  61. pageSize: classroomData?.pagination?.pageSize || 10,
  62. total: classroomData?.pagination?.total || 0,
  63. totalPages: classroomData?.pagination?.totalPages || 1
  64. }), [classroomData]);
  65. // 获取单个教室数据
  66. const fetchClassroom = async (id: number) => {
  67. try {
  68. const response = await ClassroomDataAPI.getClassroomData(id);
  69. return response.data;
  70. } catch (error) {
  71. message.error('获取教室数据详情失败');
  72. return null;
  73. }
  74. };
  75. // 处理表单提交
  76. const handleSubmit = async (values: Partial<ClassroomData>) => {
  77. try {
  78. const response = formMode === 'create'
  79. ? await ClassroomDataAPI.createClassroomData(values)
  80. : await ClassroomDataAPI.updateClassroomData(editingId!, values);
  81. message.success(formMode === 'create' ? '创建教室数据成功' : '更新教室数据成功');
  82. setModalVisible(false);
  83. form.resetFields();
  84. refetch();
  85. } catch (error) {
  86. message.error((error as Error).message);
  87. }
  88. };
  89. // 处理编辑
  90. const handleEdit = async (id: number) => {
  91. const classroom = await fetchClassroom(id);
  92. if (classroom) {
  93. setFormMode('edit');
  94. setEditingId(id);
  95. form.setFieldsValue({
  96. ...classroom,
  97. training_date: dayjs(classroom.training_date)
  98. });
  99. setModalVisible(true);
  100. }
  101. };
  102. // 处理删除
  103. const handleDelete = async (id: number) => {
  104. try {
  105. await ClassroomDataAPI.deleteClassroomData(id);
  106. message.success('删除教室数据成功');
  107. refetch();
  108. } catch (error) {
  109. message.error((error as Error).message);
  110. }
  111. };
  112. // 处理搜索
  113. const handleSearch = async (values: any) => {
  114. try {
  115. queryClient.removeQueries({ queryKey: ['classroomData'] });
  116. setSearchParams({
  117. classroom_no: values.classroom_no || '',
  118. code: values.code || '',
  119. page: 1,
  120. limit: searchParams.limit,
  121. });
  122. } catch (error) {
  123. message.error('搜索失败');
  124. }
  125. };
  126. // 处理分页
  127. const handlePageChange = (page: number, pageSize?: number) => {
  128. setSearchParams(prev => ({
  129. ...prev,
  130. page,
  131. limit: pageSize || prev.limit,
  132. }));
  133. };
  134. // 处理添加
  135. const handleAdd = () => {
  136. setFormMode('create');
  137. setEditingId(null);
  138. form.resetFields();
  139. setModalVisible(true);
  140. };
  141. // 复制链接到剪贴板
  142. const copyLink = (type: 'exam' | 'stock' | 'admin', classroom_no: string) => {
  143. const baseUrl = window.location.origin;
  144. let url = '';
  145. let successMsg = '';
  146. switch(type) {
  147. case 'exam':
  148. url = `${baseUrl}/mobile/exam/card?classroom=${classroom_no}`;
  149. successMsg = '答题卡链接已复制';
  150. break;
  151. case 'stock':
  152. url = `${baseUrl}/mobile/stock?classroom=${classroom_no}`;
  153. successMsg = '股票训练链接已复制';
  154. break;
  155. case 'admin':
  156. url = `${baseUrl}/mobile/exam/admin?classroom=${classroom_no}`;
  157. successMsg = '管理员链接已复制';
  158. break;
  159. }
  160. navigator.clipboard.writeText(url).then(() => {
  161. message.success(successMsg);
  162. }).catch(() => {
  163. message.error('复制失败,请手动复制');
  164. });
  165. };
  166. // 教室状态选项
  167. const statusOptions = getEnumOptions(ClassroomStatus, ClassroomStatusNameMap);
  168. // 表格列定义
  169. const columns = [
  170. {
  171. title: 'ID',
  172. dataIndex: 'id',
  173. key: 'id',
  174. width: 80,
  175. },
  176. {
  177. title: '教室号',
  178. dataIndex: 'classroom_no',
  179. key: 'classroom_no',
  180. },
  181. {
  182. title: '训练日期',
  183. dataIndex: 'training_date',
  184. key: 'training_date',
  185. render: (date: string) => dayjs(date).format('YYYY-MM-DD'),
  186. },
  187. {
  188. title: '持股',
  189. dataIndex: 'holding_stock',
  190. key: 'holding_stock',
  191. },
  192. {
  193. title: '持币',
  194. dataIndex: 'holding_cash',
  195. key: 'holding_cash',
  196. },
  197. {
  198. title: '价格',
  199. dataIndex: 'price',
  200. key: 'price',
  201. },
  202. {
  203. title: '代码',
  204. dataIndex: 'code',
  205. key: 'code',
  206. },
  207. {
  208. title: '状态',
  209. dataIndex: 'status',
  210. key: 'status',
  211. render: (status: ClassroomStatus) => {
  212. const color = status === ClassroomStatus.OPEN ? 'green' : 'red';
  213. const text = ClassroomStatusNameMap[status];
  214. return <Tag color={color}>{text}</Tag>;
  215. },
  216. },
  217. {
  218. title: '链接',
  219. key: 'links',
  220. render: (_: any, record: ClassroomData) => (
  221. <Space direction="vertical" size={4}>
  222. <div>
  223. <Button
  224. type="link"
  225. size="small"
  226. onClick={() => copyLink('stock', record.classroom_no)}
  227. >
  228. 复制股票训练链接
  229. </Button>
  230. </div>
  231. <div>
  232. <Button
  233. type="link"
  234. size="small"
  235. onClick={() => copyLink('exam', record.classroom_no)}
  236. >
  237. 复制答题卡链接
  238. </Button>
  239. </div>
  240. <div>
  241. <Button
  242. type="link"
  243. size="small"
  244. onClick={() => copyLink('admin', record.classroom_no)}
  245. >
  246. 复制管理员链接
  247. </Button>
  248. </div>
  249. </Space>
  250. ),
  251. },
  252. {
  253. title: '操作',
  254. key: 'action',
  255. render: (_: any, record: ClassroomData) => (
  256. <Space size="middle">
  257. <Button type="link" onClick={() => handleEdit(record.id)}>编辑</Button>
  258. <Popconfirm
  259. title="确定要删除这条教室数据吗?"
  260. onConfirm={() => handleDelete(record.id)}
  261. okText="确定"
  262. cancelText="取消"
  263. >
  264. <Button type="link" danger>删除</Button>
  265. </Popconfirm>
  266. </Space>
  267. ),
  268. },
  269. ];
  270. return (
  271. <div>
  272. <Card title="教室数据管理" className="mb-4">
  273. <Form
  274. form={searchForm}
  275. layout="inline"
  276. onFinish={handleSearch}
  277. style={{ marginBottom: '16px' }}
  278. >
  279. <Form.Item name="classroom_no" label="教室号">
  280. <Input placeholder="要搜索的教室号" />
  281. </Form.Item>
  282. <Form.Item name="code" label="代码">
  283. <Input placeholder="要搜索的代码" />
  284. </Form.Item>
  285. <Form.Item>
  286. <Space>
  287. <Button type="primary" htmlType="submit">
  288. 搜索
  289. </Button>
  290. <Button htmlType="reset" onClick={() => {
  291. searchForm.resetFields();
  292. setSearchParams({
  293. classroom_no: '',
  294. code: '',
  295. page: 1,
  296. limit: 10,
  297. });
  298. }}>
  299. 重置
  300. </Button>
  301. <Button type="primary" onClick={handleAdd}>
  302. 添加教室数据
  303. </Button>
  304. </Space>
  305. </Form.Item>
  306. </Form>
  307. <Table
  308. columns={columns}
  309. dataSource={classrooms}
  310. rowKey="id"
  311. loading={{
  312. spinning: isListLoading,
  313. tip: '正在加载数据...',
  314. }}
  315. pagination={{
  316. current: pagination.current,
  317. pageSize: pagination.pageSize,
  318. total: pagination.total,
  319. onChange: handlePageChange,
  320. showSizeChanger: true,
  321. showTotal: (total) => `共 ${total} 条`,
  322. }}
  323. />
  324. </Card>
  325. <Modal
  326. title={formMode === 'create' ? '添加教室数据' : '编辑教室数据'}
  327. open={modalVisible}
  328. onOk={() => {
  329. form.validateFields()
  330. .then(values => {
  331. handleSubmit({
  332. ...values,
  333. training_date: values.training_date.format('YYYY-MM-DD')
  334. });
  335. })
  336. .catch(info => {
  337. console.log('表单验证失败:', info);
  338. });
  339. }}
  340. onCancel={() => setModalVisible(false)}
  341. width={800}
  342. okText="确定"
  343. cancelText="取消"
  344. destroyOnClose
  345. >
  346. <Form
  347. form={form}
  348. layout="vertical"
  349. initialValues={{
  350. status: ClassroomStatus.OPEN,
  351. }}
  352. >
  353. <Row gutter={16}>
  354. <Col span={12}>
  355. <Form.Item
  356. name="classroom_no"
  357. label="教室号"
  358. rules={[{ required: true, message: '请输入教室号' }]}
  359. >
  360. <Input placeholder="请输入教室号" />
  361. </Form.Item>
  362. </Col>
  363. <Col span={12}>
  364. <Form.Item
  365. name="training_date"
  366. label="训练日期"
  367. rules={[{ required: true, message: '请选择训练日期' }]}
  368. >
  369. <DatePicker style={{ width: '100%' }} />
  370. </Form.Item>
  371. </Col>
  372. </Row>
  373. <Row gutter={16}>
  374. <Col span={8}>
  375. <Form.Item
  376. name="holding_stock"
  377. label="持股"
  378. >
  379. <Input placeholder="请输入持股" />
  380. </Form.Item>
  381. </Col>
  382. <Col span={8}>
  383. <Form.Item
  384. name="holding_cash"
  385. label="持币"
  386. >
  387. <Input placeholder="请输入持币" />
  388. </Form.Item>
  389. </Col>
  390. <Col span={8}>
  391. <Form.Item
  392. name="price"
  393. label="价格"
  394. >
  395. <Input placeholder="请输入价格" />
  396. </Form.Item>
  397. </Col>
  398. </Row>
  399. <Row gutter={16}>
  400. <Col span={12}>
  401. <Form.Item
  402. name="code"
  403. label="代码"
  404. rules={[{ required: true, message: '请输入代码' }]}
  405. >
  406. <Input placeholder="请输入代码" />
  407. </Form.Item>
  408. </Col>
  409. <Col span={12}>
  410. <Form.Item
  411. name="status"
  412. label="状态"
  413. rules={[{ required: true, message: '请选择状态' }]}
  414. >
  415. <Select options={statusOptions} />
  416. </Form.Item>
  417. </Col>
  418. </Row>
  419. <Form.Item
  420. name="spare"
  421. label="备用字段"
  422. >
  423. <Input placeholder="请输入备用字段" />
  424. </Form.Item>
  425. </Form>
  426. </Modal>
  427. </div>
  428. );
  429. };