tokenFactory/web/src/components/table/model-pricing/view/card/PricingCardView.jsx

1146 lines
38 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import React from 'react';
import {
Card,
Tag,
Tooltip,
Checkbox,
Empty,
Pagination,
Button,
Avatar,
} from '@douyinfe/semi-ui';
import { IconHelpCircle } from '@douyinfe/semi-icons';
import { Copy } from 'lucide-react';
import {
IllustrationNoResult,
IllustrationNoResultDark,
} from '@douyinfe/semi-illustrations';
import {
stringToColor,
calculateModelPrice,
getModelPriceItems,
getLobeHubIcon,
getUsedGroupContext,
pickChannelScopedModelFloat,
computeChannelBillingRates,
} from '../../../../../helpers';
import PricingCardSkeleton from './PricingCardSkeleton';
import { useMinimumLoadingTime } from '../../../../../hooks/common/useMinimumLoadingTime';
import { renderLimitedItems } from '../../../../common/ui/RenderUtils';
import { useIsMobile } from '../../../../../hooks/common/useIsMobile';
const CARD_STYLES = {
container:
'w-12 h-12 rounded-2xl flex items-center justify-center relative shadow-md',
icon: 'w-8 h-8 flex items-center justify-center',
selected: 'border-blue-500 bg-blue-50',
default: 'border-gray-200 hover:border-gray-300',
};
const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const PricingCardView = ({
filteredModels,
loading,
rowSelection,
pageSize,
setPageSize,
currentPage,
setCurrentPage,
selectedGroup,
groupRatio,
groupModelPrice,
groupModelRatio,
copyText,
setModalImageUrl,
setIsModalOpenurl,
currency,
siteDisplayType,
tokenUnit,
displayPrice,
showRatio,
t,
selectedRowKeys = [],
setSelectedRowKeys,
openModelDetail,
showSizeChanger = true,
blurPricing = false,
searchValue = '',
channelVideoRatio = {},
channelVideoCompletionRatio = {},
channelVideoPrice = {},
gridCols = 0, // 当 > 0 时使用 grid 布局(如 gridCols=2 表示 2 列)
}) => {
const showSkeleton = useMinimumLoadingTime(loading);
const startIndex = (currentPage - 1) * pageSize;
const paginatedModels = filteredModels.slice(
startIndex,
startIndex + pageSize,
);
const getModelKey = (model) => model.key ?? model.model_name ?? model.id;
const isMobile = useIsMobile();
const normalizedSearchValue = String(searchValue || '').trim();
const renderHighlightedText = (value) => {
const text = value == null ? '' : String(value);
if (!normalizedSearchValue) return text;
const regex = new RegExp(`(${escapeRegExp(normalizedSearchValue)})`, 'ig');
return text.split(regex).map((part, idx) =>
part.toLowerCase() === normalizedSearchValue.toLowerCase() ? (
<span
key={idx}
style={{
color: '#ef4444',
fontWeight: 700,
backgroundColor: 'rgba(239, 68, 68, 0.12)',
borderRadius: 4,
}}
>
{part}
</span>
) : (
part
),
);
};
const handleCheckboxChange = (model, checked) => {
if (!setSelectedRowKeys) return;
const modelKey = getModelKey(model);
const newKeys = checked
? Array.from(new Set([...selectedRowKeys, modelKey]))
: selectedRowKeys.filter((key) => key !== modelKey);
setSelectedRowKeys(newKeys);
rowSelection?.onChange?.(newKeys, null);
};
// 根据 supplier_type 返回对应的 Tag 颜色
const getSupplierTypeColor = (supplierType) => {
switch (supplierType) {
case '公有云':
return 'green';
case 'AIDC':
return 'light-green';
case '企业中转站':
return 'lime';
case '个人中转站':
return 'yellow';
default:
return stringToColor(supplierType);
}
};
// 根据模型的 channel_list 推导可展示的供应商项。
// 无 logo 时不展示 supplier_alias仅保留供应商类型标签。
const getSupplierLogos = (model) => {
if (!model?.channel_list || model.channel_list.length === 0) return [];
const seen = new Set();
const items = [];
model.channel_list.forEach((ch, idx) => {
const logo =
(ch?.company_logo_url && String(ch.company_logo_url).trim()) || '';
const supplierType =
(ch?.supplier_type && String(ch.supplier_type).trim()) || '';
const alias =
(ch?.supplier_alias && String(ch.supplier_alias).trim()) || '';
const name = ch?.channel_name || '';
if (!logo && !supplierType) return;
const displayAlias = logo ? alias : '';
const dedupKey = `${logo}|${supplierType}|${displayAlias}`;
if (seen.has(dedupKey)) return;
seen.add(dedupKey);
items.push({
key: ch?.channel_id ?? `${dedupKey}-${idx}`,
logo,
supplierType,
alias: displayAlias,
name,
});
});
return items;
};
const calculateChannelPrices = (model, opts = {}) => {
const { skipSimpleVideoFlat = false, skipSimpleFixed = false } = opts;
if (!model.channel_list || model.channel_list.length === 0) {
return null;
}
const { usedGroupRatio } = getUsedGroupContext(
model,
selectedGroup,
groupRatio,
);
// 辅助函数:格式化价格
const formatPrice = (priceUSD) => {
const rawDisplayPrice = displayPrice(priceUSD);
const unitDivisor = tokenUnit === 'K' ? 1000 : 1;
const numericPrice =
parseFloat(rawDisplayPrice.replace(/[^0-9.]/g, '')) / unitDivisor;
let symbol = '$';
if (currency === 'CNY') {
symbol = '¥';
} else if (currency === 'CUSTOM') {
try {
const statusStr = localStorage.getItem('status');
if (statusStr) {
const s = JSON.parse(statusStr);
symbol = s?.custom_currency_symbol || '¤';
}
} catch (e) {
symbol = '¤';
}
}
return { value: parseFloat(numericPrice.toFixed(2)), symbol };
};
const modelHasVideoRatio =
model.video_ratio != null &&
model.video_ratio !== undefined &&
Number.isFinite(Number(model.video_ratio));
const modelHasVideoCompletion =
model.video_completion_ratio != null &&
model.video_completion_ratio !== undefined &&
Number.isFinite(Number(model.video_completion_ratio));
const modelHasVideoFlatPrice =
model.video_price != null &&
model.video_price !== undefined &&
Number.isFinite(Number(model.video_price));
// 提取所有通道的价格(与 relay 一致ch.model_ratio 已含渠道折扣;再乘分组倍率)
const prices = {
input: [],
output: [],
cache: [],
createCache: [],
fixed: [],
videoToken: [],
videoFlat: [],
};
const originalPrices = {
input: [],
output: [],
cache: [],
createCache: [],
fixed: [],
videoToken: [],
videoFlat: [],
};
model.channel_list.forEach((ch) => {
const cid = ch.channel_id;
const mname = model.model_name;
// ============================================================
// 新计费公式参数:
// ch.model_ratio / ch.model_price 为原始渠道倍率(后端不再预乘成本折扣)
// 成本折扣率 = price_discount_percent / 100
// 加价倍率 = markup_discount_rate / 100
//
// 输入 = (ch.model_ratio × costDisc + globalMr × markupRate) × 2 × groupRatio
// 输出 = (ch.model_ratio × cr × costDisc + globalMr × globalCR × markupRate) × 2 × groupRatio
// 缓存读 = (ch.model_ratio × cacheRatio × costDisc + globalMr × globalCacheR × markupRate) × 2 × groupRatio
// 缓存写 = (ch.model_ratio × createCacheRatio × costDisc + globalMr × globalCreateCacheR × markupRate) × 2 × groupRatio
// 固定价 = (ch.model_price × costDisc + globalMp × markupRate) × groupRatio
// ============================================================
const priceDiscountPercent =
ch.price_discount_percent != null ? ch.price_discount_percent : 100;
const markupDiscountPercent = ch.markup_discount_rate || 0;
const globalMr = model.model_ratio || 0;
const globalMp = model.model_price || 0;
const channelVideoFlatUsd =
pickChannelScopedModelFloat(channelVideoPrice, cid, mname);
const flatUsd =
channelVideoFlatUsd != null
? channelVideoFlatUsd
: modelHasVideoFlatPrice
? Number(model.video_price)
: null;
if (!skipSimpleVideoFlat && flatUsd != null && flatUsd > 0) {
prices.videoFlat.push(formatPrice(flatUsd * usedGroupRatio));
originalPrices.videoFlat.push(formatPrice(flatUsd));
}
// 全局子倍率(用于加价侧)
const globalCR = model.completion_ratio || 0;
const globalCacheR =
model.cache_ratio != null ? Number(model.cache_ratio) : 0;
const globalCreateCacheR =
model.create_cache_ratio != null
? Number(model.create_cache_ratio)
: 0;
const billingRates = computeChannelBillingRates({
channelModelRatio: ch.model_ratio,
channelCompletionRatio: ch.completion_ratio,
channelCacheRatio: ch.cache_ratio,
channelCreateCacheRatio: ch.create_cache_ratio,
channelModelPrice: ch.model_price,
priceDiscountPercent,
markupDiscountPercent,
globalModelRatio: globalMr,
globalModelPrice: globalMp,
globalCompletionRatio: globalCR,
globalCacheRatio: globalCacheR,
globalCreateCacheRatio: globalCreateCacheR,
});
// 按量计费
if (model.quota_type === 0) {
if (ch.model_ratio !== undefined && ch.model_ratio !== null) {
prices.input.push(
formatPrice(billingRates.inputRatioPrice * usedGroupRatio),
);
originalPrices.input.push(formatPrice(billingRates.inputRatioPrice));
// 输出价格:仅当全局模型配置了 completion_ratio 时才展示
if (
ch.completion_ratio !== undefined &&
ch.completion_ratio !== null &&
model.completion_ratio != null
) {
prices.output.push(
formatPrice(billingRates.completionRatioPrice * usedGroupRatio),
);
originalPrices.output.push(
formatPrice(billingRates.completionRatioPrice),
);
}
// 缓存读取价格:仅当全局模型配置了 cache_ratio 时才展示
if (
ch.cache_ratio !== undefined &&
ch.cache_ratio !== null &&
model.cache_ratio != null
) {
prices.cache.push(
formatPrice(billingRates.cacheRatioPrice * usedGroupRatio),
);
originalPrices.cache.push(formatPrice(billingRates.cacheRatioPrice));
}
// 缓存创建价格:仅当全局模型配置了 create_cache_ratio 时才展示
if (
ch.create_cache_ratio !== undefined &&
ch.create_cache_ratio !== null &&
model.create_cache_ratio != null
) {
prices.createCache.push(
formatPrice(
billingRates.cacheCreationRatioPrice * usedGroupRatio,
),
);
originalPrices.createCache.push(
formatPrice(billingRates.cacheCreationRatioPrice),
);
}
const vrCh = pickChannelScopedModelFloat(
channelVideoRatio,
cid,
mname,
);
const vcrCh = pickChannelScopedModelFloat(
channelVideoCompletionRatio,
cid,
mname,
);
const effVr =
vrCh != null
? vrCh
: modelHasVideoRatio
? Number(model.video_ratio)
: 1;
const effVcr =
vcrCh != null
? vcrCh
: modelHasVideoCompletion
? Number(model.video_completion_ratio)
: 1;
const showVideoToken =
modelHasVideoRatio ||
modelHasVideoCompletion ||
vrCh != null ||
vcrCh != null;
if (showVideoToken) {
const videoTokUsd =
billingRates.inputRatioPrice * effVr * effVcr * usedGroupRatio;
prices.videoToken.push(formatPrice(videoTokUsd));
originalPrices.videoToken.push(
formatPrice(billingRates.inputRatioPrice * effVr * effVcr),
);
}
}
}
// 按次计费
else if (model.quota_type === 1 || ch.quota_type === 1) {
if (!skipSimpleFixed && ch.model_price !== undefined && ch.model_price !== null) {
prices.fixed.push(
formatPrice(billingRates.effModelPrice * usedGroupRatio),
);
originalPrices.fixed.push(formatPrice(billingRates.effModelPrice));
}
}
});
// 根数据价格(用同一口径计算,用于与 channel 价格比较)
const rootPrices = {};
if (model.quota_type === 0) {
if (model.model_ratio !== undefined && model.model_ratio !== null) {
rootPrices.input = formatPrice(model.model_ratio * 2);
if (
model.completion_ratio !== undefined &&
model.completion_ratio !== null
) {
rootPrices.output = formatPrice(
model.model_ratio * model.completion_ratio * 2,
);
}
if (model.cache_ratio !== undefined && model.cache_ratio !== null) {
rootPrices.cache = formatPrice(
model.model_ratio * model.cache_ratio * 2,
);
}
if (
model.create_cache_ratio !== undefined &&
model.create_cache_ratio !== null
) {
rootPrices.createCache = formatPrice(
model.model_ratio * model.create_cache_ratio * 2,
);
}
}
} else if (model.quota_type === 1) {
if (
!skipSimpleFixed &&
model.model_price !== undefined &&
model.model_price !== null
) {
rootPrices.fixed = formatPrice(model.model_price);
}
}
if (model.quota_type === 0) {
if (model.model_ratio !== undefined && model.model_ratio !== null) {
const rootVr = modelHasVideoRatio ? Number(model.video_ratio) : 1;
const rootVcr = modelHasVideoCompletion
? Number(model.video_completion_ratio)
: 1;
if (modelHasVideoRatio || modelHasVideoCompletion) {
rootPrices.videoToken = formatPrice(
model.model_ratio * rootVr * rootVcr * 2,
);
}
}
}
if (modelHasVideoFlatPrice && !skipSimpleVideoFlat) {
rootPrices.videoFlat = formatPrice(Number(model.video_price));
}
// 若根价格高于任意一个 channel 的对应价格,则返回划线原价与折扣
const getOriginal = (rootPrice, channelPriceArray) => {
if (!rootPrice || !channelPriceArray || channelPriceArray.length === 0)
return null;
const minChannel = Math.min(...channelPriceArray.map((p) => p.value));
if (rootPrice.value > minChannel && rootPrice.value > 0) {
const discount = Math.round((1 - minChannel / rootPrice.value) * 100);
return {
text: `${rootPrice.symbol}${rootPrice.value}`,
discount,
};
}
return null;
};
// 计算范围
const calculateRange = (priceArray) => {
if (priceArray.length === 0) return null;
if (priceArray.length === 1) {
const p = priceArray[0];
return {
single: `${p.symbol}${p.value}`,
min: null,
max: null,
symbol: p.symbol,
};
}
const values = priceArray.map((p) => p.value);
const uniqueValues = [...new Set(values)];
if (uniqueValues.length === 1) {
const p = priceArray[0];
return {
single: `${p.symbol}${p.value}`,
min: null,
max: null,
symbol: p.symbol,
};
}
const min = Math.min(...values);
const max = Math.max(...values);
const symbol = priceArray[0].symbol;
return { single: null, min, max, symbol };
};
const unitLabel = tokenUnit === 'K' ? 'K' : 'M';
const unitSuffix = ` / 1${unitLabel} Tokens`;
const fixedSuffix = ` / ${t('次')}`;
return {
input: calculateRange(prices.input),
output: calculateRange(prices.output),
cache: calculateRange(prices.cache),
createCache: calculateRange(prices.createCache),
fixed: calculateRange(prices.fixed),
original: {
input: getOriginal(rootPrices.input, originalPrices.input),
output: getOriginal(rootPrices.output, originalPrices.output),
cache: getOriginal(rootPrices.cache, originalPrices.cache),
createCache: getOriginal(
rootPrices.createCache,
originalPrices.createCache,
),
fixed: getOriginal(rootPrices.fixed, originalPrices.fixed),
videoToken: getOriginal(
rootPrices.videoToken,
originalPrices.videoToken,
),
videoFlat: getOriginal(rootPrices.videoFlat, originalPrices.videoFlat),
},
videoToken: calculateRange(prices.videoToken),
videoFlat: calculateRange(prices.videoFlat),
unitSuffix,
fixedSuffix,
videoFlatSuffix: ` / ${t('条')}`,
quotaType: model.quota_type,
};
};
// 获取模型的价格项(优先使用 channel 价格)
const getModelPriceItemsForCard = (model, priceData) => {
const hint = model.video_flat_clip_hint;
const useTieredVideoFlat =
hint &&
Number(hint.tier_count) > 0 &&
Number(hint.min_usd_after_channel_discount) > 0;
const imageHint = model.image_per_image_hint;
const useTieredImagePerImage =
imageHint &&
Number(imageHint.tier_count) > 0 &&
Number(imageHint.min_usd_after_channel_discount) > 0;
const channelPrices = calculateChannelPrices(model, {
skipSimpleVideoFlat: useTieredVideoFlat,
skipSimpleFixed: useTieredImagePerImage,
});
// 如果没有 channel 价格,使用原有逻辑
if (!channelPrices) {
return getModelPriceItems(priceData, t, siteDisplayType);
}
// 使用 channel 价格构建价格项
const items = [];
const {
input,
output,
cache,
createCache,
fixed,
original,
unitSuffix,
fixedSuffix,
videoToken,
videoFlat,
videoFlatSuffix,
quotaType,
} = channelPrices;
// 按次计费
if (quotaType === 1 && fixed) {
items.push({
key: 'fixed',
label: t('模型价格'),
value:
fixed.single ||
`${fixed.symbol}${fixed.min} ~ ${fixed.symbol}${fixed.max}`,
suffix: fixedSuffix,
original: original?.fixed,
});
}
// 按量计费
else {
if (input) {
items.push({
key: 'input',
label: t('输入价格'),
value:
input.single ||
`${input.symbol}${input.min} ~ ${input.symbol}${input.max}`,
suffix: unitSuffix,
original: original?.input,
});
}
if (output) {
items.push({
key: 'output',
label: t('输出价格'),
value:
output.single ||
`${output.symbol}${output.min} ~ ${output.symbol}${output.max}`,
suffix: unitSuffix,
original: original?.output,
});
}
if (videoToken) {
items.push({
key: 'video-token',
label: t('视频(倍率计价)'),
value:
videoToken.single ||
`${videoToken.symbol}${videoToken.min} ~ ${videoToken.symbol}${videoToken.max}`,
suffix: unitSuffix,
original: original?.videoToken,
});
}
// if (cache) {
// items.push({
// key: 'cache',
// label: t('缓存读取价格'),
// value: cache.single || `${cache.symbol}${cache.min} ~ ${cache.symbol}${cache.max}`,
// suffix: unitSuffix,
// original: original?.cache,
// });
// }
// if (createCache) {
// items.push({
// key: 'create-cache',
// label: t('缓存创建价格'),
// value: createCache.single || `${createCache.symbol}${createCache.min} ~ ${createCache.symbol}${createCache.max}`,
// suffix: unitSuffix,
// original: original?.createCache,
// });
// }
}
if (videoFlat) {
items.push({
key: 'video-flat',
label: t('视频按条(固定价)'),
value:
videoFlat.single ||
`${videoFlat.symbol}${videoFlat.min} ~ ${videoFlat.symbol}${videoFlat.max}`,
suffix: videoFlatSuffix || ` / ${t('条')}`,
original: original?.videoFlat,
});
}
if (useTieredVideoFlat) {
const { usedGroupRatio } = getUsedGroupContext(
model,
selectedGroup,
groupRatio,
);
const usd = Number(hint.min_usd_after_channel_discount) * usedGroupRatio;
const perSecond = hint.billing_mode === 'per_second';
items.push({
key: 'video-flat-tiered',
label: perSecond ? t('按秒') : t('按条'),
valueNode: (
<span className='font-bold text-black'>
{t('最低价')}
{displayPrice(usd)}
{perSecond ? t('/秒起') : t('/条起')}
</span>
),
});
}
if (useTieredImagePerImage) {
const { usedGroupRatio } = getUsedGroupContext(
model,
selectedGroup,
groupRatio,
);
const usd =
Number(imageHint.min_usd_after_channel_discount) * usedGroupRatio;
items.push({
key: 'image-per-image-tiered',
label: t('按张'),
valueNode: (
<span className='font-bold text-black'>
{t('最低价')}
{displayPrice(usd)}
{t('/张起')}
</span>
),
});
}
return items;
};
// 获取模型图标
const getModelIcon = (model) => {
if (!model || !model.model_name) {
return (
<div className={CARD_STYLES.container}>
<Avatar size='large'>?</Avatar>
</div>
);
}
// 1) 优先使用模型自定义图标
if (model.icon) {
return (
<div className={CARD_STYLES.container}>
<div className={CARD_STYLES.icon}>
{getLobeHubIcon(model.icon, 32)}
</div>
</div>
);
}
// 2) 退化为供应商图标
if (model.vendor_icon) {
return (
<div className={CARD_STYLES.container}>
<div className={CARD_STYLES.icon}>
{getLobeHubIcon(model.vendor_icon, 32)}
</div>
</div>
);
}
// 如果没有供应商图标,使用模型名称生成头像
const avatarText =
(model.model_name || '').slice(0, 2).toUpperCase() || 'AI';
return (
<div className={CARD_STYLES.container}>
<Avatar
size='large'
style={{
width: 48,
height: 48,
borderRadius: 16,
fontSize: 16,
fontWeight: 'bold',
}}
>
{avatarText}
</Avatar>
</div>
);
};
// 获取模型描述
const getModelDescription = (record) => {
return record.description || '';
};
// 渲染标签
const renderTags = (record) => {
// 计费类型标签(左边)- 使用 channel_list[0].quota_type
const channelQuotaType =
record.channel_list && record.channel_list.length > 0
? record.channel_list[0].quota_type
: record.quota_type;
let billingTag = (
<Tag key='billing' shape='circle' color='white' size='small'>
-
</Tag>
);
if (channelQuotaType === 1) {
billingTag = (
<Tag key='billing' shape='circle' color='teal' size='small'>
{t('按次计费')}
</Tag>
);
} else if (channelQuotaType === 0) {
billingTag = (
<Tag key='billing' shape='circle' color='violet' size='small'>
{t('按量计费')}
</Tag>
);
}
// 自定义标签(右边)
const customTags = [];
if (record.tags) {
const tagArr = record.tags.split(',').filter(Boolean);
tagArr.forEach((tg, idx) => {
customTags.push(
<Tag
key={`custom-${idx}`}
shape='circle'
color={stringToColor(tg)}
size='small'
>
{renderHighlightedText(tg)}
</Tag>,
);
});
}
return (
<div className='flex items-center justify-between'>
<div className='flex items-center gap-2'>{billingTag}</div>
<div className='flex items-center gap-1'>
{customTags.length > 0 &&
renderLimitedItems({
items: customTags.map((tag, idx) => ({
key: `custom-${idx}`,
element: tag,
})),
renderItem: (item, idx) => item.element,
maxDisplay: 3,
})}
</div>
</div>
);
};
// 显示骨架屏
if (showSkeleton) {
return (
<>
<PricingCardSkeleton
rowSelection={!!rowSelection}
showRatio={showRatio}
/>
</>
);
}
if (!filteredModels || filteredModels.length === 0) {
return (
<>
<div className='flex justify-center items-center py-20'>
<Empty
image={<IllustrationNoResult style={{ width: 150, height: 150 }} />}
darkModeImage={
<IllustrationNoResultDark style={{ width: 150, height: 150 }} />
}
description={t('搜索无结果')}
/>
</div>
</>
);
}
return (
<>
<div className={gridCols > 0 ? '' : 'px-2 pt-2'}>
<div className={gridCols > 0 ? 'grid gap-4' : 'flex flex-wrap gap-4'} style={gridCols > 0 ? { gridTemplateColumns: `repeat(${gridCols}, 1fr)` } : undefined}>
{paginatedModels.map((model, index) => {
const modelKey = getModelKey(model);
const isSelected = selectedRowKeys.includes(modelKey);
const priceData = calculateModelPrice({
record: model,
selectedGroup,
groupRatio,
groupModelPrice,
groupModelRatio,
tokenUnit,
displayPrice,
currency,
quotaDisplayType: siteDisplayType,
});
const supplierLogos = getSupplierLogos(model);
const hasChannelList =
Array.isArray(model.channel_list) &&
model.channel_list.length > 0;
return (
<Card
key={modelKey || index}
className={`${gridCols > 0 ? '!w-full' : 'flex-1 min-w-[350px] max-w-[600px]'} !rounded-2xl transition-all duration-200 hover:shadow-lg border ${blurPricing ? '' : 'cursor-pointer'} ${isSelected ? CARD_STYLES.selected : CARD_STYLES.default}`}
bodyStyle={{ height: '100%' }}
onClick={() =>
!blurPricing && openModelDetail && openModelDetail(model)
}
>
<div className='flex flex-col h-full'>
{/* 头部:图标 + 模型名称 + 操作按钮 */}
<div className='flex items-start justify-between mb-3'>
<div className='flex items-start space-x-3 flex-1 min-w-0'>
{getModelIcon(model)}
<div className='flex-1 min-w-0'>
<h3 className='text-lg font-bold text-gray-900 truncate'>
{renderHighlightedText(model.model_name)}
</h3>
<div
className='flex flex-col gap-1 text-xs mt-1'
style={
blurPricing
? {
filter: 'blur(6px)',
userSelect: 'none',
pointerEvents: 'none',
}
: undefined
}
>
{getModelPriceItemsForCard(model, priceData).map(
(item) => (
<div key={item.key} className='flex items-center'>
<span className='w-20 flex-shrink-0'>
{item.label}
</span>
<span className='flex-1 font-bold text-black inline-flex items-center flex-wrap gap-1'>
{item.valueNode ? (
item.valueNode
) : item.original ? (
<>
<span className='line-through text-gray-400 font-normal text-[10px]'>
<span
style={{
color: 'var(--semi-color-primary)',
}}
>
官方
</span>{' '}
{item.original.text}
</span>
<Tag
color='red'
size='small'
shape='circle'
>
-{item.original.discount}%
</Tag>
<span>
<span
style={{
color: 'var(--semi-color-warning)',
}}
>
我们
</span>{' '}
{item.value}
{item.suffix}
</span>
</>
) : (
<span>
{item.value}
{item.suffix}
</span>
)}
</span>
</div>
),
)}
<div className='flex items-center'>
<span className='w-20 flex-shrink-0'>
{t('供应商')}
</span>
<div className='flex-1 flex items-center flex-wrap gap-1'>
{supplierLogos.length === 0 ? (
hasChannelList ? null : (
<span className='font-bold text-black'>
{t('官方')}
</span>
)
) : (
supplierLogos.map((s) => (
<div
key={s.key}
className='h-7 rounded-md flex items-center overflow-hidden'
style={{backgroundColor: 'var(--semi-color-fill-0)'}}
>
{s.logo ? (
<img
src={s.logo}
alt={s.alias || s.name || ''}
className='w-7 h-7 object-contain rounded-md'
/>
) : null}
{s.supplierType && (
<Tag
size='small'
shape='circle'
color={getSupplierTypeColor(
s.supplierType,
)}
className='mx-1'
>
{s.supplierType}
</Tag>
)}
</div>
))
)}
</div>
</div>
</div>
</div>
</div>
<div className='flex items-center space-x-2 ml-3'>
{/* 复制按钮 */}
<Button
size='small'
theme='outline'
type='tertiary'
icon={<Copy size={12} />}
onClick={(e) => {
e.stopPropagation();
copyText(model.model_name);
}}
/>
{/* 选择框 */}
{rowSelection && (
<Checkbox
checked={isSelected}
onChange={(e) => {
e.stopPropagation();
handleCheckboxChange(model, e.target.checked);
}}
/>
)}
</div>
</div>
{/* 模型描述 - 占据剩余空间 */}
<div
className='flex-1 mb-4'
style={
blurPricing
? {
filter: 'blur(6px)',
userSelect: 'none',
pointerEvents: 'none',
}
: undefined
}
>
<p
className='text-xs line-clamp-2 leading-relaxed'
style={{ color: 'var(--semi-color-text-2)' }}
>
{renderHighlightedText(getModelDescription(model))}
</p>
</div>
{/* 底部区域 */}
<div
className='mt-auto'
style={
blurPricing
? {
filter: 'blur(6px)',
userSelect: 'none',
pointerEvents: 'none',
}
: undefined
}
>
{/* 标签区域 */}
{renderTags(model)}
{/* 倍率信息(可选) */}
{showRatio && (
<div className='pt-3'>
<div className='flex items-center space-x-1 mb-2'>
<span className='text-xs font-medium text-gray-700'>
{t('倍率信息')}
</span>
<Tooltip
content={t('倍率是为了方便换算不同价格的模型')}
>
<IconHelpCircle
className='text-blue-500 cursor-pointer'
size='small'
onClick={(e) => {
e.stopPropagation();
setModalImageUrl('/ratio.png');
setIsModalOpenurl(true);
}}
/>
</Tooltip>
</div>
<div className='grid grid-cols-3 gap-2 text-xs text-gray-600'>
<div>
{t('模型')}:{' '}
{model.quota_type === 0
? (priceData?.inputRatio ?? model.model_ratio)
: t('无')}
</div>
<div>
{t('输出')}:{' '}
{model.quota_type === 0
? parseFloat(model.completion_ratio.toFixed(2))
: t('无')}
</div>
<div>
{t('分组')}: {priceData?.usedGroupRatio ?? '-'}
</div>
</div>
</div>
)}
</div>
</div>
</Card>
);
})}
</div>
{/* 分页 */}
{filteredModels.length > 0 && (
<div className='flex justify-center mt-6 py-4 border-t pricing-pagination-divider'>
<Pagination
currentPage={currentPage}
pageSize={pageSize}
total={filteredModels.length}
showSizeChanger={showSizeChanger}
pageSizeOptions={[10, 20, 50, 100]}
size={isMobile ? 'small' : 'default'}
showQuickJumper={isMobile}
onPageChange={(page) => setCurrentPage(page)}
onPageSizeChange={(size) => {
setPageSize(size);
setCurrentPage(1);
}}
/>
</div>
)}
</div>
</>
);
};
export default PricingCardView;