ChannelsTable.js 34 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207
  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 [enableTagMode, setEnableTagMode] = useState(false);
  436. const removeRecord = (record) => {
  437. let newDataSource = [...channels];
  438. if (record.id != null) {
  439. let idx = newDataSource.findIndex((data) => {
  440. if (data.children !== undefined) {
  441. for (let i = 0; i < data.children.length; i++) {
  442. if (data.children[i].id === record.id) {
  443. data.children.splice(i, 1);
  444. return false;
  445. }
  446. }
  447. } else {
  448. return data.id === record.id
  449. }
  450. });
  451. if (idx > -1) {
  452. newDataSource.splice(idx, 1);
  453. setChannels(newDataSource);
  454. }
  455. }
  456. };
  457. const setChannelFormat = (channels, enableTagMode) => {
  458. let channelDates = [];
  459. let channelTags = {};
  460. for (let i = 0; i < channels.length; i++) {
  461. channels[i].key = '' + channels[i].id;
  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. if (!enableTagMode) {
  474. channelDates.push(channels[i]);
  475. } else {
  476. let tag = channels[i].tag?channels[i].tag:"";
  477. // find from channelTags
  478. let tagIndex = channelTags[tag];
  479. let tagChannelDates = undefined;
  480. if (tagIndex === undefined) {
  481. // not found, create a new tag
  482. channelTags[tag] = 1;
  483. tagChannelDates = {
  484. key: tag,
  485. id: tag,
  486. tag: tag,
  487. name: '标签:' + tag,
  488. group: '',
  489. used_quota: 0,
  490. response_time: 0,
  491. priority: -1,
  492. weight: -1,
  493. };
  494. tagChannelDates.children = [];
  495. channelDates.push(tagChannelDates);
  496. } else {
  497. // found, add to the tag
  498. tagChannelDates = channelDates.find((item) => item.key === tag);
  499. }
  500. if (tagChannelDates.priority === -1) {
  501. tagChannelDates.priority = channels[i].priority;
  502. } else {
  503. if (tagChannelDates.priority !== channels[i].priority) {
  504. tagChannelDates.priority = '';
  505. }
  506. }
  507. if (tagChannelDates.weight === -1) {
  508. tagChannelDates.weight = channels[i].weight;
  509. } else {
  510. if (tagChannelDates.weight !== channels[i].weight) {
  511. tagChannelDates.weight = '';
  512. }
  513. }
  514. if (tagChannelDates.group === '') {
  515. tagChannelDates.group = channels[i].group;
  516. } else {
  517. let channelGroupsStr = channels[i].group;
  518. channelGroupsStr.split(',').forEach((item, index) => {
  519. if (tagChannelDates.group.indexOf(item) === -1) {
  520. // join
  521. tagChannelDates.group += ',' + item;
  522. }
  523. });
  524. }
  525. tagChannelDates.children.push(channels[i]);
  526. if (channels[i].status === 1) {
  527. tagChannelDates.status = 1;
  528. }
  529. tagChannelDates.used_quota += channels[i].used_quota;
  530. tagChannelDates.response_time += channels[i].response_time;
  531. tagChannelDates.response_time = tagChannelDates.response_time / 2;
  532. }
  533. }
  534. // data.key = '' + data.id
  535. setChannels(channelDates);
  536. if (channelDates.length >= pageSize) {
  537. setChannelCount(channelDates.length + pageSize);
  538. } else {
  539. setChannelCount(channelDates.length);
  540. }
  541. };
  542. const loadChannels = async (startIdx, pageSize, idSort, enableTagMode) => {
  543. setLoading(true);
  544. const res = await API.get(
  545. `/api/channel/?p=${startIdx}&page_size=${pageSize}&id_sort=${idSort}&tag_mode=${enableTagMode}`
  546. );
  547. if (res === undefined) {
  548. return;
  549. }
  550. const { success, message, data } = res.data;
  551. if (success) {
  552. if (startIdx === 0) {
  553. setChannelFormat(data, enableTagMode);
  554. } else {
  555. let newChannels = [...channels];
  556. newChannels.splice(startIdx * pageSize, data.length, ...data);
  557. setChannelFormat(newChannels, enableTagMode);
  558. }
  559. } else {
  560. showError(message);
  561. }
  562. setLoading(false);
  563. };
  564. const copySelectedChannel = async (record) => {
  565. const channelToCopy = record
  566. channelToCopy.name += '_复制';
  567. channelToCopy.created_time = null;
  568. channelToCopy.balance = 0;
  569. channelToCopy.used_quota = 0;
  570. if (!channelToCopy) {
  571. showError('渠道未找到,请刷新页面后重试。');
  572. return;
  573. }
  574. try {
  575. const newChannel = { ...channelToCopy, id: undefined };
  576. const response = await API.post('/api/channel/', newChannel);
  577. if (response.data.success) {
  578. showSuccess('渠道复制成功');
  579. await refresh();
  580. } else {
  581. showError(response.data.message);
  582. }
  583. } catch (error) {
  584. showError('渠道复制失败: ' + error.message);
  585. }
  586. };
  587. const refresh = async () => {
  588. await loadChannels(activePage - 1, pageSize, idSort, enableTagMode);
  589. };
  590. useEffect(() => {
  591. // console.log('default effect')
  592. const localIdSort = localStorage.getItem('id-sort') === 'true';
  593. const localPageSize =
  594. parseInt(localStorage.getItem('page-size')) || ITEMS_PER_PAGE;
  595. setIdSort(localIdSort);
  596. setPageSize(localPageSize);
  597. loadChannels(0, localPageSize, localIdSort, enableTagMode)
  598. .then()
  599. .catch((reason) => {
  600. showError(reason);
  601. });
  602. fetchGroups().then();
  603. loadChannelModels().then();
  604. }, []);
  605. const manageChannel = async (id, action, record, value) => {
  606. let data = { id };
  607. let res;
  608. switch (action) {
  609. case 'delete':
  610. res = await API.delete(`/api/channel/${id}/`);
  611. break;
  612. case 'enable':
  613. data.status = 1;
  614. res = await API.put('/api/channel/', data);
  615. break;
  616. case 'disable':
  617. data.status = 2;
  618. res = await API.put('/api/channel/', data);
  619. break;
  620. case 'priority':
  621. if (value === '') {
  622. return;
  623. }
  624. data.priority = parseInt(value);
  625. res = await API.put('/api/channel/', data);
  626. break;
  627. case 'weight':
  628. if (value === '') {
  629. return;
  630. }
  631. data.weight = parseInt(value);
  632. if (data.weight < 0) {
  633. data.weight = 0;
  634. }
  635. res = await API.put('/api/channel/', data);
  636. break;
  637. }
  638. const { success, message } = res.data;
  639. if (success) {
  640. showSuccess('操作成功完成!');
  641. let channel = res.data.data;
  642. let newChannels = [...channels];
  643. if (action === 'delete') {
  644. } else {
  645. record.status = channel.status;
  646. }
  647. setChannels(newChannels);
  648. } else {
  649. showError(message);
  650. }
  651. };
  652. const manageTag = async (tag, action) => {
  653. console.log(tag, action);
  654. let res;
  655. switch (action) {
  656. case 'enable':
  657. res = await API.post('/api/channel/tag/enabled', {
  658. tag: tag
  659. });
  660. break;
  661. case 'disable':
  662. res = await API.post('/api/channel/tag/disabled', {
  663. tag: tag
  664. });
  665. break;
  666. }
  667. const { success, message } = res.data;
  668. if (success) {
  669. showSuccess('操作成功完成!');
  670. let newChannels = [...channels];
  671. for (let i = 0; i < newChannels.length; i++) {
  672. if (newChannels[i].tag === tag) {
  673. let status = action === 'enable' ? 1 : 2;
  674. newChannels[i]?.children?.forEach((channel) => {
  675. channel.status = status;
  676. });
  677. newChannels[i].status = status;
  678. }
  679. }
  680. setChannels(newChannels);
  681. } else {
  682. showError(message);
  683. }
  684. };
  685. const renderStatus = (status) => {
  686. switch (status) {
  687. case 1:
  688. return (
  689. <Tag size="large" color="green">
  690. 已启用
  691. </Tag>
  692. );
  693. case 2:
  694. return (
  695. <Tag size="large" color="yellow">
  696. 已禁用
  697. </Tag>
  698. );
  699. case 3:
  700. return (
  701. <Tag size="large" color="yellow">
  702. 自动禁用
  703. </Tag>
  704. );
  705. default:
  706. return (
  707. <Tag size="large" color="grey">
  708. 未知状态
  709. </Tag>
  710. );
  711. }
  712. };
  713. const renderResponseTime = (responseTime) => {
  714. let time = responseTime / 1000;
  715. time = time.toFixed(2) + ' 秒';
  716. if (responseTime === 0) {
  717. return (
  718. <Tag size="large" color="grey">
  719. 未测试
  720. </Tag>
  721. );
  722. } else if (responseTime <= 1000) {
  723. return (
  724. <Tag size="large" color="green">
  725. {time}
  726. </Tag>
  727. );
  728. } else if (responseTime <= 3000) {
  729. return (
  730. <Tag size="large" color="lime">
  731. {time}
  732. </Tag>
  733. );
  734. } else if (responseTime <= 5000) {
  735. return (
  736. <Tag size="large" color="yellow">
  737. {time}
  738. </Tag>
  739. );
  740. } else {
  741. return (
  742. <Tag size="large" color="red">
  743. {time}
  744. </Tag>
  745. );
  746. }
  747. };
  748. const searchChannels = async (searchKeyword, searchGroup, searchModel) => {
  749. if (searchKeyword === '' && searchGroup === '' && searchModel === '') {
  750. await loadChannels(0, pageSize, idSort, enableTagMode);
  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}&tag_mode=${enableTagMode}`
  757. );
  758. const { success, message, data } = res.data;
  759. if (success) {
  760. setChannelFormat(data, enableTagMode);
  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, enableTagMode).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, enableTagMode)
  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, enableTagMode)
  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. <div style={{ marginTop: 20 }}>
  1116. <Space>
  1117. <Typography.Text strong>标签聚合模式</Typography.Text>
  1118. <Switch
  1119. checked={enableTagMode}
  1120. label="标签聚合模式"
  1121. uncheckedText="关"
  1122. aria-label="是否启用标签聚合"
  1123. onChange={(v) => {
  1124. setEnableTagMode(v);
  1125. // 切换模式时重新加载数据
  1126. loadChannels(0, pageSize, idSort, v);
  1127. }}
  1128. />
  1129. </Space>
  1130. </div>
  1131. <Table
  1132. className={'channel-table'}
  1133. style={{ marginTop: 15 }}
  1134. columns={columns}
  1135. dataSource={pageData}
  1136. pagination={{
  1137. currentPage: activePage,
  1138. pageSize: pageSize,
  1139. total: channelCount,
  1140. pageSizeOpts: [10, 20, 50, 100],
  1141. showSizeChanger: true,
  1142. formatPageText: (page) => '',
  1143. onPageSizeChange: (size) => {
  1144. handlePageSizeChange(size).then();
  1145. },
  1146. onPageChange: handlePageChange
  1147. }}
  1148. loading={loading}
  1149. onRow={handleRow}
  1150. rowSelection={
  1151. enableBatchDelete
  1152. ? {
  1153. onChange: (selectedRowKeys, selectedRows) => {
  1154. // console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
  1155. setSelectedChannels(selectedRows);
  1156. }
  1157. }
  1158. : null
  1159. }
  1160. />
  1161. </>
  1162. );
  1163. };
  1164. export default ChannelsTable;