ChannelsTable.js 23 KB

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