✨ feat(ratio-sync): support /api/pricing parsing, confidence verification & UI enhancements
Backend - controller/ratio_sync.go • Parse /api/pricing response and convert to ratio / price maps. • Introduce confidence heuristic (model_ratio = 37.5 && completion_ratio = 1) to flag unreliable data. • Include confidence map when building differences and filter “same”/empty entries. - dto/ratio_sync.go • Add `ID` to UpstreamDTO, `upstreams` to UpstreamRequest, and `Confidence` to DifferenceItem. Frontend - ChannelSelectorModal.js • Re-implement with table layout, pagination, search, endpoint-type selector and mobile support. - UpstreamRatioSync.js • Send full upstream objects, add ratio-type filter, confidence badges/tooltips, retain endpoints. • Leverage ChannelSelectorModal’s pagination reset. - ChannelsTable.js – fix tag color for disabled status. - en.json – add translations for new UI labels. Motivation These changes let users sync model ratios / prices from different upstream endpoints and visually identify potentially unreliable data, improving operational safety and flexibility.
This commit is contained in:
@@ -1,115 +1,183 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect, forwardRef, useImperativeHandle } from 'react';
|
||||
import { isMobile } from '../../helpers';
|
||||
import {
|
||||
Modal,
|
||||
Transfer,
|
||||
Table,
|
||||
Input,
|
||||
Space,
|
||||
Checkbox,
|
||||
Avatar,
|
||||
Highlight,
|
||||
Select,
|
||||
Tag,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import { IconClose } from '@douyinfe/semi-icons';
|
||||
import { IconSearch } from '@douyinfe/semi-icons';
|
||||
import { CheckCircle, XCircle, AlertCircle, HelpCircle } from 'lucide-react';
|
||||
|
||||
const CHANNEL_STATUS_CONFIG = {
|
||||
1: { color: 'green', text: '启用' },
|
||||
2: { color: 'red', text: '禁用' },
|
||||
3: { color: 'amber', text: '自禁' },
|
||||
default: { color: 'grey', text: '未知' }
|
||||
};
|
||||
|
||||
const getChannelStatusConfig = (status) => {
|
||||
return CHANNEL_STATUS_CONFIG[status] || CHANNEL_STATUS_CONFIG.default;
|
||||
};
|
||||
|
||||
export default function ChannelSelectorModal({
|
||||
t,
|
||||
const ChannelSelectorModal = forwardRef(({
|
||||
visible,
|
||||
onCancel,
|
||||
onOk,
|
||||
allChannels = [],
|
||||
selectedChannelIds = [],
|
||||
allChannels,
|
||||
selectedChannelIds,
|
||||
setSelectedChannelIds,
|
||||
channelEndpoints,
|
||||
updateChannelEndpoint,
|
||||
}) {
|
||||
t,
|
||||
}, ref) => {
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
|
||||
const ChannelInfo = ({ item, showEndpoint = false, isSelected = false }) => {
|
||||
const channelId = item.key || item.value;
|
||||
const currentEndpoint = channelEndpoints[channelId];
|
||||
const baseUrl = item._originalData?.base_url || '';
|
||||
const status = item._originalData?.status || 0;
|
||||
const statusConfig = getChannelStatusConfig(status);
|
||||
const [filteredData, setFilteredData] = useState([]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Avatar color={statusConfig.color} size="small">
|
||||
{statusConfig.text}
|
||||
</Avatar>
|
||||
<div className="info">
|
||||
<div className="name">
|
||||
{isSelected ? (
|
||||
item.label
|
||||
) : (
|
||||
<Highlight sourceString={item.label} searchWords={[searchText]} />
|
||||
)}
|
||||
</div>
|
||||
<div className="email" style={showEndpoint ? { display: 'flex', alignItems: 'center', gap: '4px' } : {}}>
|
||||
<span className="text-xs text-gray-500 truncate max-w-[200px]" title={baseUrl}>
|
||||
{isSelected ? (
|
||||
baseUrl
|
||||
) : (
|
||||
<Highlight sourceString={baseUrl} searchWords={[searchText]} />
|
||||
)}
|
||||
</span>
|
||||
{showEndpoint && (
|
||||
<Input
|
||||
size="small"
|
||||
value={currentEndpoint}
|
||||
onChange={(value) => updateChannelEndpoint(channelId, value)}
|
||||
placeholder="/api/ratio_config"
|
||||
className="flex-1 text-xs"
|
||||
style={{ fontSize: '12px' }}
|
||||
/>
|
||||
)}
|
||||
{isSelected && !showEndpoint && (
|
||||
<span className="text-xs text-gray-700 font-mono bg-gray-100 px-2 py-1 rounded ml-2">
|
||||
{currentEndpoint}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
useImperativeHandle(ref, () => ({
|
||||
resetPagination: () => {
|
||||
setCurrentPage(1);
|
||||
setSearchText('');
|
||||
},
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
if (!allChannels) return;
|
||||
|
||||
const searchLower = searchText.trim().toLowerCase();
|
||||
const matched = searchLower
|
||||
? allChannels.filter((item) => {
|
||||
const name = (item.label || '').toLowerCase();
|
||||
const baseUrl = (item._originalData?.base_url || '').toLowerCase();
|
||||
return name.includes(searchLower) || baseUrl.includes(searchLower);
|
||||
})
|
||||
: allChannels;
|
||||
|
||||
setFilteredData(matched);
|
||||
}, [allChannels, searchText]);
|
||||
|
||||
const total = filteredData.length;
|
||||
|
||||
const paginatedData = filteredData.slice(
|
||||
(currentPage - 1) * pageSize,
|
||||
currentPage * pageSize,
|
||||
);
|
||||
|
||||
const updateEndpoint = (channelId, endpoint) => {
|
||||
if (typeof updateChannelEndpoint === 'function') {
|
||||
updateChannelEndpoint(channelId, endpoint);
|
||||
}
|
||||
};
|
||||
|
||||
const renderSourceItem = (item) => {
|
||||
const renderEndpointCell = (text, record) => {
|
||||
const channelId = record.key || record.value;
|
||||
const currentEndpoint = channelEndpoints[channelId] || '';
|
||||
|
||||
const getEndpointType = (ep) => {
|
||||
if (ep === '/api/ratio_config') return 'ratio_config';
|
||||
if (ep === '/api/pricing') return 'pricing';
|
||||
return 'custom';
|
||||
};
|
||||
|
||||
const currentType = getEndpointType(currentEndpoint);
|
||||
|
||||
const handleTypeChange = (val) => {
|
||||
if (val === 'ratio_config') {
|
||||
updateEndpoint(channelId, '/api/ratio_config');
|
||||
} else if (val === 'pricing') {
|
||||
updateEndpoint(channelId, '/api/pricing');
|
||||
} else {
|
||||
if (currentType !== 'custom') {
|
||||
updateEndpoint(channelId, '');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="components-transfer-source-item" key={item.key}>
|
||||
<Checkbox
|
||||
onChange={item.onChange}
|
||||
checked={item.checked}
|
||||
style={{ height: 52, alignItems: 'center' }}
|
||||
>
|
||||
<ChannelInfo item={item} showEndpoint={true} />
|
||||
</Checkbox>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Select
|
||||
size="small"
|
||||
value={currentType}
|
||||
onChange={handleTypeChange}
|
||||
style={{ width: 120 }}
|
||||
optionList={[
|
||||
{ label: 'ratio_config', value: 'ratio_config' },
|
||||
{ label: 'pricing', value: 'pricing' },
|
||||
{ label: 'custom', value: 'custom' },
|
||||
]}
|
||||
/>
|
||||
{currentType === 'custom' && (
|
||||
<Input
|
||||
size="small"
|
||||
value={currentEndpoint}
|
||||
onChange={(val) => updateEndpoint(channelId, val)}
|
||||
placeholder="/your/endpoint"
|
||||
style={{ width: 160, fontSize: 12 }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderSelectedItem = (item) => {
|
||||
return (
|
||||
<div className="components-transfer-selected-item" key={item.key}>
|
||||
<ChannelInfo item={item} isSelected={true} />
|
||||
<IconClose style={{ cursor: 'pointer' }} onClick={item.onRemove} />
|
||||
</div>
|
||||
);
|
||||
const renderStatusCell = (status) => {
|
||||
switch (status) {
|
||||
case 1:
|
||||
return (
|
||||
<Tag size='large' color='green' shape='circle' prefixIcon={<CheckCircle size={14} />}>
|
||||
{t('已启用')}
|
||||
</Tag>
|
||||
);
|
||||
case 2:
|
||||
return (
|
||||
<Tag size='large' color='red' shape='circle' prefixIcon={<XCircle size={14} />}>
|
||||
{t('已禁用')}
|
||||
</Tag>
|
||||
);
|
||||
case 3:
|
||||
return (
|
||||
<Tag size='large' color='yellow' shape='circle' prefixIcon={<AlertCircle size={14} />}>
|
||||
{t('自动禁用')}
|
||||
</Tag>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<Tag size='large' color='grey' shape='circle' prefixIcon={<HelpCircle size={14} />}>
|
||||
{t('未知状态')}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const channelFilter = (input, item) => {
|
||||
const searchLower = input.toLowerCase();
|
||||
return item.label.toLowerCase().includes(searchLower) ||
|
||||
(item._originalData?.base_url || '').toLowerCase().includes(searchLower);
|
||||
const renderNameCell = (text) => (
|
||||
<Highlight sourceString={text} searchWords={[searchText]} />
|
||||
);
|
||||
|
||||
const renderBaseUrlCell = (text) => (
|
||||
<Highlight sourceString={text} searchWords={[searchText]} />
|
||||
);
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: t('名称'),
|
||||
dataIndex: 'label',
|
||||
render: renderNameCell,
|
||||
},
|
||||
{
|
||||
title: t('源地址'),
|
||||
dataIndex: '_originalData.base_url',
|
||||
render: (_, record) => renderBaseUrlCell(record._originalData?.base_url || ''),
|
||||
},
|
||||
{
|
||||
title: t('状态'),
|
||||
dataIndex: '_originalData.status',
|
||||
render: (_, record) => renderStatusCell(record._originalData?.status || 0),
|
||||
},
|
||||
{
|
||||
title: t('同步接口'),
|
||||
dataIndex: 'endpoint',
|
||||
fixed: 'right',
|
||||
render: renderEndpointCell,
|
||||
},
|
||||
];
|
||||
|
||||
const rowSelection = {
|
||||
selectedRowKeys: selectedChannelIds,
|
||||
onChange: (keys) => setSelectedChannelIds(keys),
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -118,26 +186,51 @@ export default function ChannelSelectorModal({
|
||||
onCancel={onCancel}
|
||||
onOk={onOk}
|
||||
title={<span className="text-lg font-semibold">{t('选择同步渠道')}</span>}
|
||||
width={1000}
|
||||
size={isMobile() ? 'full-width' : 'large'}
|
||||
keepDOM
|
||||
lazyRender={false}
|
||||
>
|
||||
<Space vertical style={{ width: '100%' }}>
|
||||
<Transfer
|
||||
style={{ width: '100%' }}
|
||||
dataSource={allChannels}
|
||||
value={selectedChannelIds}
|
||||
onChange={setSelectedChannelIds}
|
||||
renderSourceItem={renderSourceItem}
|
||||
renderSelectedItem={renderSelectedItem}
|
||||
filter={channelFilter}
|
||||
inputProps={{ placeholder: t('搜索渠道名称或地址') }}
|
||||
onSearch={setSearchText}
|
||||
emptyContent={{
|
||||
left: t('暂无渠道'),
|
||||
right: t('暂无选择'),
|
||||
search: t('无搜索结果'),
|
||||
<Input
|
||||
prefix={<IconSearch size={14} />}
|
||||
placeholder={t('搜索渠道名称或地址')}
|
||||
value={searchText}
|
||||
onChange={setSearchText}
|
||||
showClear
|
||||
className="!rounded-full"
|
||||
/>
|
||||
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={paginatedData}
|
||||
rowKey="key"
|
||||
rowSelection={rowSelection}
|
||||
pagination={{
|
||||
currentPage: currentPage,
|
||||
pageSize: pageSize,
|
||||
total: total,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
pageSizeOptions: ['10', '20', '50', '100'],
|
||||
formatPageText: (page) => t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
|
||||
start: page.currentStart,
|
||||
end: page.currentEnd,
|
||||
total: total,
|
||||
}),
|
||||
onChange: (page, size) => {
|
||||
setCurrentPage(page);
|
||||
setPageSize(size);
|
||||
},
|
||||
onShowSizeChange: (curr, size) => {
|
||||
setCurrentPage(1);
|
||||
setPageSize(size);
|
||||
},
|
||||
}}
|
||||
size="small"
|
||||
/>
|
||||
</Space>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default ChannelSelectorModal;
|
||||
@@ -114,7 +114,7 @@ const ChannelsTable = () => {
|
||||
);
|
||||
case 2:
|
||||
return (
|
||||
<Tag size='large' color='yellow' shape='circle' prefixIcon={<XCircle size={14} />}>
|
||||
<Tag size='large' color='red' shape='circle' prefixIcon={<XCircle size={14} />}>
|
||||
{t('已禁用')}
|
||||
</Tag>
|
||||
);
|
||||
|
||||
@@ -1701,5 +1701,14 @@
|
||||
"充值分组倍率": "Recharge group ratio",
|
||||
"充值方式设置": "Recharge method settings",
|
||||
"更新支付设置": "Update payment settings",
|
||||
"通知": "Notice"
|
||||
"通知": "Notice",
|
||||
"源地址": "Source address",
|
||||
"同步接口": "Synchronization interface",
|
||||
"置信度": "Confidence",
|
||||
"谨慎": "Cautious",
|
||||
"该数据可能不可信,请谨慎使用": "This data may not be reliable, please use with caution",
|
||||
"可信": "Reliable",
|
||||
"所有上游数据均可信": "All upstream data is reliable",
|
||||
"以下上游数据可能不可信:": "The following upstream data may not be reliable: ",
|
||||
"按倍率类型筛选": "Filter by ratio type"
|
||||
}
|
||||
@@ -7,11 +7,15 @@ import {
|
||||
Checkbox,
|
||||
Form,
|
||||
Input,
|
||||
Tooltip,
|
||||
Select,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import { IconSearch } from '@douyinfe/semi-icons';
|
||||
import {
|
||||
RefreshCcw,
|
||||
CheckSquare,
|
||||
AlertTriangle,
|
||||
CheckCircle,
|
||||
} from 'lucide-react';
|
||||
import { API, showError, showSuccess, showWarning, stringToColor } from '../../../helpers';
|
||||
import { DEFAULT_ENDPOINT } from '../../../constants';
|
||||
@@ -49,6 +53,11 @@ export default function UpstreamRatioSync(props) {
|
||||
// 搜索相关状态
|
||||
const [searchKeyword, setSearchKeyword] = useState('');
|
||||
|
||||
// 倍率类型过滤
|
||||
const [ratioTypeFilter, setRatioTypeFilter] = useState('');
|
||||
|
||||
const channelSelectorRef = React.useRef(null);
|
||||
|
||||
const fetchAllChannels = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
@@ -67,11 +76,16 @@ export default function UpstreamRatioSync(props) {
|
||||
|
||||
setAllChannels(transferData);
|
||||
|
||||
const initialEndpoints = {};
|
||||
transferData.forEach(channel => {
|
||||
initialEndpoints[channel.key] = DEFAULT_ENDPOINT;
|
||||
// 合并已有 endpoints,避免每次打开弹窗都重置
|
||||
setChannelEndpoints(prev => {
|
||||
const merged = { ...prev };
|
||||
transferData.forEach(channel => {
|
||||
if (!merged[channel.key]) {
|
||||
merged[channel.key] = DEFAULT_ENDPOINT;
|
||||
}
|
||||
});
|
||||
return merged;
|
||||
});
|
||||
setChannelEndpoints(initialEndpoints);
|
||||
} else {
|
||||
showError(res.data.message);
|
||||
}
|
||||
@@ -99,8 +113,15 @@ export default function UpstreamRatioSync(props) {
|
||||
const fetchRatiosFromChannels = async (channelList) => {
|
||||
setSyncLoading(true);
|
||||
|
||||
const upstreams = channelList.map(ch => ({
|
||||
id: ch.id,
|
||||
name: ch.name,
|
||||
base_url: ch.base_url,
|
||||
endpoint: channelEndpoints[ch.id] || DEFAULT_ENDPOINT,
|
||||
}));
|
||||
|
||||
const payload = {
|
||||
channel_ids: channelList.map(ch => parseInt(ch.id)),
|
||||
upstreams: upstreams,
|
||||
timeout: 10,
|
||||
};
|
||||
|
||||
@@ -215,13 +236,15 @@ export default function UpstreamRatioSync(props) {
|
||||
const renderHeader = () => (
|
||||
<div className="flex flex-col w-full">
|
||||
<div className="flex flex-col md:flex-row justify-between items-center gap-4 w-full">
|
||||
<div className="flex gap-2 w-full md:w-auto order-2 md:order-1">
|
||||
<div className="flex flex-col md:flex-row gap-2 w-full md:w-auto order-2 md:order-1">
|
||||
<Button
|
||||
icon={<RefreshCcw size={14} />}
|
||||
className="!rounded-full w-full md:w-auto mt-2"
|
||||
onClick={() => {
|
||||
setModalVisible(true);
|
||||
fetchAllChannels();
|
||||
if (allChannels.length === 0) {
|
||||
fetchAllChannels();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t('选择同步渠道')}
|
||||
@@ -243,14 +266,30 @@ export default function UpstreamRatioSync(props) {
|
||||
);
|
||||
})()}
|
||||
|
||||
<Input
|
||||
prefix={<IconSearch size={14} />}
|
||||
placeholder={t('搜索模型名称')}
|
||||
value={searchKeyword}
|
||||
onChange={setSearchKeyword}
|
||||
className="!rounded-full w-full md:w-64 mt-2"
|
||||
showClear
|
||||
/>
|
||||
<div className="flex flex-col sm:flex-row gap-2 w-full md:w-auto mt-2">
|
||||
<Input
|
||||
prefix={<IconSearch size={14} />}
|
||||
placeholder={t('搜索模型名称')}
|
||||
value={searchKeyword}
|
||||
onChange={setSearchKeyword}
|
||||
className="!rounded-full w-full sm:w-64"
|
||||
showClear
|
||||
/>
|
||||
|
||||
<Select
|
||||
placeholder={t('按倍率类型筛选')}
|
||||
value={ratioTypeFilter}
|
||||
onChange={setRatioTypeFilter}
|
||||
className="!rounded-full w-full sm:w-48"
|
||||
showClear
|
||||
onClear={() => setRatioTypeFilter('')}
|
||||
>
|
||||
<Select.Option value="model_ratio">{t('模型倍率')}</Select.Option>
|
||||
<Select.Option value="completion_ratio">{t('补全倍率')}</Select.Option>
|
||||
<Select.Option value="cache_ratio">{t('缓存倍率')}</Select.Option>
|
||||
<Select.Option value="model_price">{t('固定价格')}</Select.Option>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -268,6 +307,7 @@ export default function UpstreamRatioSync(props) {
|
||||
ratioType,
|
||||
current: diff.current,
|
||||
upstreams: diff.upstreams,
|
||||
confidence: diff.confidence || {},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -276,15 +316,20 @@ export default function UpstreamRatioSync(props) {
|
||||
}, [differences]);
|
||||
|
||||
const filteredDataSource = useMemo(() => {
|
||||
if (!searchKeyword.trim()) {
|
||||
if (!searchKeyword.trim() && !ratioTypeFilter) {
|
||||
return dataSource;
|
||||
}
|
||||
|
||||
const keyword = searchKeyword.toLowerCase().trim();
|
||||
return dataSource.filter(item =>
|
||||
item.model.toLowerCase().includes(keyword)
|
||||
);
|
||||
}, [dataSource, searchKeyword]);
|
||||
return dataSource.filter(item => {
|
||||
const matchesKeyword = !searchKeyword.trim() ||
|
||||
item.model.toLowerCase().includes(searchKeyword.toLowerCase().trim());
|
||||
|
||||
const matchesRatioType = !ratioTypeFilter ||
|
||||
item.ratioType === ratioTypeFilter;
|
||||
|
||||
return matchesKeyword && matchesRatioType;
|
||||
});
|
||||
}, [dataSource, searchKeyword, ratioTypeFilter]);
|
||||
|
||||
const upstreamNames = useMemo(() => {
|
||||
const set = new Set();
|
||||
@@ -330,6 +375,36 @@ export default function UpstreamRatioSync(props) {
|
||||
return <Tag color={stringToColor(text)} shape="circle">{typeMap[text] || text}</Tag>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t('置信度'),
|
||||
dataIndex: 'confidence',
|
||||
render: (_, record) => {
|
||||
const allConfident = Object.values(record.confidence || {}).every(v => v !== false);
|
||||
|
||||
if (allConfident) {
|
||||
return (
|
||||
<Tooltip content={t('所有上游数据均可信')}>
|
||||
<Tag color="green" shape="circle" type="light" prefixIcon={<CheckCircle size={14} />}>
|
||||
{t('可信')}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
);
|
||||
} else {
|
||||
const untrustedSources = Object.entries(record.confidence || {})
|
||||
.filter(([_, isConfident]) => isConfident === false)
|
||||
.map(([name]) => name)
|
||||
.join(', ');
|
||||
|
||||
return (
|
||||
<Tooltip content={t('以下上游数据可能不可信:') + untrustedSources}>
|
||||
<Tag color="yellow" shape="circle" type="light" prefixIcon={<AlertTriangle size={14} />}>
|
||||
{t('谨慎')}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t('当前值'),
|
||||
dataIndex: 'current',
|
||||
@@ -404,6 +479,7 @@ export default function UpstreamRatioSync(props) {
|
||||
dataIndex: upName,
|
||||
render: (_, record) => {
|
||||
const upstreamVal = record.upstreams?.[upName];
|
||||
const isConfident = record.confidence?.[upName] !== false;
|
||||
|
||||
if (upstreamVal === null || upstreamVal === undefined) {
|
||||
return <Tag color="default" shape="circle">{t('未设置')}</Tag>;
|
||||
@@ -416,28 +492,35 @@ export default function UpstreamRatioSync(props) {
|
||||
const isSelected = resolutions[record.model]?.[record.ratioType] === upstreamVal;
|
||||
|
||||
return (
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
onChange={(e) => {
|
||||
const isChecked = e.target.checked;
|
||||
if (isChecked) {
|
||||
selectValue(record.model, record.ratioType, upstreamVal);
|
||||
} else {
|
||||
setResolutions((prev) => {
|
||||
const newRes = { ...prev };
|
||||
if (newRes[record.model]) {
|
||||
delete newRes[record.model][record.ratioType];
|
||||
if (Object.keys(newRes[record.model]).length === 0) {
|
||||
delete newRes[record.model];
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
onChange={(e) => {
|
||||
const isChecked = e.target.checked;
|
||||
if (isChecked) {
|
||||
selectValue(record.model, record.ratioType, upstreamVal);
|
||||
} else {
|
||||
setResolutions((prev) => {
|
||||
const newRes = { ...prev };
|
||||
if (newRes[record.model]) {
|
||||
delete newRes[record.model][record.ratioType];
|
||||
if (Object.keys(newRes[record.model]).length === 0) {
|
||||
delete newRes[record.model];
|
||||
}
|
||||
}
|
||||
}
|
||||
return newRes;
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
{upstreamVal}
|
||||
</Checkbox>
|
||||
return newRes;
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
{upstreamVal}
|
||||
</Checkbox>
|
||||
{!isConfident && (
|
||||
<Tooltip position='left' content={t('该数据可能不可信,请谨慎使用')}>
|
||||
<AlertTriangle size={16} className="text-yellow-500" />
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
@@ -481,6 +564,13 @@ export default function UpstreamRatioSync(props) {
|
||||
setChannelEndpoints(prev => ({ ...prev, [channelId]: endpoint }));
|
||||
}, []);
|
||||
|
||||
const handleModalClose = () => {
|
||||
setModalVisible(false);
|
||||
if (channelSelectorRef.current) {
|
||||
channelSelectorRef.current.resetPagination();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form.Section text={renderHeader()}>
|
||||
@@ -488,9 +578,10 @@ export default function UpstreamRatioSync(props) {
|
||||
</Form.Section>
|
||||
|
||||
<ChannelSelectorModal
|
||||
ref={channelSelectorRef}
|
||||
t={t}
|
||||
visible={modalVisible}
|
||||
onCancel={() => setModalVisible(false)}
|
||||
onCancel={handleModalClose}
|
||||
onOk={confirmChannelSelection}
|
||||
allChannels={allChannels}
|
||||
selectedChannelIds={selectedChannelIds}
|
||||
|
||||
Reference in New Issue
Block a user