登录用户查看渠道定价时,按用户分组倍率和渠道个人倍率计算实际折扣价 并在价格列展示划线原价与折扣价,新增 /pricing/user/*model 接口。 Co-Authored-By: Claude Sonnet 4 <noreply@anthropic.com>master
| @@ -301,7 +301,23 @@ func GetChannelPricingByModelWithChannelInfo(c *gin.Context) { | |||||
| return | return | ||||
| } | } | ||||
| common.ApiSuccess(c, list) | |||||
| loggedIn, userId, userGroup, groupRatio := resolveCurrentUser(c) | |||||
| if loggedIn { | |||||
| for _, item := range list { | |||||
| userChannelRatio := model.GetUserChannelRatio(userId, modelName, item.ChannelId) | |||||
| item.UserRatio = groupRatio * userChannelRatio | |||||
| } | |||||
| } | |||||
| c.JSON(200, gin.H{ | |||||
| "success": true, | |||||
| "message": "", | |||||
| "data": list, | |||||
| "logged_in": loggedIn, | |||||
| "group": userGroup, | |||||
| "group_ratio": groupRatio, | |||||
| }) | |||||
| } | } | ||||
| // GetChannelPricingWithTags 获取渠道定价(带标签详情) | // GetChannelPricingWithTags 获取渠道定价(带标签详情) | ||||
| @@ -1,6 +1,10 @@ | |||||
| package controller | package controller | ||||
| import ( | import ( | ||||
| "fmt" | |||||
| "math" | |||||
| "strings" | |||||
| "github.com/QuantumNous/new-api/common" | "github.com/QuantumNous/new-api/common" | ||||
| "github.com/QuantumNous/new-api/model" | "github.com/QuantumNous/new-api/model" | ||||
| "github.com/QuantumNous/new-api/service" | "github.com/QuantumNous/new-api/service" | ||||
| @@ -9,6 +13,25 @@ import ( | |||||
| "github.com/gin-gonic/gin" | "github.com/gin-gonic/gin" | ||||
| ) | ) | ||||
| // resolveCurrentUser 从 gin.Context 解析当前登录用户信息。 | |||||
| // 未登录或查询失败时 loggedIn=false,groupRatio=1.0。 | |||||
| func resolveCurrentUser(c *gin.Context) (loggedIn bool, userId int, userGroup string, groupRatio float64) { | |||||
| groupRatio = 1.0 | |||||
| raw, exists := c.Get("id") | |||||
| if !exists { | |||||
| return | |||||
| } | |||||
| userId = raw.(int) | |||||
| user, err := model.GetUserCache(userId) | |||||
| if err != nil { | |||||
| return | |||||
| } | |||||
| loggedIn = true | |||||
| userGroup = user.Group | |||||
| groupRatio = service.GetUserGroupRatio(userGroup, userGroup) | |||||
| return | |||||
| } | |||||
| func filterPricingByUsableGroups(pricing []model.Pricing, usableGroup map[string]string) []model.Pricing { | func filterPricingByUsableGroups(pricing []model.Pricing, usableGroup map[string]string) []model.Pricing { | ||||
| if len(pricing) == 0 { | if len(pricing) == 0 { | ||||
| return pricing | return pricing | ||||
| @@ -76,6 +99,95 @@ func GetPricing(c *gin.Context) { | |||||
| }) | }) | ||||
| } | } | ||||
| // GetUserPricing 获取用户对指定模型的价格信息(原价 vs 用户价) | |||||
| func GetUserPricing(c *gin.Context) { | |||||
| modelName := c.Param("model") | |||||
| // *model 通配符返回值带前导 /,需要去掉 | |||||
| modelName = strings.TrimPrefix(modelName, "/") | |||||
| if modelName == "" { | |||||
| common.ApiErrorMsg(c, "模型名不能为空") | |||||
| return | |||||
| } | |||||
| pricingData := model.GetPricingByModel(modelName) | |||||
| if pricingData == nil { | |||||
| common.ApiErrorMsg(c, "未找到该模型的定价信息") | |||||
| return | |||||
| } | |||||
| loggedIn, userId, userGroup, groupRatio := resolveCurrentUser(c) | |||||
| if !loggedIn { | |||||
| respondOriginalPrice(c, pricingData) | |||||
| return | |||||
| } | |||||
| bestUserChannelRatio := model.GetBestUserChannelRatio(userId, modelName) | |||||
| totalRatio := groupRatio * bestUserChannelRatio | |||||
| savingsPercent := int(math.Round((1 - totalRatio) * 100)) | |||||
| result := gin.H{ | |||||
| "success": true, | |||||
| "model_name": pricingData.ModelName, | |||||
| "quota_type": pricingData.QuotaType, | |||||
| "group": userGroup, | |||||
| "group_ratio": groupRatio, | |||||
| "user_channel_ratio": bestUserChannelRatio, | |||||
| "savings_percent": savingsPercent, | |||||
| "logged_in": true, | |||||
| } | |||||
| if pricingData.QuotaType == model.QuotaTypeByTokens { | |||||
| originalInput := pricingData.ModelRatio * 2 | |||||
| originalOutput := pricingData.ModelRatio * pricingData.CompletionRatio * 2 | |||||
| result["original_input"] = originalInput | |||||
| result["original_output"] = originalOutput | |||||
| result["user_input"] = originalInput * totalRatio | |||||
| result["user_output"] = originalOutput * totalRatio | |||||
| } else { | |||||
| result["original_price"] = pricingData.ModelPrice | |||||
| result["user_price"] = pricingData.ModelPrice * totalRatio | |||||
| } | |||||
| if savingsPercent > 0 { | |||||
| result["discount"] = formatDiscount(totalRatio) | |||||
| } | |||||
| c.JSON(200, result) | |||||
| } | |||||
| // respondOriginalPrice 未登录或用户查询失败时,只返回原价 | |||||
| func respondOriginalPrice(c *gin.Context, pricingData *model.Pricing) { | |||||
| result := gin.H{ | |||||
| "success": true, | |||||
| "model_name": pricingData.ModelName, | |||||
| "quota_type": pricingData.QuotaType, | |||||
| "logged_in": false, | |||||
| } | |||||
| if pricingData.QuotaType == model.QuotaTypeByTokens { | |||||
| result["original_input"] = pricingData.ModelRatio * 2 | |||||
| result["original_output"] = pricingData.ModelRatio * pricingData.CompletionRatio * 2 | |||||
| } else { | |||||
| result["original_price"] = pricingData.ModelPrice | |||||
| } | |||||
| c.JSON(200, result) | |||||
| } | |||||
| // formatDiscount 将倍率转换为中文折扣格式 | |||||
| func formatDiscount(ratio float64) string { | |||||
| if ratio <= 0 { | |||||
| return "免费" | |||||
| } | |||||
| discount := int(math.Round(ratio * 10)) | |||||
| if discount >= 10 { | |||||
| return "" | |||||
| } | |||||
| remainder := int(math.Round(ratio*100)) % 10 | |||||
| if remainder == 0 { | |||||
| return fmt.Sprintf("%d折", discount) | |||||
| } | |||||
| return fmt.Sprintf("%.1f折", ratio*10) | |||||
| } | |||||
| func ResetModelRatio(c *gin.Context) { | func ResetModelRatio(c *gin.Context) { | ||||
| defaultStr := ratio_setting.DefaultModelRatio2JSONString() | defaultStr := ratio_setting.DefaultModelRatio2JSONString() | ||||
| err := model.UpdateOption("ModelRatio", defaultStr) | err := model.UpdateOption("ModelRatio", defaultStr) | ||||
| @@ -267,6 +267,7 @@ type ChannelPricingWithChannel struct { | |||||
| AudioRatio float64 `json:"audio_ratio"` | AudioRatio float64 `json:"audio_ratio"` | ||||
| AudioCompletionRatio float64 `json:"audio_completion_ratio"` | AudioCompletionRatio float64 `json:"audio_completion_ratio"` | ||||
| IsDefault bool `json:"is_default"` | IsDefault bool `json:"is_default"` | ||||
| UserRatio float64 `json:"user_ratio"` | |||||
| } | } | ||||
| // GetChannelPricingByModelWithChannelInfo 获取指定模型的渠道定价(带渠道信息) | // GetChannelPricingByModelWithChannelInfo 获取指定模型的渠道定价(带渠道信息) | ||||
| @@ -44,6 +44,7 @@ type PricingVendor struct { | |||||
| var ( | var ( | ||||
| pricingMap []Pricing | pricingMap []Pricing | ||||
| pricingByModel map[string]*Pricing | |||||
| vendorsList []PricingVendor | vendorsList []PricingVendor | ||||
| supportedEndpointMap map[string]common.EndpointInfo | supportedEndpointMap map[string]common.EndpointInfo | ||||
| lastGetPricingTime time.Time | lastGetPricingTime time.Time | ||||
| @@ -60,6 +61,19 @@ var ( | |||||
| modelSupportEndpointsLock = sync.RWMutex{} | modelSupportEndpointsLock = sync.RWMutex{} | ||||
| ) | ) | ||||
| // SetTestPricing 设置测试用的定价缓存(仅用于测试) | |||||
| func SetTestPricing(pricing []Pricing) { | |||||
| updatePricingLock.Lock() | |||||
| defer updatePricingLock.Unlock() | |||||
| pricingMap = pricing | |||||
| idx := make(map[string]*Pricing, len(pricing)) | |||||
| for i := range pricing { | |||||
| idx[pricing[i].ModelName] = &pricingMap[i] | |||||
| } | |||||
| pricingByModel = idx | |||||
| lastGetPricingTime = time.Now() | |||||
| } | |||||
| func GetPricing() []Pricing { | func GetPricing() []Pricing { | ||||
| if time.Since(lastGetPricingTime) > time.Minute*1 || len(pricingMap) == 0 { | if time.Since(lastGetPricingTime) > time.Minute*1 || len(pricingMap) == 0 { | ||||
| updatePricingLock.Lock() | updatePricingLock.Lock() | ||||
| @@ -74,6 +88,15 @@ func GetPricing() []Pricing { | |||||
| return pricingMap | return pricingMap | ||||
| } | } | ||||
| // GetPricingByModel 从缓存中查找指定模型的定价信息 | |||||
| func GetPricingByModel(modelName string) *Pricing { | |||||
| GetPricing() // 确保缓存已刷新 | |||||
| modelEnableGroupsLock.RLock() | |||||
| p := pricingByModel[modelName] | |||||
| modelEnableGroupsLock.RUnlock() | |||||
| return p | |||||
| } | |||||
| // GetVendors 返回当前定价接口使用到的供应商信息 | // GetVendors 返回当前定价接口使用到的供应商信息 | ||||
| func GetVendors() []PricingVendor { | func GetVendors() []PricingVendor { | ||||
| if time.Since(lastGetPricingTime) > time.Minute*1 || len(pricingMap) == 0 { | if time.Since(lastGetPricingTime) > time.Minute*1 || len(pricingMap) == 0 { | ||||
| @@ -364,10 +387,14 @@ func updatePricing() { | |||||
| modelEnableGroupsLock.Lock() | modelEnableGroupsLock.Lock() | ||||
| modelEnableGroups = make(map[string][]string) | modelEnableGroups = make(map[string][]string) | ||||
| modelQuotaTypeMap = make(map[string]int) | modelQuotaTypeMap = make(map[string]int) | ||||
| for _, p := range pricingMap { | |||||
| byModel := make(map[string]*Pricing, len(pricingMap)) | |||||
| for i := range pricingMap { | |||||
| p := &pricingMap[i] | |||||
| modelEnableGroups[p.ModelName] = p.EnableGroup | modelEnableGroups[p.ModelName] = p.EnableGroup | ||||
| modelQuotaTypeMap[p.ModelName] = p.QuotaType | modelQuotaTypeMap[p.ModelName] = p.QuotaType | ||||
| byModel[p.ModelName] = p | |||||
| } | } | ||||
| pricingByModel = byModel | |||||
| modelEnableGroupsLock.Unlock() | modelEnableGroupsLock.Unlock() | ||||
| lastGetPricingTime = time.Now() | lastGetPricingTime = time.Now() | ||||
| @@ -2,6 +2,7 @@ package model | |||||
| import ( | import ( | ||||
| "strconv" | "strconv" | ||||
| "strings" | |||||
| "sync" | "sync" | ||||
| "github.com/QuantumNous/new-api/common" | "github.com/QuantumNous/new-api/common" | ||||
| @@ -112,3 +113,17 @@ func GetUserChannelRatiosByUserId(userId int) ([]*UserChannelRatio, error) { | |||||
| err := DB.Where("user_id = ?", userId).Find(&list).Error | err := DB.Where("user_id = ?", userId).Find(&list).Error | ||||
| return list, err | return list, err | ||||
| } | } | ||||
| // GetBestUserChannelRatio 从内存缓存中查找指定用户+模型的最低渠道倍率(未命中返回 1.0) | |||||
| func GetBestUserChannelRatio(userId int, modelName string) float64 { | |||||
| best := 1.0 | |||||
| prefix := strconv.Itoa(userId) + ":" + modelName + ":" | |||||
| userChannelRatioCacheLock.RLock() | |||||
| for key, ratio := range userChannelRatioCache { | |||||
| if len(key) > len(prefix) && strings.HasPrefix(key, prefix) && ratio < best { | |||||
| best = ratio | |||||
| } | |||||
| } | |||||
| userChannelRatioCacheLock.RUnlock() | |||||
| return best | |||||
| } | |||||
| @@ -32,6 +32,7 @@ func SetApiRouter(router *gin.Engine) { | |||||
| //apiRouter.GET("/midjourney", controller.GetMidjourney) | //apiRouter.GET("/midjourney", controller.GetMidjourney) | ||||
| apiRouter.GET("/home_page_content", controller.GetHomePageContent) | apiRouter.GET("/home_page_content", controller.GetHomePageContent) | ||||
| apiRouter.GET("/pricing", middleware.TryUserAuth(), controller.GetPricing) | apiRouter.GET("/pricing", middleware.TryUserAuth(), controller.GetPricing) | ||||
| apiRouter.GET("/pricing/user/*model", middleware.TryUserAuth(), controller.GetUserPricing) | |||||
| apiRouter.GET("/channel-pricing/model/*name", middleware.TryUserAuth(), controller.GetChannelPricingByModelWithChannelInfo) | apiRouter.GET("/channel-pricing/model/*name", middleware.TryUserAuth(), controller.GetChannelPricingByModelWithChannelInfo) | ||||
| apiRouter.GET("/captcha", controller.GetCaptcha) | apiRouter.GET("/captcha", controller.GetCaptcha) | ||||
| apiRouter.GET("/verification", middleware.EmailVerificationRateLimit(), middleware.TurnstileCheck(), controller.SendEmailVerification) | apiRouter.GET("/verification", middleware.EmailVerificationRateLimit(), middleware.TurnstileCheck(), controller.SendEmailVerification) | ||||
| @@ -37,6 +37,7 @@ const PricingPage = () => { | |||||
| setShowRatio, | setShowRatio, | ||||
| viewMode, | viewMode, | ||||
| setViewMode, | setViewMode, | ||||
| showTryButton: true, | |||||
| }; | }; | ||||
| return ( | return ( | ||||
| @@ -122,7 +122,7 @@ const PricingSidebar = ({ | |||||
| t={t} | t={t} | ||||
| /> | /> | ||||
| <PricingGroups | |||||
| {/* <PricingGroups | |||||
| filterGroup={filterGroup} | filterGroup={filterGroup} | ||||
| setFilterGroup={handleGroupClick} | setFilterGroup={handleGroupClick} | ||||
| usableGroup={categoryProps.usableGroup} | usableGroup={categoryProps.usableGroup} | ||||
| @@ -130,7 +130,7 @@ const PricingSidebar = ({ | |||||
| models={groupCountModels} | models={groupCountModels} | ||||
| loading={loading} | loading={loading} | ||||
| t={t} | t={t} | ||||
| /> | |||||
| /> */} | |||||
| <PricingQuotaTypes | <PricingQuotaTypes | ||||
| filterQuotaType={filterQuotaType} | filterQuotaType={filterQuotaType} | ||||
| @@ -61,7 +61,7 @@ const ModelDetailSideSheet = ({ | |||||
| borderBottom: '1px solid var(--semi-color-border)', | borderBottom: '1px solid var(--semi-color-border)', | ||||
| }} | }} | ||||
| visible={visible} | visible={visible} | ||||
| width={isMobile ? '100%' : 600} | |||||
| width={isMobile ? '100%' : 750} | |||||
| closeIcon={ | closeIcon={ | ||||
| <Button | <Button | ||||
| className='semi-button-tertiary semi-button-size-small semi-button-borderless' | className='semi-button-tertiary semi-button-size-small semi-button-borderless' | ||||
| @@ -17,10 +17,10 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. | |||||
| For commercial licensing, please contact support@quantumnous.com | For commercial licensing, please contact support@quantumnous.com | ||||
| */ | */ | ||||
| import React, { useState, useEffect, useMemo } from 'react'; | |||||
| import React, { useState, useEffect } from 'react'; | |||||
| import { Card, Avatar, Typography, Table, Tag, Spin, Banner, Tooltip } from '@douyinfe/semi-ui'; | import { Card, Avatar, Typography, Table, Tag, Spin, Banner, Tooltip } from '@douyinfe/semi-ui'; | ||||
| import { IconServer, IconCopy } from '@douyinfe/semi-icons'; | |||||
| import { API, copy, showSuccess } from '../../../../../helpers'; | |||||
| import { IconServer, IconCopy, IconArrowDown } from '@douyinfe/semi-icons'; | |||||
| import { API, copy, showSuccess, getCurrencyConfig } from '../../../../../helpers'; | |||||
| const { Text } = Typography; | const { Text } = Typography; | ||||
| @@ -33,6 +33,7 @@ const ChannelPricingCard = ({ | |||||
| }) => { | }) => { | ||||
| const [loading, setLoading] = useState(false); | const [loading, setLoading] = useState(false); | ||||
| const [channelPricingData, setChannelPricingData] = useState([]); | const [channelPricingData, setChannelPricingData] = useState([]); | ||||
| const [priceMeta, setPriceMeta] = useState({ loggedIn: false, group: '', groupRatio: 1.0 }); | |||||
| useEffect(() => { | useEffect(() => { | ||||
| const fetchChannelPricing = async () => { | const fetchChannelPricing = async () => { | ||||
| @@ -41,14 +42,18 @@ const ChannelPricingCard = ({ | |||||
| setLoading(true); | setLoading(true); | ||||
| try { | try { | ||||
| const res = await API.get(`/api/channel-pricing/model/${encodeURIComponent(modelName)}`); | const res = await API.get(`/api/channel-pricing/model/${encodeURIComponent(modelName)}`); | ||||
| const { success, data } = res.data; | |||||
| if (success && Array.isArray(data)) { | |||||
| setChannelPricingData(data); | |||||
| const result = res.data; | |||||
| if (result.success && Array.isArray(result.data)) { | |||||
| setChannelPricingData(result.data); | |||||
| setPriceMeta({ | |||||
| loggedIn: result.logged_in || false, | |||||
| group: result.group || '', | |||||
| groupRatio: result.group_ratio || 1.0, | |||||
| }); | |||||
| } else { | } else { | ||||
| setChannelPricingData([]); | setChannelPricingData([]); | ||||
| } | } | ||||
| } catch (error) { | } catch (error) { | ||||
| // 静默处理错误,不显示给用户 | |||||
| setChannelPricingData([]); | setChannelPricingData([]); | ||||
| } finally { | } finally { | ||||
| setLoading(false); | setLoading(false); | ||||
| @@ -58,23 +63,7 @@ const ChannelPricingCard = ({ | |||||
| fetchChannelPricing(); | fetchChannelPricing(); | ||||
| }, [modelName]); | }, [modelName]); | ||||
| // 获取货币符号 | |||||
| const currencySymbol = useMemo(() => { | |||||
| if (currency === 'CNY') return '¥'; | |||||
| if (currency === 'CUSTOM') { | |||||
| try { | |||||
| const statusStr = localStorage.getItem('status'); | |||||
| if (statusStr) { | |||||
| const s = JSON.parse(statusStr); | |||||
| return s?.custom_currency_symbol || '¤'; | |||||
| } | |||||
| } catch (e) { | |||||
| // ignore | |||||
| } | |||||
| return '¤'; | |||||
| } | |||||
| return '$'; | |||||
| }, [currency]); | |||||
| const currencySymbol = getCurrencyConfig().symbol; | |||||
| // 格式化价格显示 | // 格式化价格显示 | ||||
| const formatPrice = (price) => { | const formatPrice = (price) => { | ||||
| @@ -105,7 +94,7 @@ const ChannelPricingCard = ({ | |||||
| key: item.channel_id || index, | key: item.channel_id || index, | ||||
| channelId: item.channel_id, | channelId: item.channel_id, | ||||
| channelName: item.channel_public_name || ('通道' + (index + 1)), | channelName: item.channel_public_name || ('通道' + (index + 1)), | ||||
| channelTags: item.tags || [], // 渠道定价的标签列表 | |||||
| channelTags: item.tags || [], | |||||
| channelType: item.channel_type, | channelType: item.channel_type, | ||||
| quotaType: item.quota_type, | quotaType: item.quota_type, | ||||
| modelRatio: item.model_ratio, | modelRatio: item.model_ratio, | ||||
| @@ -115,6 +104,7 @@ const ChannelPricingCard = ({ | |||||
| cacheRatio: item.cache_ratio, | cacheRatio: item.cache_ratio, | ||||
| cacheCreationRatio: item.cache_creation_ratio, | cacheCreationRatio: item.cache_creation_ratio, | ||||
| isDefault: item.is_default || false, | isDefault: item.is_default || false, | ||||
| userRatio: item.user_ratio ?? 1.0, | |||||
| })); | })); | ||||
| // 判断是否存在按次计费的渠道 | // 判断是否存在按次计费的渠道 | ||||
| @@ -180,7 +170,55 @@ const ChannelPricingCard = ({ | |||||
| }, | }, | ||||
| ]; | ]; | ||||
| // 渲染带折扣的价格 | |||||
| const renderDiscountedPrice = (originalPrice, record, unit) => { | |||||
| const { userRatio } = record; | |||||
| const hasDiscount = priceMeta.loggedIn && userRatio < 1; | |||||
| if (hasDiscount) { | |||||
| const discountedPrice = originalPrice * userRatio; | |||||
| return ( | |||||
| <div style={{ whiteSpace: 'nowrap' }}> | |||||
| <div style={{ textDecoration: 'line-through', color: '#999', fontSize: '13px' }}> | |||||
| {formatPrice(originalPrice)} / {unit} | |||||
| </div> | |||||
| <div style={{ fontSize: '15px', fontWeight: 700 }}> | |||||
| {formatPrice(discountedPrice)} / {unit} | |||||
| </div> | |||||
| </div> | |||||
| ); | |||||
| } | |||||
| return ( | |||||
| <div style={{ whiteSpace: 'nowrap' }}> | |||||
| <span className='font-semibold'> | |||||
| {formatPrice(originalPrice)} | |||||
| </span> | |||||
| <span className='text-xs text-gray-500'> / {unit}</span> | |||||
| </div> | |||||
| ); | |||||
| }; | |||||
| // 折扣列(仅登录且有折扣时显示) | |||||
| const discountColumn = priceMeta.loggedIn | |||||
| ? [ | |||||
| { | |||||
| title: t('折扣'), | |||||
| dataIndex: 'userRatio', | |||||
| render: (userRatio) => { | |||||
| if (userRatio >= 1) return '-'; | |||||
| return ( | |||||
| <Tag color='red' size='small' shape='circle' style={{ fontSize: '12px', lineHeight: '18px' }}> | |||||
| <IconArrowDown size='tiny' style={{ marginRight: 2, verticalAlign: 'middle' }} />{Math.round((1 - userRatio) * 100)}% | |||||
| </Tag> | |||||
| ); | |||||
| }, | |||||
| }, | |||||
| ] | |||||
| : []; | |||||
| // 只有存在按量计费的渠道时才显示输入/输出价格列 | // 只有存在按量计费的渠道时才显示输入/输出价格列 | ||||
| const tokenUnitLabel = tokenUnit === 'K' ? '1K' : '1M'; | |||||
| const tokenBasedColumns = hasTokenBased | const tokenBasedColumns = hasTokenBased | ||||
| ? [ | ? [ | ||||
| { | { | ||||
| @@ -188,18 +226,8 @@ const ChannelPricingCard = ({ | |||||
| dataIndex: 'modelRatio', | dataIndex: 'modelRatio', | ||||
| render: (ratio, record) => { | render: (ratio, record) => { | ||||
| if (record.quotaType !== 0) return '-'; | if (record.quotaType !== 0) return '-'; | ||||
| // 输入价格 = model_ratio * 2 (渠道定价不乘分组倍率) | |||||
| const inputPrice = ratio * 2; | const inputPrice = ratio * 2; | ||||
| return ( | |||||
| <> | |||||
| <div className='font-semibold text-orange-600'> | |||||
| {formatPrice(inputPrice)} | |||||
| </div> | |||||
| <div className='text-xs text-gray-500'> | |||||
| / {tokenUnit === 'K' ? '1K' : '1M'} tokens | |||||
| </div> | |||||
| </> | |||||
| ); | |||||
| return renderDiscountedPrice(inputPrice, record, `${tokenUnitLabel} tokens`); | |||||
| }, | }, | ||||
| }, | }, | ||||
| { | { | ||||
| @@ -207,18 +235,8 @@ const ChannelPricingCard = ({ | |||||
| dataIndex: 'completionRatio', | dataIndex: 'completionRatio', | ||||
| render: (completionRatio, record) => { | render: (completionRatio, record) => { | ||||
| if (record.quotaType !== 0) return '-'; | if (record.quotaType !== 0) return '-'; | ||||
| // 输出价格 = model_ratio * completion_ratio * 2 | |||||
| const outputPrice = record.modelRatio * completionRatio * 2; | const outputPrice = record.modelRatio * completionRatio * 2; | ||||
| return ( | |||||
| <> | |||||
| <div className='font-semibold text-orange-600'> | |||||
| {formatPrice(outputPrice)} | |||||
| </div> | |||||
| <div className='text-xs text-gray-500'> | |||||
| / {tokenUnit === 'K' ? '1K' : '1M'} tokens | |||||
| </div> | |||||
| </> | |||||
| ); | |||||
| return renderDiscountedPrice(outputPrice, record, `${tokenUnitLabel} tokens`); | |||||
| }, | }, | ||||
| }, | }, | ||||
| ] | ] | ||||
| @@ -231,16 +249,8 @@ const ChannelPricingCard = ({ | |||||
| const renderCachePrice = (v, record) => { | const renderCachePrice = (v, record) => { | ||||
| if (record.quotaType !== 0) return '-'; | if (record.quotaType !== 0) return '-'; | ||||
| return ( | |||||
| <> | |||||
| <div className='font-semibold text-orange-600'> | |||||
| {formatPrice(record.modelRatio * v * 2)} | |||||
| </div> | |||||
| <div className='text-xs text-gray-500'> | |||||
| / {tokenUnit === 'K' ? '1K' : '1M'} tokens | |||||
| </div> | |||||
| </> | |||||
| ); | |||||
| const cachePrice = record.modelRatio * v * 2; | |||||
| return renderDiscountedPrice(cachePrice, record, `${tokenUnitLabel} tokens`); | |||||
| }; | }; | ||||
| const advancedColumns = hasAdvancedPricing | const advancedColumns = hasAdvancedPricing | ||||
| @@ -266,20 +276,13 @@ const ChannelPricingCard = ({ | |||||
| dataIndex: 'modelPrice', | dataIndex: 'modelPrice', | ||||
| render: (price, record) => { | render: (price, record) => { | ||||
| if (record.quotaType !== 1) return '-'; | if (record.quotaType !== 1) return '-'; | ||||
| return ( | |||||
| <> | |||||
| <div className='font-semibold text-orange-600'> | |||||
| {displayPrice(price)} | |||||
| </div> | |||||
| <div className='text-xs text-gray-500'>/ {t('次')}</div> | |||||
| </> | |||||
| ); | |||||
| return renderDiscountedPrice(price, record, t('次')); | |||||
| }, | }, | ||||
| }, | }, | ||||
| ] | ] | ||||
| : []; | : []; | ||||
| const columns = [...baseColumns, ...tokenBasedColumns, ...advancedColumns, ...callBasedColumn]; | |||||
| const columns = [...baseColumns, ...discountColumn, ...tokenBasedColumns, ...advancedColumns, ...callBasedColumn]; | |||||
| return ( | return ( | ||||
| <Card className='!rounded-2xl shadow-sm border-0 mb-6'> | <Card className='!rounded-2xl shadow-sm border-0 mb-6'> | ||||
| @@ -105,7 +105,7 @@ const FilterModalContent = ({ sidebarProps, t }) => { | |||||
| t={t} | t={t} | ||||
| /> | /> | ||||
| <PricingGroups | |||||
| {/* <PricingGroups | |||||
| filterGroup={filterGroup} | filterGroup={filterGroup} | ||||
| setFilterGroup={setFilterGroup} | setFilterGroup={setFilterGroup} | ||||
| usableGroup={categoryProps.usableGroup} | usableGroup={categoryProps.usableGroup} | ||||
| @@ -113,7 +113,7 @@ const FilterModalContent = ({ sidebarProps, t }) => { | |||||
| models={groupCountModels} | models={groupCountModels} | ||||
| loading={loading} | loading={loading} | ||||
| t={t} | t={t} | ||||
| /> | |||||
| /> */} | |||||
| <PricingQuotaTypes | <PricingQuotaTypes | ||||
| filterQuotaType={filterQuotaType} | filterQuotaType={filterQuotaType} | ||||
| @@ -184,7 +184,7 @@ export const getPricingTableColumns = ({ | |||||
| modelNameColumn, | modelNameColumn, | ||||
| vendorColumn, | vendorColumn, | ||||
| descriptionColumn, | descriptionColumn, | ||||
| tagsColumn, | |||||
| // tagsColumn, | |||||
| quotaColumn, | quotaColumn, | ||||
| ]; | ]; | ||||
| @@ -255,7 +255,7 @@ export const getPricingTableColumns = ({ | |||||
| }; | }; | ||||
| const columns = [...baseColumns]; | const columns = [...baseColumns]; | ||||
| columns.push(endpointColumn); | |||||
| // columns.push(endpointColumn); | |||||
| if (showRatio) { | if (showRatio) { | ||||
| columns.push(ratioColumn); | columns.push(ratioColumn); | ||||
| } | } | ||||
| @@ -451,12 +451,12 @@ export const getTokensColumns = ({ | |||||
| key: 'quota_usage', | key: 'quota_usage', | ||||
| render: (text, record) => renderQuotaUsage(text, record, t), | render: (text, record) => renderQuotaUsage(text, record, t), | ||||
| }, | }, | ||||
| { | |||||
| title: t('分组'), | |||||
| dataIndex: 'group', | |||||
| key: 'group', | |||||
| render: (text, record) => renderGroupColumn(text, record, t), | |||||
| }, | |||||
| // { | |||||
| // title: t('分组'), | |||||
| // dataIndex: 'group', | |||||
| // key: 'group', | |||||
| // render: (text, record) => renderGroupColumn(text, record, t), | |||||
| // }, | |||||
| { | { | ||||
| title: t('密钥'), | title: t('密钥'), | ||||
| key: 'token_key', | key: 'token_key', | ||||
| @@ -378,7 +378,7 @@ const EditTokenModal = (props) => { | |||||
| showClear | showClear | ||||
| /> | /> | ||||
| </Col> | </Col> | ||||
| <Col span={24}> | |||||
| {/* <Col span={24}> | |||||
| {groups.length > 0 ? ( | {groups.length > 0 ? ( | ||||
| <Form.Select | <Form.Select | ||||
| field='group' | field='group' | ||||
| @@ -412,7 +412,7 @@ const EditTokenModal = (props) => { | |||||
| '开启后,当前分组渠道失败时会按顺序尝试下一个分组的渠道', | '开启后,当前分组渠道失败时会按顺序尝试下一个分组的渠道', | ||||
| )} | )} | ||||
| /> | /> | ||||
| </Col> | |||||
| </Col> */} | |||||
| <Col xs={24} sm={24} md={24} lg={10} xl={10}> | <Col xs={24} sm={24} md={24} lg={10} xl={10}> | ||||
| <Form.DatePicker | <Form.DatePicker | ||||
| field='expired_time' | field='expired_time' | ||||
| @@ -890,14 +890,14 @@ const TopUp = () => { | |||||
| allSubscriptions={allSubscriptions} | allSubscriptions={allSubscriptions} | ||||
| reloadSubscriptionSelf={getSubscriptionSelf} | reloadSubscriptionSelf={getSubscriptionSelf} | ||||
| /> | /> | ||||
| <InvitationCard | |||||
| {/* <InvitationCard | |||||
| t={t} | t={t} | ||||
| userState={userState} | userState={userState} | ||||
| renderQuota={renderQuota} | renderQuota={renderQuota} | ||||
| setOpenTransfer={setOpenTransfer} | setOpenTransfer={setOpenTransfer} | ||||
| affLink={affLink} | affLink={affLink} | ||||
| handleAffLinkClick={handleAffLinkClick} | handleAffLinkClick={handleAffLinkClick} | ||||
| /> | |||||
| /> */} | |||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| ); | ); | ||||