refactor: streamline model card to show only logo, name, description, and IO prices

This commit is contained in:
xiezhouwei 2026-06-18 16:50:02 +08:00
parent 4fbdafee1c
commit 77b885b190
3 changed files with 59 additions and 393 deletions

View File

@ -378,7 +378,6 @@ const HomeModelList = () => {
<PricingCardView <PricingCardView
filteredModels={pricingData.filteredModels} filteredModels={pricingData.filteredModels}
loading={pricingData.loading} loading={pricingData.loading}
rowSelection={null}
pageSize={pricingData.pageSize} pageSize={pricingData.pageSize}
setPageSize={pricingData.setPageSize} setPageSize={pricingData.setPageSize}
currentPage={pricingData.currentPage} currentPage={pricingData.currentPage}
@ -387,9 +386,6 @@ const HomeModelList = () => {
groupRatio={pricingData.groupRatio} groupRatio={pricingData.groupRatio}
groupModelPrice={pricingData.groupModelPrice} groupModelPrice={pricingData.groupModelPrice}
groupModelRatio={pricingData.groupModelRatio} groupModelRatio={pricingData.groupModelRatio}
copyText={pricingData.copyText}
setModalImageUrl={pricingData.setModalImageUrl}
setIsModalOpenurl={pricingData.setIsModalOpenurl}
currency={pricingData.currency} currency={pricingData.currency}
siteDisplayType={pricingData.siteDisplayType} siteDisplayType={pricingData.siteDisplayType}
tokenUnit={pricingData.tokenUnit} tokenUnit={pricingData.tokenUnit}
@ -399,10 +395,7 @@ const HomeModelList = () => {
pricingData.channelVideoCompletionRatio pricingData.channelVideoCompletionRatio
} }
channelVideoPrice={pricingData.channelVideoPrice} channelVideoPrice={pricingData.channelVideoPrice}
showRatio={false}
t={pricingData.t} t={pricingData.t}
selectedRowKeys={[]}
setSelectedRowKeys={() => {}}
openModelDetail={pricingData.openModelDetail} openModelDetail={pricingData.openModelDetail}
showSizeChanger={false} showSizeChanger={false}
blurPricing={blurPricing} blurPricing={blurPricing}

View File

@ -22,8 +22,6 @@ import { Card, Skeleton } from '@douyinfe/semi-ui';
const PricingCardSkeleton = ({ const PricingCardSkeleton = ({
skeletonCount = 100, skeletonCount = 100,
rowSelection = false,
showRatio = false,
}) => { }) => {
const placeholder = ( const placeholder = (
<div className='px-2 pt-2'> <div className='px-2 pt-2'>
@ -34,8 +32,8 @@ const PricingCardSkeleton = ({
className='!rounded-2xl border border-gray-200' className='!rounded-2xl border border-gray-200'
bodyStyle={{ padding: '24px' }} bodyStyle={{ padding: '24px' }}
> >
{/* 头部:图标 + 模型名称 + 操作按钮 */} {/* 头部:图标 + 模型名称 */}
<div className='flex items-start justify-between mb-3'> <div className='flex items-start mb-3'>
<div className='flex items-start space-x-3 flex-1 min-w-0'> <div className='flex items-start space-x-3 flex-1 min-w-0'>
{/* 模型图标骨架 */} {/* 模型图标骨架 */}
<div className='w-12 h-12 rounded-2xl flex items-center justify-center relative shadow-sm'> <div className='w-12 h-12 rounded-2xl flex items-center justify-center relative shadow-sm'>
@ -44,41 +42,17 @@ const PricingCardSkeleton = ({
style={{ width: 48, height: 48, borderRadius: 16 }} style={{ width: 48, height: 48, borderRadius: 16 }}
/> />
</div> </div>
{/* 模型名称和价格区域 */} {/* 模型名称 */}
<div className='flex-1 min-w-0'> <div className='flex-1 min-w-0'>
{/* 模型名称骨架 */}
<Skeleton.Title <Skeleton.Title
style={{ style={{
width: `${120 + (index % 3) * 30}px`, width: `${120 + (index % 3) * 30}px`,
height: 20, height: 20,
marginBottom: 8,
}}
/>
{/* 价格信息骨架 */}
<Skeleton.Title
style={{
width: `${160 + (index % 4) * 20}px`,
height: 20,
marginBottom: 0, marginBottom: 0,
}} }}
/> />
</div> </div>
</div> </div>
<div className='flex items-center space-x-2 ml-3'>
{/* 复制按钮骨架 */}
<Skeleton.Button
size='small'
style={{ width: 16, height: 16, borderRadius: 4 }}
/>
{/* 勾选框骨架 */}
{rowSelection && (
<Skeleton.Button
size='small'
style={{ width: 16, height: 16, borderRadius: 2 }}
/>
)}
</div>
</div> </div>
{/* 模型描述骨架 */} {/* 模型描述骨架 */}
@ -90,43 +64,25 @@ const PricingCardSkeleton = ({
/> />
</div> </div>
{/* 标签区域骨架 */} {/* 价格骨架:输入 + 输出并排 */}
<div className='flex flex-wrap gap-2'> <div className='flex gap-3'>
{Array.from({ length: 2 + (index % 3) }).map((_, tagIndex) => ( <div className='flex-1 rounded-xl px-3 py-2.5' style={{backgroundColor: 'var(--semi-color-fill-0)'}}>
<Skeleton.Button <Skeleton.Title
key={tagIndex} style={{ width: '80%', height: 14, marginBottom: 8 }}
size='small' />
style={{ <Skeleton.Title
width: 64, style={{ width: '100%', height: 18, marginBottom: 0 }}
height: 18,
borderRadius: 10,
}}
/> />
))}
</div>
{/* 倍率信息骨架(可选) */}
{showRatio && (
<div className='mt-4 pt-3 border-t border-gray-100'>
<div className='flex items-center space-x-1 mb-2'>
<Skeleton.Title
style={{ width: 60, height: 12, marginBottom: 0 }}
/>
<Skeleton.Button
size='small'
style={{ width: 14, height: 14, borderRadius: 7 }}
/>
</div>
<div className='grid grid-cols-3 gap-2'>
{Array.from({ length: 3 }).map((_, ratioIndex) => (
<Skeleton.Title
key={ratioIndex}
style={{ width: '100%', height: 12, marginBottom: 0 }}
/>
))}
</div>
</div> </div>
)} <div className='flex-1 rounded-xl px-3 py-2.5' style={{backgroundColor: 'var(--semi-color-fill-0)'}}>
<Skeleton.Title
style={{ width: '80%', height: 14, marginBottom: 8 }}
/>
<Skeleton.Title
style={{ width: '100%', height: 18, marginBottom: 0 }}
/>
</div>
</div>
</Card> </Card>
))} ))}
</div> </div>

View File

@ -20,22 +20,15 @@ For commercial licensing, please contact support@quantumnous.com
import React from 'react'; import React from 'react';
import { import {
Card, Card,
Tag,
Tooltip,
Checkbox,
Empty, Empty,
Pagination, Pagination,
Button,
Avatar, Avatar,
} from '@douyinfe/semi-ui'; } from '@douyinfe/semi-ui';
import { IconHelpCircle } from '@douyinfe/semi-icons';
import { Copy } from 'lucide-react';
import { import {
IllustrationNoResult, IllustrationNoResult,
IllustrationNoResultDark, IllustrationNoResultDark,
} from '@douyinfe/semi-illustrations'; } from '@douyinfe/semi-illustrations';
import { import {
stringToColor,
calculateModelPrice, calculateModelPrice,
getModelPriceItems, getModelPriceItems,
getLobeHubIcon, getLobeHubIcon,
@ -45,7 +38,6 @@ import {
} from '../../../../../helpers'; } from '../../../../../helpers';
import PricingCardSkeleton from './PricingCardSkeleton'; import PricingCardSkeleton from './PricingCardSkeleton';
import { useMinimumLoadingTime } from '../../../../../hooks/common/useMinimumLoadingTime'; import { useMinimumLoadingTime } from '../../../../../hooks/common/useMinimumLoadingTime';
import { renderLimitedItems } from '../../../../common/ui/RenderUtils';
import { useIsMobile } from '../../../../../hooks/common/useIsMobile'; import { useIsMobile } from '../../../../../hooks/common/useIsMobile';
const CARD_STYLES = { const CARD_STYLES = {
container: container:
@ -60,7 +52,6 @@ const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const PricingCardView = ({ const PricingCardView = ({
filteredModels, filteredModels,
loading, loading,
rowSelection,
pageSize, pageSize,
setPageSize, setPageSize,
currentPage, currentPage,
@ -69,17 +60,11 @@ const PricingCardView = ({
groupRatio, groupRatio,
groupModelPrice, groupModelPrice,
groupModelRatio, groupModelRatio,
copyText,
setModalImageUrl,
setIsModalOpenurl,
currency, currency,
siteDisplayType, siteDisplayType,
tokenUnit, tokenUnit,
displayPrice, displayPrice,
showRatio,
t, t,
selectedRowKeys = [],
setSelectedRowKeys,
openModelDetail, openModelDetail,
showSizeChanger = true, showSizeChanger = true,
blurPricing = false, blurPricing = false,
@ -122,62 +107,6 @@ const PricingCardView = ({
); );
}; };
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 calculateChannelPrices = (model, opts = {}) => {
const { skipSimpleVideoFlat = false, skipSimpleFixed = false } = opts; const { skipSimpleVideoFlat = false, skipSimpleFixed = false } = opts;
if (!model.channel_list || model.channel_list.length === 0) { if (!model.channel_list || model.channel_list.length === 0) {
@ -766,77 +695,11 @@ const PricingCardView = ({
return record.description || ''; 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) { if (showSkeleton) {
return ( return (
<> <>
<PricingCardSkeleton <PricingCardSkeleton />
rowSelection={!!rowSelection}
showRatio={showRatio}
/>
</> </>
); );
} }
@ -863,7 +726,6 @@ const PricingCardView = ({
<div className={gridCols > 0 ? 'grid gap-4' : 'flex flex-wrap gap-4'} style={gridCols > 0 ? { gridTemplateColumns: `repeat(${gridCols}, 1fr)` } : undefined}> <div className={gridCols > 0 ? 'grid gap-4' : 'flex flex-wrap gap-4'} style={gridCols > 0 ? { gridTemplateColumns: `repeat(${gridCols}, 1fr)` } : undefined}>
{paginatedModels.map((model, index) => { {paginatedModels.map((model, index) => {
const modelKey = getModelKey(model); const modelKey = getModelKey(model);
const isSelected = selectedRowKeys.includes(modelKey);
const priceData = calculateModelPrice({ const priceData = calculateModelPrice({
record: model, record: model,
@ -877,166 +739,31 @@ const PricingCardView = ({
quotaDisplayType: siteDisplayType, quotaDisplayType: siteDisplayType,
}); });
const supplierLogos = getSupplierLogos(model);
const hasChannelList =
Array.isArray(model.channel_list) &&
model.channel_list.length > 0;
return ( return (
<Card <Card
key={modelKey || index} 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}`} 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'} ${CARD_STYLES.default}`}
bodyStyle={{ height: '100%' }} bodyStyle={{ height: '100%' }}
onClick={() => onClick={() =>
!blurPricing && openModelDetail && openModelDetail(model) !blurPricing && openModelDetail && openModelDetail(model)
} }
> >
<div className='flex flex-col h-full'> <div className='flex flex-col h-full'>
{/* 头部:图标 + 模型名称 + 操作按钮 */} {/* 头部:图标 + 模型名称 */}
<div className='flex items-start justify-between mb-3'> <div className='flex items-start mb-3'>
<div className='flex items-start space-x-3 flex-1 min-w-0'> <div className='flex items-start space-x-3 flex-1 min-w-0'>
{getModelIcon(model)} {getModelIcon(model)}
<div className='flex-1 min-w-0'> <div className='flex-1 min-w-0'>
<h3 className='text-lg font-bold text-gray-900 truncate'> <h3 className='text-lg font-bold text-gray-900 truncate'>
{renderHighlightedText(model.model_name)} {renderHighlightedText(model.model_name)}
</h3> </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> </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>
{/* 模型描述 - 占据剩余空间 */} {/* 模型描述 */}
<div <div
className='flex-1 mb-4' className='mb-4'
style={ style={
blurPricing blurPricing
? { ? {
@ -1055,7 +782,7 @@ const PricingCardView = ({
</p> </p>
</div> </div>
{/* 底部区域 */} {/* 价格:输入 + 输出并排 */}
<div <div
className='mt-auto' className='mt-auto'
style={ style={
@ -1068,49 +795,39 @@ const PricingCardView = ({
: undefined : undefined
} }
> >
{/* 标签区域 */} {(() => {
{renderTags(model)} const items = getModelPriceItemsForCard(model, priceData);
const inputItem = items.find(i => i.key === 'input');
{/* 倍率信息(可选) */} const outputItem = items.find(i => i.key === 'output');
{showRatio && ( return (
<div className='pt-3'> <div className='flex gap-3'>
<div className='flex items-center space-x-1 mb-2'> {inputItem && (
<span className='text-xs font-medium text-gray-700'> <div className='flex-1 rounded-xl px-3 py-2.5' style={{backgroundColor: 'var(--semi-color-fill-0)'}}>
{t('倍率信息')} <div className='text-[10px] mb-0.5' style={{color: 'var(--semi-color-text-2)'}}>{inputItem.label}</div>
</span> <div className='text-sm font-bold text-gray-900'>
<Tooltip {inputItem.value}{inputItem.suffix}
content={t('倍率是为了方便换算不同价格的模型')} </div>
> </div>
<IconHelpCircle )}
className='text-blue-500 cursor-pointer' {outputItem && (
size='small' <div className='flex-1 rounded-xl px-3 py-2.5' style={{backgroundColor: 'var(--semi-color-fill-0)'}}>
onClick={(e) => { <div className='text-[10px] mb-0.5' style={{color: 'var(--semi-color-text-2)'}}>{outputItem.label}</div>
e.stopPropagation(); <div className='text-sm font-bold text-gray-900'>
setModalImageUrl('/ratio.png'); {outputItem.value}{outputItem.suffix}
setIsModalOpenurl(true); </div>
}} </div>
/> )}
</Tooltip> {!inputItem && !outputItem && items.map(item => (
<div key={item.key} className='flex-1 rounded-xl px-3 py-2.5' style={{backgroundColor: 'var(--semi-color-fill-0)'}}>
<div className='text-[10px] mb-0.5' style={{color: 'var(--semi-color-text-2)'}}>{item.label}</div>
<div className='text-sm font-bold text-gray-900'>
{item.valueNode || (item.value + (item.suffix || ''))}
</div>
</div>
))}
</div> </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>
</div> </div>
</Card> </Card>