ChannelsTable.js 33 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192
  1. import React, { useEffect, useState } from 'react';
  2. import {
  3. API,
  4. isMobile,
  5. shouldShowPrompt,
  6. showError,
  7. showInfo,
  8. showSuccess,
  9. showWarning,
  10. timestamp2string
  11. } from '../helpers';
  12. import { CHANNEL_OPTIONS, ITEMS_PER_PAGE } from '../constants';
  13. import {
  14. getQuotaPerUnit,
  15. renderGroup,
  16. renderNumberWithPoint,
  17. renderQuota, renderQuotaWithPrompt
  18. } from '../helpers/render';
  19. import {
  20. Button, Divider,
  21. Dropdown,
  22. Form, Input,
  23. InputNumber, Modal,
  24. Popconfirm,
  25. Space,
  26. SplitButtonGroup,
  27. Switch,
  28. Table,
  29. Tag,
  30. Tooltip,
  31. Typography
  32. } from '@douyinfe/semi-ui';
  33. import EditChannel from '../pages/Channel/EditChannel';
  34. import { IconList, IconTreeTriangleDown } from '@douyinfe/semi-icons';
  35. import { loadChannelModels } from './utils.js';
  36. import EditTagModal from '../pages/Channel/EditTagModal.js';
  37. import TextNumberInput from './custom/TextNumberInput.js';
  38. function renderTimestamp(timestamp) {
  39. return <>{timestamp2string(timestamp)}</>;
  40. }
  41. let type2label = undefined;
  42. function renderType(type) {
  43. if (!type2label) {
  44. type2label = new Map();
  45. for (let i = 0; i < CHANNEL_OPTIONS.length; i++) {
  46. type2label[CHANNEL_OPTIONS[i].value] = CHANNEL_OPTIONS[i];
  47. }
  48. type2label[0] = { value: 0, text: '未知类型', color: 'grey' };
  49. }
  50. return (
  51. <Tag size="large" color={type2label[type]?.color}>
  52. {type2label[type]?.text}
  53. </Tag>
  54. );
  55. }
  56. function renderTagType(type) {
  57. return (
  58. <Tag
  59. color='light-blue'
  60. prefixIcon={<IconList />}
  61. size='large'
  62. shape='circle'
  63. type='light'
  64. >
  65. 标签聚合
  66. </Tag>
  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. {text?.split(',').map((item, index) => {
  92. return renderGroup(item);
  93. })}
  94. </Space>
  95. </div>
  96. );
  97. }
  98. },
  99. {
  100. title: '类型',
  101. dataIndex: 'type',
  102. render: (text, record, index) => {
  103. if (record.children === undefined) {
  104. return <>{renderType(text)}</>;
  105. } else {
  106. return <>{renderTagType(0)}</>;
  107. }
  108. }
  109. },
  110. {
  111. title: '状态',
  112. dataIndex: 'status',
  113. render: (text, record, index) => {
  114. if (text === 3) {
  115. if (record.other_info === '') {
  116. record.other_info = '{}';
  117. }
  118. let otherInfo = JSON.parse(record.other_info);
  119. let reason = otherInfo['status_reason'];
  120. let time = otherInfo['status_time'];
  121. return (
  122. <div>
  123. <Tooltip content={'原因:' + reason + ',时间:' + timestamp2string(time)}>
  124. {renderStatus(text)}
  125. </Tooltip>
  126. </div>
  127. );
  128. } else {
  129. return renderStatus(text);
  130. }
  131. }
  132. },
  133. {
  134. title: '响应时间',
  135. dataIndex: 'response_time',
  136. render: (text, record, index) => {
  137. return <div>{renderResponseTime(text)}</div>;
  138. }
  139. },
  140. {
  141. title: '已用/剩余',
  142. dataIndex: 'expired_time',
  143. render: (text, record, index) => {
  144. if (record.children === undefined) {
  145. return (
  146. <div>
  147. <Space spacing={1}>
  148. <Tooltip content={'已用额度'}>
  149. <Tag color="white" type="ghost" size="large">
  150. {renderQuota(record.used_quota)}
  151. </Tag>
  152. </Tooltip>
  153. <Tooltip content={'剩余额度' + record.balance + ',点击更新'}>
  154. <Tag
  155. color="white"
  156. type="ghost"
  157. size="large"
  158. onClick={() => {
  159. updateChannelBalance(record);
  160. }}
  161. >
  162. ${renderNumberWithPoint(record.balance)}
  163. </Tag>
  164. </Tooltip>
  165. </Space>
  166. </div>
  167. );
  168. } else {
  169. return <Tooltip content={'已用额度'}>
  170. <Tag color="white" type="ghost" size="large">
  171. {renderQuota(record.used_quota)}
  172. </Tag>
  173. </Tooltip>;
  174. }
  175. }
  176. },
  177. {
  178. title: '优先级',
  179. dataIndex: 'priority',
  180. render: (text, record, index) => {
  181. if (record.children === undefined) {
  182. return (
  183. <div>
  184. <InputNumber
  185. style={{ width: 70 }}
  186. name="priority"
  187. onBlur={(e) => {
  188. manageChannel(record.id, 'priority', record, e.target.value);
  189. }}
  190. keepFocus={true}
  191. innerButtons
  192. defaultValue={record.priority}
  193. min={-999}
  194. />
  195. </div>
  196. );
  197. } else {
  198. return <>
  199. <InputNumber
  200. style={{ width: 70 }}
  201. name="priority"
  202. keepFocus={true}
  203. onBlur={(e) => {
  204. Modal.warning({
  205. title: '修改子渠道优先级',
  206. content: '确定要修改所有子渠道优先级为 ' + e.target.value + ' 吗?',
  207. onOk: () => {
  208. if (e.target.value === '') {
  209. return;
  210. }
  211. submitTagEdit('priority', {
  212. tag: record.key,
  213. priority: e.target.value
  214. })
  215. },
  216. })
  217. }}
  218. innerButtons
  219. defaultValue={record.priority}
  220. min={-999}
  221. />
  222. </>;
  223. }
  224. }
  225. },
  226. {
  227. title: '权重',
  228. dataIndex: 'weight',
  229. render: (text, record, index) => {
  230. if (record.children === undefined) {
  231. return (
  232. <div>
  233. <InputNumber
  234. style={{ width: 70 }}
  235. name="weight"
  236. onBlur={(e) => {
  237. manageChannel(record.id, 'weight', record, e.target.value);
  238. }}
  239. keepFocus={true}
  240. innerButtons
  241. defaultValue={record.weight}
  242. min={0}
  243. />
  244. </div>
  245. );
  246. } else {
  247. return (
  248. <InputNumber
  249. style={{ width: 70 }}
  250. name="weight"
  251. keepFocus={true}
  252. onBlur={(e) => {
  253. Modal.warning({
  254. title: '修改子渠道权重',
  255. content: '确定要修改所有子渠道权重为 ' + e.target.value + ' 吗?',
  256. onOk: () => {
  257. if (e.target.value === '') {
  258. return;
  259. }
  260. submitTagEdit('weight', {
  261. tag: record.key,
  262. weight: e.target.value
  263. })
  264. },
  265. })
  266. }}
  267. innerButtons
  268. defaultValue={record.weight}
  269. min={-999}
  270. />
  271. );
  272. }
  273. }
  274. },
  275. {
  276. title: '',
  277. dataIndex: 'operate',
  278. render: (text, record, index) => {
  279. if (record.children === undefined) {
  280. return (
  281. <div>
  282. <SplitButtonGroup
  283. style={{ marginRight: 1 }}
  284. aria-label="测试单个渠道操作项目组"
  285. >
  286. <Button
  287. theme="light"
  288. onClick={() => {
  289. testChannel(record, '');
  290. }}
  291. >
  292. 测试
  293. </Button>
  294. <Dropdown
  295. trigger="click"
  296. position="bottomRight"
  297. menu={record.test_models}
  298. >
  299. <Button
  300. style={{ padding: '8px 4px' }}
  301. type="primary"
  302. icon={<IconTreeTriangleDown />}
  303. ></Button>
  304. </Dropdown>
  305. </SplitButtonGroup>
  306. {/*<Button theme='light' type='primary' style={{marginRight: 1}} onClick={()=>testChannel(record)}>测试</Button>*/}
  307. <Popconfirm
  308. title="确定是否要删除此渠道?"
  309. content="此修改将不可逆"
  310. okType={'danger'}
  311. position={'left'}
  312. onConfirm={() => {
  313. manageChannel(record.id, 'delete', record).then(() => {
  314. removeRecord(record);
  315. });
  316. }}
  317. >
  318. <Button theme="light" type="danger" style={{ marginRight: 1 }}>
  319. 删除
  320. </Button>
  321. </Popconfirm>
  322. {record.status === 1 ? (
  323. <Button
  324. theme="light"
  325. type="warning"
  326. style={{ marginRight: 1 }}
  327. onClick={async () => {
  328. manageChannel(record.id, 'disable', record);
  329. }}
  330. >
  331. 禁用
  332. </Button>
  333. ) : (
  334. <Button
  335. theme="light"
  336. type="secondary"
  337. style={{ marginRight: 1 }}
  338. onClick={async () => {
  339. manageChannel(record.id, 'enable', record);
  340. }}
  341. >
  342. 启用
  343. </Button>
  344. )}
  345. <Button
  346. theme="light"
  347. type="tertiary"
  348. style={{ marginRight: 1 }}
  349. onClick={() => {
  350. setEditingChannel(record);
  351. setShowEdit(true);
  352. }}
  353. >
  354. 编辑
  355. </Button>
  356. <Popconfirm
  357. title="确定是否要复制此渠道?"
  358. content="复制渠道的所有信息"
  359. okType={'danger'}
  360. position={'left'}
  361. onConfirm={async () => {
  362. copySelectedChannel(record);
  363. }}
  364. >
  365. <Button theme="light" type="primary" style={{ marginRight: 1 }}>
  366. 复制
  367. </Button>
  368. </Popconfirm>
  369. </div>
  370. );
  371. } else {
  372. return (
  373. <>
  374. <Button
  375. theme="light"
  376. type="secondary"
  377. style={{ marginRight: 1 }}
  378. onClick={async () => {
  379. manageTag(record.key, 'enable');
  380. }}
  381. >
  382. 启用全部
  383. </Button>
  384. <Button
  385. theme="light"
  386. type="warning"
  387. style={{ marginRight: 1 }}
  388. onClick={async () => {
  389. manageTag(record.key, 'disable');
  390. }}
  391. >
  392. 禁用全部
  393. </Button>
  394. <Button
  395. theme="light"
  396. type="tertiary"
  397. style={{ marginRight: 1 }}
  398. onClick={() => {
  399. setShowEditTag(true);
  400. setEditingTag(record.key);
  401. }}
  402. >
  403. 编辑
  404. </Button>
  405. </>
  406. );
  407. }
  408. }
  409. }
  410. ];
  411. const [channels, setChannels] = useState([]);
  412. const [loading, setLoading] = useState(true);
  413. const [activePage, setActivePage] = useState(1);
  414. const [idSort, setIdSort] = useState(false);
  415. const [searchKeyword, setSearchKeyword] = useState('');
  416. const [searchGroup, setSearchGroup] = useState('');
  417. const [searchModel, setSearchModel] = useState('');
  418. const [searching, setSearching] = useState(false);
  419. const [updatingBalance, setUpdatingBalance] = useState(false);
  420. const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE);
  421. const [showPrompt, setShowPrompt] = useState(
  422. shouldShowPrompt('channel-test')
  423. );
  424. const [channelCount, setChannelCount] = useState(pageSize);
  425. const [groupOptions, setGroupOptions] = useState([]);
  426. const [showEdit, setShowEdit] = useState(false);
  427. const [enableBatchDelete, setEnableBatchDelete] = useState(false);
  428. const [editingChannel, setEditingChannel] = useState({
  429. id: undefined
  430. });
  431. const [showEditTag, setShowEditTag] = useState(false);
  432. const [editingTag, setEditingTag] = useState('');
  433. const [selectedChannels, setSelectedChannels] = useState([]);
  434. const [showEditPriority, setShowEditPriority] = useState(false);
  435. const removeRecord = (record) => {
  436. let newDataSource = [...channels];
  437. if (record.id != null) {
  438. let idx = newDataSource.findIndex((data) => {
  439. if (data.children !== undefined) {
  440. for (let i = 0; i < data.children.length; i++) {
  441. if (data.children[i].id === record.id) {
  442. data.children.splice(i, 1);
  443. return false;
  444. }
  445. }
  446. } else {
  447. return data.id === record.id
  448. }
  449. });
  450. if (idx > -1) {
  451. newDataSource.splice(idx, 1);
  452. setChannels(newDataSource);
  453. }
  454. }
  455. };
  456. const setChannelFormat = (channels) => {
  457. let channelDates = [];
  458. let channelTags = {};
  459. for (let i = 0; i < channels.length; i++) {
  460. channels[i].key = '' + channels[i].id;
  461. if (channels[i].tag === '' || channels[i].tag === null) {
  462. let test_models = [];
  463. channels[i].models.split(',').forEach((item, index) => {
  464. test_models.push({
  465. node: 'item',
  466. name: item,
  467. onClick: () => {
  468. testChannel(channels[i], item);
  469. }
  470. });
  471. });
  472. channels[i].test_models = test_models;
  473. channelDates.push(channels[i]);
  474. } else {
  475. let tag = channels[i].tag;
  476. // find from channelTags
  477. let tagIndex = channelTags[tag];
  478. let tagChannelDates = undefined;
  479. if (tagIndex === undefined) {
  480. // not found, create a new tag
  481. channelTags[tag] = 1;
  482. tagChannelDates = {
  483. key: tag,
  484. id: tag,
  485. tag: tag,
  486. name: '标签:' + tag,
  487. group: '',
  488. used_quota: 0,
  489. response_time: 0,
  490. priority: -1,
  491. weight: -1,
  492. };
  493. tagChannelDates.children = [];
  494. channelDates.push(tagChannelDates);
  495. } else {
  496. // found, add to the tag
  497. tagChannelDates = channelDates.find((item) => item.key === tag);
  498. }
  499. if (tagChannelDates.priority === -1) {
  500. tagChannelDates.priority = channels[i].priority;
  501. } else {
  502. if (tagChannelDates.priority !== channels[i].priority) {
  503. tagChannelDates.priority = '';
  504. }
  505. }
  506. if (tagChannelDates.weight === -1) {
  507. tagChannelDates.weight = channels[i].weight;
  508. } else {
  509. if (tagChannelDates.weight !== channels[i].weight) {
  510. tagChannelDates.weight = '';
  511. }
  512. }
  513. if (tagChannelDates.group === '') {
  514. tagChannelDates.group = channels[i].group;
  515. } else {
  516. let channelGroupsStr = channels[i].group;
  517. channelGroupsStr.split(',').forEach((item, index) => {
  518. if (tagChannelDates.group.indexOf(item) === -1) {
  519. // join
  520. tagChannelDates.group += ',' + item;
  521. }
  522. });
  523. }
  524. tagChannelDates.children.push(channels[i]);
  525. if (channels[i].status === 1) {
  526. tagChannelDates.status = 1;
  527. }
  528. tagChannelDates.used_quota += channels[i].used_quota;
  529. tagChannelDates.response_time += channels[i].response_time;
  530. tagChannelDates.response_time = tagChannelDates.response_time / 2;
  531. }
  532. }
  533. // data.key = '' + data.id
  534. setChannels(channelDates);
  535. if (channelDates.length >= pageSize) {
  536. setChannelCount(channelDates.length + pageSize);
  537. } else {
  538. setChannelCount(channelDates.length);
  539. }
  540. };
  541. const loadChannels = async (startIdx, pageSize, idSort) => {
  542. setLoading(true);
  543. const res = await API.get(
  544. `/api/channel/?p=${startIdx}&page_size=${pageSize}&id_sort=${idSort}`
  545. );
  546. if (res === undefined) {
  547. return;
  548. }
  549. const { success, message, data } = res.data;
  550. if (success) {
  551. if (startIdx === 0) {
  552. setChannelFormat(data);
  553. } else {
  554. let newChannels = [...channels];
  555. newChannels.splice(startIdx * pageSize, data.length, ...data);
  556. setChannelFormat(newChannels);
  557. }
  558. } else {
  559. showError(message);
  560. }
  561. setLoading(false);
  562. };
  563. const copySelectedChannel = async (record) => {
  564. const channelToCopy = record
  565. channelToCopy.name += '_复制';
  566. channelToCopy.created_time = null;
  567. channelToCopy.balance = 0;
  568. channelToCopy.used_quota = 0;
  569. if (!channelToCopy) {
  570. showError('渠道未找到,请刷新页面后重试。');
  571. return;
  572. }
  573. try {
  574. const newChannel = { ...channelToCopy, id: undefined };
  575. const response = await API.post('/api/channel/', newChannel);
  576. if (response.data.success) {
  577. showSuccess('渠道复制成功');
  578. await refresh();
  579. } else {
  580. showError(response.data.message);
  581. }
  582. } catch (error) {
  583. showError('渠道复制失败: ' + error.message);
  584. }
  585. };
  586. const refresh = async () => {
  587. await loadChannels(activePage - 1, pageSize, idSort);
  588. };
  589. useEffect(() => {
  590. // console.log('default effect')
  591. const localIdSort = localStorage.getItem('id-sort') === 'true';
  592. const localPageSize =
  593. parseInt(localStorage.getItem('page-size')) || ITEMS_PER_PAGE;
  594. setIdSort(localIdSort);
  595. setPageSize(localPageSize);
  596. loadChannels(0, localPageSize, localIdSort)
  597. .then()
  598. .catch((reason) => {
  599. showError(reason);
  600. });
  601. fetchGroups().then();
  602. loadChannelModels().then();
  603. }, []);
  604. const manageChannel = async (id, action, record, value) => {
  605. let data = { id };
  606. let res;
  607. switch (action) {
  608. case 'delete':
  609. res = await API.delete(`/api/channel/${id}/`);
  610. break;
  611. case 'enable':
  612. data.status = 1;
  613. res = await API.put('/api/channel/', data);
  614. break;
  615. case 'disable':
  616. data.status = 2;
  617. res = await API.put('/api/channel/', data);
  618. break;
  619. case 'priority':
  620. if (value === '') {
  621. return;
  622. }
  623. data.priority = parseInt(value);
  624. res = await API.put('/api/channel/', data);
  625. break;
  626. case 'weight':
  627. if (value === '') {
  628. return;
  629. }
  630. data.weight = parseInt(value);
  631. if (data.weight < 0) {
  632. data.weight = 0;
  633. }
  634. res = await API.put('/api/channel/', data);
  635. break;
  636. }
  637. const { success, message } = res.data;
  638. if (success) {
  639. showSuccess('操作成功完成!');
  640. let channel = res.data.data;
  641. let newChannels = [...channels];
  642. if (action === 'delete') {
  643. } else {
  644. record.status = channel.status;
  645. }
  646. setChannels(newChannels);
  647. } else {
  648. showError(message);
  649. }
  650. };
  651. const manageTag = async (tag, action) => {
  652. console.log(tag, action);
  653. let res;
  654. switch (action) {
  655. case 'enable':
  656. res = await API.post('/api/channel/tag/enabled', {
  657. tag: tag
  658. });
  659. break;
  660. case 'disable':
  661. res = await API.post('/api/channel/tag/disabled', {
  662. tag: tag
  663. });
  664. break;
  665. }
  666. const { success, message } = res.data;
  667. if (success) {
  668. showSuccess('操作成功完成!');
  669. let newChannels = [...channels];
  670. for (let i = 0; i < newChannels.length; i++) {
  671. if (newChannels[i].tag === tag) {
  672. let status = action === 'enable' ? 1 : 2;
  673. newChannels[i]?.children?.forEach((channel) => {
  674. channel.status = status;
  675. });
  676. newChannels[i].status = status;
  677. }
  678. }
  679. setChannels(newChannels);
  680. } else {
  681. showError(message);
  682. }
  683. };
  684. const renderStatus = (status) => {
  685. switch (status) {
  686. case 1:
  687. return (
  688. <Tag size="large" color="green">
  689. 已启用
  690. </Tag>
  691. );
  692. case 2:
  693. return (
  694. <Tag size="large" color="yellow">
  695. 已禁用
  696. </Tag>
  697. );
  698. case 3:
  699. return (
  700. <Tag size="large" color="yellow">
  701. 自动禁用
  702. </Tag>
  703. );
  704. default:
  705. return (
  706. <Tag size="large" color="grey">
  707. 未知状态
  708. </Tag>
  709. );
  710. }
  711. };
  712. const renderResponseTime = (responseTime) => {
  713. let time = responseTime / 1000;
  714. time = time.toFixed(2) + ' 秒';
  715. if (responseTime === 0) {
  716. return (
  717. <Tag size="large" color="grey">
  718. 未测试
  719. </Tag>
  720. );
  721. } else if (responseTime <= 1000) {
  722. return (
  723. <Tag size="large" color="green">
  724. {time}
  725. </Tag>
  726. );
  727. } else if (responseTime <= 3000) {
  728. return (
  729. <Tag size="large" color="lime">
  730. {time}
  731. </Tag>
  732. );
  733. } else if (responseTime <= 5000) {
  734. return (
  735. <Tag size="large" color="yellow">
  736. {time}
  737. </Tag>
  738. );
  739. } else {
  740. return (
  741. <Tag size="large" color="red">
  742. {time}
  743. </Tag>
  744. );
  745. }
  746. };
  747. const searchChannels = async (searchKeyword, searchGroup, searchModel) => {
  748. if (searchKeyword === '' && searchGroup === '' && searchModel === '') {
  749. // if keyword is blank, load files instead.
  750. await loadChannels(0, pageSize, idSort);
  751. setActivePage(1);
  752. return;
  753. }
  754. setSearching(true);
  755. const res = await API.get(
  756. `/api/channel/search?keyword=${searchKeyword}&group=${searchGroup}&model=${searchModel}&id_sort=${idSort}`
  757. );
  758. const { success, message, data } = res.data;
  759. if (success) {
  760. setChannelFormat(data);
  761. setActivePage(1);
  762. } else {
  763. showError(message);
  764. }
  765. setSearching(false);
  766. };
  767. const testChannel = async (record, model) => {
  768. const res = await API.get(`/api/channel/test/${record.id}?model=${model}`);
  769. const { success, message, time } = res.data;
  770. if (success) {
  771. record.response_time = time * 1000;
  772. record.test_time = Date.now() / 1000;
  773. showInfo(`通道 ${record.name} 测试成功,耗时 ${time.toFixed(2)} 秒。`);
  774. } else {
  775. showError(message);
  776. }
  777. };
  778. const testAllChannels = async () => {
  779. const res = await API.get(`/api/channel/test`);
  780. const { success, message } = res.data;
  781. if (success) {
  782. showInfo('已成功开始测试所有通道,请刷新页面查看结果。');
  783. } else {
  784. showError(message);
  785. }
  786. };
  787. const deleteAllDisabledChannels = async () => {
  788. const res = await API.delete(`/api/channel/disabled`);
  789. const { success, message, data } = res.data;
  790. if (success) {
  791. showSuccess(`已删除所有禁用渠道,共计 ${data} 个`);
  792. await refresh();
  793. } else {
  794. showError(message);
  795. }
  796. };
  797. const updateChannelBalance = async (record) => {
  798. const res = await API.get(`/api/channel/update_balance/${record.id}/`);
  799. const { success, message, balance } = res.data;
  800. if (success) {
  801. record.balance = balance;
  802. record.balance_updated_time = Date.now() / 1000;
  803. showInfo(`通道 ${record.name} 余额更新成功!`);
  804. } else {
  805. showError(message);
  806. }
  807. };
  808. const updateAllChannelsBalance = async () => {
  809. setUpdatingBalance(true);
  810. const res = await API.get(`/api/channel/update_balance`);
  811. const { success, message } = res.data;
  812. if (success) {
  813. showInfo('已更新完毕所有已启用通道余额!');
  814. } else {
  815. showError(message);
  816. }
  817. setUpdatingBalance(false);
  818. };
  819. const batchDeleteChannels = async () => {
  820. if (selectedChannels.length === 0) {
  821. showError('请先选择要删除的通道!');
  822. return;
  823. }
  824. setLoading(true);
  825. let ids = [];
  826. selectedChannels.forEach((channel) => {
  827. ids.push(channel.id);
  828. });
  829. const res = await API.post(`/api/channel/batch`, { ids: ids });
  830. const { success, message, data } = res.data;
  831. if (success) {
  832. showSuccess(`已删除 ${data} 个通道!`);
  833. await refresh();
  834. } else {
  835. showError(message);
  836. }
  837. setLoading(false);
  838. };
  839. const fixChannelsAbilities = async () => {
  840. const res = await API.post(`/api/channel/fix`);
  841. const { success, message, data } = res.data;
  842. if (success) {
  843. showSuccess(`已修复 ${data} 个通道!`);
  844. await refresh();
  845. } else {
  846. showError(message);
  847. }
  848. };
  849. let pageData = channels.slice(
  850. (activePage - 1) * pageSize,
  851. activePage * pageSize
  852. );
  853. const handlePageChange = (page) => {
  854. setActivePage(page);
  855. if (page === Math.ceil(channels.length / pageSize) + 1) {
  856. // In this case we have to load more data and then append them.
  857. loadChannels(page - 1, pageSize, idSort).then((r) => {
  858. });
  859. }
  860. };
  861. const handlePageSizeChange = async (size) => {
  862. localStorage.setItem('page-size', size + '');
  863. setPageSize(size);
  864. setActivePage(1);
  865. loadChannels(0, size, idSort)
  866. .then()
  867. .catch((reason) => {
  868. showError(reason);
  869. });
  870. };
  871. const fetchGroups = async () => {
  872. try {
  873. let res = await API.get(`/api/group/`);
  874. // add 'all' option
  875. // res.data.data.unshift('all');
  876. if (res === undefined) {
  877. return;
  878. }
  879. setGroupOptions(
  880. res.data.data.map((group) => ({
  881. label: group,
  882. value: group
  883. }))
  884. );
  885. } catch (error) {
  886. showError(error.message);
  887. }
  888. };
  889. const submitTagEdit = async (type, data) => {
  890. switch (type) {
  891. case 'priority':
  892. if (data.priority === undefined || data.priority === '') {
  893. showInfo('优先级必须是整数!');
  894. return;
  895. }
  896. data.priority = parseInt(data.priority);
  897. break;
  898. case 'weight':
  899. if (data.weight === undefined || data.weight < 0 || data.weight === '') {
  900. showInfo('权重必须是非负整数!');
  901. return;
  902. }
  903. data.weight = parseInt(data.weight);
  904. break
  905. }
  906. try {
  907. const res = await API.put('/api/channel/tag', data);
  908. if (res?.data?.success) {
  909. showSuccess('更新成功!');
  910. await refresh();
  911. }
  912. } catch (error) {
  913. showError(error);
  914. }
  915. }
  916. const closeEdit = () => {
  917. setShowEdit(false);
  918. };
  919. const handleRow = (record, index) => {
  920. if (record.status !== 1) {
  921. return {
  922. style: {
  923. background: 'var(--semi-color-disabled-border)'
  924. }
  925. };
  926. } else {
  927. return {};
  928. }
  929. };
  930. return (
  931. <>
  932. <EditTagModal
  933. visible={showEditTag}
  934. tag={editingTag}
  935. handleClose={() => setShowEditTag(false)}
  936. refresh={refresh}
  937. />
  938. <EditChannel
  939. refresh={refresh}
  940. visible={showEdit}
  941. handleClose={closeEdit}
  942. editingChannel={editingChannel}
  943. />
  944. <Form
  945. onSubmit={() => {
  946. searchChannels(searchKeyword, searchGroup, searchModel);
  947. }}
  948. labelPosition="left"
  949. >
  950. <div style={{ display: 'flex' }}>
  951. <Space>
  952. <Form.Input
  953. field="search_keyword"
  954. label="搜索渠道关键词"
  955. placeholder="ID,名称和密钥 ..."
  956. value={searchKeyword}
  957. loading={searching}
  958. onChange={(v) => {
  959. setSearchKeyword(v.trim());
  960. }}
  961. />
  962. <Form.Input
  963. field="search_model"
  964. label="模型"
  965. placeholder="模型关键字"
  966. value={searchModel}
  967. loading={searching}
  968. onChange={(v) => {
  969. setSearchModel(v.trim());
  970. }}
  971. />
  972. <Form.Select
  973. field="group"
  974. label="分组"
  975. optionList={[{ label: '选择分组', value: null }, ...groupOptions]}
  976. initValue={null}
  977. onChange={(v) => {
  978. setSearchGroup(v);
  979. searchChannels(searchKeyword, v, searchModel);
  980. }}
  981. />
  982. <Button
  983. label="查询"
  984. type="primary"
  985. htmlType="submit"
  986. className="btn-margin-right"
  987. style={{ marginRight: 8 }}
  988. >
  989. 查询
  990. </Button>
  991. </Space>
  992. </div>
  993. </Form>
  994. <Divider style={{ marginBottom: 15 }} />
  995. <div
  996. style={{
  997. display: isMobile() ? '' : 'flex',
  998. marginTop: isMobile() ? 0 : -45,
  999. zIndex: 999,
  1000. pointerEvents: 'none'
  1001. }}
  1002. >
  1003. <Space
  1004. style={{ pointerEvents: 'auto', marginTop: isMobile() ? 0 : 45 }}
  1005. >
  1006. <Typography.Text strong>使用ID排序</Typography.Text>
  1007. <Switch
  1008. checked={idSort}
  1009. label="使用ID排序"
  1010. uncheckedText="关"
  1011. aria-label="是否用ID排序"
  1012. onChange={(v) => {
  1013. localStorage.setItem('id-sort', v + '');
  1014. setIdSort(v);
  1015. loadChannels(0, pageSize, v)
  1016. .then()
  1017. .catch((reason) => {
  1018. showError(reason);
  1019. });
  1020. }}
  1021. ></Switch>
  1022. <Button
  1023. theme="light"
  1024. type="primary"
  1025. style={{ marginRight: 8 }}
  1026. onClick={() => {
  1027. setEditingChannel({
  1028. id: undefined
  1029. });
  1030. setShowEdit(true);
  1031. }}
  1032. >
  1033. 添加渠道
  1034. </Button>
  1035. <Popconfirm
  1036. title="确定?"
  1037. okType={'warning'}
  1038. onConfirm={testAllChannels}
  1039. position={isMobile() ? 'top' : 'top'}
  1040. >
  1041. <Button theme="light" type="warning" style={{ marginRight: 8 }}>
  1042. 测试所有通道
  1043. </Button>
  1044. </Popconfirm>
  1045. <Popconfirm
  1046. title="确定?"
  1047. okType={'secondary'}
  1048. onConfirm={updateAllChannelsBalance}
  1049. >
  1050. <Button theme="light" type="secondary" style={{ marginRight: 8 }}>
  1051. 更新所有已启用通道余额
  1052. </Button>
  1053. </Popconfirm>
  1054. <Popconfirm
  1055. title="确定是否要删除禁用通道?"
  1056. content="此修改将不可逆"
  1057. okType={'danger'}
  1058. onConfirm={deleteAllDisabledChannels}
  1059. >
  1060. <Button theme="light" type="danger" style={{ marginRight: 8 }}>
  1061. 删除禁用通道
  1062. </Button>
  1063. </Popconfirm>
  1064. <Button
  1065. theme="light"
  1066. type="primary"
  1067. style={{ marginRight: 8 }}
  1068. onClick={refresh}
  1069. >
  1070. 刷新
  1071. </Button>
  1072. </Space>
  1073. </div>
  1074. <div style={{ marginTop: 20 }}>
  1075. <Space>
  1076. <Typography.Text strong>开启批量删除</Typography.Text>
  1077. <Switch
  1078. label="开启批量删除"
  1079. uncheckedText="关"
  1080. aria-label="是否开启批量删除"
  1081. onChange={(v) => {
  1082. setEnableBatchDelete(v);
  1083. }}
  1084. ></Switch>
  1085. <Popconfirm
  1086. title="确定是否要删除所选通道?"
  1087. content="此修改将不可逆"
  1088. okType={'danger'}
  1089. onConfirm={batchDeleteChannels}
  1090. disabled={!enableBatchDelete}
  1091. position={'top'}
  1092. >
  1093. <Button
  1094. disabled={!enableBatchDelete}
  1095. theme="light"
  1096. type="danger"
  1097. style={{ marginRight: 8 }}
  1098. >
  1099. 删除所选通道
  1100. </Button>
  1101. </Popconfirm>
  1102. <Popconfirm
  1103. title="确定是否要修复数据库一致性?"
  1104. content="进行该操作时,可能导致渠道访问错误,请仅在数据库出现问题时使用"
  1105. okType={'warning'}
  1106. onConfirm={fixChannelsAbilities}
  1107. position={'top'}
  1108. >
  1109. <Button theme="light" type="secondary" style={{ marginRight: 8 }}>
  1110. 修复数据库一致性
  1111. </Button>
  1112. </Popconfirm>
  1113. </Space>
  1114. </div>
  1115. <Table
  1116. className={'channel-table'}
  1117. style={{ marginTop: 15 }}
  1118. columns={columns}
  1119. dataSource={pageData}
  1120. pagination={{
  1121. currentPage: activePage,
  1122. pageSize: pageSize,
  1123. total: channelCount,
  1124. pageSizeOpts: [10, 20, 50, 100],
  1125. showSizeChanger: true,
  1126. formatPageText: (page) => '',
  1127. onPageSizeChange: (size) => {
  1128. handlePageSizeChange(size).then();
  1129. },
  1130. onPageChange: handlePageChange
  1131. }}
  1132. loading={loading}
  1133. onRow={handleRow}
  1134. rowSelection={
  1135. enableBatchDelete
  1136. ? {
  1137. onChange: (selectedRowKeys, selectedRows) => {
  1138. // console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
  1139. setSelectedChannels(selectedRows);
  1140. }
  1141. }
  1142. : null
  1143. }
  1144. />
  1145. </>
  1146. );
  1147. };
  1148. export default ChannelsTable;