ChannelsTable.js 52 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746
  1. import React, { useEffect, useState } from 'react';
  2. import {
  3. API,
  4. showError,
  5. showInfo,
  6. showSuccess,
  7. timestamp2string,
  8. renderGroup,
  9. renderNumberWithPoint,
  10. renderQuota,
  11. getChannelIcon
  12. } from '../../helpers/index.js';
  13. import {
  14. CheckCircle,
  15. XCircle,
  16. AlertCircle,
  17. HelpCircle,
  18. TestTube,
  19. Zap,
  20. Timer,
  21. Clock,
  22. AlertTriangle,
  23. Coins,
  24. Tags
  25. } from 'lucide-react';
  26. import { CHANNEL_OPTIONS, ITEMS_PER_PAGE } from '../../constants/index.js';
  27. import {
  28. Button,
  29. Divider,
  30. Dropdown,
  31. Empty,
  32. Input,
  33. InputNumber,
  34. Modal,
  35. Space,
  36. SplitButtonGroup,
  37. Switch,
  38. Table,
  39. Tag,
  40. Tooltip,
  41. Typography,
  42. Card,
  43. Form
  44. } from '@douyinfe/semi-ui';
  45. import {
  46. IllustrationNoResult,
  47. IllustrationNoResultDark
  48. } from '@douyinfe/semi-illustrations';
  49. import EditChannel from '../../pages/Channel/EditChannel.js';
  50. import {
  51. IconTreeTriangleDown,
  52. IconFilter,
  53. IconPlus,
  54. IconRefresh,
  55. IconSetting,
  56. IconSearch,
  57. IconEdit,
  58. IconDelete,
  59. IconStop,
  60. IconPlay,
  61. IconMore,
  62. IconCopy,
  63. IconSmallTriangleRight
  64. } from '@douyinfe/semi-icons';
  65. import { loadChannelModels } from '../../helpers/index.js';
  66. import EditTagModal from '../../pages/Channel/EditTagModal.js';
  67. import { useTranslation } from 'react-i18next';
  68. const ChannelsTable = () => {
  69. const { t } = useTranslation();
  70. let type2label = undefined;
  71. const renderType = (type) => {
  72. if (!type2label) {
  73. type2label = new Map();
  74. for (let i = 0; i < CHANNEL_OPTIONS.length; i++) {
  75. type2label[CHANNEL_OPTIONS[i].value] = CHANNEL_OPTIONS[i];
  76. }
  77. type2label[0] = { value: 0, label: t('未知类型'), color: 'grey' };
  78. }
  79. return (
  80. <Tag
  81. size='large'
  82. color={type2label[type]?.color}
  83. shape='circle'
  84. prefixIcon={getChannelIcon(type)}
  85. >
  86. {type2label[type]?.label}
  87. </Tag>
  88. );
  89. };
  90. const renderTagType = () => {
  91. return (
  92. <Tag
  93. color='light-blue'
  94. prefixIcon={<Tags size={14} />}
  95. size='large'
  96. shape='circle'
  97. type='light'
  98. >
  99. {t('标签聚合')}
  100. </Tag>
  101. );
  102. };
  103. const renderStatus = (status) => {
  104. switch (status) {
  105. case 1:
  106. return (
  107. <Tag size='large' color='green' shape='circle' prefixIcon={<CheckCircle size={14} />}>
  108. {t('已启用')}
  109. </Tag>
  110. );
  111. case 2:
  112. return (
  113. <Tag size='large' color='yellow' shape='circle' prefixIcon={<XCircle size={14} />}>
  114. {t('已禁用')}
  115. </Tag>
  116. );
  117. case 3:
  118. return (
  119. <Tag size='large' color='yellow' shape='circle' prefixIcon={<AlertCircle size={14} />}>
  120. {t('自动禁用')}
  121. </Tag>
  122. );
  123. default:
  124. return (
  125. <Tag size='large' color='grey' shape='circle' prefixIcon={<HelpCircle size={14} />}>
  126. {t('未知状态')}
  127. </Tag>
  128. );
  129. }
  130. };
  131. const renderResponseTime = (responseTime) => {
  132. let time = responseTime / 1000;
  133. time = time.toFixed(2) + t(' 秒');
  134. if (responseTime === 0) {
  135. return (
  136. <Tag size='large' color='grey' shape='circle' prefixIcon={<TestTube size={14} />}>
  137. {t('未测试')}
  138. </Tag>
  139. );
  140. } else if (responseTime <= 1000) {
  141. return (
  142. <Tag size='large' color='green' shape='circle' prefixIcon={<Zap size={14} />}>
  143. {time}
  144. </Tag>
  145. );
  146. } else if (responseTime <= 3000) {
  147. return (
  148. <Tag size='large' color='lime' shape='circle' prefixIcon={<Timer size={14} />}>
  149. {time}
  150. </Tag>
  151. );
  152. } else if (responseTime <= 5000) {
  153. return (
  154. <Tag size='large' color='yellow' shape='circle' prefixIcon={<Clock size={14} />}>
  155. {time}
  156. </Tag>
  157. );
  158. } else {
  159. return (
  160. <Tag size='large' color='red' shape='circle' prefixIcon={<AlertTriangle size={14} />}>
  161. {time}
  162. </Tag>
  163. );
  164. }
  165. };
  166. // Define all columns
  167. const columns = [
  168. {
  169. title: t('ID'),
  170. dataIndex: 'id',
  171. },
  172. {
  173. title: t('名称'),
  174. dataIndex: 'name',
  175. },
  176. {
  177. title: t('分组'),
  178. dataIndex: 'group',
  179. render: (text, record, index) => (
  180. <div>
  181. <Space spacing={2}>
  182. {text
  183. ?.split(',')
  184. .sort((a, b) => {
  185. if (a === 'default') return -1;
  186. if (b === 'default') return 1;
  187. return a.localeCompare(b);
  188. })
  189. .map((item, index) => renderGroup(item))}
  190. </Space>
  191. </div>
  192. ),
  193. },
  194. {
  195. title: t('类型'),
  196. dataIndex: 'type',
  197. render: (text, record, index) => {
  198. if (record.children === undefined) {
  199. return <>{renderType(text)}</>;
  200. } else {
  201. return <>{renderTagType()}</>;
  202. }
  203. },
  204. },
  205. {
  206. title: t('状态'),
  207. dataIndex: 'status',
  208. render: (text, record, index) => {
  209. if (text === 3) {
  210. if (record.other_info === '') {
  211. record.other_info = '{}';
  212. }
  213. let otherInfo = JSON.parse(record.other_info);
  214. let reason = otherInfo['status_reason'];
  215. let time = otherInfo['status_time'];
  216. return (
  217. <div>
  218. <Tooltip
  219. content={t('原因:') + reason + t(',时间:') + timestamp2string(time)}
  220. >
  221. {renderStatus(text)}
  222. </Tooltip>
  223. </div>
  224. );
  225. } else {
  226. return renderStatus(text);
  227. }
  228. },
  229. },
  230. {
  231. title: t('响应时间'),
  232. dataIndex: 'response_time',
  233. render: (text, record, index) => (
  234. <div>{renderResponseTime(text)}</div>
  235. ),
  236. },
  237. {
  238. title: t('已用/剩余'),
  239. dataIndex: 'expired_time',
  240. render: (text, record, index) => {
  241. if (record.children === undefined) {
  242. return (
  243. <div>
  244. <Space spacing={1}>
  245. <Tooltip content={t('已用额度')}>
  246. <Tag color='white' type='ghost' size='large' shape='circle' prefixIcon={<Coins size={14} />}>
  247. {renderQuota(record.used_quota)}
  248. </Tag>
  249. </Tooltip>
  250. <Tooltip content={t('剩余额度') + record.balance + t(',点击更新')}>
  251. <Tag
  252. color='white'
  253. type='ghost'
  254. size='large'
  255. shape='circle'
  256. prefixIcon={<Coins size={14} />}
  257. onClick={() => updateChannelBalance(record)}
  258. >
  259. ${renderNumberWithPoint(record.balance)}
  260. </Tag>
  261. </Tooltip>
  262. </Space>
  263. </div>
  264. );
  265. } else {
  266. return (
  267. <Tooltip content={t('已用额度')}>
  268. <Tag color='white' type='ghost' size='large' shape='circle' prefixIcon={<Coins size={14} />}>
  269. {renderQuota(record.used_quota)}
  270. </Tag>
  271. </Tooltip>
  272. );
  273. }
  274. },
  275. },
  276. {
  277. title: t('优先级'),
  278. dataIndex: 'priority',
  279. render: (text, record, index) => {
  280. if (record.children === undefined) {
  281. return (
  282. <div>
  283. <InputNumber
  284. style={{ width: 70 }}
  285. name='priority'
  286. onBlur={(e) => {
  287. manageChannel(record.id, 'priority', record, e.target.value);
  288. }}
  289. keepFocus={true}
  290. innerButtons
  291. defaultValue={record.priority}
  292. min={-999}
  293. size="small"
  294. />
  295. </div>
  296. );
  297. } else {
  298. return (
  299. <InputNumber
  300. style={{ width: 70 }}
  301. name='priority'
  302. keepFocus={true}
  303. onBlur={(e) => {
  304. Modal.warning({
  305. title: t('修改子渠道优先级'),
  306. content: t('确定要修改所有子渠道优先级为 ') + e.target.value + t(' 吗?'),
  307. onOk: () => {
  308. if (e.target.value === '') {
  309. return;
  310. }
  311. submitTagEdit('priority', {
  312. tag: record.key,
  313. priority: e.target.value,
  314. });
  315. },
  316. });
  317. }}
  318. innerButtons
  319. defaultValue={record.priority}
  320. min={-999}
  321. size="small"
  322. />
  323. );
  324. }
  325. },
  326. },
  327. {
  328. title: t('权重'),
  329. dataIndex: 'weight',
  330. render: (text, record, index) => {
  331. if (record.children === undefined) {
  332. return (
  333. <div>
  334. <InputNumber
  335. style={{ width: 70 }}
  336. name='weight'
  337. onBlur={(e) => {
  338. manageChannel(record.id, 'weight', record, e.target.value);
  339. }}
  340. keepFocus={true}
  341. innerButtons
  342. defaultValue={record.weight}
  343. min={0}
  344. size="small"
  345. />
  346. </div>
  347. );
  348. } else {
  349. return (
  350. <InputNumber
  351. style={{ width: 70 }}
  352. name='weight'
  353. keepFocus={true}
  354. onBlur={(e) => {
  355. Modal.warning({
  356. title: t('修改子渠道权重'),
  357. content: t('确定要修改所有子渠道权重为 ') + e.target.value + t(' 吗?'),
  358. onOk: () => {
  359. if (e.target.value === '') {
  360. return;
  361. }
  362. submitTagEdit('weight', {
  363. tag: record.key,
  364. weight: e.target.value,
  365. });
  366. },
  367. });
  368. }}
  369. innerButtons
  370. defaultValue={record.weight}
  371. min={-999}
  372. size="small"
  373. />
  374. );
  375. }
  376. },
  377. },
  378. {
  379. title: '',
  380. dataIndex: 'operate',
  381. fixed: 'right',
  382. render: (text, record, index) => {
  383. if (record.children === undefined) {
  384. // 创建更多操作的下拉菜单项
  385. const moreMenuItems = [
  386. {
  387. node: 'item',
  388. name: t('删除'),
  389. icon: <IconDelete />,
  390. type: 'danger',
  391. onClick: () => {
  392. Modal.confirm({
  393. title: t('确定是否要删除此渠道?'),
  394. content: t('此修改将不可逆'),
  395. onOk: () => {
  396. manageChannel(record.id, 'delete', record).then(() => {
  397. removeRecord(record);
  398. });
  399. },
  400. });
  401. },
  402. },
  403. {
  404. node: 'item',
  405. name: t('复制'),
  406. icon: <IconCopy />,
  407. type: 'primary',
  408. onClick: () => {
  409. Modal.confirm({
  410. title: t('确定是否要复制此渠道?'),
  411. content: t('复制渠道的所有信息'),
  412. onOk: () => copySelectedChannel(record),
  413. });
  414. },
  415. },
  416. ];
  417. return (
  418. <Space wrap>
  419. <SplitButtonGroup
  420. className="!rounded-full overflow-hidden"
  421. aria-label={t('测试单个渠道操作项目组')}
  422. >
  423. <Button
  424. theme='light'
  425. size="small"
  426. onClick={() => testChannel(record, '')}
  427. >
  428. {t('测试')}
  429. </Button>
  430. <Button
  431. theme='light'
  432. size="small"
  433. icon={<IconTreeTriangleDown />}
  434. onClick={() => {
  435. setCurrentTestChannel(record);
  436. setShowModelTestModal(true);
  437. }}
  438. />
  439. </SplitButtonGroup>
  440. {record.status === 1 ? (
  441. <Button
  442. theme='light'
  443. type='warning'
  444. size="small"
  445. className="!rounded-full"
  446. icon={<IconStop />}
  447. onClick={() => manageChannel(record.id, 'disable', record)}
  448. >
  449. {t('禁用')}
  450. </Button>
  451. ) : (
  452. <Button
  453. theme='light'
  454. type='secondary'
  455. size="small"
  456. className="!rounded-full"
  457. icon={<IconPlay />}
  458. onClick={() => manageChannel(record.id, 'enable', record)}
  459. >
  460. {t('启用')}
  461. </Button>
  462. )}
  463. <Button
  464. theme='light'
  465. type='tertiary'
  466. size="small"
  467. className="!rounded-full"
  468. icon={<IconEdit />}
  469. onClick={() => {
  470. setEditingChannel(record);
  471. setShowEdit(true);
  472. }}
  473. >
  474. {t('编辑')}
  475. </Button>
  476. <Dropdown
  477. trigger='click'
  478. position='bottomRight'
  479. menu={moreMenuItems}
  480. >
  481. <Button
  482. icon={<IconMore />}
  483. theme='light'
  484. type='tertiary'
  485. size="small"
  486. className="!rounded-full"
  487. />
  488. </Dropdown>
  489. </Space>
  490. );
  491. } else {
  492. // 标签操作的下拉菜单项
  493. const tagMenuItems = [
  494. {
  495. node: 'item',
  496. name: t('编辑'),
  497. icon: <IconEdit />,
  498. onClick: () => {
  499. setShowEditTag(true);
  500. setEditingTag(record.key);
  501. },
  502. },
  503. ];
  504. return (
  505. <Space wrap>
  506. <Button
  507. theme='light'
  508. type='secondary'
  509. size="small"
  510. className="!rounded-full"
  511. icon={<IconPlay />}
  512. onClick={() => manageTag(record.key, 'enable')}
  513. >
  514. {t('启用全部')}
  515. </Button>
  516. <Button
  517. theme='light'
  518. type='warning'
  519. size="small"
  520. className="!rounded-full"
  521. icon={<IconStop />}
  522. onClick={() => manageTag(record.key, 'disable')}
  523. >
  524. {t('禁用全部')}
  525. </Button>
  526. <Dropdown
  527. trigger='click'
  528. position='bottomRight'
  529. menu={tagMenuItems}
  530. >
  531. <Button
  532. icon={<IconMore />}
  533. theme='light'
  534. type='tertiary'
  535. size="small"
  536. className="!rounded-full"
  537. />
  538. </Dropdown>
  539. </Space>
  540. );
  541. }
  542. },
  543. },
  544. ];
  545. const [channels, setChannels] = useState([]);
  546. const [loading, setLoading] = useState(true);
  547. const [activePage, setActivePage] = useState(1);
  548. const [idSort, setIdSort] = useState(false);
  549. const [searching, setSearching] = useState(false);
  550. const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE);
  551. const [channelCount, setChannelCount] = useState(pageSize);
  552. const [groupOptions, setGroupOptions] = useState([]);
  553. const [showEdit, setShowEdit] = useState(false);
  554. const [enableBatchDelete, setEnableBatchDelete] = useState(false);
  555. const [editingChannel, setEditingChannel] = useState({
  556. id: undefined,
  557. });
  558. const [showEditTag, setShowEditTag] = useState(false);
  559. const [editingTag, setEditingTag] = useState('');
  560. const [selectedChannels, setSelectedChannels] = useState([]);
  561. const [enableTagMode, setEnableTagMode] = useState(false);
  562. const [showBatchSetTag, setShowBatchSetTag] = useState(false);
  563. const [batchSetTagValue, setBatchSetTagValue] = useState('');
  564. const [showModelTestModal, setShowModelTestModal] = useState(false);
  565. const [currentTestChannel, setCurrentTestChannel] = useState(null);
  566. const [modelSearchKeyword, setModelSearchKeyword] = useState('');
  567. const [modelTestResults, setModelTestResults] = useState({});
  568. const [testingModels, setTestingModels] = useState(new Set());
  569. const [isBatchTesting, setIsBatchTesting] = useState(false);
  570. const [testQueue, setTestQueue] = useState([]);
  571. const [isProcessingQueue, setIsProcessingQueue] = useState(false);
  572. // Form API 引用
  573. const [formApi, setFormApi] = useState(null);
  574. // Form 初始值
  575. const formInitValues = {
  576. searchKeyword: '',
  577. searchGroup: '',
  578. searchModel: '',
  579. };
  580. const removeRecord = (record) => {
  581. let newDataSource = [...channels];
  582. if (record.id != null) {
  583. let idx = newDataSource.findIndex((data) => {
  584. if (data.children !== undefined) {
  585. for (let i = 0; i < data.children.length; i++) {
  586. if (data.children[i].id === record.id) {
  587. data.children.splice(i, 1);
  588. return false;
  589. }
  590. }
  591. } else {
  592. return data.id === record.id;
  593. }
  594. });
  595. if (idx > -1) {
  596. newDataSource.splice(idx, 1);
  597. setChannels(newDataSource);
  598. }
  599. }
  600. };
  601. const setChannelFormat = (channels, enableTagMode) => {
  602. let channelDates = [];
  603. let channelTags = {};
  604. for (let i = 0; i < channels.length; i++) {
  605. channels[i].key = '' + channels[i].id;
  606. if (!enableTagMode) {
  607. channelDates.push(channels[i]);
  608. } else {
  609. let tag = channels[i].tag ? channels[i].tag : '';
  610. // find from channelTags
  611. let tagIndex = channelTags[tag];
  612. let tagChannelDates = undefined;
  613. if (tagIndex === undefined) {
  614. // not found, create a new tag
  615. channelTags[tag] = 1;
  616. tagChannelDates = {
  617. key: tag,
  618. id: tag,
  619. tag: tag,
  620. name: '标签:' + tag,
  621. group: '',
  622. used_quota: 0,
  623. response_time: 0,
  624. priority: -1,
  625. weight: -1,
  626. };
  627. tagChannelDates.children = [];
  628. channelDates.push(tagChannelDates);
  629. } else {
  630. // found, add to the tag
  631. tagChannelDates = channelDates.find((item) => item.key === tag);
  632. }
  633. if (tagChannelDates.priority === -1) {
  634. tagChannelDates.priority = channels[i].priority;
  635. } else {
  636. if (tagChannelDates.priority !== channels[i].priority) {
  637. tagChannelDates.priority = '';
  638. }
  639. }
  640. if (tagChannelDates.weight === -1) {
  641. tagChannelDates.weight = channels[i].weight;
  642. } else {
  643. if (tagChannelDates.weight !== channels[i].weight) {
  644. tagChannelDates.weight = '';
  645. }
  646. }
  647. if (tagChannelDates.group === '') {
  648. tagChannelDates.group = channels[i].group;
  649. } else {
  650. let channelGroupsStr = channels[i].group;
  651. channelGroupsStr.split(',').forEach((item, index) => {
  652. if (tagChannelDates.group.indexOf(item) === -1) {
  653. // join
  654. tagChannelDates.group += ',' + item;
  655. }
  656. });
  657. }
  658. tagChannelDates.children.push(channels[i]);
  659. if (channels[i].status === 1) {
  660. tagChannelDates.status = 1;
  661. }
  662. tagChannelDates.used_quota += channels[i].used_quota;
  663. tagChannelDates.response_time += channels[i].response_time;
  664. tagChannelDates.response_time = tagChannelDates.response_time / 2;
  665. }
  666. }
  667. // data.key = '' + data.id
  668. setChannels(channelDates);
  669. if (channelDates.length >= pageSize) {
  670. setChannelCount(channelDates.length + pageSize);
  671. } else {
  672. setChannelCount(channelDates.length);
  673. }
  674. };
  675. const loadChannels = async (startIdx, pageSize, idSort, enableTagMode) => {
  676. setLoading(true);
  677. const res = await API.get(
  678. `/api/channel/?p=${startIdx}&page_size=${pageSize}&id_sort=${idSort}&tag_mode=${enableTagMode}`,
  679. );
  680. if (res === undefined) {
  681. return;
  682. }
  683. const { success, message, data } = res.data;
  684. if (success) {
  685. if (startIdx === 0) {
  686. setChannelFormat(data, enableTagMode);
  687. } else {
  688. let newChannels = [...channels];
  689. newChannels.splice(startIdx * pageSize, data.length, ...data);
  690. setChannelFormat(newChannels, enableTagMode);
  691. }
  692. } else {
  693. showError(message);
  694. }
  695. setLoading(false);
  696. };
  697. const copySelectedChannel = async (record) => {
  698. const channelToCopy = { ...record };
  699. channelToCopy.name += t('_复制');
  700. channelToCopy.created_time = null;
  701. channelToCopy.balance = 0;
  702. channelToCopy.used_quota = 0;
  703. // 删除可能导致类型不匹配的字段
  704. delete channelToCopy.test_time;
  705. delete channelToCopy.response_time;
  706. if (!channelToCopy) {
  707. showError(t('渠道未找到,请刷新页面后重试。'));
  708. return;
  709. }
  710. try {
  711. const newChannel = { ...channelToCopy, id: undefined };
  712. const response = await API.post('/api/channel/', newChannel);
  713. if (response.data.success) {
  714. showSuccess(t('渠道复制成功'));
  715. await refresh();
  716. } else {
  717. showError(response.data.message);
  718. }
  719. } catch (error) {
  720. showError(t('渠道复制失败: ') + error.message);
  721. }
  722. };
  723. const refresh = async () => {
  724. const { searchKeyword, searchGroup, searchModel } = getFormValues();
  725. if (searchKeyword === '' && searchGroup === '' && searchModel === '') {
  726. await loadChannels(activePage - 1, pageSize, idSort, enableTagMode);
  727. } else {
  728. await searchChannels(enableTagMode);
  729. }
  730. };
  731. useEffect(() => {
  732. // console.log('default effect')
  733. const localIdSort = localStorage.getItem('id-sort') === 'true';
  734. const localPageSize =
  735. parseInt(localStorage.getItem('page-size')) || ITEMS_PER_PAGE;
  736. const localEnableTagMode = localStorage.getItem('enable-tag-mode') === 'true';
  737. const localEnableBatchDelete = localStorage.getItem('enable-batch-delete') === 'true';
  738. setIdSort(localIdSort);
  739. setPageSize(localPageSize);
  740. setEnableTagMode(localEnableTagMode);
  741. setEnableBatchDelete(localEnableBatchDelete);
  742. loadChannels(0, localPageSize, localIdSort, localEnableTagMode)
  743. .then()
  744. .catch((reason) => {
  745. showError(reason);
  746. });
  747. fetchGroups().then();
  748. loadChannelModels().then();
  749. }, []);
  750. const manageChannel = async (id, action, record, value) => {
  751. let data = { id };
  752. let res;
  753. switch (action) {
  754. case 'delete':
  755. res = await API.delete(`/api/channel/${id}/`);
  756. break;
  757. case 'enable':
  758. data.status = 1;
  759. res = await API.put('/api/channel/', data);
  760. break;
  761. case 'disable':
  762. data.status = 2;
  763. res = await API.put('/api/channel/', data);
  764. break;
  765. case 'priority':
  766. if (value === '') {
  767. return;
  768. }
  769. data.priority = parseInt(value);
  770. res = await API.put('/api/channel/', data);
  771. break;
  772. case 'weight':
  773. if (value === '') {
  774. return;
  775. }
  776. data.weight = parseInt(value);
  777. if (data.weight < 0) {
  778. data.weight = 0;
  779. }
  780. res = await API.put('/api/channel/', data);
  781. break;
  782. }
  783. const { success, message } = res.data;
  784. if (success) {
  785. showSuccess(t('操作成功完成!'));
  786. let channel = res.data.data;
  787. let newChannels = [...channels];
  788. if (action === 'delete') {
  789. } else {
  790. record.status = channel.status;
  791. }
  792. setChannels(newChannels);
  793. } else {
  794. showError(message);
  795. }
  796. };
  797. const manageTag = async (tag, action) => {
  798. console.log(tag, action);
  799. let res;
  800. switch (action) {
  801. case 'enable':
  802. res = await API.post('/api/channel/tag/enabled', {
  803. tag: tag,
  804. });
  805. break;
  806. case 'disable':
  807. res = await API.post('/api/channel/tag/disabled', {
  808. tag: tag,
  809. });
  810. break;
  811. }
  812. const { success, message } = res.data;
  813. if (success) {
  814. showSuccess('操作成功完成!');
  815. let newChannels = [...channels];
  816. for (let i = 0; i < newChannels.length; i++) {
  817. if (newChannels[i].tag === tag) {
  818. let status = action === 'enable' ? 1 : 2;
  819. newChannels[i]?.children?.forEach((channel) => {
  820. channel.status = status;
  821. });
  822. newChannels[i].status = status;
  823. }
  824. }
  825. setChannels(newChannels);
  826. } else {
  827. showError(message);
  828. }
  829. };
  830. // 获取表单值的辅助函数,确保所有值都是字符串
  831. const getFormValues = () => {
  832. const formValues = formApi ? formApi.getValues() : {};
  833. return {
  834. searchKeyword: formValues.searchKeyword || '',
  835. searchGroup: formValues.searchGroup || '',
  836. searchModel: formValues.searchModel || '',
  837. };
  838. };
  839. const searchChannels = async (enableTagMode) => {
  840. const { searchKeyword, searchGroup, searchModel } = getFormValues();
  841. setSearching(true);
  842. try {
  843. if (searchKeyword === '' && searchGroup === '' && searchModel === '') {
  844. await loadChannels(activePage - 1, pageSize, idSort, enableTagMode);
  845. // setActivePage(1);
  846. return;
  847. }
  848. const res = await API.get(
  849. `/api/channel/search?keyword=${searchKeyword}&group=${searchGroup}&model=${searchModel}&id_sort=${idSort}&tag_mode=${enableTagMode}`,
  850. );
  851. const { success, message, data } = res.data;
  852. if (success) {
  853. setChannelFormat(data, enableTagMode);
  854. setActivePage(1);
  855. } else {
  856. showError(message);
  857. }
  858. } finally {
  859. setSearching(false);
  860. }
  861. };
  862. const updateChannelProperty = (channelId, updateFn) => {
  863. // Create a new copy of channels array
  864. const newChannels = [...channels];
  865. let updated = false;
  866. // Find and update the correct channel
  867. newChannels.forEach((channel) => {
  868. if (channel.children !== undefined) {
  869. // If this is a tag group, search in its children
  870. channel.children.forEach((child) => {
  871. if (child.id === channelId) {
  872. updateFn(child);
  873. updated = true;
  874. }
  875. });
  876. } else if (channel.id === channelId) {
  877. // Direct channel match
  878. updateFn(channel);
  879. updated = true;
  880. }
  881. });
  882. // Only update state if we actually modified a channel
  883. if (updated) {
  884. setChannels(newChannels);
  885. }
  886. };
  887. const processTestQueue = async () => {
  888. if (!isProcessingQueue || testQueue.length === 0) return;
  889. const { channel, model } = testQueue[0];
  890. try {
  891. setTestingModels(prev => new Set([...prev, model]));
  892. const res = await API.get(`/api/channel/test/${channel.id}?model=${model}`);
  893. const { success, message, time } = res.data;
  894. setModelTestResults(prev => ({
  895. ...prev,
  896. [`${channel.id}-${model}`]: { success, time }
  897. }));
  898. if (success) {
  899. updateChannelProperty(channel.id, (ch) => {
  900. ch.response_time = time * 1000;
  901. ch.test_time = Date.now() / 1000;
  902. });
  903. if (!model) {
  904. showInfo(
  905. t('通道 ${name} 测试成功,耗时 ${time.toFixed(2)} 秒。')
  906. .replace('${name}', channel.name)
  907. .replace('${time.toFixed(2)}', time.toFixed(2)),
  908. );
  909. }
  910. } else {
  911. showError(message);
  912. }
  913. } catch (error) {
  914. showError(error.message);
  915. } finally {
  916. setTestingModels(prev => {
  917. const newSet = new Set(prev);
  918. newSet.delete(model);
  919. return newSet;
  920. });
  921. }
  922. // 移除已处理的测试
  923. setTestQueue(prev => prev.slice(1));
  924. };
  925. // 监听队列变化
  926. useEffect(() => {
  927. if (testQueue.length > 0 && isProcessingQueue) {
  928. processTestQueue();
  929. } else if (testQueue.length === 0 && isProcessingQueue) {
  930. setIsProcessingQueue(false);
  931. setIsBatchTesting(false);
  932. }
  933. }, [testQueue, isProcessingQueue]);
  934. const testChannel = async (record, model) => {
  935. setTestQueue(prev => [...prev, { channel: record, model }]);
  936. if (!isProcessingQueue) {
  937. setIsProcessingQueue(true);
  938. }
  939. };
  940. const batchTestModels = async () => {
  941. if (!currentTestChannel) return;
  942. setIsBatchTesting(true);
  943. const models = currentTestChannel.models
  944. .split(',')
  945. .filter((model) =>
  946. model.toLowerCase().includes(modelSearchKeyword.toLowerCase())
  947. );
  948. setTestQueue(models.map(model => ({
  949. channel: currentTestChannel,
  950. model
  951. })));
  952. setIsProcessingQueue(true);
  953. };
  954. const handleCloseModal = () => {
  955. if (isBatchTesting) {
  956. // 清空测试队列来停止测试
  957. setTestQueue([]);
  958. setIsProcessingQueue(false);
  959. setIsBatchTesting(false);
  960. showSuccess(t('已停止测试'));
  961. } else {
  962. setShowModelTestModal(false);
  963. setModelSearchKeyword('');
  964. }
  965. };
  966. let pageData = channels.slice(
  967. (activePage - 1) * pageSize,
  968. activePage * pageSize,
  969. );
  970. const handlePageChange = (page) => {
  971. setActivePage(page);
  972. if (page === Math.ceil(channels.length / pageSize) + 1) {
  973. // In this case we have to load more data and then append them.
  974. loadChannels(page - 1, pageSize, idSort, enableTagMode).then((r) => { });
  975. }
  976. };
  977. const handlePageSizeChange = async (size) => {
  978. localStorage.setItem('page-size', size + '');
  979. setPageSize(size);
  980. setActivePage(1);
  981. loadChannels(0, size, idSort, enableTagMode)
  982. .then()
  983. .catch((reason) => {
  984. showError(reason);
  985. });
  986. };
  987. const fetchGroups = async () => {
  988. try {
  989. let res = await API.get(`/api/group/`);
  990. // add 'all' option
  991. // res.data.data.unshift('all');
  992. if (res === undefined) {
  993. return;
  994. }
  995. setGroupOptions(
  996. res.data.data.map((group) => ({
  997. label: group,
  998. value: group,
  999. })),
  1000. );
  1001. } catch (error) {
  1002. showError(error.message);
  1003. }
  1004. };
  1005. const submitTagEdit = async (type, data) => {
  1006. switch (type) {
  1007. case 'priority':
  1008. if (data.priority === undefined || data.priority === '') {
  1009. showInfo('优先级必须是整数!');
  1010. return;
  1011. }
  1012. data.priority = parseInt(data.priority);
  1013. break;
  1014. case 'weight':
  1015. if (
  1016. data.weight === undefined ||
  1017. data.weight < 0 ||
  1018. data.weight === ''
  1019. ) {
  1020. showInfo('权重必须是非负整数!');
  1021. return;
  1022. }
  1023. data.weight = parseInt(data.weight);
  1024. break;
  1025. }
  1026. try {
  1027. const res = await API.put('/api/channel/tag', data);
  1028. if (res?.data?.success) {
  1029. showSuccess('更新成功!');
  1030. await refresh();
  1031. }
  1032. } catch (error) {
  1033. showError(error);
  1034. }
  1035. };
  1036. const closeEdit = () => {
  1037. setShowEdit(false);
  1038. };
  1039. const handleRow = (record, index) => {
  1040. if (record.status !== 1) {
  1041. return {
  1042. style: {
  1043. background: 'var(--semi-color-disabled-border)',
  1044. },
  1045. };
  1046. } else {
  1047. return {};
  1048. }
  1049. };
  1050. const batchSetChannelTag = async () => {
  1051. if (selectedChannels.length === 0) {
  1052. showError(t('请先选择要设置标签的渠道!'));
  1053. return;
  1054. }
  1055. if (batchSetTagValue === '') {
  1056. showError(t('标签不能为空!'));
  1057. return;
  1058. }
  1059. let ids = selectedChannels.map((channel) => channel.id);
  1060. const res = await API.post('/api/channel/batch/tag', {
  1061. ids: ids,
  1062. tag: batchSetTagValue === '' ? null : batchSetTagValue,
  1063. });
  1064. if (res.data.success) {
  1065. showSuccess(
  1066. t('已为 ${count} 个渠道设置标签!').replace('${count}', res.data.data),
  1067. );
  1068. await refresh();
  1069. setShowBatchSetTag(false);
  1070. } else {
  1071. showError(res.data.message);
  1072. }
  1073. };
  1074. const testAllChannels = async () => {
  1075. const res = await API.get(`/api/channel/test`);
  1076. const { success, message } = res.data;
  1077. if (success) {
  1078. showInfo(t('已成功开始测试所有已启用通道,请刷新页面查看结果。'));
  1079. } else {
  1080. showError(message);
  1081. }
  1082. };
  1083. const deleteAllDisabledChannels = async () => {
  1084. const res = await API.delete(`/api/channel/disabled`);
  1085. const { success, message, data } = res.data;
  1086. if (success) {
  1087. showSuccess(
  1088. t('已删除所有禁用渠道,共计 ${data} 个').replace('${data}', data),
  1089. );
  1090. await refresh();
  1091. } else {
  1092. showError(message);
  1093. }
  1094. };
  1095. const updateAllChannelsBalance = async () => {
  1096. const res = await API.get(`/api/channel/update_balance`);
  1097. const { success, message } = res.data;
  1098. if (success) {
  1099. showInfo(t('已更新完毕所有已启用通道余额!'));
  1100. } else {
  1101. showError(message);
  1102. }
  1103. };
  1104. const updateChannelBalance = async (record) => {
  1105. const res = await API.get(`/api/channel/update_balance/${record.id}/`);
  1106. const { success, message, balance } = res.data;
  1107. if (success) {
  1108. updateChannelProperty(record.id, (channel) => {
  1109. channel.balance = balance;
  1110. channel.balance_updated_time = Date.now() / 1000;
  1111. });
  1112. showInfo(
  1113. t('通道 ${name} 余额更新成功!').replace('${name}', record.name),
  1114. );
  1115. } else {
  1116. showError(message);
  1117. }
  1118. };
  1119. const batchDeleteChannels = async () => {
  1120. if (selectedChannels.length === 0) {
  1121. showError(t('请先选择要删除的通道!'));
  1122. return;
  1123. }
  1124. setLoading(true);
  1125. let ids = [];
  1126. selectedChannels.forEach((channel) => {
  1127. ids.push(channel.id);
  1128. });
  1129. const res = await API.post(`/api/channel/batch`, { ids: ids });
  1130. const { success, message, data } = res.data;
  1131. if (success) {
  1132. showSuccess(t('已删除 ${data} 个通道!').replace('${data}', data));
  1133. await refresh();
  1134. } else {
  1135. showError(message);
  1136. }
  1137. setLoading(false);
  1138. };
  1139. const fixChannelsAbilities = async () => {
  1140. const res = await API.post(`/api/channel/fix`);
  1141. const { success, message, data } = res.data;
  1142. if (success) {
  1143. showSuccess(t('已修复 ${data} 个通道!').replace('${data}', data));
  1144. await refresh();
  1145. } else {
  1146. showError(message);
  1147. }
  1148. };
  1149. const renderHeader = () => (
  1150. <div className="flex flex-col w-full">
  1151. <div className="flex flex-col md:flex-row justify-between gap-4">
  1152. <div className="flex flex-wrap md:flex-nowrap items-center gap-2 w-full md:w-auto order-2 md:order-1">
  1153. <Button
  1154. disabled={!enableBatchDelete}
  1155. theme='light'
  1156. type='danger'
  1157. className="!rounded-full w-full md:w-auto"
  1158. onClick={() => {
  1159. Modal.confirm({
  1160. title: t('确定是否要删除所选通道?'),
  1161. content: t('此修改将不可逆'),
  1162. onOk: () => batchDeleteChannels(),
  1163. });
  1164. }}
  1165. >
  1166. {t('删除所选通道')}
  1167. </Button>
  1168. <Button
  1169. disabled={!enableBatchDelete}
  1170. theme='light'
  1171. type='primary'
  1172. onClick={() => setShowBatchSetTag(true)}
  1173. className="!rounded-full w-full md:w-auto"
  1174. >
  1175. {t('批量设置标签')}
  1176. </Button>
  1177. <Dropdown
  1178. trigger='click'
  1179. render={
  1180. <Dropdown.Menu>
  1181. <Dropdown.Item>
  1182. <Button
  1183. theme='light'
  1184. type='warning'
  1185. className="!rounded-full w-full"
  1186. onClick={() => {
  1187. Modal.confirm({
  1188. title: t('确定?'),
  1189. content: t('确定要测试所有通道吗?'),
  1190. onOk: () => testAllChannels(),
  1191. size: 'small',
  1192. centered: true,
  1193. });
  1194. }}
  1195. >
  1196. {t('测试所有通道')}
  1197. </Button>
  1198. </Dropdown.Item>
  1199. <Dropdown.Item>
  1200. <Button
  1201. theme='light'
  1202. type='secondary'
  1203. className="!rounded-full w-full"
  1204. onClick={() => {
  1205. Modal.confirm({
  1206. title: t('确定?'),
  1207. content: t('确定要更新所有已启用通道余额吗?'),
  1208. onOk: () => updateAllChannelsBalance(),
  1209. size: 'sm',
  1210. centered: true,
  1211. });
  1212. }}
  1213. >
  1214. {t('更新所有已启用通道余额')}
  1215. </Button>
  1216. </Dropdown.Item>
  1217. <Dropdown.Item>
  1218. <Button
  1219. theme='light'
  1220. type='danger'
  1221. className="!rounded-full w-full"
  1222. onClick={() => {
  1223. Modal.confirm({
  1224. title: t('确定是否要删除禁用通道?'),
  1225. content: t('此修改将不可逆'),
  1226. onOk: () => deleteAllDisabledChannels(),
  1227. size: 'sm',
  1228. centered: true,
  1229. });
  1230. }}
  1231. >
  1232. {t('删除禁用通道')}
  1233. </Button>
  1234. </Dropdown.Item>
  1235. <Dropdown.Item>
  1236. <Button
  1237. theme='light'
  1238. type='tertiary'
  1239. className="!rounded-full w-full"
  1240. onClick={() => {
  1241. Modal.confirm({
  1242. title: t('确定是否要修复数据库一致性?'),
  1243. content: t('进行该操作时,可能导致渠道访问错误,请仅在数据库出现问题时使用'),
  1244. onOk: () => fixChannelsAbilities(),
  1245. size: 'sm',
  1246. centered: true,
  1247. });
  1248. }}
  1249. >
  1250. {t('修复数据库一致性')}
  1251. </Button>
  1252. </Dropdown.Item>
  1253. </Dropdown.Menu>
  1254. }
  1255. >
  1256. <Button theme='light' type='tertiary' icon={<IconSetting />} className="!rounded-full w-full md:w-auto">
  1257. {t('批量操作')}
  1258. </Button>
  1259. </Dropdown>
  1260. </div>
  1261. <div className="flex flex-col md:flex-row items-start md:items-center gap-4 w-full md:w-auto order-1 md:order-2">
  1262. <div className="flex items-center justify-between w-full md:w-auto">
  1263. <Typography.Text strong className="mr-2">
  1264. {t('使用ID排序')}
  1265. </Typography.Text>
  1266. <Switch
  1267. checked={idSort}
  1268. onChange={(v) => {
  1269. localStorage.setItem('id-sort', v + '');
  1270. setIdSort(v);
  1271. loadChannels(0, pageSize, v, enableTagMode);
  1272. }}
  1273. />
  1274. </div>
  1275. <div className="flex items-center justify-between w-full md:w-auto">
  1276. <Typography.Text strong className="mr-2">
  1277. {t('开启批量操作')}
  1278. </Typography.Text>
  1279. <Switch
  1280. checked={enableBatchDelete}
  1281. onChange={(v) => {
  1282. localStorage.setItem('enable-batch-delete', v + '');
  1283. setEnableBatchDelete(v);
  1284. }}
  1285. />
  1286. </div>
  1287. <div className="flex items-center justify-between w-full md:w-auto">
  1288. <Typography.Text strong className="mr-2">
  1289. {t('标签聚合模式')}
  1290. </Typography.Text>
  1291. <Switch
  1292. checked={enableTagMode}
  1293. onChange={(v) => {
  1294. localStorage.setItem('enable-tag-mode', v + '');
  1295. setEnableTagMode(v);
  1296. loadChannels(0, pageSize, idSort, v);
  1297. }}
  1298. />
  1299. </div>
  1300. </div>
  1301. </div>
  1302. <Divider margin="12px" />
  1303. <div className="flex flex-col md:flex-row justify-between items-center gap-4 w-full">
  1304. <div className="flex gap-2 w-full md:w-auto order-2 md:order-1">
  1305. <Button
  1306. theme='light'
  1307. type='primary'
  1308. icon={<IconPlus />}
  1309. className="!rounded-full w-full md:w-auto"
  1310. onClick={() => {
  1311. setEditingChannel({
  1312. id: undefined,
  1313. });
  1314. setShowEdit(true);
  1315. }}
  1316. >
  1317. {t('添加渠道')}
  1318. </Button>
  1319. <Button
  1320. theme='light'
  1321. type='primary'
  1322. icon={<IconRefresh />}
  1323. className="!rounded-full w-full md:w-auto"
  1324. onClick={refresh}
  1325. >
  1326. {t('刷新')}
  1327. </Button>
  1328. </div>
  1329. <div className="flex flex-col md:flex-row items-center gap-4 w-full md:w-auto order-1 md:order-2">
  1330. <Form
  1331. initValues={formInitValues}
  1332. getFormApi={(api) => setFormApi(api)}
  1333. onSubmit={() => searchChannels(enableTagMode)}
  1334. allowEmpty={true}
  1335. autoComplete="off"
  1336. layout="horizontal"
  1337. trigger="change"
  1338. stopValidateWithError={false}
  1339. className="flex flex-col md:flex-row items-center gap-4 w-full"
  1340. >
  1341. <div className="relative w-full md:w-64">
  1342. <Form.Input
  1343. field="searchKeyword"
  1344. prefix={<IconSearch />}
  1345. placeholder={t('搜索渠道的 ID,名称,密钥和API地址 ...')}
  1346. className="!rounded-full"
  1347. showClear
  1348. pure
  1349. />
  1350. </div>
  1351. <div className="w-full md:w-48">
  1352. <Form.Input
  1353. field="searchModel"
  1354. prefix={<IconFilter />}
  1355. placeholder={t('模型关键字')}
  1356. className="!rounded-full"
  1357. showClear
  1358. pure
  1359. />
  1360. </div>
  1361. <div className="w-full md:w-48">
  1362. <Form.Select
  1363. field="searchGroup"
  1364. placeholder={t('选择分组')}
  1365. optionList={[
  1366. { label: t('选择分组'), value: null },
  1367. ...groupOptions,
  1368. ]}
  1369. className="!rounded-full w-full"
  1370. showClear
  1371. pure
  1372. onChange={() => {
  1373. // 延迟执行搜索,让表单值先更新
  1374. setTimeout(() => {
  1375. searchChannels(enableTagMode);
  1376. }, 0);
  1377. }}
  1378. />
  1379. </div>
  1380. <Button
  1381. type="primary"
  1382. htmlType="submit"
  1383. loading={loading || searching}
  1384. className="!rounded-full w-full md:w-auto"
  1385. >
  1386. {t('查询')}
  1387. </Button>
  1388. <Button
  1389. theme='light'
  1390. onClick={() => {
  1391. if (formApi) {
  1392. formApi.reset();
  1393. // 重置后立即查询,使用setTimeout确保表单重置完成
  1394. setTimeout(() => {
  1395. refresh();
  1396. }, 100);
  1397. }
  1398. }}
  1399. className="!rounded-full w-full md:w-auto"
  1400. >
  1401. {t('重置')}
  1402. </Button>
  1403. </Form>
  1404. </div>
  1405. </div>
  1406. </div>
  1407. );
  1408. return (
  1409. <>
  1410. <EditTagModal
  1411. visible={showEditTag}
  1412. tag={editingTag}
  1413. handleClose={() => setShowEditTag(false)}
  1414. refresh={refresh}
  1415. />
  1416. <EditChannel
  1417. refresh={refresh}
  1418. visible={showEdit}
  1419. handleClose={closeEdit}
  1420. editingChannel={editingChannel}
  1421. />
  1422. <Card
  1423. className="!rounded-2xl"
  1424. title={renderHeader()}
  1425. shadows='always'
  1426. bordered={false}
  1427. >
  1428. <Table
  1429. columns={columns}
  1430. dataSource={pageData}
  1431. scroll={{ x: 'max-content' }}
  1432. pagination={{
  1433. currentPage: activePage,
  1434. pageSize: pageSize,
  1435. total: channelCount,
  1436. pageSizeOpts: [10, 20, 50, 100],
  1437. showSizeChanger: true,
  1438. formatPageText: (page) => t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
  1439. start: page.currentStart,
  1440. end: page.currentEnd,
  1441. total: channels.length,
  1442. }),
  1443. onPageSizeChange: (size) => {
  1444. handlePageSizeChange(size);
  1445. },
  1446. onPageChange: handlePageChange,
  1447. }}
  1448. expandAllRows={false}
  1449. onRow={handleRow}
  1450. rowSelection={
  1451. enableBatchDelete
  1452. ? {
  1453. onChange: (selectedRowKeys, selectedRows) => {
  1454. setSelectedChannels(selectedRows);
  1455. },
  1456. }
  1457. : null
  1458. }
  1459. empty={
  1460. <Empty
  1461. image={<IllustrationNoResult style={{ width: 150, height: 150 }} />}
  1462. darkModeImage={<IllustrationNoResultDark style={{ width: 150, height: 150 }} />}
  1463. description={t('搜索无结果')}
  1464. style={{ padding: 30 }}
  1465. />
  1466. }
  1467. className="rounded-xl overflow-hidden"
  1468. size="middle"
  1469. loading={loading}
  1470. />
  1471. </Card>
  1472. {/* 批量设置标签模态框 */}
  1473. <Modal
  1474. title={t('批量设置标签')}
  1475. visible={showBatchSetTag}
  1476. onOk={batchSetChannelTag}
  1477. onCancel={() => setShowBatchSetTag(false)}
  1478. maskClosable={false}
  1479. centered={true}
  1480. size="small"
  1481. className="!rounded-lg"
  1482. >
  1483. <div className="mb-5">
  1484. <Typography.Text>{t('请输入要设置的标签名称')}</Typography.Text>
  1485. </div>
  1486. <Input
  1487. placeholder={t('请输入标签名称')}
  1488. value={batchSetTagValue}
  1489. onChange={(v) => setBatchSetTagValue(v)}
  1490. size='large'
  1491. className="!rounded-full"
  1492. />
  1493. <div className="mt-4">
  1494. <Typography.Text type='secondary'>
  1495. {t('已选择 ${count} 个渠道').replace('${count}', selectedChannels.length)}
  1496. </Typography.Text>
  1497. </div>
  1498. </Modal>
  1499. {/* 模型测试弹窗 */}
  1500. <Modal
  1501. title={
  1502. currentTestChannel && (
  1503. <div className="flex items-center gap-2">
  1504. <Typography.Text strong className="!text-[var(--semi-color-text-0)] !text-base">
  1505. {currentTestChannel.name} {t('渠道的模型测试')}
  1506. </Typography.Text>
  1507. <Typography.Text type="tertiary" className="!text-xs flex items-center">
  1508. {t('共')} {currentTestChannel.models.split(',').length} {t('个模型')}
  1509. </Typography.Text>
  1510. </div>
  1511. )
  1512. }
  1513. visible={showModelTestModal && currentTestChannel !== null}
  1514. onCancel={handleCloseModal}
  1515. footer={
  1516. <div className="flex justify-end">
  1517. {isBatchTesting ? (
  1518. <Button
  1519. theme='light'
  1520. type='warning'
  1521. className="!rounded-full"
  1522. onClick={handleCloseModal}
  1523. >
  1524. {t('停止测试')}
  1525. </Button>
  1526. ) : (
  1527. <Button
  1528. theme='light'
  1529. type='tertiary'
  1530. className="!rounded-full"
  1531. onClick={handleCloseModal}
  1532. >
  1533. {t('取消')}
  1534. </Button>
  1535. )}
  1536. <Button
  1537. theme='light'
  1538. type='primary'
  1539. className="!rounded-full"
  1540. onClick={batchTestModels}
  1541. loading={isBatchTesting}
  1542. disabled={isBatchTesting}
  1543. >
  1544. {isBatchTesting ? t('测试中...') : t('批量测试${count}个模型').replace(
  1545. '${count}',
  1546. currentTestChannel
  1547. ? currentTestChannel.models
  1548. .split(',')
  1549. .filter((model) =>
  1550. model.toLowerCase().includes(modelSearchKeyword.toLowerCase())
  1551. ).length
  1552. : 0
  1553. )}
  1554. </Button>
  1555. </div>
  1556. }
  1557. maskClosable={!isBatchTesting}
  1558. centered={true}
  1559. className="!rounded-lg"
  1560. size="large"
  1561. >
  1562. <div className="max-h-[600px] overflow-y-auto">
  1563. {currentTestChannel && (
  1564. <div>
  1565. <div className="flex items-center justify-end mb-2">
  1566. <Input
  1567. placeholder={t('搜索模型...')}
  1568. value={modelSearchKeyword}
  1569. onChange={(v) => setModelSearchKeyword(v)}
  1570. className="w-64 !rounded-full"
  1571. prefix={<IconSearch />}
  1572. showClear
  1573. />
  1574. </div>
  1575. <Table
  1576. columns={[
  1577. {
  1578. title: t('模型名称'),
  1579. dataIndex: 'model',
  1580. render: (text) => (
  1581. <div className="flex items-center">
  1582. <Typography.Text strong>{text}</Typography.Text>
  1583. </div>
  1584. )
  1585. },
  1586. {
  1587. title: t('状态'),
  1588. dataIndex: 'status',
  1589. render: (text, record) => {
  1590. const testResult = modelTestResults[`${currentTestChannel.id}-${record.model}`];
  1591. const isTesting = testingModels.has(record.model);
  1592. if (isTesting) {
  1593. return (
  1594. <Tag size='large' color='blue' className="!rounded-full">
  1595. {t('测试中')}
  1596. </Tag>
  1597. );
  1598. }
  1599. if (!testResult) {
  1600. return (
  1601. <Tag size='large' color='grey' className="!rounded-full">
  1602. {t('未开始')}
  1603. </Tag>
  1604. );
  1605. }
  1606. return (
  1607. <div className="flex items-center gap-2">
  1608. <Tag
  1609. size='large'
  1610. color={testResult.success ? 'green' : 'red'}
  1611. className="!rounded-full"
  1612. >
  1613. {testResult.success ? t('成功') : t('失败')}
  1614. </Tag>
  1615. {testResult.success && (
  1616. <Typography.Text type="tertiary">
  1617. {t('请求时长: ${time}s').replace('${time}', testResult.time.toFixed(2))}
  1618. </Typography.Text>
  1619. )}
  1620. </div>
  1621. );
  1622. }
  1623. },
  1624. {
  1625. title: '',
  1626. dataIndex: 'operate',
  1627. render: (text, record) => {
  1628. const isTesting = testingModels.has(record.model);
  1629. return (
  1630. <Button
  1631. theme='light'
  1632. type='primary'
  1633. className="!rounded-full"
  1634. onClick={() => testChannel(currentTestChannel, record.model)}
  1635. loading={isTesting}
  1636. size='small'
  1637. icon={<IconSmallTriangleRight />}
  1638. >
  1639. {t('测试')}
  1640. </Button>
  1641. );
  1642. }
  1643. }
  1644. ]}
  1645. dataSource={currentTestChannel.models
  1646. .split(',')
  1647. .filter((model) =>
  1648. model.toLowerCase().includes(modelSearchKeyword.toLowerCase())
  1649. )
  1650. .map((model) => ({
  1651. model,
  1652. key: model
  1653. }))}
  1654. pagination={false}
  1655. size="middle"
  1656. />
  1657. </div>
  1658. )}
  1659. </div>
  1660. </Modal>
  1661. </>
  1662. );
  1663. };
  1664. export default ChannelsTable;