ChannelsTable.js 25 KB

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