/* 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 (
{avatarText}
); }; // 获取模型描述 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;