ChannelsTable.js 28 KB

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