From cc97834e5b8da3fe4a273fa0893a9f41eb11bfcc Mon Sep 17 00:00:00 2001 From: fengsilin Date: Wed, 1 Jul 2026 11:58:48 +0800 Subject: [PATCH] feat: add display pricing frontend helpers --- web/src/helpers/utils.jsx | 101 ++++++++++++++++++++++++++++++++++ web/src/helpers/utils.test.js | 92 +++++++++++++++++++++++++++++++ 2 files changed, 193 insertions(+) diff --git a/web/src/helpers/utils.jsx b/web/src/helpers/utils.jsx index afc0566..8253b1b 100644 --- a/web/src/helpers/utils.jsx +++ b/web/src/helpers/utils.jsx @@ -609,6 +609,93 @@ export const selectFilter = (input, option) => { // ------------------------------- // 模型定价计算工具函数 +export const hasDisplayPricing = (record) => + Array.isArray(record?.display_pricing) && record.display_pricing.length > 0; + +export const normalizeDisplayPricingItems = (record) => { + if (!hasDisplayPricing(record)) return []; + return [...record.display_pricing] + .map((item) => { + const discountRate = + Number(item.discount_rate) > 0 && Number(item.discount_rate) <= 1 + ? Number(item.discount_rate) + : 1; + const price = Number(item.price || 0); + return { + ...item, + price, + discount_rate: discountRate, + effectivePrice: price * discountRate, + unit: item.unit || 'call', + sort_order: Number(item.sort_order || 0), + }; + }) + .sort((a, b) => { + if (a.sort_order !== b.sort_order) return a.sort_order - b.sort_order; + return String(a.specification || '').localeCompare( + String(b.specification || ''), + ); + }); +}; + +export const formatDisplayPricingItem = ({ + item, + displayPrice, + t = (key) => key, +}) => { + const unit = item.unit || 'call'; + const effectivePrice = Number(item.effectivePrice ?? item.price ?? 0); + const hasDiscount = item.discount_rate > 0 && item.discount_rate < 1; + return { + ...item, + formattedPrice: `${displayPrice(effectivePrice)} / ${t(unit)}`, + originalPrice: hasDiscount + ? `${displayPrice(Number(item.price || 0))} / ${t(unit)}` + : '', + discountText: hasDiscount + ? `${Math.round(item.discount_rate * 100) / 10}折` + : '-', + }; +}; + +export const summarizeDisplayPricing = ({ + record, + displayPrice, + t = (key) => key, +}) => { + const items = normalizeDisplayPricingItems(record); + if (items.length === 0) return null; + + const groups = new Map(); + items.forEach((item) => { + const unit = item.unit || 'call'; + const list = groups.get(unit) || []; + list.push(item); + groups.set(unit, list); + }); + + const lines = Array.from(groups.entries()).map(([unit, list]) => { + const prices = list.map((item) => Number(item.effectivePrice || 0)); + const min = Math.min(...prices); + const max = Math.max(...prices); + const priceText = + min === max + ? displayPrice(min) + : `${displayPrice(min)} - ${displayPrice(max)}`; + return `${priceText} / ${t(unit)}`; + }); + + return { + isDisplayPricing: true, + price: lines.join('\n'), + unit: '', + details: items + .slice(0, 3) + .map((item) => formatDisplayPricingItem({ item, displayPrice, t })), + hiddenDetailCount: Math.max(0, items.length - 3), + }; +}; + export const hasMatrixPricing = (record) => !!( record?.pricing_config && @@ -724,6 +811,7 @@ export const calculateModelPrice = ({ displayPrice, currency, precision = 4, + t = (key) => key, }) => { // 1. 选择实际使用的分组 const ratios = groupRatio || {}; @@ -754,6 +842,19 @@ export const calculateModelPrice = ({ } // 2. 根据计费类型计算价格 + if (hasDisplayPricing(record)) { + const summary = summarizeDisplayPricing({ + record, + displayPrice, + t, + }); + return { + ...summary, + usedGroup, + usedGroupRatio, + }; + } + if (hasMatrixPricing(record)) { const summary = summarizeMatrixPricing({ record, diff --git a/web/src/helpers/utils.test.js b/web/src/helpers/utils.test.js index 39bfe77..1b4e0e3 100644 --- a/web/src/helpers/utils.test.js +++ b/web/src/helpers/utils.test.js @@ -18,7 +18,10 @@ globalThis.localStorage = { const { calculateModelPrice, formatMatrixPrice, + hasDisplayPricing, hasMatrixPricing, + normalizeDisplayPricingItems, + summarizeDisplayPricing, summarizeMatrixPricing, } = await import('./utils.jsx'); @@ -119,4 +122,93 @@ describe('model pricing helpers', () => { usedGroupRatio: 1, }); }); + + it('normalizes and sorts display pricing items', () => { + const record = { + display_pricing: [ + { + specification: '1080p', + price: 0.58333, + unit: 'second', + sort_order: 3, + discount_rate: 0, + }, + { + specification: '768p-6s', + price: 0.33333, + unit: 'second', + sort_order: 1, + discount_rate: 0.8, + }, + ], + }; + + expect(hasDisplayPricing(record)).toBe(true); + const items = normalizeDisplayPricingItems(record); + expect(items.map((item) => item.specification)).toEqual([ + '768p-6s', + '1080p', + ]); + expect(items[0].effectivePrice).toBeCloseTo(0.266664); + expect(items[1].discount_rate).toBe(1); + }); + + it('summarizes display pricing by unit', () => { + const summary = summarizeDisplayPricing({ + record: { + display_pricing: [ + { + specification: '768p-6s', + price: 0.33333, + unit: 'second', + sort_order: 1, + discount_rate: 1, + }, + { + specification: '1080p', + price: 0.58333, + unit: 'second', + sort_order: 2, + discount_rate: 1, + }, + ], + }, + displayPrice, + t: (key) => key, + }); + + expect(summary.isDisplayPricing).toBe(true); + expect(summary.price).toBe('$0.333 - $0.583 / second'); + expect(summary.details).toHaveLength(2); + }); + + it('calculateModelPrice prefers display pricing over matrix pricing', () => { + const result = calculateModelPrice({ + record: { + quota_type: 1, + model_price: 99, + pricing_config: { + billing_unit: 'per_call', + table: [{ duration: 5, price: 88, source: 'manual' }], + }, + display_pricing: [ + { + specification: '768p-6s', + price: 0.33333, + unit: 'second', + sort_order: 1, + discount_rate: 1, + }, + ], + }, + selectedGroup: 'default', + groupRatio: { default: 1 }, + tokenUnit: 'M', + displayPrice, + currency: 'USD', + }); + + expect(result.isDisplayPricing).toBe(true); + expect(result.price).toBe('$0.333 / second'); + }); });