index.tsx 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682
  1. import {
  2. addMatch,
  3. batchDeleteMatches,
  4. createTeam,
  5. deleteMatch,
  6. getMatches,
  7. getTeamLogoByName,
  8. updateMatch,
  9. updatePredictionsForMatch,
  10. uploadImage,
  11. } from '@/services/api';
  12. import { PlusOutlined } from '@ant-design/icons';
  13. import type { ActionType, ProColumns } from '@ant-design/pro-components';
  14. import { debounce } from 'lodash';
  15. import {
  16. ModalForm,
  17. ProFormDatePicker,
  18. ProFormDigit,
  19. ProFormSelect,
  20. ProFormText,
  21. ProFormUploadButton,
  22. } from '@ant-design/pro-form';
  23. import { FooterToolbar, PageContainer, ProTable } from '@ant-design/pro-components';
  24. import { Button, FormInstance, message, Popconfirm } from 'antd';
  25. import Modal from 'antd/lib/modal';
  26. import React, { useCallback, useMemo, useRef, useState } from 'react';
  27. import UpdateForm from './UpdateForm';
  28. const BasketballMatch: React.FC = () => {
  29. const [createModalOpen, handleModalOpen] = useState<boolean>(false);
  30. const [updateModalOpen, handleUpdateModalOpen] = useState<boolean>(false);
  31. const actionRef = useRef<ActionType>();
  32. const [currentRow, setCurrentRow] = useState<API.MatchItem>();
  33. const [selectedRows, setSelectedRows] = useState<API.MatchItem[]>([]);
  34. const [pageSize, setPageSize] = useState<number>(10);
  35. const formRef = useRef<FormInstance>(null);
  36. const columns: ProColumns<API.MatchItem>[] = [
  37. {
  38. title: '主队',
  39. dataIndex: ['homeTeam', 'name'],
  40. valueType: 'text',
  41. width: 100,
  42. ellipsis: true,
  43. },
  44. {
  45. title: '客队',
  46. dataIndex: ['awayTeam', 'name'],
  47. valueType: 'text',
  48. width: 100,
  49. ellipsis: true,
  50. },
  51. {
  52. title: '日期',
  53. dataIndex: 'date',
  54. valueType: 'date',
  55. width: 100,
  56. ellipsis: true,
  57. },
  58. {
  59. title: '时间',
  60. dataIndex: 'time',
  61. valueType: 'text',
  62. },
  63. {
  64. title: '联赛',
  65. dataIndex: 'league',
  66. valueType: 'text',
  67. width: 100,
  68. ellipsis: true,
  69. },
  70. {
  71. title: '状态',
  72. dataIndex: 'status',
  73. valueEnum: {
  74. 未开始: { text: '未开始', status: 'Default' },
  75. 进行中: { text: '进行中', status: 'Processing' },
  76. 已结束: { text: '已结束', status: 'Success' },
  77. },
  78. },
  79. {
  80. title: '参与人数',
  81. dataIndex: 'canyuNumber',
  82. valueType: 'digit',
  83. },
  84. {
  85. title: '主队得分',
  86. dataIndex: 'homeTeamScore',
  87. valueType: 'digit',
  88. },
  89. {
  90. title: '客队得分',
  91. dataIndex: 'awayTeamScore',
  92. valueType: 'digit',
  93. },
  94. {
  95. title: '让分',
  96. dataIndex: ['basketball', 'spread', 'points'],
  97. valueType: 'digit',
  98. render: (_, record) => {
  99. const points = record.basketball?.spread?.points;
  100. const favorite = record.basketball?.spread?.favorite;
  101. if (!points || !favorite) return '-';
  102. return `${favorite === 'home' ? '主队' : '客队'}让${Math.abs(points)}分`;
  103. },
  104. },
  105. {
  106. title: '总分线',
  107. dataIndex: ['basketball', 'totalPoints', 'value'],
  108. valueType: 'digit',
  109. },
  110. {
  111. title: '胜负结果',
  112. dataIndex: ['basketball', 'result', 'whoWillWin'],
  113. valueEnum: {
  114. home: { text: '主队胜' },
  115. away: { text: '客队胜' },
  116. },
  117. },
  118. {
  119. title: '让分结果',
  120. dataIndex: ['basketball', 'result', 'spreadWinner'],
  121. valueEnum: {
  122. home: { text: '主队赢盘' },
  123. away: { text: '客队赢盘' },
  124. },
  125. },
  126. {
  127. title: '总分结果',
  128. dataIndex: ['basketball', 'result', 'totalPointsResult'],
  129. valueEnum: {
  130. over: { text: '大分' },
  131. under: { text: '小分' },
  132. },
  133. },
  134. {
  135. title: '胜负预测积分',
  136. dataIndex: ['basketball', 'pointRewards', 'whoWillWin'],
  137. valueType: 'digit',
  138. },
  139. {
  140. title: '总分预测积分',
  141. dataIndex: ['basketball', 'pointRewards', 'totalPoints'],
  142. valueType: 'digit',
  143. },
  144. {
  145. title: '让分预测积分',
  146. dataIndex: ['basketball', 'pointRewards', 'spread'],
  147. valueType: 'digit',
  148. },
  149. {
  150. title: '操作',
  151. dataIndex: 'option',
  152. valueType: 'option',
  153. render: (_, record) => [
  154. <a
  155. key="edit"
  156. onClick={() => {
  157. console.log('record', record);
  158. handleUpdateModalOpen(true);
  159. setCurrentRow(record);
  160. }}
  161. >
  162. 编辑
  163. </a>,
  164. <Popconfirm
  165. key="delete"
  166. title="确认删除"
  167. description={`您确定要删除 ${record.homeTeam.name} vs ${record.awayTeam.name} 的比赛记录吗?`}
  168. onConfirm={() => handleDelete(record)}
  169. okText="确定"
  170. cancelText="取消"
  171. >
  172. <a>删除</a>
  173. </Popconfirm>,
  174. ],
  175. },
  176. ];
  177. const handleDelete = async (record: API.MatchItem) => {
  178. const res = await deleteMatch(record._id);
  179. if (res.success) {
  180. message.success('比赛记录已成功删除');
  181. actionRef.current?.reload();
  182. } else {
  183. message.error('删除失败,请重试');
  184. }
  185. };
  186. // 批量删除
  187. const handleBatchDelete = async (selectedRowKeys: string[]) => {
  188. if (selectedRowKeys.length === 0) return;
  189. console.log('selectedRowKeys', selectedRowKeys);
  190. const res = await batchDeleteMatches(selectedRowKeys);
  191. if (res.success) {
  192. message.success(`${res.message}`);
  193. actionRef.current?.reload();
  194. }
  195. };
  196. const [homeTeamLogoUrl, setHomeTeamLogoUrl] = useState('');
  197. const [awayTeamLogoUrl, setAwayTeamLogoUrl] = useState('');
  198. const [homeTeamName, setHomeTeamName] = useState('');
  199. const [awayTeamName, setAwayTeamName] = useState('');
  200. console.log(
  201. 'homeTeamName,awayTeamName, homeTeamLogoUrl,awayTeamLogoUrl',
  202. homeTeamName,
  203. awayTeamName,
  204. homeTeamLogoUrl,
  205. awayTeamLogoUrl,
  206. );
  207. const handleUpload = useCallback((teamType) => async (file) => {
  208. const formData = new FormData();
  209. formData.append('file', file);
  210. formData.append('teamName', teamType === 'home' ? homeTeamName : awayTeamName);
  211. formData.append('teamType', teamType);
  212. try {
  213. const uploadData = await uploadImage(formData);
  214. console.log('uploadData', uploadData);
  215. if (uploadData.success) {
  216. // 文件上传成功后,更新球队信息
  217. const teamData = await createTeam({
  218. teamName: teamType === 'home' ? homeTeamName : awayTeamName,
  219. logoUrl: uploadData.url,
  220. teamType,
  221. });
  222. if (teamData.success) {
  223. message.success(`图片上传成功`);
  224. if (teamType === 'home') {
  225. setHomeTeamLogoUrl(uploadData.url);
  226. } else {
  227. setAwayTeamLogoUrl(uploadData.url);
  228. }
  229. return {
  230. status: 'done',
  231. url: uploadData.url,
  232. };
  233. }
  234. }
  235. } catch (error) {
  236. console.error('Upload error:', error);
  237. message.error(`文件上传失败: ${error.message}`);
  238. return {
  239. status: 'error',
  240. error: error.message,
  241. };
  242. }
  243. });
  244. const debouncedFetchTeamLogo = useRef(
  245. debounce(async (teamName, setLogoFunc) => {
  246. console.log('teamName', teamName);
  247. if (!teamName) {
  248. setLogoFunc('');
  249. return;
  250. }
  251. try {
  252. const res = await getTeamLogoByName({ name: teamName });
  253. if (res.success && res.team && res.team.logo) {
  254. setLogoFunc(res.team.logo);
  255. } else {
  256. setLogoFunc('');
  257. }
  258. } catch (error) {
  259. console.error('Error fetching team logo:', error);
  260. setLogoFunc('');
  261. }
  262. }, 300),
  263. ).current;
  264. const handleUpdateSubmit = async (value: API.MatchItem) => {
  265. console.log('value', value);
  266. if (value.status === '已结束') {
  267. // 检查必填字段
  268. const missingFields = checkRequiredFields(value);
  269. if (missingFields.length > 0) {
  270. message.error(`以下字段不能为空: ${missingFields.join(', ')}`);
  271. return;
  272. }
  273. Modal.confirm({
  274. title: '确认结束比赛?',
  275. content: '比赛结束后将会给预测对本次比赛的用户添加积分,是否确认结束比赛?',
  276. okText: '确定',
  277. cancelText: '取消',
  278. onOk: async () => {
  279. await updateMatchAndHandleResponse(value);
  280. },
  281. onCancel: () => {
  282. message.info('取消了比赛结束操作');
  283. },
  284. });
  285. } else {
  286. await updateMatchAndHandleResponse(value);
  287. }
  288. };
  289. const checkRequiredFields = (value: API.MatchItem): string[] => {
  290. const missingFields: string[] = [];
  291. // 检查 basketball.result 字段
  292. // if (!value.basketball?.result?.spreadWinner) missingFields.push('让分结果');
  293. // if (!value.basketball?.result?.totalPointsResult) missingFields.push('总分结果');
  294. // 检查 basketball.pointRewards 字段
  295. if (
  296. value.basketball?.pointRewards?.spread === undefined ||
  297. value.basketball?.pointRewards?.spread === null
  298. ) {
  299. missingFields.push('让分预测积分');
  300. }
  301. if (
  302. value.basketball?.pointRewards?.totalPoints === undefined ||
  303. value.basketball?.pointRewards?.totalPoints === null
  304. ) {
  305. missingFields.push('总分预测积分');
  306. }
  307. // 检查比分
  308. if (value.homeTeamScore === undefined || value.homeTeamScore === null) {
  309. missingFields.push('主队得分');
  310. }
  311. if (value.awayTeamScore === undefined || value.awayTeamScore === null) {
  312. missingFields.push('客队得分');
  313. }
  314. return missingFields;
  315. };
  316. const updateMatchAndHandleResponse = async (value: API.MatchItem) => {
  317. try {
  318. // 确保更新的数据包含足球类型
  319. const updateData = {
  320. ...value,
  321. type: 'basketball',
  322. };
  323. const res = await updateMatch(value._id, 'basketball', updateData);
  324. console.log('updateMatch res', res);
  325. if (res.success) {
  326. message.success('修改成功');
  327. if (value.status === '已结束') {
  328. await handleMatchEnd(value._id);
  329. }
  330. handleUpdateModalOpen(false);
  331. setCurrentRow(undefined);
  332. actionRef.current?.reload();
  333. } else {
  334. message.error(res.message || '修改失败,请重试');
  335. }
  336. } catch (error) {
  337. console.error('更新比赛时出错:', error);
  338. message.error('修改失败,请重试');
  339. }
  340. };
  341. // 新增函数来处理比赛结束后的预测更新
  342. const handleMatchEnd = async (matchId: string) => {
  343. try {
  344. const res = await updatePredictionsForMatch(matchId);
  345. if (res.success) {
  346. message.success('用户积分已更新');
  347. } else {
  348. message.error('更新用户积分时出错');
  349. }
  350. } catch (error) {
  351. console.error('处理比赛结束时出错:', error);
  352. message.error('更新用户积分失败');
  353. }
  354. };
  355. return (
  356. <PageContainer>
  357. <ProTable<API.MatchItem, API.PageParams>
  358. actionRef={actionRef}
  359. rowKey="_id"
  360. search={{
  361. labelWidth: 120,
  362. }}
  363. toolBarRender={() => [
  364. <Button
  365. type="primary"
  366. key="primary"
  367. onClick={() => {
  368. setHomeTeamLogoUrl('');
  369. setAwayTeamLogoUrl('');
  370. setHomeTeamName('');
  371. setAwayTeamName('');
  372. formRef.current?.resetFields();
  373. handleModalOpen(true);
  374. }}
  375. >
  376. <PlusOutlined /> 新建
  377. </Button>,
  378. ]}
  379. request={(params) =>
  380. getMatches({
  381. ...params,
  382. type: 'basketball',
  383. })
  384. }
  385. columns={columns}
  386. scroll={{ x: 1480 }}
  387. pagination={{
  388. showSizeChanger: true,
  389. pageSize: pageSize,
  390. onChange: (page, pageSize) => {
  391. setPageSize(pageSize);
  392. // 这里可以处理分页参数的变化
  393. console.log('当前页:', page, '每页显示条数:', pageSize);
  394. if (actionRef.current) {
  395. actionRef.current.reload();
  396. }
  397. },
  398. }}
  399. rowSelection={{
  400. onChange: (_, selectedRows) => {
  401. setSelectedRows(selectedRows);
  402. },
  403. }}
  404. />
  405. {selectedRows?.length > 0 && (
  406. <FooterToolbar
  407. extra={
  408. <div>
  409. 已选择 <a style={{ fontWeight: 600 }}>{selectedRows.length}</a> 项 &nbsp;&nbsp;
  410. </div>
  411. }
  412. >
  413. <Button
  414. onClick={async () => {
  415. console.log('selectedRows', selectedRows);
  416. const selectedRowKeys = selectedRows.map((row) => row._id);
  417. await handleBatchDelete(selectedRowKeys);
  418. setSelectedRows([]);
  419. actionRef.current?.reloadAndRest?.();
  420. }}
  421. >
  422. 批量删除
  423. </Button>
  424. </FooterToolbar>
  425. )}
  426. {/* 新建弹框 */}
  427. <ModalForm
  428. title="新建篮球赛事"
  429. width="400px"
  430. open={createModalOpen}
  431. onOpenChange={handleModalOpen}
  432. formRef={formRef}
  433. onFinish={async (value) => {
  434. const formData = {
  435. type: 'basketball',
  436. ...value,
  437. homeTeam: {
  438. ...value.homeTeam,
  439. logo: homeTeamLogoUrl,
  440. },
  441. awayTeam: {
  442. ...value.awayTeam,
  443. logo: awayTeamLogoUrl,
  444. },
  445. };
  446. const res = await addMatch('basketball', formData as API.MatchItem);
  447. if (res.success) {
  448. message.success('创建成功');
  449. handleModalOpen(false);
  450. if (actionRef.current) {
  451. actionRef.current.reload();
  452. }
  453. formRef.current?.resetFields();
  454. }
  455. }}
  456. >
  457. <ProFormText
  458. rules={[{ required: true }]}
  459. width="md"
  460. name={['homeTeam', 'name']}
  461. label="主队名称"
  462. fieldProps={{
  463. onChange: (e) => {
  464. const name = e.target.value;
  465. setHomeTeamName(name);
  466. debouncedFetchTeamLogo(name, setHomeTeamLogoUrl);
  467. },
  468. }}
  469. />
  470. <ProFormUploadButton
  471. name={['homeTeam', 'logo']}
  472. label="主队Logo"
  473. max={1}
  474. action={handleUpload('home')}
  475. fileList={useMemo(() => {
  476. return homeTeamLogoUrl ? [{ url: `${API_URL}${homeTeamLogoUrl}` }] : [];
  477. }, [homeTeamLogoUrl])}
  478. onChange={({ fileList }) => {
  479. if (fileList.length === 0) {
  480. setHomeTeamLogoUrl('');
  481. }
  482. }}
  483. disabled={!homeTeamName}
  484. />
  485. <ProFormText
  486. rules={[
  487. {
  488. required: true,
  489. message: '客队名称是必填项',
  490. },
  491. ]}
  492. width="md"
  493. name={['awayTeam', 'name']}
  494. label="客队名称"
  495. fieldProps={{
  496. onChange: (e) => {
  497. const name = e.target.value;
  498. setAwayTeamName(name);
  499. debouncedFetchTeamLogo(name, setAwayTeamLogoUrl);
  500. },
  501. }}
  502. />
  503. <ProFormUploadButton
  504. name={['awayTeam', 'logo']}
  505. label="客队Logo"
  506. max={1}
  507. action={handleUpload('away')}
  508. fileList={useMemo(() => {
  509. return awayTeamLogoUrl ? [{ url: `${API_URL}/${awayTeamLogoUrl}` }] : [];
  510. }, [awayTeamLogoUrl])}
  511. onChange={({ fileList }) => {
  512. if (fileList.length === 0) {
  513. setAwayTeamLogoUrl('');
  514. }
  515. }}
  516. disabled={!awayTeamName}
  517. />
  518. <ProFormDatePicker
  519. rules={[
  520. {
  521. required: true,
  522. message: '比赛日期是必填项',
  523. },
  524. ]}
  525. width="md"
  526. name="date"
  527. label="比赛日期"
  528. />
  529. <ProFormText
  530. rules={[
  531. {
  532. required: true,
  533. message: '比赛时间是必填项',
  534. },
  535. ]}
  536. width="md"
  537. name="time"
  538. label="比赛时间"
  539. />
  540. <ProFormText width="md" name="league" label="联赛" />
  541. <ProFormDigit width="md" name={['basketball', 'spread', 'points']} label="预测让分值"
  542. rules={[
  543. {
  544. required: true,
  545. message: '预测让分值是必填项',
  546. },
  547. ]} />
  548. <ProFormSelect
  549. width="md"
  550. name={['basketball', 'spread', 'favorite']}
  551. label="预测让分方"
  552. valueEnum={{
  553. home: '主队',
  554. away: '客队',
  555. }}
  556. rules={[
  557. {
  558. required: true,
  559. message: '预测让分方是必填项',
  560. },
  561. ]}
  562. />
  563. <ProFormDigit width="md" name={['basketball', 'totalPoints', 'value']} label="预测总得分" rules={[
  564. {
  565. required: true,
  566. message: '预测总得分是必填项',
  567. },
  568. ]} />
  569. <ProFormDigit
  570. width="md"
  571. name={['basketball', 'pointRewards', 'whoWillWin']}
  572. label="总分预测积分"
  573. rules={[
  574. {
  575. required: true,
  576. message: '总分预测积分是必填项',
  577. },
  578. ]}
  579. />
  580. <ProFormDigit
  581. width="md"
  582. name={['basketball', 'pointRewards', 'spread']}
  583. label="让分预测积分"
  584. rules={[
  585. {
  586. required: true,
  587. message: '让分预测积分是必填项',
  588. },
  589. ]}
  590. />
  591. <ProFormDigit
  592. width="md"
  593. name={['basketball', 'pointRewards', 'totalPoints']}
  594. label="总分预测积分"
  595. rules={[
  596. {
  597. required: true,
  598. message: '总分预测积分是必填项',
  599. },
  600. ]}
  601. />
  602. <ProFormSelect
  603. rules={[
  604. {
  605. required: true,
  606. message: '比赛状态是必填项',
  607. },
  608. ]}
  609. width="md"
  610. name="status"
  611. label="比赛状态"
  612. valueEnum={{
  613. 未开始: '未开始',
  614. 进行中: '进行中',
  615. 已结束: '已结束',
  616. }}
  617. />
  618. </ModalForm>
  619. {/* 新建弹框--END */}
  620. <UpdateForm
  621. onSubmit={handleUpdateSubmit}
  622. onCancel={() => {
  623. handleUpdateModalOpen(false);
  624. }}
  625. updateModalOpen={updateModalOpen}
  626. values={currentRow || {}}
  627. />
  628. </PageContainer>
  629. );
  630. };
  631. export default BasketballMatch;