ChannelsTable.js 24 KB

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