/*
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 .
For commercial licensing, please contact support@quantumnous.com
*/
import React from 'react';
import {
Card,
Empty,
Pagination,
Avatar,
} from '@douyinfe/semi-ui';
import {
IllustrationNoResult,
IllustrationNoResultDark,
} from '@douyinfe/semi-illustrations';
import {
calculateModelPrice,
getModelPriceItems,
getLobeHubIcon,
getUsedGroupContext,
pickChannelScopedModelFloat,
computeChannelBillingRates,
} from '../../../../../helpers';
import PricingCardSkeleton from './PricingCardSkeleton';
import { useMinimumLoadingTime } from '../../../../../hooks/common/useMinimumLoadingTime';
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,
pageSize,
setPageSize,
currentPage,
setCurrentPage,
selectedGroup,
groupRatio,
groupModelPrice,
groupModelRatio,
currency,
siteDisplayType,
tokenUnit,
displayPrice,
t,
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() ? (
{part}
) : (
part
),
);
};
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
) {
prices.output.push(
formatPrice(billingRates.completionRatioPrice * usedGroupRatio),
);
originalPrices.output.push(
formatPrice(billingRates.completionRatioPrice),
);
}
// 缓存读取价格:渠道配置了 cache_ratio 即可展示
if (
ch.cache_ratio !== undefined &&
ch.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
) {
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: (
{t('最低价')}
{displayPrice(usd)}
{perSecond ? t('/秒起') : t('/条起')}
),
});
}
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: (
{t('最低价')}
{displayPrice(usd)}
{t('/张起')}
),
});
}
return items;
};
// 获取模型图标
const getModelIcon = (model) => {
if (!model || !model.model_name) {
return (
);
}
// 1) 优先使用模型自定义图标
if (model.icon) {
return (
{getLobeHubIcon(model.icon, 32)}
);
}
// 2) 退化为供应商图标
if (model.vendor_icon) {
return (
{getLobeHubIcon(model.vendor_icon, 32)}
);
}
// 如果没有供应商图标,使用模型名称生成头像
const avatarText =
(model.model_name || '').slice(0, 2).toUpperCase() || 'AI';
return (
);
};
// 获取模型描述
const getModelDescription = (record) => {
return record.description || '';
};
// 显示骨架屏
if (showSkeleton) {
return (
<>
>
);
}
if (!filteredModels || filteredModels.length === 0) {
return (
<>
}
darkModeImage={
}
description={t('搜索无结果')}
/>
>
);
}
return (
<>
0 ? '' : 'px-2 pt-2'}>
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 priceData = calculateModelPrice({
record: model,
selectedGroup,
groupRatio,
groupModelPrice,
groupModelRatio,
tokenUnit,
displayPrice,
currency,
quotaDisplayType: siteDisplayType,
});
return (
0 ? '!w-full' : 'flex-1 min-w-[350px] max-w-[600px]'} !rounded-2xl transition-all duration-200 hover:shadow-lg border ${blurPricing ? '' : 'cursor-pointer'} ${CARD_STYLES.default}`}
bodyStyle={{ height: '100%' }}
onClick={() =>
!blurPricing && openModelDetail && openModelDetail(model)
}
>
{/* 头部:图标 + 模型名称 */}
{getModelIcon(model)}
{renderHighlightedText(model.model_name)}
{/* 模型描述 */}
{renderHighlightedText(getModelDescription(model))}
{/* 价格:输入 + 输出并排 */}
{(() => {
const items = getModelPriceItemsForCard(model, priceData);
if (items.length === 0) return null;
return (
{items.map(item => (
{item.label}
{item.valueNode || (item.value + (item.suffix || ''))}
))}
);
})()}
);
})}
{/* 分页 */}
{filteredModels.length > 0 && (
setCurrentPage(page)}
onPageSizeChange={(size) => {
setPageSize(size);
setCurrentPage(1);
}}
/>
)}
>
);
};
export default PricingCardView;