ChannelsTable.js 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933
  1. import React, { useEffect, useState } from 'react';
  2. import {
  3. API,
  4. isMobile,
  5. shouldShowPrompt,
  6. showError,
  7. showInfo,
  8. showSuccess,
  9. showWarning,
  10. timestamp2string,
  11. } from '../helpers';
  12. import { CHANNEL_OPTIONS, ITEMS_PER_PAGE } from '../constants';
  13. import {
  14. renderGroup,
  15. renderNumberWithPoint,
  16. renderQuota,
  17. } from '../helpers/render';
  18. import {
  19. Button,
  20. Dropdown,
  21. Form,
  22. InputNumber,
  23. Popconfirm,
  24. Space,
  25. SplitButtonGroup,
  26. Switch,
  27. Table,
  28. Tag,
  29. Tooltip,
  30. Typography,
  31. } from '@douyinfe/semi-ui';
  32. import EditChannel from '../pages/Channel/EditChannel';
  33. import { IconTreeTriangleDown } from '@douyinfe/semi-icons';
  34. import { loadChannelModels } from './utils.js';
  35. function renderTimestamp(timestamp) {
  36. return <>{timestamp2string(timestamp)}</>;
  37. }
  38. let type2label = undefined;
  39. function renderType(type) {
  40. if (!type2label) {
  41. type2label = new Map();
  42. for (let i = 0; i < CHANNEL_OPTIONS.length; i++) {
  43. type2label[CHANNEL_OPTIONS[i].value] = CHANNEL_OPTIONS[i];
  44. }
  45. type2label[0] = { value: 0, text: '未知类型', color: 'grey' };
  46. }
  47. return (
  48. <Tag size='large' color={type2label[type]?.color}>
  49. {type2label[type]?.text}
  50. </Tag>
  51. );
  52. }
  53. const ChannelsTable = () => {
  54. const columns = [
  55. // {
  56. // title: '',
  57. // dataIndex: 'checkbox',
  58. // className: 'checkbox',
  59. // },
  60. {
  61. title: 'ID',
  62. dataIndex: 'id',
  63. },
  64. {
  65. title: '名称',
  66. dataIndex: 'name',
  67. },
  68. {
  69. title: '分组',
  70. dataIndex: 'group',
  71. render: (text, record, index) => {
  72. return (
  73. <div>
  74. <Space spacing={2}>
  75. {text.split(',').map((item, index) => {
  76. return renderGroup(item);
  77. })}
  78. </Space>
  79. </div>
  80. );
  81. },
  82. },
  83. {
  84. title: '类型',
  85. dataIndex: 'type',
  86. render: (text, record, index) => {
  87. return <div>{renderType(text)}</div>;
  88. },
  89. },
  90. {
  91. title: '状态',
  92. dataIndex: 'status',
  93. render: (text, record, index) => {
  94. if (text === 3) {
  95. if (record.other_info === '') {
  96. record.other_info = '{}'
  97. }
  98. let otherInfo = JSON.parse(record.other_info);
  99. let reason = otherInfo['status_reason'];
  100. let time = otherInfo['status_time'];
  101. return (
  102. <div>
  103. <Tooltip content={'原因:' + reason + ',时间:' + timestamp2string(time)}>
  104. {renderStatus(text)}
  105. </Tooltip>
  106. </div>
  107. );
  108. } else {
  109. return renderStatus(text);
  110. }
  111. },
  112. },
  113. {
  114. title: '响应时间',
  115. dataIndex: 'response_time',
  116. render: (text, record, index) => {
  117. return <div>{renderResponseTime(text)}</div>;
  118. },
  119. },
  120. {
  121. title: '已用/剩余',
  122. dataIndex: 'expired_time',
  123. render: (text, record, index) => {
  124. return (
  125. <div>
  126. <Space spacing={1}>
  127. <Tooltip content={'已用额度'}>
  128. <Tag color='white' type='ghost' size='large'>
  129. {renderQuota(record.used_quota)}
  130. </Tag>
  131. </Tooltip>
  132. <Tooltip content={'剩余额度' + record.balance + ',点击更新'}>
  133. <Tag
  134. color='white'
  135. type='ghost'
  136. size='large'
  137. onClick={() => {
  138. updateChannelBalance(record);
  139. }}
  140. >
  141. ${renderNumberWithPoint(record.balance)}
  142. </Tag>
  143. </Tooltip>
  144. </Space>
  145. </div>
  146. );
  147. },
  148. },
  149. {
  150. title: '优先级',
  151. dataIndex: 'priority',
  152. render: (text, record, index) => {
  153. return (
  154. <div>
  155. <InputNumber
  156. style={{ width: 70 }}
  157. name='priority'
  158. onBlur={(e) => {
  159. manageChannel(record.id, 'priority', record, e.target.value);
  160. }}
  161. keepFocus={true}
  162. innerButtons
  163. defaultValue={record.priority}
  164. min={-999}
  165. />
  166. </div>
  167. );
  168. },
  169. },
  170. {
  171. title: '权重',
  172. dataIndex: 'weight',
  173. render: (text, record, index) => {
  174. return (
  175. <div>
  176. <InputNumber
  177. style={{ width: 70 }}
  178. name='weight'
  179. onBlur={(e) => {
  180. manageChannel(record.id, 'weight', record, e.target.value);
  181. }}
  182. keepFocus={true}
  183. innerButtons
  184. defaultValue={record.weight}
  185. min={0}
  186. />
  187. </div>
  188. );
  189. },
  190. },
  191. {
  192. title: '',
  193. dataIndex: 'operate',
  194. render: (text, record, index) => (
  195. <div>
  196. <SplitButtonGroup
  197. style={{ marginRight: 1 }}
  198. aria-label='测试操作项目组'
  199. >
  200. <Button
  201. theme='light'
  202. onClick={() => {
  203. testChannel(record, '');
  204. }}
  205. >
  206. 测试
  207. </Button>
  208. <Dropdown
  209. trigger='click'
  210. position='bottomRight'
  211. menu={record.test_models}
  212. >
  213. <Button
  214. style={{ padding: '8px 4px' }}
  215. type='primary'
  216. icon={<IconTreeTriangleDown />}
  217. ></Button>
  218. </Dropdown>
  219. </SplitButtonGroup>
  220. {/*<Button theme='light' type='primary' style={{marginRight: 1}} onClick={()=>testChannel(record)}>测试</Button>*/}
  221. <Popconfirm
  222. title='确定是否要删除此渠道?'
  223. content='此修改将不可逆'
  224. okType={'danger'}
  225. position={'left'}
  226. onConfirm={() => {
  227. manageChannel(record.id, 'delete', record).then(() => {
  228. removeRecord(record.id);
  229. });
  230. }}
  231. >
  232. <Button theme='light' type='danger' style={{ marginRight: 1 }}>
  233. 删除
  234. </Button>
  235. </Popconfirm>
  236. {record.status === 1 ? (
  237. <Button
  238. theme='light'
  239. type='warning'
  240. style={{ marginRight: 1 }}
  241. onClick={async () => {
  242. manageChannel(record.id, 'disable', record);
  243. }}
  244. >
  245. 禁用
  246. </Button>
  247. ) : (
  248. <Button
  249. theme='light'
  250. type='secondary'
  251. style={{ marginRight: 1 }}
  252. onClick={async () => {
  253. manageChannel(record.id, 'enable', record);
  254. }}
  255. >
  256. 启用
  257. </Button>
  258. )}
  259. <Button
  260. theme='light'
  261. type='tertiary'
  262. style={{ marginRight: 1 }}
  263. onClick={() => {
  264. setEditingChannel(record);
  265. setShowEdit(true);
  266. }}
  267. >
  268. 编辑
  269. </Button>
  270. <Popconfirm
  271. title='确定是否要复制此渠道?'
  272. content='复制渠道的所有信息'
  273. okType={'danger'}
  274. position={'left'}
  275. onConfirm={async () => {
  276. copySelectedChannel(record.id);
  277. }}
  278. >
  279. <Button theme='light' type='primary' style={{ marginRight: 1 }}>
  280. 复制
  281. </Button>
  282. </Popconfirm>
  283. </div>
  284. ),
  285. },
  286. ];
  287. const [channels, setChannels] = useState([]);
  288. const [loading, setLoading] = useState(true);
  289. const [activePage, setActivePage] = useState(1);
  290. const [idSort, setIdSort] = useState(false);
  291. const [searchKeyword, setSearchKeyword] = useState('');
  292. const [searchGroup, setSearchGroup] = useState('');
  293. const [searchModel, setSearchModel] = useState('');
  294. const [searching, setSearching] = useState(false);
  295. const [updatingBalance, setUpdatingBalance] = useState(false);
  296. const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE);
  297. const [showPrompt, setShowPrompt] = useState(
  298. shouldShowPrompt('channel-test'),
  299. );
  300. const [channelCount, setChannelCount] = useState(pageSize);
  301. const [groupOptions, setGroupOptions] = useState([]);
  302. const [showEdit, setShowEdit] = useState(false);
  303. const [enableBatchDelete, setEnableBatchDelete] = useState(false);
  304. const [editingChannel, setEditingChannel] = useState({
  305. id: undefined,
  306. });
  307. const [selectedChannels, setSelectedChannels] = useState([]);
  308. const removeRecord = (id) => {
  309. let newDataSource = [...channels];
  310. if (id != null) {
  311. let idx = newDataSource.findIndex((data) => data.id === id);
  312. if (idx > -1) {
  313. newDataSource.splice(idx, 1);
  314. setChannels(newDataSource);
  315. }
  316. }
  317. };
  318. const setChannelFormat = (channels) => {
  319. for (let i = 0; i < channels.length; i++) {
  320. // if (channels[i].type === 8) {
  321. // showWarning(
  322. // '检测到您使用了“自定义渠道”类型,请更换为“OpenAI”渠道类型!',
  323. // );
  324. // showWarning('下个版本将不再支持“自定义渠道”类型!');
  325. // }
  326. channels[i].key = '' + channels[i].id;
  327. let test_models = [];
  328. channels[i].models.split(',').forEach((item, index) => {
  329. test_models.push({
  330. node: 'item',
  331. name: item,
  332. onClick: () => {
  333. testChannel(channels[i], item);
  334. },
  335. });
  336. });
  337. channels[i].test_models = test_models;
  338. }
  339. // data.key = '' + data.id
  340. setChannels(channels);
  341. if (channels.length >= pageSize) {
  342. setChannelCount(channels.length + pageSize);
  343. } else {
  344. setChannelCount(channels.length);
  345. }
  346. };
  347. const loadChannels = async (startIdx, pageSize, idSort) => {
  348. setLoading(true);
  349. const res = await API.get(
  350. `/api/channel/?p=${startIdx}&page_size=${pageSize}&id_sort=${idSort}`,
  351. );
  352. if (res === undefined) {
  353. return;
  354. }
  355. const { success, message, data } = res.data;
  356. if (success) {
  357. if (startIdx === 0) {
  358. setChannelFormat(data);
  359. } else {
  360. let newChannels = [...channels];
  361. newChannels.splice(startIdx * pageSize, data.length, ...data);
  362. setChannelFormat(newChannels);
  363. }
  364. } else {
  365. showError(message);
  366. }
  367. setLoading(false);
  368. };
  369. const copySelectedChannel = async (id) => {
  370. const channelToCopy = channels.find(
  371. (channel) => String(channel.id) === String(id),
  372. );
  373. console.log(channelToCopy);
  374. channelToCopy.name += '_复制';
  375. channelToCopy.created_time = null;
  376. channelToCopy.balance = 0;
  377. channelToCopy.used_quota = 0;
  378. if (!channelToCopy) {
  379. showError('渠道未找到,请刷新页面后重试。');
  380. return;
  381. }
  382. try {
  383. const newChannel = { ...channelToCopy, id: undefined };
  384. const response = await API.post('/api/channel/', newChannel);
  385. if (response.data.success) {
  386. showSuccess('渠道复制成功');
  387. await refresh();
  388. } else {
  389. showError(response.data.message);
  390. }
  391. } catch (error) {
  392. showError('渠道复制失败: ' + error.message);
  393. }
  394. };
  395. const refresh = async () => {
  396. await loadChannels(activePage - 1, pageSize, idSort);
  397. };
  398. useEffect(() => {
  399. // console.log('default effect')
  400. const localIdSort = localStorage.getItem('id-sort') === 'true';
  401. const localPageSize =
  402. parseInt(localStorage.getItem('page-size')) || ITEMS_PER_PAGE;
  403. setIdSort(localIdSort);
  404. setPageSize(localPageSize);
  405. loadChannels(0, localPageSize, localIdSort)
  406. .then()
  407. .catch((reason) => {
  408. showError(reason);
  409. });
  410. fetchGroups().then();
  411. loadChannelModels().then();
  412. }, []);
  413. const manageChannel = async (id, action, record, value) => {
  414. let data = { id };
  415. let res;
  416. switch (action) {
  417. case 'delete':
  418. res = await API.delete(`/api/channel/${id}/`);
  419. break;
  420. case 'enable':
  421. data.status = 1;
  422. res = await API.put('/api/channel/', data);
  423. break;
  424. case 'disable':
  425. data.status = 2;
  426. res = await API.put('/api/channel/', data);
  427. break;
  428. case 'priority':
  429. if (value === '') {
  430. return;
  431. }
  432. data.priority = parseInt(value);
  433. res = await API.put('/api/channel/', data);
  434. break;
  435. case 'weight':
  436. if (value === '') {
  437. return;
  438. }
  439. data.weight = parseInt(value);
  440. if (data.weight < 0) {
  441. data.weight = 0;
  442. }
  443. res = await API.put('/api/channel/', data);
  444. break;
  445. }
  446. const { success, message } = res.data;
  447. if (success) {
  448. showSuccess('操作成功完成!');
  449. let channel = res.data.data;
  450. let newChannels = [...channels];
  451. if (action === 'delete') {
  452. } else {
  453. record.status = channel.status;
  454. }
  455. setChannels(newChannels);
  456. } else {
  457. showError(message);
  458. }
  459. };
  460. const renderStatus = (status) => {
  461. switch (status) {
  462. case 1:
  463. return (
  464. <Tag size='large' color='green'>
  465. 已启用
  466. </Tag>
  467. );
  468. case 2:
  469. return (
  470. <Tag size='large' color='yellow'>
  471. 已禁用
  472. </Tag>
  473. );
  474. case 3:
  475. return (
  476. <Tag size='large' color='yellow'>
  477. 自动禁用
  478. </Tag>
  479. );
  480. default:
  481. return (
  482. <Tag size='large' color='grey'>
  483. 未知状态
  484. </Tag>
  485. );
  486. }
  487. };
  488. const renderResponseTime = (responseTime) => {
  489. let time = responseTime / 1000;
  490. time = time.toFixed(2) + ' 秒';
  491. if (responseTime === 0) {
  492. return (
  493. <Tag size='large' color='grey'>
  494. 未测试
  495. </Tag>
  496. );
  497. } else if (responseTime <= 1000) {
  498. return (
  499. <Tag size='large' color='green'>
  500. {time}
  501. </Tag>
  502. );
  503. } else if (responseTime <= 3000) {
  504. return (
  505. <Tag size='large' color='lime'>
  506. {time}
  507. </Tag>
  508. );
  509. } else if (responseTime <= 5000) {
  510. return (
  511. <Tag size='large' color='yellow'>
  512. {time}
  513. </Tag>
  514. );
  515. } else {
  516. return (
  517. <Tag size='large' color='red'>
  518. {time}
  519. </Tag>
  520. );
  521. }
  522. };
  523. const searchChannels = async (searchKeyword, searchGroup, searchModel) => {
  524. if (searchKeyword === '' && searchGroup === '' && searchModel === '') {
  525. // if keyword is blank, load files instead.
  526. await loadChannels(0, pageSize, idSort);
  527. setActivePage(1);
  528. return;
  529. }
  530. setSearching(true);
  531. const res = await API.get(
  532. `/api/channel/search?keyword=${searchKeyword}&group=${searchGroup}&model=${searchModel}`,
  533. );
  534. const { success, message, data } = res.data;
  535. if (success) {
  536. setChannelFormat(data);
  537. setActivePage(1);
  538. } else {
  539. showError(message);
  540. }
  541. setSearching(false);
  542. };
  543. const testChannel = async (record, model) => {
  544. const res = await API.get(`/api/channel/test/${record.id}?model=${model}`);
  545. const { success, message, time } = res.data;
  546. if (success) {
  547. record.response_time = time * 1000;
  548. record.test_time = Date.now() / 1000;
  549. showInfo(`通道 ${record.name} 测试成功,耗时 ${time.toFixed(2)} 秒。`);
  550. } else {
  551. showError(message);
  552. }
  553. };
  554. const testAllChannels = async () => {
  555. const res = await API.get(`/api/channel/test`);
  556. const { success, message } = res.data;
  557. if (success) {
  558. showInfo('已成功开始测试所有通道,请刷新页面查看结果。');
  559. } else {
  560. showError(message);
  561. }
  562. };
  563. const deleteAllDisabledChannels = async () => {
  564. const res = await API.delete(`/api/channel/disabled`);
  565. const { success, message, data } = res.data;
  566. if (success) {
  567. showSuccess(`已删除所有禁用渠道,共计 ${data} 个`);
  568. await refresh();
  569. } else {
  570. showError(message);
  571. }
  572. };
  573. const updateChannelBalance = async (record) => {
  574. const res = await API.get(`/api/channel/update_balance/${record.id}/`);
  575. const { success, message, balance } = res.data;
  576. if (success) {
  577. record.balance = balance;
  578. record.balance_updated_time = Date.now() / 1000;
  579. showInfo(`通道 ${record.name} 余额更新成功!`);
  580. } else {
  581. showError(message);
  582. }
  583. };
  584. const updateAllChannelsBalance = async () => {
  585. setUpdatingBalance(true);
  586. const res = await API.get(`/api/channel/update_balance`);
  587. const { success, message } = res.data;
  588. if (success) {
  589. showInfo('已更新完毕所有已启用通道余额!');
  590. } else {
  591. showError(message);
  592. }
  593. setUpdatingBalance(false);
  594. };
  595. const batchDeleteChannels = async () => {
  596. if (selectedChannels.length === 0) {
  597. showError('请先选择要删除的通道!');
  598. return;
  599. }
  600. setLoading(true);
  601. let ids = [];
  602. selectedChannels.forEach((channel) => {
  603. ids.push(channel.id);
  604. });
  605. const res = await API.post(`/api/channel/batch`, { ids: ids });
  606. const { success, message, data } = res.data;
  607. if (success) {
  608. showSuccess(`已删除 ${data} 个通道!`);
  609. await refresh();
  610. } else {
  611. showError(message);
  612. }
  613. setLoading(false);
  614. };
  615. const fixChannelsAbilities = async () => {
  616. const res = await API.post(`/api/channel/fix`);
  617. const { success, message, data } = res.data;
  618. if (success) {
  619. showSuccess(`已修复 ${data} 个通道!`);
  620. await refresh();
  621. } else {
  622. showError(message);
  623. }
  624. };
  625. let pageData = channels.slice(
  626. (activePage - 1) * pageSize,
  627. activePage * pageSize,
  628. );
  629. const handlePageChange = (page) => {
  630. setActivePage(page);
  631. if (page === Math.ceil(channels.length / pageSize) + 1) {
  632. // In this case we have to load more data and then append them.
  633. loadChannels(page - 1, pageSize, idSort).then((r) => {});
  634. }
  635. };
  636. const handlePageSizeChange = async (size) => {
  637. localStorage.setItem('page-size', size + '');
  638. setPageSize(size);
  639. setActivePage(1);
  640. loadChannels(0, size, idSort)
  641. .then()
  642. .catch((reason) => {
  643. showError(reason);
  644. });
  645. };
  646. const fetchGroups = async () => {
  647. try {
  648. let res = await API.get(`/api/group/`);
  649. // add 'all' option
  650. // res.data.data.unshift('all');
  651. if (res === undefined) {
  652. return;
  653. }
  654. setGroupOptions(
  655. res.data.data.map((group) => ({
  656. label: group,
  657. value: group,
  658. })),
  659. );
  660. } catch (error) {
  661. showError(error.message);
  662. }
  663. };
  664. const closeEdit = () => {
  665. setShowEdit(false);
  666. };
  667. const handleRow = (record, index) => {
  668. if (record.status !== 1) {
  669. return {
  670. style: {
  671. background: 'var(--semi-color-disabled-border)',
  672. },
  673. };
  674. } else {
  675. return {};
  676. }
  677. };
  678. return (
  679. <>
  680. <EditChannel
  681. refresh={refresh}
  682. visible={showEdit}
  683. handleClose={closeEdit}
  684. editingChannel={editingChannel}
  685. />
  686. <Form
  687. onSubmit={() => {
  688. searchChannels(searchKeyword, searchGroup, searchModel);
  689. }}
  690. labelPosition='left'
  691. >
  692. <div style={{ display: 'flex' }}>
  693. <Space>
  694. <Form.Input
  695. field='search_keyword'
  696. label='搜索渠道关键词'
  697. placeholder='ID,名称和密钥 ...'
  698. value={searchKeyword}
  699. loading={searching}
  700. onChange={(v) => {
  701. setSearchKeyword(v.trim());
  702. }}
  703. />
  704. <Form.Input
  705. field='search_model'
  706. label='模型'
  707. placeholder='模型关键字'
  708. value={searchModel}
  709. loading={searching}
  710. onChange={(v) => {
  711. setSearchModel(v.trim());
  712. }}
  713. />
  714. <Form.Select
  715. field='group'
  716. label='分组'
  717. optionList={[{ label: '选择分组', value: null}, ...groupOptions]}
  718. initValue={null}
  719. onChange={(v) => {
  720. setSearchGroup(v);
  721. searchChannels(searchKeyword, v, searchModel);
  722. }}
  723. />
  724. <Button
  725. label='查询'
  726. type='primary'
  727. htmlType='submit'
  728. className='btn-margin-right'
  729. style={{ marginRight: 8 }}
  730. >
  731. 查询
  732. </Button>
  733. </Space>
  734. </div>
  735. </Form>
  736. <div style={{ marginTop: 10, display: 'flex' }}>
  737. <Space>
  738. <Space>
  739. <Typography.Text strong>使用ID排序</Typography.Text>
  740. <Switch
  741. checked={idSort}
  742. label='使用ID排序'
  743. uncheckedText='关'
  744. aria-label='是否用ID排序'
  745. onChange={(v) => {
  746. localStorage.setItem('id-sort', v + '');
  747. setIdSort(v);
  748. loadChannels(0, pageSize, v)
  749. .then()
  750. .catch((reason) => {
  751. showError(reason);
  752. });
  753. }}
  754. ></Switch>
  755. </Space>
  756. </Space>
  757. </div>
  758. <Table
  759. className={'channel-table'}
  760. style={{ marginTop: 15 }}
  761. columns={columns}
  762. dataSource={pageData}
  763. pagination={{
  764. currentPage: activePage,
  765. pageSize: pageSize,
  766. total: channelCount,
  767. pageSizeOpts: [10, 20, 50, 100],
  768. showSizeChanger: true,
  769. formatPageText: (page) => '',
  770. onPageSizeChange: (size) => {
  771. handlePageSizeChange(size).then();
  772. },
  773. onPageChange: handlePageChange,
  774. }}
  775. loading={loading}
  776. onRow={handleRow}
  777. rowSelection={
  778. enableBatchDelete
  779. ? {
  780. onChange: (selectedRowKeys, selectedRows) => {
  781. // console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
  782. setSelectedChannels(selectedRows);
  783. },
  784. }
  785. : null
  786. }
  787. />
  788. <div
  789. style={{
  790. display: isMobile() ? '' : 'flex',
  791. marginTop: isMobile() ? 0 : -45,
  792. zIndex: 999,
  793. position: 'relative',
  794. pointerEvents: 'none',
  795. }}
  796. >
  797. <Space
  798. style={{ pointerEvents: 'auto', marginTop: isMobile() ? 0 : 45 }}
  799. >
  800. <Button
  801. theme='light'
  802. type='primary'
  803. style={{ marginRight: 8 }}
  804. onClick={() => {
  805. setEditingChannel({
  806. id: undefined,
  807. });
  808. setShowEdit(true);
  809. }}
  810. >
  811. 添加渠道
  812. </Button>
  813. <Popconfirm
  814. title='确定?'
  815. okType={'warning'}
  816. onConfirm={testAllChannels}
  817. position={isMobile() ? 'top' : 'top'}
  818. >
  819. <Button theme='light' type='warning' style={{ marginRight: 8 }}>
  820. 测试所有通道
  821. </Button>
  822. </Popconfirm>
  823. <Popconfirm
  824. title='确定?'
  825. okType={'secondary'}
  826. onConfirm={updateAllChannelsBalance}
  827. >
  828. <Button theme='light' type='secondary' style={{ marginRight: 8 }}>
  829. 更新所有已启用通道余额
  830. </Button>
  831. </Popconfirm>
  832. <Popconfirm
  833. title='确定是否要删除禁用通道?'
  834. content='此修改将不可逆'
  835. okType={'danger'}
  836. onConfirm={deleteAllDisabledChannels}
  837. >
  838. <Button theme='light' type='danger' style={{ marginRight: 8 }}>
  839. 删除禁用通道
  840. </Button>
  841. </Popconfirm>
  842. <Button
  843. theme='light'
  844. type='primary'
  845. style={{ marginRight: 8 }}
  846. onClick={refresh}
  847. >
  848. 刷新
  849. </Button>
  850. </Space>
  851. {/*<div style={{width: '100%', pointerEvents: 'none', position: 'absolute'}}>*/}
  852. {/*</div>*/}
  853. </div>
  854. <div style={{ marginTop: 20 }}>
  855. <Space>
  856. <Typography.Text strong>开启批量删除</Typography.Text>
  857. <Switch
  858. label='开启批量删除'
  859. uncheckedText='关'
  860. aria-label='是否开启批量删除'
  861. onChange={(v) => {
  862. setEnableBatchDelete(v);
  863. }}
  864. ></Switch>
  865. <Popconfirm
  866. title='确定是否要删除所选通道?'
  867. content='此修改将不可逆'
  868. okType={'danger'}
  869. onConfirm={batchDeleteChannels}
  870. disabled={!enableBatchDelete}
  871. position={'top'}
  872. >
  873. <Button
  874. disabled={!enableBatchDelete}
  875. theme='light'
  876. type='danger'
  877. style={{ marginRight: 8 }}
  878. >
  879. 删除所选通道
  880. </Button>
  881. </Popconfirm>
  882. <Popconfirm
  883. title='确定是否要修复数据库一致性?'
  884. content='进行该操作时,可能导致渠道访问错误,请仅在数据库出现问题时使用'
  885. okType={'warning'}
  886. onConfirm={fixChannelsAbilities}
  887. position={'top'}
  888. >
  889. <Button theme='light' type='secondary' style={{ marginRight: 8 }}>
  890. 修复数据库一致性
  891. </Button>
  892. </Popconfirm>
  893. </Space>
  894. </div>
  895. </>
  896. );
  897. };
  898. export default ChannelsTable;