| @@ -22,9 +22,12 @@ import { Card, Avatar, Typography, Table, Tag } from '@douyinfe/semi-ui'; | |||||
| import { IconCoinMoneyStroked } from '@douyinfe/semi-icons'; | import { IconCoinMoneyStroked } from '@douyinfe/semi-icons'; | ||||
| import { | import { | ||||
| calculateModelPrice, | calculateModelPrice, | ||||
| formatDisplayPricingItem, | |||||
| formatMatrixPrice, | formatMatrixPrice, | ||||
| getMatrixPricingUnit, | getMatrixPricingUnit, | ||||
| hasDisplayPricing, | |||||
| hasMatrixPricing, | hasMatrixPricing, | ||||
| normalizeDisplayPricingItems, | |||||
| } from '../../../../../helpers'; | } from '../../../../../helpers'; | ||||
| const { Text } = Typography; | const { Text } = Typography; | ||||
| @@ -44,6 +47,62 @@ const ModelPricingTable = ({ | |||||
| ? modelData.enable_groups | ? modelData.enable_groups | ||||
| : []; | : []; | ||||
| const autoChain = autoGroups.filter((g) => modelEnableGroups.includes(g)); | 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 renderMatrixPriceTable = () => { | ||||
| const cfg = modelData?.pricing_config; | const cfg = modelData?.pricing_config; | ||||
| const dimensions = cfg?.dimensions || []; | const dimensions = cfg?.dimensions || []; | ||||
| @@ -124,6 +183,9 @@ const ModelPricingTable = ({ | |||||
| }; | }; | ||||
| const renderGroupPriceTable = () => { | const renderGroupPriceTable = () => { | ||||
| if (hasDisplayPricing(modelData)) { | |||||
| return renderDisplayPricingTable(); | |||||
| } | |||||
| if (hasMatrixPricing(modelData)) { | if (hasMatrixPricing(modelData)) { | ||||
| return renderMatrixPriceTable(); | return renderMatrixPriceTable(); | ||||
| } | } | ||||
| @@ -144,6 +206,7 @@ const ModelPricingTable = ({ | |||||
| tokenUnit, | tokenUnit, | ||||
| displayPrice, | displayPrice, | ||||
| currency, | currency, | ||||
| t, | |||||
| }) | }) | ||||
| : { inputPrice: '-', outputPrice: '-', price: '-' }; | : { inputPrice: '-', outputPrice: '-', price: '-' }; | ||||
| @@ -322,9 +385,13 @@ const ModelPricingTable = ({ | |||||
| <IconCoinMoneyStroked size={16} /> | <IconCoinMoneyStroked size={16} /> | ||||
| </Avatar> | </Avatar> | ||||
| <div> | <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'> | <div className='text-xs text-gray-600'> | ||||
| {t('不同用户分组的价格信息')} | |||||
| {hasDisplayPricing(modelData) | |||||
| ? t('展示价格,仅用于估算') | |||||
| : t('不同用户分组的价格信息')} | |||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| @@ -7,17 +7,22 @@ const { Text } = Typography; | |||||
| const UserPriceComparison = ({ | const UserPriceComparison = ({ | ||||
| modelName, | modelName, | ||||
| modelData, | |||||
| currency, | currency, | ||||
| tokenUnit, | tokenUnit, | ||||
| displayPrice, | displayPrice, | ||||
| t, | t, | ||||
| }) => { | }) => { | ||||
| const hasDisplayPricing = | |||||
| Array.isArray(modelData?.display_pricing) && | |||||
| modelData.display_pricing.length > 0; | |||||
| const [loading, setLoading] = useState(false); | const [loading, setLoading] = useState(false); | ||||
| const [priceData, setPriceData] = useState(null); | const [priceData, setPriceData] = useState(null); | ||||
| useEffect(() => { | useEffect(() => { | ||||
| const fetchUserPrice = async () => { | const fetchUserPrice = async () => { | ||||
| if (!modelName) return; | |||||
| if (!modelName || hasDisplayPricing) return; | |||||
| setLoading(true); | setLoading(true); | ||||
| try { | try { | ||||
| const res = await API.get( | const res = await API.get( | ||||
| @@ -37,7 +42,7 @@ const UserPriceComparison = ({ | |||||
| } | } | ||||
| }; | }; | ||||
| fetchUserPrice(); | fetchUserPrice(); | ||||
| }, [modelName]); | |||||
| }, [modelName, hasDisplayPricing]); | |||||
| const currencySymbol = useMemo(() => { | const currencySymbol = useMemo(() => { | ||||
| if (currency === 'CNY') return '¥'; | if (currency === 'CNY') return '¥'; | ||||
| @@ -74,6 +79,8 @@ const UserPriceComparison = ({ | |||||
| return `${currencySymbol}${num.toFixed(4)}`; | return `${currencySymbol}${num.toFixed(4)}`; | ||||
| }; | }; | ||||
| if (hasDisplayPricing) return null; | |||||
| if (loading) { | if (loading) { | ||||
| return ( | return ( | ||||
| <Card className='!rounded-2xl shadow-sm border-0 mb-4'> | <Card className='!rounded-2xl shadow-sm border-0 mb-4'> | ||||
| @@ -38,6 +38,7 @@ import { | |||||
| import { | import { | ||||
| stringToColor, | stringToColor, | ||||
| getLobeHubIcon, | getLobeHubIcon, | ||||
| hasDisplayPricing, | |||||
| hasMatrixPricing, | hasMatrixPricing, | ||||
| calculateModelPrice, | calculateModelPrice, | ||||
| } from '../../../../../helpers'; | } from '../../../../../helpers'; | ||||
| @@ -163,8 +164,41 @@ const PricingCardView = ({ | |||||
| tokenUnit, | tokenUnit, | ||||
| displayPrice, | displayPrice, | ||||
| currency, | 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) { | if (!priceData?.isMatrix) { | ||||
| return null; | return null; | ||||
| } | } | ||||
| @@ -206,7 +240,13 @@ const PricingCardView = ({ | |||||
| - | - | ||||
| </Tag> | </Tag> | ||||
| ); | ); | ||||
| if (hasMatrixPricing(record)) { | |||||
| if (hasDisplayPricing(record)) { | |||||
| billingTag = ( | |||||
| <Tag key='billing' shape='circle' color='orange' size='small'> | |||||
| {t('展示价格')} | |||||
| </Tag> | |||||
| ); | |||||
| } else if (hasMatrixPricing(record)) { | |||||
| billingTag = ( | billingTag = ( | ||||
| <Tag key='billing' shape='circle' color='purple' size='small'> | <Tag key='billing' shape='circle' color='purple' size='small'> | ||||
| {t('矩阵计费')} | {t('矩阵计费')} | ||||
| @@ -24,6 +24,7 @@ import { | |||||
| renderModelTag, | renderModelTag, | ||||
| stringToColor, | stringToColor, | ||||
| calculateModelPrice, | calculateModelPrice, | ||||
| hasDisplayPricing, | |||||
| hasMatrixPricing, | hasMatrixPricing, | ||||
| getLobeHubIcon, | getLobeHubIcon, | ||||
| } from '../../../../../helpers'; | } from '../../../../../helpers'; | ||||
| @@ -53,6 +54,13 @@ function renderQuotaType(type, t) { | |||||
| } | } | ||||
| function renderBillingType(record, t) { | function renderBillingType(record, t) { | ||||
| if (hasDisplayPricing(record)) { | |||||
| return ( | |||||
| <Tag color='orange' shape='circle'> | |||||
| {t('展示价格')} | |||||
| </Tag> | |||||
| ); | |||||
| } | |||||
| if (hasMatrixPricing(record)) { | if (hasMatrixPricing(record)) { | ||||
| return ( | return ( | ||||
| <Tag color='purple' shape='circle'> | <Tag color='purple' shape='circle'> | ||||
| @@ -137,6 +145,7 @@ export const getPricingTableColumns = ({ | |||||
| tokenUnit, | tokenUnit, | ||||
| displayPrice, | displayPrice, | ||||
| currency, | currency, | ||||
| t, | |||||
| }); | }); | ||||
| priceDataCache.set(record, cache); | priceDataCache.set(record, cache); | ||||
| } | } | ||||
| @@ -244,6 +253,23 @@ export const getPricingTableColumns = ({ | |||||
| render: (text, record, index) => { | render: (text, record, index) => { | ||||
| const priceData = getPriceData(record); | 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) { | if (priceData.isMatrix) { | ||||
| return ( | return ( | ||||
| <div className='space-y-1'> | <div className='space-y-1'> | ||||
| @@ -3295,5 +3295,14 @@ | |||||
| "重试启动失败": "Failed to start retry", | "重试启动失败": "Failed to start retry", | ||||
| "重试已启动": "Retry started", | "重试已启动": "Retry started", | ||||
| "验证完成": "Verification complete", | "验证完成": "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": "个视频" | |||||
| } | } | ||||