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

View File

@ -22,8 +22,6 @@ import { Card, Skeleton } from '@douyinfe/semi-ui';
const PricingCardSkeleton = ({
skeletonCount = 100,
rowSelection = false,
showRatio = false,
}) => {
const placeholder = (
<div className='px-2 pt-2'>
@ -34,8 +32,8 @@ const PricingCardSkeleton = ({
className='!rounded-2xl border border-gray-200'
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='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 }}
/>
</div>
{/* 模型名称和价格区域 */}
{/* 模型名称 */}
<div className='flex-1 min-w-0'>
{/* 模型名称骨架 */}
<Skeleton.Title
style={{
width: `${120 + (index % 3) * 30}px`,
height: 20,
marginBottom: 8,
}}
/>
{/* 价格信息骨架 */}
<Skeleton.Title
style={{
width: `${160 + (index % 4) * 20}px`,
height: 20,
marginBottom: 0,
}}
/>
</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>
{/* 模型描述骨架 */}
@ -90,43 +64,25 @@ const PricingCardSkeleton = ({
/>
</div>
{/* 标签区域骨架 */}
<div className='flex flex-wrap gap-2'>
{Array.from({ length: 2 + (index % 3) }).map((_, tagIndex) => (
<Skeleton.Button
key={tagIndex}
size='small'
style={{
width: 64,
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'>
{/* 价格骨架:输入 + 输出并排 */}
<div className='flex gap-3'>
<div className='flex-1 rounded-xl px-3 py-2.5' style={{backgroundColor: 'var(--semi-color-fill-0)'}}>
<Skeleton.Title
style={{ width: 60, height: 12, marginBottom: 0 }}
style={{ width: '80%', height: 14, marginBottom: 8 }}
/>
<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 }}
style={{ width: '100%', height: 18, marginBottom: 0 }}
/>
</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>
))}
</div>

View File

@ -20,22 +20,15 @@ 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,
@ -45,7 +38,6 @@ import {
} 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:
@ -60,7 +52,6 @@ const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const PricingCardView = ({
filteredModels,
loading,
rowSelection,
pageSize,
setPageSize,
currentPage,
@ -69,17 +60,11 @@ const PricingCardView = ({
groupRatio,
groupModelPrice,
groupModelRatio,
copyText,
setModalImageUrl,
setIsModalOpenurl,
currency,
siteDisplayType,
tokenUnit,
displayPrice,
showRatio,
t,
selectedRowKeys = [],
setSelectedRowKeys,
openModelDetail,
showSizeChanger = true,
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 { skipSimpleVideoFlat = false, skipSimpleFixed = false } = opts;
if (!model.channel_list || model.channel_list.length === 0) {
@ -766,77 +695,11 @@ const PricingCardView = ({
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}
/>
<PricingCardSkeleton />
</>
);
}
@ -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}>
{paginatedModels.map((model, index) => {
const modelKey = getModelKey(model);
const isSelected = selectedRowKeys.includes(modelKey);
const priceData = calculateModelPrice({
record: model,
@ -877,166 +739,31 @@ const PricingCardView = ({
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}`}
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%' }}
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 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'
className='mb-4'
style={
blurPricing
? {
@ -1055,7 +782,7 @@ const PricingCardView = ({
</p>
</div>
{/* 底部区域 */}
{/* 价格:输入 + 输出并排 */}
<div
className='mt-auto'
style={
@ -1068,49 +795,39 @@ const PricingCardView = ({
: 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>
{(() => {
const items = getModelPriceItemsForCard(model, priceData);
const inputItem = items.find(i => i.key === 'input');
const outputItem = items.find(i => i.key === 'output');
return (
<div className='flex gap-3'>
{inputItem && (
<div 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)'}}>{inputItem.label}</div>
<div className='text-sm font-bold text-gray-900'>
{inputItem.value}{inputItem.suffix}
</div>
</div>
)}
{outputItem && (
<div 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)'}}>{outputItem.label}</div>
<div className='text-sm font-bold text-gray-900'>
{outputItem.value}{outputItem.suffix}
</div>
</div>
)}
{!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>
</Card>