/* 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, 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() ? ( {part} ) : ( 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: ( {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 || ''; }; // 渲染标签 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 = ( - ); if (channelQuotaType === 1) { billingTag = ( {t('按次计费')} ); } else if (channelQuotaType === 0) { billingTag = ( {t('按量计费')} ); } // 自定义标签(右边) const customTags = []; if (record.tags) { const tagArr = record.tags.split(',').filter(Boolean); tagArr.forEach((tg, idx) => { customTags.push( {renderHighlightedText(tg)} , ); }); } return (
{billingTag}
{customTags.length > 0 && renderLimitedItems({ items: customTags.map((tag, idx) => ({ key: `custom-${idx}`, element: tag, })), renderItem: (item, idx) => item.element, maxDisplay: 3, })}
); }; // 显示骨架屏 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 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 ( 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) } >
{/* 头部:图标 + 模型名称 + 操作按钮 */}
{getModelIcon(model)}

{renderHighlightedText(model.model_name)}

{getModelPriceItemsForCard(model, priceData).map( (item) => (
{item.label} {item.valueNode ? ( item.valueNode ) : item.original ? ( <> 官方 {' '} {item.original.text} -{item.original.discount}% 我们 {' '} {item.value} {item.suffix} ) : ( {item.value} {item.suffix} )}
), )}
{t('供应商')}
{supplierLogos.length === 0 ? ( hasChannelList ? null : ( {t('官方')} ) ) : ( supplierLogos.map((s) => (
{s.logo ? ( {s.alias ) : null} {s.supplierType && ( {s.supplierType} )}
)) )}
{/* 复制按钮 */}
{/* 模型描述 - 占据剩余空间 */}

{renderHighlightedText(getModelDescription(model))}

{/* 底部区域 */}
{/* 标签区域 */} {renderTags(model)} {/* 倍率信息(可选) */} {showRatio && (
{t('倍率信息')} { e.stopPropagation(); setModalImageUrl('/ratio.png'); setIsModalOpenurl(true); }} />
{t('模型')}:{' '} {model.quota_type === 0 ? (priceData?.inputRatio ?? model.model_ratio) : t('无')}
{t('输出')}:{' '} {model.quota_type === 0 ? parseFloat(model.completion_ratio.toFixed(2)) : t('无')}
{t('分组')}: {priceData?.usedGroupRatio ?? '-'}
)}
); })}
{/* 分页 */} {filteredModels.length > 0 && (
setCurrentPage(page)} onPageSizeChange={(size) => { setPageSize(size); setCurrentPage(1); }} />
)}
); }; export default PricingCardView;