UpstreamRatioSync.js 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757
  1. import React, { useState, useCallback, useMemo, useEffect } from 'react';
  2. import {
  3. Button,
  4. Table,
  5. Tag,
  6. Empty,
  7. Checkbox,
  8. Form,
  9. Input,
  10. Tooltip,
  11. Select,
  12. Modal,
  13. } from '@douyinfe/semi-ui';
  14. import { IconSearch } from '@douyinfe/semi-icons';
  15. import {
  16. RefreshCcw,
  17. CheckSquare,
  18. AlertTriangle,
  19. CheckCircle,
  20. } from 'lucide-react';
  21. import { API, showError, showSuccess, showWarning, stringToColor } from '../../../helpers';
  22. import { useIsMobile } from '../../../hooks/useIsMobile.js';
  23. import { DEFAULT_ENDPOINT } from '../../../constants';
  24. import { useTranslation } from 'react-i18next';
  25. import {
  26. IllustrationNoResult,
  27. IllustrationNoResultDark
  28. } from '@douyinfe/semi-illustrations';
  29. import ChannelSelectorModal from '../../../components/settings/ChannelSelectorModal';
  30. function ConflictConfirmModal({ t, visible, items, onOk, onCancel }) {
  31. const isMobile = useIsMobile();
  32. const columns = [
  33. { title: t('渠道'), dataIndex: 'channel' },
  34. { title: t('模型'), dataIndex: 'model' },
  35. {
  36. title: t('当前计费'),
  37. dataIndex: 'current',
  38. render: (text) => <div style={{ whiteSpace: 'pre-wrap' }}>{text}</div>,
  39. },
  40. {
  41. title: t('修改为'),
  42. dataIndex: 'newVal',
  43. render: (text) => <div style={{ whiteSpace: 'pre-wrap' }}>{text}</div>,
  44. },
  45. ];
  46. return (
  47. <Modal
  48. title={t('确认冲突项修改')}
  49. visible={visible}
  50. onCancel={onCancel}
  51. onOk={onOk}
  52. size={isMobile ? 'full-width' : 'large'}
  53. >
  54. <Table columns={columns} dataSource={items} pagination={false} size="small" />
  55. </Modal>
  56. );
  57. }
  58. export default function UpstreamRatioSync(props) {
  59. const { t } = useTranslation();
  60. const [modalVisible, setModalVisible] = useState(false);
  61. const [loading, setLoading] = useState(false);
  62. const [syncLoading, setSyncLoading] = useState(false);
  63. const isMobile = useIsMobile();
  64. // 渠道选择相关
  65. const [allChannels, setAllChannels] = useState([]);
  66. const [selectedChannelIds, setSelectedChannelIds] = useState([]);
  67. // 渠道端点配置
  68. const [channelEndpoints, setChannelEndpoints] = useState({}); // { channelId: endpoint }
  69. // 差异数据和测试结果
  70. const [differences, setDifferences] = useState({});
  71. const [resolutions, setResolutions] = useState({});
  72. // 是否已经执行过同步
  73. const [hasSynced, setHasSynced] = useState(false);
  74. // 分页相关状态
  75. const [currentPage, setCurrentPage] = useState(1);
  76. const [pageSize, setPageSize] = useState(10);
  77. // 搜索相关状态
  78. const [searchKeyword, setSearchKeyword] = useState('');
  79. // 倍率类型过滤
  80. const [ratioTypeFilter, setRatioTypeFilter] = useState('');
  81. // 冲突确认弹窗相关
  82. const [confirmVisible, setConfirmVisible] = useState(false);
  83. const [conflictItems, setConflictItems] = useState([]); // {channel, model, current, newVal, ratioType}
  84. const channelSelectorRef = React.useRef(null);
  85. useEffect(() => {
  86. setCurrentPage(1);
  87. }, [ratioTypeFilter, searchKeyword]);
  88. const fetchAllChannels = async () => {
  89. setLoading(true);
  90. try {
  91. const res = await API.get('/api/ratio_sync/channels');
  92. if (res.data.success) {
  93. const channels = res.data.data || [];
  94. const transferData = channels.map(channel => ({
  95. key: channel.id,
  96. label: channel.name,
  97. value: channel.id,
  98. disabled: false,
  99. _originalData: channel,
  100. }));
  101. setAllChannels(transferData);
  102. // 合并已有 endpoints,避免每次打开弹窗都重置
  103. setChannelEndpoints(prev => {
  104. const merged = { ...prev };
  105. transferData.forEach(channel => {
  106. if (!merged[channel.key]) {
  107. merged[channel.key] = DEFAULT_ENDPOINT;
  108. }
  109. });
  110. return merged;
  111. });
  112. } else {
  113. showError(res.data.message);
  114. }
  115. } catch (error) {
  116. showError(t('获取渠道失败:') + error.message);
  117. } finally {
  118. setLoading(false);
  119. }
  120. };
  121. const confirmChannelSelection = () => {
  122. const selected = allChannels
  123. .filter(ch => selectedChannelIds.includes(ch.value))
  124. .map(ch => ch._originalData);
  125. if (selected.length === 0) {
  126. showWarning(t('请至少选择一个渠道'));
  127. return;
  128. }
  129. setModalVisible(false);
  130. fetchRatiosFromChannels(selected);
  131. };
  132. const fetchRatiosFromChannels = async (channelList) => {
  133. setSyncLoading(true);
  134. const upstreams = channelList.map(ch => ({
  135. id: ch.id,
  136. name: ch.name,
  137. base_url: ch.base_url,
  138. endpoint: channelEndpoints[ch.id] || DEFAULT_ENDPOINT,
  139. }));
  140. const payload = {
  141. upstreams: upstreams,
  142. timeout: 10,
  143. };
  144. try {
  145. const res = await API.post('/api/ratio_sync/fetch', payload);
  146. if (!res.data.success) {
  147. showError(res.data.message || t('后端请求失败'));
  148. setSyncLoading(false);
  149. return;
  150. }
  151. const { differences = {}, test_results = [] } = res.data.data;
  152. const errorResults = test_results.filter(r => r.status === 'error');
  153. if (errorResults.length > 0) {
  154. showWarning(t('部分渠道测试失败:') + errorResults.map(r => `${r.name}: ${r.error}`).join(', '));
  155. }
  156. setDifferences(differences);
  157. setResolutions({});
  158. setHasSynced(true);
  159. if (Object.keys(differences).length === 0) {
  160. showSuccess(t('未找到差异化倍率,无需同步'));
  161. }
  162. } catch (e) {
  163. showError(t('请求后端接口失败:') + e.message);
  164. } finally {
  165. setSyncLoading(false);
  166. }
  167. };
  168. function getBillingCategory(ratioType) {
  169. return ratioType === 'model_price' ? 'price' : 'ratio';
  170. }
  171. const selectValue = useCallback((model, ratioType, value) => {
  172. const category = getBillingCategory(ratioType);
  173. setResolutions(prev => {
  174. const newModelRes = { ...(prev[model] || {}) };
  175. Object.keys(newModelRes).forEach((rt) => {
  176. if (getBillingCategory(rt) !== category) {
  177. delete newModelRes[rt];
  178. }
  179. });
  180. newModelRes[ratioType] = value;
  181. return {
  182. ...prev,
  183. [model]: newModelRes,
  184. };
  185. });
  186. }, [setResolutions]);
  187. const applySync = async () => {
  188. const currentRatios = {
  189. ModelRatio: JSON.parse(props.options.ModelRatio || '{}'),
  190. CompletionRatio: JSON.parse(props.options.CompletionRatio || '{}'),
  191. CacheRatio: JSON.parse(props.options.CacheRatio || '{}'),
  192. ModelPrice: JSON.parse(props.options.ModelPrice || '{}'),
  193. };
  194. const conflicts = [];
  195. const getLocalBillingCategory = (model) => {
  196. if (currentRatios.ModelPrice[model] !== undefined) return 'price';
  197. if (currentRatios.ModelRatio[model] !== undefined ||
  198. currentRatios.CompletionRatio[model] !== undefined ||
  199. currentRatios.CacheRatio[model] !== undefined) return 'ratio';
  200. return null;
  201. };
  202. const findSourceChannel = (model, ratioType, value) => {
  203. if (differences[model] && differences[model][ratioType]) {
  204. const upMap = differences[model][ratioType].upstreams || {};
  205. const entry = Object.entries(upMap).find(([_, v]) => v === value);
  206. if (entry) return entry[0];
  207. }
  208. return t('未知');
  209. };
  210. Object.entries(resolutions).forEach(([model, ratios]) => {
  211. const localCat = getLocalBillingCategory(model);
  212. const newCat = 'model_price' in ratios ? 'price' : 'ratio';
  213. if (localCat && localCat !== newCat) {
  214. const currentDesc = localCat === 'price'
  215. ? `${t('固定价格')} : ${currentRatios.ModelPrice[model]}`
  216. : `${t('模型倍率')} : ${currentRatios.ModelRatio[model] ?? '-'}\n${t('补全倍率')} : ${currentRatios.CompletionRatio[model] ?? '-'}`;
  217. let newDesc = '';
  218. if (newCat === 'price') {
  219. newDesc = `${t('固定价格')} : ${ratios['model_price']}`;
  220. } else {
  221. const newModelRatio = ratios['model_ratio'] ?? '-';
  222. const newCompRatio = ratios['completion_ratio'] ?? '-';
  223. newDesc = `${t('模型倍率')} : ${newModelRatio}\n${t('补全倍率')} : ${newCompRatio}`;
  224. }
  225. const channels = Object.entries(ratios)
  226. .map(([rt, val]) => findSourceChannel(model, rt, val))
  227. .filter((v, idx, arr) => arr.indexOf(v) === idx)
  228. .join(', ');
  229. conflicts.push({
  230. channel: channels,
  231. model,
  232. current: currentDesc,
  233. newVal: newDesc,
  234. });
  235. }
  236. });
  237. if (conflicts.length > 0) {
  238. setConflictItems(conflicts);
  239. setConfirmVisible(true);
  240. return;
  241. }
  242. await performSync(currentRatios);
  243. };
  244. const performSync = useCallback(async (currentRatios) => {
  245. const finalRatios = {
  246. ModelRatio: { ...currentRatios.ModelRatio },
  247. CompletionRatio: { ...currentRatios.CompletionRatio },
  248. CacheRatio: { ...currentRatios.CacheRatio },
  249. ModelPrice: { ...currentRatios.ModelPrice },
  250. };
  251. Object.entries(resolutions).forEach(([model, ratios]) => {
  252. const selectedTypes = Object.keys(ratios);
  253. const hasPrice = selectedTypes.includes('model_price');
  254. const hasRatio = selectedTypes.some(rt => rt !== 'model_price');
  255. if (hasPrice) {
  256. delete finalRatios.ModelRatio[model];
  257. delete finalRatios.CompletionRatio[model];
  258. delete finalRatios.CacheRatio[model];
  259. }
  260. if (hasRatio) {
  261. delete finalRatios.ModelPrice[model];
  262. }
  263. Object.entries(ratios).forEach(([ratioType, value]) => {
  264. const optionKey = ratioType
  265. .split('_')
  266. .map(word => word.charAt(0).toUpperCase() + word.slice(1))
  267. .join('');
  268. finalRatios[optionKey][model] = parseFloat(value);
  269. });
  270. });
  271. setLoading(true);
  272. try {
  273. const updates = Object.entries(finalRatios).map(([key, value]) =>
  274. API.put('/api/option/', {
  275. key,
  276. value: JSON.stringify(value, null, 2),
  277. })
  278. );
  279. const results = await Promise.all(updates);
  280. if (results.every(res => res.data.success)) {
  281. showSuccess(t('同步成功'));
  282. props.refresh();
  283. setDifferences(prevDifferences => {
  284. const newDifferences = { ...prevDifferences };
  285. Object.entries(resolutions).forEach(([model, ratios]) => {
  286. Object.keys(ratios).forEach(ratioType => {
  287. if (newDifferences[model] && newDifferences[model][ratioType]) {
  288. delete newDifferences[model][ratioType];
  289. if (Object.keys(newDifferences[model]).length === 0) {
  290. delete newDifferences[model];
  291. }
  292. }
  293. });
  294. });
  295. return newDifferences;
  296. });
  297. setResolutions({});
  298. } else {
  299. showError(t('部分保存失败'));
  300. }
  301. } catch (error) {
  302. showError(t('保存失败'));
  303. } finally {
  304. setLoading(false);
  305. }
  306. }, [resolutions, props.options, props.refresh]);
  307. const getCurrentPageData = (dataSource) => {
  308. const startIndex = (currentPage - 1) * pageSize;
  309. const endIndex = startIndex + pageSize;
  310. return dataSource.slice(startIndex, endIndex);
  311. };
  312. const renderHeader = () => (
  313. <div className="flex flex-col w-full">
  314. <div className="flex flex-col md:flex-row justify-between items-center gap-4 w-full">
  315. <div className="flex flex-col md:flex-row gap-2 w-full md:w-auto order-2 md:order-1">
  316. <Button
  317. icon={<RefreshCcw size={14} />}
  318. className="w-full md:w-auto mt-2"
  319. onClick={() => {
  320. setModalVisible(true);
  321. if (allChannels.length === 0) {
  322. fetchAllChannels();
  323. }
  324. }}
  325. >
  326. {t('选择同步渠道')}
  327. </Button>
  328. {(() => {
  329. const hasSelections = Object.keys(resolutions).length > 0;
  330. return (
  331. <Button
  332. icon={<CheckSquare size={14} />}
  333. type='secondary'
  334. onClick={applySync}
  335. disabled={!hasSelections}
  336. className="w-full md:w-auto mt-2"
  337. >
  338. {t('应用同步')}
  339. </Button>
  340. );
  341. })()}
  342. <div className="flex flex-col sm:flex-row gap-2 w-full md:w-auto mt-2">
  343. <Input
  344. prefix={<IconSearch size={14} />}
  345. placeholder={t('搜索模型名称')}
  346. value={searchKeyword}
  347. onChange={setSearchKeyword}
  348. className="w-full sm:w-64"
  349. showClear
  350. />
  351. <Select
  352. placeholder={t('按倍率类型筛选')}
  353. value={ratioTypeFilter}
  354. onChange={setRatioTypeFilter}
  355. className="w-full sm:w-48"
  356. showClear
  357. onClear={() => setRatioTypeFilter('')}
  358. >
  359. <Select.Option value="model_ratio">{t('模型倍率')}</Select.Option>
  360. <Select.Option value="completion_ratio">{t('补全倍率')}</Select.Option>
  361. <Select.Option value="cache_ratio">{t('缓存倍率')}</Select.Option>
  362. <Select.Option value="model_price">{t('固定价格')}</Select.Option>
  363. </Select>
  364. </div>
  365. </div>
  366. </div>
  367. </div>
  368. );
  369. const renderDifferenceTable = () => {
  370. const dataSource = useMemo(() => {
  371. const tmp = [];
  372. Object.entries(differences).forEach(([model, ratioTypes]) => {
  373. const hasPrice = 'model_price' in ratioTypes;
  374. const hasOtherRatio = ['model_ratio', 'completion_ratio', 'cache_ratio'].some(rt => rt in ratioTypes);
  375. const billingConflict = hasPrice && hasOtherRatio;
  376. Object.entries(ratioTypes).forEach(([ratioType, diff]) => {
  377. tmp.push({
  378. key: `${model}_${ratioType}`,
  379. model,
  380. ratioType,
  381. current: diff.current,
  382. upstreams: diff.upstreams,
  383. confidence: diff.confidence || {},
  384. billingConflict,
  385. });
  386. });
  387. });
  388. return tmp;
  389. }, [differences]);
  390. const filteredDataSource = useMemo(() => {
  391. if (!searchKeyword.trim() && !ratioTypeFilter) {
  392. return dataSource;
  393. }
  394. return dataSource.filter(item => {
  395. const matchesKeyword = !searchKeyword.trim() ||
  396. item.model.toLowerCase().includes(searchKeyword.toLowerCase().trim());
  397. const matchesRatioType = !ratioTypeFilter ||
  398. item.ratioType === ratioTypeFilter;
  399. return matchesKeyword && matchesRatioType;
  400. });
  401. }, [dataSource, searchKeyword, ratioTypeFilter]);
  402. const upstreamNames = useMemo(() => {
  403. const set = new Set();
  404. filteredDataSource.forEach((row) => {
  405. Object.keys(row.upstreams || {}).forEach((name) => set.add(name));
  406. });
  407. return Array.from(set);
  408. }, [filteredDataSource]);
  409. if (filteredDataSource.length === 0) {
  410. return (
  411. <Empty
  412. image={<IllustrationNoResult style={{ width: 150, height: 150 }} />}
  413. darkModeImage={<IllustrationNoResultDark style={{ width: 150, height: 150 }} />}
  414. description={
  415. searchKeyword.trim()
  416. ? t('未找到匹配的模型')
  417. : (Object.keys(differences).length === 0 ?
  418. (hasSynced ? t('暂无差异化倍率显示') : t('请先选择同步渠道'))
  419. : t('请先选择同步渠道'))
  420. }
  421. style={{ padding: 30 }}
  422. />
  423. );
  424. }
  425. const columns = [
  426. {
  427. title: t('模型'),
  428. dataIndex: 'model',
  429. fixed: 'left',
  430. },
  431. {
  432. title: t('倍率类型'),
  433. dataIndex: 'ratioType',
  434. render: (text, record) => {
  435. const typeMap = {
  436. model_ratio: t('模型倍率'),
  437. completion_ratio: t('补全倍率'),
  438. cache_ratio: t('缓存倍率'),
  439. model_price: t('固定价格'),
  440. };
  441. const baseTag = <Tag color={stringToColor(text)} shape="circle">{typeMap[text] || text}</Tag>;
  442. if (record?.billingConflict) {
  443. return (
  444. <div className="flex items-center gap-1">
  445. {baseTag}
  446. <Tooltip position="top" content={t('该模型存在固定价格与倍率计费方式冲突,请确认选择')}>
  447. <AlertTriangle size={14} className="text-yellow-500" />
  448. </Tooltip>
  449. </div>
  450. );
  451. }
  452. return baseTag;
  453. },
  454. },
  455. {
  456. title: t('置信度'),
  457. dataIndex: 'confidence',
  458. render: (_, record) => {
  459. const allConfident = Object.values(record.confidence || {}).every(v => v !== false);
  460. if (allConfident) {
  461. return (
  462. <Tooltip content={t('所有上游数据均可信')}>
  463. <Tag color="green" shape="circle" type="light" prefixIcon={<CheckCircle size={14} />}>
  464. {t('可信')}
  465. </Tag>
  466. </Tooltip>
  467. );
  468. } else {
  469. const untrustedSources = Object.entries(record.confidence || {})
  470. .filter(([_, isConfident]) => isConfident === false)
  471. .map(([name]) => name)
  472. .join(', ');
  473. return (
  474. <Tooltip content={t('以下上游数据可能不可信:') + untrustedSources}>
  475. <Tag color="yellow" shape="circle" type="light" prefixIcon={<AlertTriangle size={14} />}>
  476. {t('谨慎')}
  477. </Tag>
  478. </Tooltip>
  479. );
  480. }
  481. },
  482. },
  483. {
  484. title: t('当前值'),
  485. dataIndex: 'current',
  486. render: (text) => (
  487. <Tag color={text !== null && text !== undefined ? 'blue' : 'default'} shape="circle">
  488. {text !== null && text !== undefined ? text : t('未设置')}
  489. </Tag>
  490. ),
  491. },
  492. ...upstreamNames.map((upName) => {
  493. const channelStats = (() => {
  494. let selectableCount = 0;
  495. let selectedCount = 0;
  496. filteredDataSource.forEach((row) => {
  497. const upstreamVal = row.upstreams?.[upName];
  498. if (upstreamVal !== null && upstreamVal !== undefined && upstreamVal !== 'same') {
  499. selectableCount++;
  500. const isSelected = resolutions[row.model]?.[row.ratioType] === upstreamVal;
  501. if (isSelected) {
  502. selectedCount++;
  503. }
  504. }
  505. });
  506. return {
  507. selectableCount,
  508. selectedCount,
  509. allSelected: selectableCount > 0 && selectedCount === selectableCount,
  510. partiallySelected: selectedCount > 0 && selectedCount < selectableCount,
  511. hasSelectableItems: selectableCount > 0
  512. };
  513. })();
  514. const handleBulkSelect = (checked) => {
  515. if (checked) {
  516. filteredDataSource.forEach((row) => {
  517. const upstreamVal = row.upstreams?.[upName];
  518. if (upstreamVal !== null && upstreamVal !== undefined && upstreamVal !== 'same') {
  519. selectValue(row.model, row.ratioType, upstreamVal);
  520. }
  521. });
  522. } else {
  523. setResolutions((prev) => {
  524. const newRes = { ...prev };
  525. filteredDataSource.forEach((row) => {
  526. if (newRes[row.model]) {
  527. delete newRes[row.model][row.ratioType];
  528. if (Object.keys(newRes[row.model]).length === 0) {
  529. delete newRes[row.model];
  530. }
  531. }
  532. });
  533. return newRes;
  534. });
  535. }
  536. };
  537. return {
  538. title: channelStats.hasSelectableItems ? (
  539. <Checkbox
  540. checked={channelStats.allSelected}
  541. indeterminate={channelStats.partiallySelected}
  542. onChange={(e) => handleBulkSelect(e.target.checked)}
  543. >
  544. {upName}
  545. </Checkbox>
  546. ) : (
  547. <span>{upName}</span>
  548. ),
  549. dataIndex: upName,
  550. render: (_, record) => {
  551. const upstreamVal = record.upstreams?.[upName];
  552. const isConfident = record.confidence?.[upName] !== false;
  553. if (upstreamVal === null || upstreamVal === undefined) {
  554. return <Tag color="default" shape="circle">{t('未设置')}</Tag>;
  555. }
  556. if (upstreamVal === 'same') {
  557. return <Tag color="blue" shape="circle">{t('与本地相同')}</Tag>;
  558. }
  559. const isSelected = resolutions[record.model]?.[record.ratioType] === upstreamVal;
  560. return (
  561. <div className="flex items-center gap-2">
  562. <Checkbox
  563. checked={isSelected}
  564. onChange={(e) => {
  565. const isChecked = e.target.checked;
  566. if (isChecked) {
  567. selectValue(record.model, record.ratioType, upstreamVal);
  568. } else {
  569. setResolutions((prev) => {
  570. const newRes = { ...prev };
  571. if (newRes[record.model]) {
  572. delete newRes[record.model][record.ratioType];
  573. if (Object.keys(newRes[record.model]).length === 0) {
  574. delete newRes[record.model];
  575. }
  576. }
  577. return newRes;
  578. });
  579. }
  580. }}
  581. >
  582. {upstreamVal}
  583. </Checkbox>
  584. {!isConfident && (
  585. <Tooltip position='left' content={t('该数据可能不可信,请谨慎使用')}>
  586. <AlertTriangle size={16} className="text-yellow-500" />
  587. </Tooltip>
  588. )}
  589. </div>
  590. );
  591. },
  592. };
  593. }),
  594. ];
  595. return (
  596. <Table
  597. columns={columns}
  598. dataSource={getCurrentPageData(filteredDataSource)}
  599. pagination={{
  600. currentPage: currentPage,
  601. pageSize: pageSize,
  602. total: filteredDataSource.length,
  603. showSizeChanger: true,
  604. showQuickJumper: true,
  605. pageSizeOptions: ['5', '10', '20', '50'],
  606. onChange: (page, size) => {
  607. setCurrentPage(page);
  608. setPageSize(size);
  609. },
  610. onShowSizeChange: (current, size) => {
  611. setCurrentPage(1);
  612. setPageSize(size);
  613. }
  614. }}
  615. scroll={{ x: 'max-content' }}
  616. size='middle'
  617. loading={loading || syncLoading}
  618. />
  619. );
  620. };
  621. const updateChannelEndpoint = useCallback((channelId, endpoint) => {
  622. setChannelEndpoints(prev => ({ ...prev, [channelId]: endpoint }));
  623. }, []);
  624. const handleModalClose = () => {
  625. setModalVisible(false);
  626. if (channelSelectorRef.current) {
  627. channelSelectorRef.current.resetPagination();
  628. }
  629. };
  630. return (
  631. <>
  632. <Form.Section text={renderHeader()}>
  633. {renderDifferenceTable()}
  634. </Form.Section>
  635. <ChannelSelectorModal
  636. ref={channelSelectorRef}
  637. t={t}
  638. visible={modalVisible}
  639. onCancel={handleModalClose}
  640. onOk={confirmChannelSelection}
  641. allChannels={allChannels}
  642. selectedChannelIds={selectedChannelIds}
  643. setSelectedChannelIds={setSelectedChannelIds}
  644. channelEndpoints={channelEndpoints}
  645. updateChannelEndpoint={updateChannelEndpoint}
  646. />
  647. <ConflictConfirmModal
  648. t={t}
  649. visible={confirmVisible}
  650. items={conflictItems}
  651. onOk={async () => {
  652. setConfirmVisible(false);
  653. const curRatios = {
  654. ModelRatio: JSON.parse(props.options.ModelRatio || '{}'),
  655. CompletionRatio: JSON.parse(props.options.CompletionRatio || '{}'),
  656. CacheRatio: JSON.parse(props.options.CacheRatio || '{}'),
  657. ModelPrice: JSON.parse(props.options.ModelPrice || '{}'),
  658. };
  659. await performSync(curRatios);
  660. }}
  661. onCancel={() => setConfirmVisible(false)}
  662. />
  663. </>
  664. );
  665. }