ChannelsTable.js 26 KB

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