ChannelsColumnDefs.jsx 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649
  1. /*
  2. Copyright (C) 2025 QuantumNous
  3. This program is free software: you can redistribute it and/or modify
  4. it under the terms of the GNU Affero General Public License as
  5. published by the Free Software Foundation, either version 3 of the
  6. License, or (at your option) any later version.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. GNU Affero General Public License for more details.
  11. You should have received a copy of the GNU Affero General Public License
  12. along with this program. If not, see <https://www.gnu.org/licenses/>.
  13. For commercial licensing, please contact support@quantumnous.com
  14. */
  15. import React from 'react';
  16. import {
  17. Button,
  18. Dropdown,
  19. InputNumber,
  20. Modal,
  21. Space,
  22. SplitButtonGroup,
  23. Tag,
  24. Tooltip,
  25. Typography,
  26. } from '@douyinfe/semi-ui';
  27. import {
  28. timestamp2string,
  29. renderGroup,
  30. renderQuota,
  31. getChannelIcon,
  32. renderQuotaWithAmount,
  33. showSuccess,
  34. showError,
  35. } from '../../../helpers';
  36. import { CHANNEL_OPTIONS } from '../../../constants';
  37. import { IconTreeTriangleDown, IconMore } from '@douyinfe/semi-icons';
  38. import { FaRandom } from 'react-icons/fa';
  39. // Render functions
  40. const renderType = (type, channelInfo = undefined, t) => {
  41. let type2label = new Map();
  42. for (let i = 0; i < CHANNEL_OPTIONS.length; i++) {
  43. type2label[CHANNEL_OPTIONS[i].value] = CHANNEL_OPTIONS[i];
  44. }
  45. type2label[0] = { value: 0, label: t('未知类型'), color: 'grey' };
  46. let icon = getChannelIcon(type);
  47. if (channelInfo?.is_multi_key) {
  48. icon =
  49. channelInfo?.multi_key_mode === 'random' ? (
  50. <div className='flex items-center gap-1'>
  51. <FaRandom className='text-blue-500' />
  52. {icon}
  53. </div>
  54. ) : (
  55. <div className='flex items-center gap-1'>
  56. <IconTreeTriangleDown className='text-blue-500' />
  57. {icon}
  58. </div>
  59. );
  60. }
  61. return (
  62. <Tag color={type2label[type]?.color} shape='circle' prefixIcon={icon}>
  63. {type2label[type]?.label}
  64. </Tag>
  65. );
  66. };
  67. const renderTagType = (t) => {
  68. return (
  69. <Tag color='light-blue' shape='circle' type='light'>
  70. {t('标签聚合')}
  71. </Tag>
  72. );
  73. };
  74. const renderStatus = (status, channelInfo = undefined, t) => {
  75. if (channelInfo) {
  76. if (channelInfo.is_multi_key) {
  77. let keySize = channelInfo.multi_key_size;
  78. let enabledKeySize = keySize;
  79. if (channelInfo.multi_key_status_list) {
  80. enabledKeySize =
  81. keySize - Object.keys(channelInfo.multi_key_status_list).length;
  82. }
  83. return renderMultiKeyStatus(status, keySize, enabledKeySize, t);
  84. }
  85. }
  86. switch (status) {
  87. case 1:
  88. return (
  89. <Tag color='green' shape='circle'>
  90. {t('已启用')}
  91. </Tag>
  92. );
  93. case 2:
  94. return (
  95. <Tag color='red' shape='circle'>
  96. {t('已禁用')}
  97. </Tag>
  98. );
  99. case 3:
  100. return (
  101. <Tag color='yellow' shape='circle'>
  102. {t('自动禁用')}
  103. </Tag>
  104. );
  105. default:
  106. return (
  107. <Tag color='grey' shape='circle'>
  108. {t('未知状态')}
  109. </Tag>
  110. );
  111. }
  112. };
  113. const renderMultiKeyStatus = (status, keySize, enabledKeySize, t) => {
  114. switch (status) {
  115. case 1:
  116. return (
  117. <Tag color='green' shape='circle'>
  118. {t('已启用')} {enabledKeySize}/{keySize}
  119. </Tag>
  120. );
  121. case 2:
  122. return (
  123. <Tag color='red' shape='circle'>
  124. {t('已禁用')} {enabledKeySize}/{keySize}
  125. </Tag>
  126. );
  127. case 3:
  128. return (
  129. <Tag color='yellow' shape='circle'>
  130. {t('自动禁用')} {enabledKeySize}/{keySize}
  131. </Tag>
  132. );
  133. default:
  134. return (
  135. <Tag color='grey' shape='circle'>
  136. {t('未知状态')} {enabledKeySize}/{keySize}
  137. </Tag>
  138. );
  139. }
  140. };
  141. const renderResponseTime = (responseTime, t) => {
  142. let time = responseTime / 1000;
  143. time = time.toFixed(2) + t(' 秒');
  144. if (responseTime === 0) {
  145. return (
  146. <Tag color='grey' shape='circle'>
  147. {t('未测试')}
  148. </Tag>
  149. );
  150. } else if (responseTime <= 1000) {
  151. return (
  152. <Tag color='green' shape='circle'>
  153. {time}
  154. </Tag>
  155. );
  156. } else if (responseTime <= 3000) {
  157. return (
  158. <Tag color='lime' shape='circle'>
  159. {time}
  160. </Tag>
  161. );
  162. } else if (responseTime <= 5000) {
  163. return (
  164. <Tag color='yellow' shape='circle'>
  165. {time}
  166. </Tag>
  167. );
  168. } else {
  169. return (
  170. <Tag color='red' shape='circle'>
  171. {time}
  172. </Tag>
  173. );
  174. }
  175. };
  176. export const getChannelsColumns = ({
  177. t,
  178. COLUMN_KEYS,
  179. updateChannelBalance,
  180. manageChannel,
  181. manageTag,
  182. submitTagEdit,
  183. testChannel,
  184. setCurrentTestChannel,
  185. setShowModelTestModal,
  186. setEditingChannel,
  187. setShowEdit,
  188. setShowEditTag,
  189. setEditingTag,
  190. copySelectedChannel,
  191. refresh,
  192. activePage,
  193. channels,
  194. setShowMultiKeyManageModal,
  195. setCurrentMultiKeyChannel,
  196. }) => {
  197. return [
  198. {
  199. key: COLUMN_KEYS.ID,
  200. title: t('ID'),
  201. dataIndex: 'id',
  202. },
  203. {
  204. key: COLUMN_KEYS.NAME,
  205. title: t('名称'),
  206. dataIndex: 'name',
  207. render: (text, record, index) => {
  208. if (record.remark && record.remark.trim() !== '') {
  209. return (
  210. <Tooltip
  211. content={
  212. <div className='flex flex-col gap-2 max-w-xs'>
  213. <div className='text-sm'>{record.remark}</div>
  214. <Button
  215. size='small'
  216. type='primary'
  217. theme='outline'
  218. onClick={(e) => {
  219. e.stopPropagation();
  220. navigator.clipboard.writeText(record.remark).then(() => {
  221. showSuccess(t('复制成功'));
  222. }).catch(() => {
  223. showError(t('复制失败'));
  224. });
  225. }}
  226. >
  227. {t('复制')}
  228. </Button>
  229. </div>
  230. }
  231. trigger='hover'
  232. position='topLeft'
  233. >
  234. <span>{text}</span>
  235. </Tooltip>
  236. );
  237. }
  238. return text;
  239. },
  240. },
  241. {
  242. key: COLUMN_KEYS.GROUP,
  243. title: t('分组'),
  244. dataIndex: 'group',
  245. render: (text, record, index) => (
  246. <div>
  247. <Space spacing={2}>
  248. {text
  249. ?.split(',')
  250. .sort((a, b) => {
  251. if (a === 'default') return -1;
  252. if (b === 'default') return 1;
  253. return a.localeCompare(b);
  254. })
  255. .map((item, index) => renderGroup(item))}
  256. </Space>
  257. </div>
  258. ),
  259. },
  260. {
  261. key: COLUMN_KEYS.TYPE,
  262. title: t('类型'),
  263. dataIndex: 'type',
  264. render: (text, record, index) => {
  265. if (record.children === undefined) {
  266. if (record.channel_info) {
  267. if (record.channel_info.is_multi_key) {
  268. return <>{renderType(text, record.channel_info, t)}</>;
  269. }
  270. }
  271. return <>{renderType(text, undefined, t)}</>;
  272. } else {
  273. return <>{renderTagType(t)}</>;
  274. }
  275. },
  276. },
  277. {
  278. key: COLUMN_KEYS.STATUS,
  279. title: t('状态'),
  280. dataIndex: 'status',
  281. render: (text, record, index) => {
  282. if (text === 3) {
  283. if (record.other_info === '') {
  284. record.other_info = '{}';
  285. }
  286. let otherInfo = JSON.parse(record.other_info);
  287. let reason = otherInfo['status_reason'];
  288. let time = otherInfo['status_time'];
  289. return (
  290. <div>
  291. <Tooltip
  292. content={
  293. t('原因:') + reason + t(',时间:') + timestamp2string(time)
  294. }
  295. >
  296. {renderStatus(text, record.channel_info, t)}
  297. </Tooltip>
  298. </div>
  299. );
  300. } else {
  301. return renderStatus(text, record.channel_info, t);
  302. }
  303. },
  304. },
  305. {
  306. key: COLUMN_KEYS.RESPONSE_TIME,
  307. title: t('响应时间'),
  308. dataIndex: 'response_time',
  309. render: (text, record, index) => <div>{renderResponseTime(text, t)}</div>,
  310. },
  311. {
  312. key: COLUMN_KEYS.BALANCE,
  313. title: t('已用/剩余'),
  314. dataIndex: 'expired_time',
  315. render: (text, record, index) => {
  316. if (record.children === undefined) {
  317. return (
  318. <div>
  319. <Space spacing={1}>
  320. <Tooltip content={t('已用额度')}>
  321. <Tag color='white' type='ghost' shape='circle'>
  322. {renderQuota(record.used_quota)}
  323. </Tag>
  324. </Tooltip>
  325. <Tooltip
  326. content={t('剩余额度$') + record.balance + t(',点击更新')}
  327. >
  328. <Tag
  329. color='white'
  330. type='ghost'
  331. shape='circle'
  332. onClick={() => updateChannelBalance(record)}
  333. >
  334. {renderQuotaWithAmount(record.balance)}
  335. </Tag>
  336. </Tooltip>
  337. </Space>
  338. </div>
  339. );
  340. } else {
  341. return (
  342. <Tooltip content={t('已用额度')}>
  343. <Tag color='white' type='ghost' shape='circle'>
  344. {renderQuota(record.used_quota)}
  345. </Tag>
  346. </Tooltip>
  347. );
  348. }
  349. },
  350. },
  351. {
  352. key: COLUMN_KEYS.PRIORITY,
  353. title: t('优先级'),
  354. dataIndex: 'priority',
  355. render: (text, record, index) => {
  356. if (record.children === undefined) {
  357. return (
  358. <div>
  359. <InputNumber
  360. style={{ width: 70 }}
  361. name='priority'
  362. onBlur={(e) => {
  363. manageChannel(record.id, 'priority', record, e.target.value);
  364. }}
  365. keepFocus={true}
  366. innerButtons
  367. defaultValue={record.priority}
  368. min={-999}
  369. size='small'
  370. />
  371. </div>
  372. );
  373. } else {
  374. return (
  375. <InputNumber
  376. style={{ width: 70 }}
  377. name='priority'
  378. keepFocus={true}
  379. onBlur={(e) => {
  380. Modal.warning({
  381. title: t('修改子渠道优先级'),
  382. content:
  383. t('确定要修改所有子渠道优先级为 ') +
  384. e.target.value +
  385. t(' 吗?'),
  386. onOk: () => {
  387. if (e.target.value === '') {
  388. return;
  389. }
  390. submitTagEdit('priority', {
  391. tag: record.key,
  392. priority: e.target.value,
  393. });
  394. },
  395. });
  396. }}
  397. innerButtons
  398. defaultValue={record.priority}
  399. min={-999}
  400. size='small'
  401. />
  402. );
  403. }
  404. },
  405. },
  406. {
  407. key: COLUMN_KEYS.WEIGHT,
  408. title: t('权重'),
  409. dataIndex: 'weight',
  410. render: (text, record, index) => {
  411. if (record.children === undefined) {
  412. return (
  413. <div>
  414. <InputNumber
  415. style={{ width: 70 }}
  416. name='weight'
  417. onBlur={(e) => {
  418. manageChannel(record.id, 'weight', record, e.target.value);
  419. }}
  420. keepFocus={true}
  421. innerButtons
  422. defaultValue={record.weight}
  423. min={0}
  424. size='small'
  425. />
  426. </div>
  427. );
  428. } else {
  429. return (
  430. <InputNumber
  431. style={{ width: 70 }}
  432. name='weight'
  433. keepFocus={true}
  434. onBlur={(e) => {
  435. Modal.warning({
  436. title: t('修改子渠道权重'),
  437. content:
  438. t('确定要修改所有子渠道权重为 ') +
  439. e.target.value +
  440. t(' 吗?'),
  441. onOk: () => {
  442. if (e.target.value === '') {
  443. return;
  444. }
  445. submitTagEdit('weight', {
  446. tag: record.key,
  447. weight: e.target.value,
  448. });
  449. },
  450. });
  451. }}
  452. innerButtons
  453. defaultValue={record.weight}
  454. min={-999}
  455. size='small'
  456. />
  457. );
  458. }
  459. },
  460. },
  461. {
  462. key: COLUMN_KEYS.OPERATE,
  463. title: '',
  464. dataIndex: 'operate',
  465. fixed: 'right',
  466. render: (text, record, index) => {
  467. if (record.children === undefined) {
  468. const moreMenuItems = [
  469. {
  470. node: 'item',
  471. name: t('删除'),
  472. type: 'danger',
  473. onClick: () => {
  474. Modal.confirm({
  475. title: t('确定是否要删除此渠道?'),
  476. content: t('此修改将不可逆'),
  477. onOk: () => {
  478. (async () => {
  479. await manageChannel(record.id, 'delete', record);
  480. await refresh();
  481. setTimeout(() => {
  482. if (channels.length === 0 && activePage > 1) {
  483. refresh(activePage - 1);
  484. }
  485. }, 100);
  486. })();
  487. },
  488. });
  489. },
  490. },
  491. {
  492. node: 'item',
  493. name: t('复制'),
  494. type: 'tertiary',
  495. onClick: () => {
  496. Modal.confirm({
  497. title: t('确定是否要复制此渠道?'),
  498. content: t('复制渠道的所有信息'),
  499. onOk: () => copySelectedChannel(record),
  500. });
  501. },
  502. },
  503. ];
  504. return (
  505. <Space wrap>
  506. <SplitButtonGroup
  507. className='overflow-hidden'
  508. aria-label={t('测试单个渠道操作项目组')}
  509. >
  510. <Button
  511. size='small'
  512. type='tertiary'
  513. onClick={() => testChannel(record, '')}
  514. >
  515. {t('测试')}
  516. </Button>
  517. <Button
  518. size='small'
  519. type='tertiary'
  520. icon={<IconTreeTriangleDown />}
  521. onClick={() => {
  522. setCurrentTestChannel(record);
  523. setShowModelTestModal(true);
  524. }}
  525. />
  526. </SplitButtonGroup>
  527. {record.status === 1 ? (
  528. <Button
  529. type='danger'
  530. size='small'
  531. onClick={() => manageChannel(record.id, 'disable', record)}
  532. >
  533. {t('禁用')}
  534. </Button>
  535. ) : (
  536. <Button
  537. size='small'
  538. onClick={() => manageChannel(record.id, 'enable', record)}
  539. >
  540. {t('启用')}
  541. </Button>
  542. )}
  543. {record.channel_info?.is_multi_key ? (
  544. <SplitButtonGroup aria-label={t('多密钥渠道操作项目组')}>
  545. <Button
  546. type='tertiary'
  547. size='small'
  548. onClick={() => {
  549. setEditingChannel(record);
  550. setShowEdit(true);
  551. }}
  552. >
  553. {t('编辑')}
  554. </Button>
  555. <Dropdown
  556. trigger='click'
  557. position='bottomRight'
  558. menu={[
  559. {
  560. node: 'item',
  561. name: t('多密钥管理'),
  562. onClick: () => {
  563. setCurrentMultiKeyChannel(record);
  564. setShowMultiKeyManageModal(true);
  565. },
  566. },
  567. ]}
  568. >
  569. <Button
  570. type='tertiary'
  571. size='small'
  572. icon={<IconTreeTriangleDown />}
  573. />
  574. </Dropdown>
  575. </SplitButtonGroup>
  576. ) : (
  577. <Button
  578. type='tertiary'
  579. size='small'
  580. onClick={() => {
  581. setEditingChannel(record);
  582. setShowEdit(true);
  583. }}
  584. >
  585. {t('编辑')}
  586. </Button>
  587. )}
  588. <Dropdown
  589. trigger='click'
  590. position='bottomRight'
  591. menu={moreMenuItems}
  592. >
  593. <Button icon={<IconMore />} type='tertiary' size='small' />
  594. </Dropdown>
  595. </Space>
  596. );
  597. } else {
  598. // 标签操作按钮
  599. return (
  600. <Space wrap>
  601. <Button
  602. type='tertiary'
  603. size='small'
  604. onClick={() => manageTag(record.key, 'enable')}
  605. >
  606. {t('启用全部')}
  607. </Button>
  608. <Button
  609. type='tertiary'
  610. size='small'
  611. onClick={() => manageTag(record.key, 'disable')}
  612. >
  613. {t('禁用全部')}
  614. </Button>
  615. <Button
  616. type='tertiary'
  617. size='small'
  618. onClick={() => {
  619. setShowEditTag(true);
  620. setEditingTag(record.key);
  621. }}
  622. >
  623. {t('编辑')}
  624. </Button>
  625. </Space>
  626. );
  627. }
  628. },
  629. },
  630. ];
  631. };