| @@ -22,9 +22,12 @@ import { Card, Avatar, Typography, Table, Tag } from '@douyinfe/semi-ui'; | |||
| import { IconCoinMoneyStroked } from '@douyinfe/semi-icons'; | |||
| import { | |||
| calculateModelPrice, | |||
| formatDisplayPricingItem, | |||
| formatMatrixPrice, | |||
| getMatrixPricingUnit, | |||
| hasDisplayPricing, | |||
| hasMatrixPricing, | |||
| normalizeDisplayPricingItems, | |||
| } from '../../../../../helpers'; | |||
| const { Text } = Typography; | |||
| @@ -44,6 +47,62 @@ const ModelPricingTable = ({ | |||
| ? modelData.enable_groups | |||
| : []; | |||
| const autoChain = autoGroups.filter((g) => modelEnableGroups.includes(g)); | |||
| const renderDisplayPricingTable = () => { | |||
| const items = normalizeDisplayPricingItems(modelData).map((item) => | |||
| formatDisplayPricingItem({ item, displayPrice, t }), | |||
| ); | |||
| const columns = [ | |||
| { | |||
| title: t('规格'), | |||
| dataIndex: 'specification', | |||
| render: (text) => <span className='font-semibold'>{text}</span>, | |||
| }, | |||
| { | |||
| title: t('供应商说明'), | |||
| dataIndex: 'official_supplier_tip', | |||
| render: (text) => <span className='text-gray-600'>{text || '-'}</span>, | |||
| }, | |||
| { | |||
| title: t('价格'), | |||
| dataIndex: 'formattedPrice', | |||
| render: (text, record) => ( | |||
| <div> | |||
| <div className='font-semibold text-orange-600'>{text}</div> | |||
| {record.originalPrice && ( | |||
| <div className='text-xs text-gray-400 line-through'> | |||
| {t('原价')} {record.originalPrice} | |||
| </div> | |||
| )} | |||
| </div> | |||
| ), | |||
| }, | |||
| { | |||
| title: t('折扣'), | |||
| dataIndex: 'discountText', | |||
| render: (text) => text || '-', | |||
| }, | |||
| ]; | |||
| return ( | |||
| <> | |||
| <div className='mb-3 rounded-lg border border-orange-100 bg-orange-50 px-3 py-2 text-xs text-orange-700'> | |||
| {t('展示价格仅用于估算,实际消耗以任务完成后的积分扣减为准。')} | |||
| </div> | |||
| <Table | |||
| dataSource={items.map((item, index) => ({ | |||
| ...item, | |||
| key: `${item.specification}-${index}`, | |||
| }))} | |||
| columns={columns} | |||
| pagination={false} | |||
| size='small' | |||
| bordered={false} | |||
| className='!rounded-lg' | |||
| /> | |||
| </> | |||
| ); | |||
| }; | |||
| const renderMatrixPriceTable = () => { | |||
| const cfg = modelData?.pricing_config; | |||
| const dimensions = cfg?.dimensions || []; | |||
| @@ -124,6 +183,9 @@ const ModelPricingTable = ({ | |||
| }; | |||
| const renderGroupPriceTable = () => { | |||
| if (hasDisplayPricing(modelData)) { | |||
| return renderDisplayPricingTable(); | |||
| } | |||
| if (hasMatrixPricing(modelData)) { | |||
| return renderMatrixPriceTable(); | |||
| } | |||
| @@ -144,6 +206,7 @@ const ModelPricingTable = ({ | |||
| tokenUnit, | |||
| displayPrice, | |||
| currency, | |||
| t, | |||
| }) | |||
| : { inputPrice: '-', outputPrice: '-', price: '-' }; | |||
| @@ -322,9 +385,13 @@ const ModelPricingTable = ({ | |||
| <IconCoinMoneyStroked size={16} /> | |||
| </Avatar> | |||
| <div> | |||
| <Text className='text-lg font-medium'>{t('分组价格')}</Text> | |||
| <Text className='text-lg font-medium'> | |||
| {hasDisplayPricing(modelData) ? t('展示价格') : t('分组价格')} | |||
| </Text> | |||
| <div className='text-xs text-gray-600'> | |||
| {t('不同用户分组的价格信息')} | |||
| {hasDisplayPricing(modelData) | |||
| ? t('展示价格,仅用于估算') | |||
| : t('不同用户分组的价格信息')} | |||
| </div> | |||
| </div> | |||
| </div> | |||
| @@ -7,17 +7,22 @@ const { Text } = Typography; | |||
| const UserPriceComparison = ({ | |||
| modelName, | |||
| modelData, | |||
| currency, | |||
| tokenUnit, | |||
| displayPrice, | |||
| t, | |||
| }) => { | |||
| const hasDisplayPricing = | |||
| Array.isArray(modelData?.display_pricing) && | |||
| modelData.display_pricing.length > 0; | |||
| const [loading, setLoading] = useState(false); | |||
| const [priceData, setPriceData] = useState(null); | |||
| useEffect(() => { | |||
| const fetchUserPrice = async () => { | |||
| if (!modelName) return; | |||
| if (!modelName || hasDisplayPricing) return; | |||
| setLoading(true); | |||
| try { | |||
| const res = await API.get( | |||
| @@ -37,7 +42,7 @@ const UserPriceComparison = ({ | |||
| } | |||
| }; | |||
| fetchUserPrice(); | |||
| }, [modelName]); | |||
| }, [modelName, hasDisplayPricing]); | |||
| const currencySymbol = useMemo(() => { | |||
| if (currency === 'CNY') return '¥'; | |||
| @@ -74,6 +79,8 @@ const UserPriceComparison = ({ | |||
| return `${currencySymbol}${num.toFixed(4)}`; | |||
| }; | |||
| if (hasDisplayPricing) return null; | |||
| if (loading) { | |||
| return ( | |||
| <Card className='!rounded-2xl shadow-sm border-0 mb-4'> | |||
| @@ -38,6 +38,7 @@ import { | |||
| import { | |||
| stringToColor, | |||
| getLobeHubIcon, | |||
| hasDisplayPricing, | |||
| hasMatrixPricing, | |||
| calculateModelPrice, | |||
| } from '../../../../../helpers'; | |||
| @@ -163,8 +164,41 @@ const PricingCardView = ({ | |||
| tokenUnit, | |||
| displayPrice, | |||
| currency, | |||
| t, | |||
| }); | |||
| if (priceData?.isDisplayPricing) { | |||
| return ( | |||
| <div className='mb-3 text-xs'> | |||
| {String(priceData.price || '') | |||
| .split('\n') | |||
| .map((line) => ( | |||
| <div key={line} className='font-semibold text-gray-900'> | |||
| {line} | |||
| </div> | |||
| ))} | |||
| <div className='mt-1 text-gray-500'>{t('展示价格')}</div> | |||
| {(priceData.details || []).map((detail, detailIndex) => ( | |||
| <div | |||
| key={`${detail.specification}-${detailIndex}`} | |||
| className='mt-1 flex items-center justify-between gap-2 text-gray-500' | |||
| title={`${detail.specification} ${detail.formattedPrice}`} | |||
| > | |||
| <span className='min-w-0 truncate'>{detail.specification}</span> | |||
| <span className='shrink-0 text-gray-700'> | |||
| {detail.formattedPrice} | |||
| </span> | |||
| </div> | |||
| ))} | |||
| {priceData.hiddenDetailCount > 0 && ( | |||
| <div className='mt-1 text-gray-400'> | |||
| + {priceData.hiddenDetailCount} | |||
| </div> | |||
| )} | |||
| </div> | |||
| ); | |||
| } | |||
| if (!priceData?.isMatrix) { | |||
| return null; | |||
| } | |||
| @@ -206,7 +240,13 @@ const PricingCardView = ({ | |||
| - | |||
| </Tag> | |||
| ); | |||
| if (hasMatrixPricing(record)) { | |||
| if (hasDisplayPricing(record)) { | |||
| billingTag = ( | |||
| <Tag key='billing' shape='circle' color='orange' size='small'> | |||
| {t('展示价格')} | |||
| </Tag> | |||
| ); | |||
| } else if (hasMatrixPricing(record)) { | |||
| billingTag = ( | |||
| <Tag key='billing' shape='circle' color='purple' size='small'> | |||
| {t('矩阵计费')} | |||
| @@ -24,6 +24,7 @@ import { | |||
| renderModelTag, | |||
| stringToColor, | |||
| calculateModelPrice, | |||
| hasDisplayPricing, | |||
| hasMatrixPricing, | |||
| getLobeHubIcon, | |||
| } from '../../../../../helpers'; | |||
| @@ -53,6 +54,13 @@ function renderQuotaType(type, t) { | |||
| } | |||
| function renderBillingType(record, t) { | |||
| if (hasDisplayPricing(record)) { | |||
| return ( | |||
| <Tag color='orange' shape='circle'> | |||
| {t('展示价格')} | |||
| </Tag> | |||
| ); | |||
| } | |||
| if (hasMatrixPricing(record)) { | |||
| return ( | |||
| <Tag color='purple' shape='circle'> | |||
| @@ -137,6 +145,7 @@ export const getPricingTableColumns = ({ | |||
| tokenUnit, | |||
| displayPrice, | |||
| currency, | |||
| t, | |||
| }); | |||
| priceDataCache.set(record, cache); | |||
| } | |||
| @@ -244,6 +253,23 @@ export const getPricingTableColumns = ({ | |||
| render: (text, record, index) => { | |||
| const priceData = getPriceData(record); | |||
| if (priceData.isDisplayPricing) { | |||
| return ( | |||
| <div className='space-y-1'> | |||
| {String(priceData.price || '') | |||
| .split('\n') | |||
| .map((line) => ( | |||
| <div key={line} className='text-gray-700 font-semibold'> | |||
| {line} | |||
| </div> | |||
| ))} | |||
| <div className='text-xs text-gray-500'> | |||
| {t('展示价格,仅用于估算')} | |||
| </div> | |||
| </div> | |||
| ); | |||
| } | |||
| if (priceData.isMatrix) { | |||
| return ( | |||
| <div className='space-y-1'> | |||
| @@ -3295,5 +3295,14 @@ | |||
| "重试启动失败": "Failed to start retry", | |||
| "重试已启动": "Retry started", | |||
| "验证完成": "Verification complete", | |||
| "暂无数据": "No data" | |||
| "暂无数据": "No data", | |||
| "展示价格,仅用于估算": "Display price, estimate only", | |||
| "展示价格仅用于估算,实际消耗以任务完成后的积分扣减为准。": "Display price is only an estimate. Actual consumption is based on the final quota deduction after the task completes.", | |||
| "规格": "Specification", | |||
| "供应商说明": "Supplier tip", | |||
| "折扣": "Discount", | |||
| "second": "second", | |||
| "call": "call", | |||
| "image": "image", | |||
| "video": "video" | |||
| } | |||
| @@ -3249,5 +3249,14 @@ | |||
| "重试启动失败": "重试启动失败", | |||
| "重试已启动": "重试已启动", | |||
| "验证完成": "验证完成", | |||
| "暂无数据": "暂无数据" | |||
| "暂无数据": "暂无数据", | |||
| "展示价格,仅用于估算": "展示价格,仅用于估算", | |||
| "展示价格仅用于估算,实际消耗以任务完成后的积分扣减为准。": "展示价格仅用于估算,实际消耗以任务完成后的积分扣减为准。", | |||
| "规格": "规格", | |||
| "供应商说明": "供应商说明", | |||
| "折扣": "折扣", | |||
| "second": "秒", | |||
| "call": "次", | |||
| "image": "张图片", | |||
| "video": "个视频" | |||
| } | |||