ChannelsTable.js 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212
  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. if (!enableTagMode) {
  463. let test_models = [];
  464. channels[i].models.split(',').forEach((item, index) => {
  465. test_models.push({
  466. node: 'item',
  467. name: item,
  468. onClick: () => {
  469. testChannel(channels[i], item);
  470. }
  471. });
  472. });
  473. channels[i].test_models = test_models;
  474. channelDates.push(channels[i]);
  475. } else {
  476. let 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. if (enableTagMode) {
  761. setChannelFormat(data, enableTagMode);
  762. } else {
  763. setChannels(data.map(channel => ({...channel, key: '' + channel.id})));
  764. setChannelCount(data.length);
  765. }
  766. setActivePage(1);
  767. } else {
  768. showError(message);
  769. }
  770. setSearching(false);
  771. };
  772. const testChannel = async (record, model) => {
  773. const res = await API.get(`/api/channel/test/${record.id}?model=${model}`);
  774. const { success, message, time } = res.data;
  775. if (success) {
  776. record.response_time = time * 1000;
  777. record.test_time = Date.now() / 1000;
  778. showInfo(`通道 ${record.name} 测试成功,耗时 ${time.toFixed(2)} 秒。`);
  779. } else {
  780. showError(message);
  781. }
  782. };
  783. const testAllChannels = async () => {
  784. const res = await API.get(`/api/channel/test`);
  785. const { success, message } = res.data;
  786. if (success) {
  787. showInfo('已成功开始测试所有通道,请刷新页面查看结果。');
  788. } else {
  789. showError(message);
  790. }
  791. };
  792. const deleteAllDisabledChannels = async () => {
  793. const res = await API.delete(`/api/channel/disabled`);
  794. const { success, message, data } = res.data;
  795. if (success) {
  796. showSuccess(`已删除所有禁用渠道,共计 ${data} 个`);
  797. await refresh();
  798. } else {
  799. showError(message);
  800. }
  801. };
  802. const updateChannelBalance = async (record) => {
  803. const res = await API.get(`/api/channel/update_balance/${record.id}/`);
  804. const { success, message, balance } = res.data;
  805. if (success) {
  806. record.balance = balance;
  807. record.balance_updated_time = Date.now() / 1000;
  808. showInfo(`通道 ${record.name} 余额更新成功!`);
  809. } else {
  810. showError(message);
  811. }
  812. };
  813. const updateAllChannelsBalance = async () => {
  814. setUpdatingBalance(true);
  815. const res = await API.get(`/api/channel/update_balance`);
  816. const { success, message } = res.data;
  817. if (success) {
  818. showInfo('已更新完毕所有已启用通道余额!');
  819. } else {
  820. showError(message);
  821. }
  822. setUpdatingBalance(false);
  823. };
  824. const batchDeleteChannels = async () => {
  825. if (selectedChannels.length === 0) {
  826. showError('请先选择要删除的通道!');
  827. return;
  828. }
  829. setLoading(true);
  830. let ids = [];
  831. selectedChannels.forEach((channel) => {
  832. ids.push(channel.id);
  833. });
  834. const res = await API.post(`/api/channel/batch`, { ids: ids });
  835. const { success, message, data } = res.data;
  836. if (success) {
  837. showSuccess(`已删除 ${data} 个通道!`);
  838. await refresh();
  839. } else {
  840. showError(message);
  841. }
  842. setLoading(false);
  843. };
  844. const fixChannelsAbilities = async () => {
  845. const res = await API.post(`/api/channel/fix`);
  846. const { success, message, data } = res.data;
  847. if (success) {
  848. showSuccess(`已修复 ${data} 个通道!`);
  849. await refresh();
  850. } else {
  851. showError(message);
  852. }
  853. };
  854. let pageData = channels.slice(
  855. (activePage - 1) * pageSize,
  856. activePage * pageSize
  857. );
  858. const handlePageChange = (page) => {
  859. setActivePage(page);
  860. if (page === Math.ceil(channels.length / pageSize) + 1) {
  861. // In this case we have to load more data and then append them.
  862. loadChannels(page - 1, pageSize, idSort, enableTagMode).then((r) => {
  863. });
  864. }
  865. };
  866. const handlePageSizeChange = async (size) => {
  867. localStorage.setItem('page-size', size + '');
  868. setPageSize(size);
  869. setActivePage(1);
  870. loadChannels(0, size, idSort, enableTagMode)
  871. .then()
  872. .catch((reason) => {
  873. showError(reason);
  874. });
  875. };
  876. const fetchGroups = async () => {
  877. try {
  878. let res = await API.get(`/api/group/`);
  879. // add 'all' option
  880. // res.data.data.unshift('all');
  881. if (res === undefined) {
  882. return;
  883. }
  884. setGroupOptions(
  885. res.data.data.map((group) => ({
  886. label: group,
  887. value: group
  888. }))
  889. );
  890. } catch (error) {
  891. showError(error.message);
  892. }
  893. };
  894. const submitTagEdit = async (type, data) => {
  895. switch (type) {
  896. case 'priority':
  897. if (data.priority === undefined || data.priority === '') {
  898. showInfo('优先级必须是整数!');
  899. return;
  900. }
  901. data.priority = parseInt(data.priority);
  902. break;
  903. case 'weight':
  904. if (data.weight === undefined || data.weight < 0 || data.weight === '') {
  905. showInfo('权重必须是非负整数!');
  906. return;
  907. }
  908. data.weight = parseInt(data.weight);
  909. break
  910. }
  911. try {
  912. const res = await API.put('/api/channel/tag', data);
  913. if (res?.data?.success) {
  914. showSuccess('更新成功!');
  915. await refresh();
  916. }
  917. } catch (error) {
  918. showError(error);
  919. }
  920. }
  921. const closeEdit = () => {
  922. setShowEdit(false);
  923. };
  924. const handleRow = (record, index) => {
  925. if (record.status !== 1) {
  926. return {
  927. style: {
  928. background: 'var(--semi-color-disabled-border)'
  929. }
  930. };
  931. } else {
  932. return {};
  933. }
  934. };
  935. return (
  936. <>
  937. <EditTagModal
  938. visible={showEditTag}
  939. tag={editingTag}
  940. handleClose={() => setShowEditTag(false)}
  941. refresh={refresh}
  942. />
  943. <EditChannel
  944. refresh={refresh}
  945. visible={showEdit}
  946. handleClose={closeEdit}
  947. editingChannel={editingChannel}
  948. />
  949. <Form
  950. onSubmit={() => {
  951. searchChannels(searchKeyword, searchGroup, searchModel);
  952. }}
  953. labelPosition="left"
  954. >
  955. <div style={{ display: 'flex' }}>
  956. <Space>
  957. <Form.Input
  958. field="search_keyword"
  959. label="搜索渠道关键词"
  960. placeholder="ID,名称和密钥 ..."
  961. value={searchKeyword}
  962. loading={searching}
  963. onChange={(v) => {
  964. setSearchKeyword(v.trim());
  965. }}
  966. />
  967. <Form.Input
  968. field="search_model"
  969. label="模型"
  970. placeholder="模型关键字"
  971. value={searchModel}
  972. loading={searching}
  973. onChange={(v) => {
  974. setSearchModel(v.trim());
  975. }}
  976. />
  977. <Form.Select
  978. field="group"
  979. label="分组"
  980. optionList={[{ label: '选择分组', value: null }, ...groupOptions]}
  981. initValue={null}
  982. onChange={(v) => {
  983. setSearchGroup(v);
  984. searchChannels(searchKeyword, v, searchModel);
  985. }}
  986. />
  987. <Button
  988. label="查询"
  989. type="primary"
  990. htmlType="submit"
  991. className="btn-margin-right"
  992. style={{ marginRight: 8 }}
  993. >
  994. 查询
  995. </Button>
  996. </Space>
  997. </div>
  998. </Form>
  999. <Divider style={{ marginBottom: 15 }} />
  1000. <div
  1001. style={{
  1002. display: isMobile() ? '' : 'flex',
  1003. marginTop: isMobile() ? 0 : -45,
  1004. zIndex: 999,
  1005. pointerEvents: 'none'
  1006. }}
  1007. >
  1008. <Space
  1009. style={{ pointerEvents: 'auto', marginTop: isMobile() ? 0 : 45 }}
  1010. >
  1011. <Typography.Text strong>使用ID排序</Typography.Text>
  1012. <Switch
  1013. checked={idSort}
  1014. label="使用ID排序"
  1015. uncheckedText="关"
  1016. aria-label="是否用ID排序"
  1017. onChange={(v) => {
  1018. localStorage.setItem('id-sort', v + '');
  1019. setIdSort(v);
  1020. loadChannels(0, pageSize, v, enableTagMode)
  1021. .then()
  1022. .catch((reason) => {
  1023. showError(reason);
  1024. });
  1025. }}
  1026. ></Switch>
  1027. <Button
  1028. theme="light"
  1029. type="primary"
  1030. style={{ marginRight: 8 }}
  1031. onClick={() => {
  1032. setEditingChannel({
  1033. id: undefined
  1034. });
  1035. setShowEdit(true);
  1036. }}
  1037. >
  1038. 添加渠道
  1039. </Button>
  1040. <Popconfirm
  1041. title="确定?"
  1042. okType={'warning'}
  1043. onConfirm={testAllChannels}
  1044. position={isMobile() ? 'top' : 'top'}
  1045. >
  1046. <Button theme="light" type="warning" style={{ marginRight: 8 }}>
  1047. 测试所有通道
  1048. </Button>
  1049. </Popconfirm>
  1050. <Popconfirm
  1051. title="确定?"
  1052. okType={'secondary'}
  1053. onConfirm={updateAllChannelsBalance}
  1054. >
  1055. <Button theme="light" type="secondary" style={{ marginRight: 8 }}>
  1056. 更新所有已启用通道余额
  1057. </Button>
  1058. </Popconfirm>
  1059. <Popconfirm
  1060. title="确定是否要删除禁用通道?"
  1061. content="此修改将不可逆"
  1062. okType={'danger'}
  1063. onConfirm={deleteAllDisabledChannels}
  1064. >
  1065. <Button theme="light" type="danger" style={{ marginRight: 8 }}>
  1066. 删除禁用通道
  1067. </Button>
  1068. </Popconfirm>
  1069. <Button
  1070. theme="light"
  1071. type="primary"
  1072. style={{ marginRight: 8 }}
  1073. onClick={refresh}
  1074. >
  1075. 刷新
  1076. </Button>
  1077. </Space>
  1078. </div>
  1079. <div style={{ marginTop: 20 }}>
  1080. <Space>
  1081. <Typography.Text strong>开启批量删除</Typography.Text>
  1082. <Switch
  1083. label="开启批量删除"
  1084. uncheckedText="关"
  1085. aria-label="是否开启批量删除"
  1086. onChange={(v) => {
  1087. setEnableBatchDelete(v);
  1088. }}
  1089. ></Switch>
  1090. <Popconfirm
  1091. title="确定是否要删除所选通道?"
  1092. content="此修改将不可逆"
  1093. okType={'danger'}
  1094. onConfirm={batchDeleteChannels}
  1095. disabled={!enableBatchDelete}
  1096. position={'top'}
  1097. >
  1098. <Button
  1099. disabled={!enableBatchDelete}
  1100. theme="light"
  1101. type="danger"
  1102. style={{ marginRight: 8 }}
  1103. >
  1104. 删除所选通道
  1105. </Button>
  1106. </Popconfirm>
  1107. <Popconfirm
  1108. title="确定是否要修复数据库一致性?"
  1109. content="进行该操作时,可能导致渠道访问错误,请仅在数据库出现问题时使用"
  1110. okType={'warning'}
  1111. onConfirm={fixChannelsAbilities}
  1112. position={'top'}
  1113. >
  1114. <Button theme="light" type="secondary" style={{ marginRight: 8 }}>
  1115. 修复数据库一致性
  1116. </Button>
  1117. </Popconfirm>
  1118. </Space>
  1119. </div>
  1120. <div style={{ marginTop: 20 }}>
  1121. <Space>
  1122. <Typography.Text strong>标签聚合模式</Typography.Text>
  1123. <Switch
  1124. checked={enableTagMode}
  1125. label="标签聚合模式"
  1126. uncheckedText="关"
  1127. aria-label="是否启用标签聚合"
  1128. onChange={(v) => {
  1129. setEnableTagMode(v);
  1130. // 切换模式时重新加载数据
  1131. loadChannels(0, pageSize, idSort, v);
  1132. }}
  1133. />
  1134. </Space>
  1135. </div>
  1136. <Table
  1137. className={'channel-table'}
  1138. style={{ marginTop: 15 }}
  1139. columns={columns}
  1140. dataSource={pageData}
  1141. pagination={{
  1142. currentPage: activePage,
  1143. pageSize: pageSize,
  1144. total: channelCount,
  1145. pageSizeOpts: [10, 20, 50, 100],
  1146. showSizeChanger: true,
  1147. formatPageText: (page) => '',
  1148. onPageSizeChange: (size) => {
  1149. handlePageSizeChange(size).then();
  1150. },
  1151. onPageChange: handlePageChange
  1152. }}
  1153. loading={loading}
  1154. onRow={handleRow}
  1155. rowSelection={
  1156. enableBatchDelete
  1157. ? {
  1158. onChange: (selectedRowKeys, selectedRows) => {
  1159. // console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
  1160. setSelectedChannels(selectedRows);
  1161. }
  1162. }
  1163. : null
  1164. }
  1165. />
  1166. </>
  1167. );
  1168. };
  1169. export default ChannelsTable;