ChannelsTable.js 24 KB

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