瀏覽代碼

feat: able to display quota in dollar

JustSong 2 年之前
父節點
當前提交
b179c2f208

+ 2 - 0
common/constants.go

@@ -15,6 +15,8 @@ var Footer = ""
 var Logo = ""
 var Logo = ""
 var TopUpLink = ""
 var TopUpLink = ""
 var ChatLink = ""
 var ChatLink = ""
+var QuotaPerUnit = 500 * 1000.0 // $0.002 / 1K tokens
+var DisplayInCurrencyEnabled = false
 
 
 var UsingSQLite = false
 var UsingSQLite = false
 
 

+ 8 - 0
common/logger.go

@@ -42,3 +42,11 @@ func FatalLog(v ...any) {
 	_, _ = fmt.Fprintf(gin.DefaultErrorWriter, "[FATAL] %v | %v \n", t.Format("2006/01/02 - 15:04:05"), v)
 	_, _ = fmt.Fprintf(gin.DefaultErrorWriter, "[FATAL] %v | %v \n", t.Format("2006/01/02 - 15:04:05"), v)
 	os.Exit(1)
 	os.Exit(1)
 }
 }
+
+func LogQuota(quota int) string {
+	if DisplayInCurrencyEnabled {
+		return fmt.Sprintf("$%.6f 额度", float64(quota)/QuotaPerUnit)
+	} else {
+		return fmt.Sprintf("%d 点额度", quota)
+	}
+}

+ 22 - 6
controller/billing.go

@@ -2,6 +2,7 @@ package controller
 
 
 import (
 import (
 	"github.com/gin-gonic/gin"
 	"github.com/gin-gonic/gin"
+	"one-api/common"
 	"one-api/model"
 	"one-api/model"
 )
 )
 
 
@@ -18,23 +19,38 @@ func GetSubscription(c *gin.Context) {
 		})
 		})
 		return
 		return
 	}
 	}
+	amount := float64(quota)
+	if common.DisplayInCurrencyEnabled {
+		amount /= common.QuotaPerUnit
+	}
 	subscription := OpenAISubscriptionResponse{
 	subscription := OpenAISubscriptionResponse{
 		Object:             "billing_subscription",
 		Object:             "billing_subscription",
 		HasPaymentMethod:   true,
 		HasPaymentMethod:   true,
-		SoftLimitUSD:       float64(quota),
-		HardLimitUSD:       float64(quota),
-		SystemHardLimitUSD: float64(quota),
+		SoftLimitUSD:       amount,
+		HardLimitUSD:       amount,
+		SystemHardLimitUSD: amount,
 	}
 	}
 	c.JSON(200, subscription)
 	c.JSON(200, subscription)
 	return
 	return
 }
 }
 
 
 func GetUsage(c *gin.Context) {
 func GetUsage(c *gin.Context) {
-	//userId := c.GetInt("id")
-	// TODO: get usage from database
+	userId := c.GetInt("id")
+	quota, err := model.GetUserUsedQuota(userId)
+	if err != nil {
+		openAIError := OpenAIError{
+			Message: err.Error(),
+			Type:    "one_api_error",
+		}
+		c.JSON(200, gin.H{
+			"error": openAIError,
+		})
+		return
+	}
+	amount := float64(quota)
 	usage := OpenAIUsageResponse{
 	usage := OpenAIUsageResponse{
 		Object:     "list",
 		Object:     "list",
-		TotalUsage: 0,
+		TotalUsage: amount,
 	}
 	}
 	c.JSON(200, usage)
 	c.JSON(200, usage)
 	return
 	return

+ 17 - 15
controller/misc.go

@@ -14,21 +14,23 @@ func GetStatus(c *gin.Context) {
 		"success": true,
 		"success": true,
 		"message": "",
 		"message": "",
 		"data": gin.H{
 		"data": gin.H{
-			"version":            common.Version,
-			"start_time":         common.StartTime,
-			"email_verification": common.EmailVerificationEnabled,
-			"github_oauth":       common.GitHubOAuthEnabled,
-			"github_client_id":   common.GitHubClientId,
-			"system_name":        common.SystemName,
-			"logo":               common.Logo,
-			"footer_html":        common.Footer,
-			"wechat_qrcode":      common.WeChatAccountQRCodeImageURL,
-			"wechat_login":       common.WeChatAuthEnabled,
-			"server_address":     common.ServerAddress,
-			"turnstile_check":    common.TurnstileCheckEnabled,
-			"turnstile_site_key": common.TurnstileSiteKey,
-			"top_up_link":        common.TopUpLink,
-			"chat_link":          common.ChatLink,
+			"version":             common.Version,
+			"start_time":          common.StartTime,
+			"email_verification":  common.EmailVerificationEnabled,
+			"github_oauth":        common.GitHubOAuthEnabled,
+			"github_client_id":    common.GitHubClientId,
+			"system_name":         common.SystemName,
+			"logo":                common.Logo,
+			"footer_html":         common.Footer,
+			"wechat_qrcode":       common.WeChatAccountQRCodeImageURL,
+			"wechat_login":        common.WeChatAuthEnabled,
+			"server_address":      common.ServerAddress,
+			"turnstile_check":     common.TurnstileCheckEnabled,
+			"turnstile_site_key":  common.TurnstileSiteKey,
+			"top_up_link":         common.TopUpLink,
+			"chat_link":           common.ChatLink,
+			"quota_per_unit":      common.QuotaPerUnit,
+			"display_in_currency": common.DisplayInCurrencyEnabled,
 		},
 		},
 	})
 	})
 	return
 	return

+ 1 - 1
controller/relay-text.go

@@ -138,7 +138,7 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
 			}
 			}
 			tokenName := c.GetString("token_name")
 			tokenName := c.GetString("token_name")
 			userId := c.GetInt("id")
 			userId := c.GetInt("id")
-			model.RecordLog(userId, model.LogTypeConsume, fmt.Sprintf("通过令牌「%s」使用模型 %s 消耗 %d 点额度(模型倍率 %.2f,分组倍率 %.2f)", tokenName, textRequest.Model, quota, modelRatio, groupRatio))
+			model.RecordLog(userId, model.LogTypeConsume, fmt.Sprintf("通过令牌「%s」使用模型 %s 消耗 %s(模型倍率 %.2f,分组倍率 %.2f)", tokenName, textRequest.Model, common.LogQuota(quota), modelRatio, groupRatio))
 			model.UpdateUserUsedQuotaAndRequestCount(userId, quota)
 			model.UpdateUserUsedQuotaAndRequestCount(userId, quota)
 			channelId := c.GetInt("channel_id")
 			channelId := c.GetInt("channel_id")
 			model.UpdateChannelUsedQuota(channelId, quota)
 			model.UpdateChannelUsedQuota(channelId, quota)

+ 1 - 1
controller/user.go

@@ -384,7 +384,7 @@ func UpdateUser(c *gin.Context) {
 		return
 		return
 	}
 	}
 	if originUser.Quota != updatedUser.Quota {
 	if originUser.Quota != updatedUser.Quota {
-		model.RecordLog(originUser.Id, model.LogTypeManage, fmt.Sprintf("管理员将用户额度从 %d 点修改为 %d 点", originUser.Quota, updatedUser.Quota))
+		model.RecordLog(originUser.Id, model.LogTypeManage, fmt.Sprintf("管理员将用户额度从 %s修改为 %s", common.LogQuota(originUser.Quota), common.LogQuota(updatedUser.Quota)))
 	}
 	}
 	c.JSON(http.StatusOK, gin.H{
 	c.JSON(http.StatusOK, gin.H{
 		"success": true,
 		"success": true,

+ 2 - 2
model/cache.go

@@ -16,12 +16,12 @@ const (
 func CacheGetTokenByKey(key string) (*Token, error) {
 func CacheGetTokenByKey(key string) (*Token, error) {
 	var token Token
 	var token Token
 	if !common.RedisEnabled {
 	if !common.RedisEnabled {
-		err := DB.Where("`key` = ?", key).First(token).Error
+		err := DB.Where("`key` = ?", key).First(&token).Error
 		return &token, err
 		return &token, err
 	}
 	}
 	tokenObjectString, err := common.RedisGet(fmt.Sprintf("token:%s", key))
 	tokenObjectString, err := common.RedisGet(fmt.Sprintf("token:%s", key))
 	if err != nil {
 	if err != nil {
-		err := DB.Where("`key` = ?", key).First(token).Error
+		err := DB.Where("`key` = ?", key).First(&token).Error
 		if err != nil {
 		if err != nil {
 			return nil, err
 			return nil, err
 		}
 		}

+ 6 - 0
model/option.go

@@ -35,6 +35,7 @@ func InitOptionMap() {
 	common.OptionMap["RegisterEnabled"] = strconv.FormatBool(common.RegisterEnabled)
 	common.OptionMap["RegisterEnabled"] = strconv.FormatBool(common.RegisterEnabled)
 	common.OptionMap["AutomaticDisableChannelEnabled"] = strconv.FormatBool(common.AutomaticDisableChannelEnabled)
 	common.OptionMap["AutomaticDisableChannelEnabled"] = strconv.FormatBool(common.AutomaticDisableChannelEnabled)
 	common.OptionMap["LogConsumeEnabled"] = strconv.FormatBool(common.LogConsumeEnabled)
 	common.OptionMap["LogConsumeEnabled"] = strconv.FormatBool(common.LogConsumeEnabled)
+	common.OptionMap["DisplayInCurrencyEnabled"] = strconv.FormatBool(common.DisplayInCurrencyEnabled)
 	common.OptionMap["ChannelDisableThreshold"] = strconv.FormatFloat(common.ChannelDisableThreshold, 'f', -1, 64)
 	common.OptionMap["ChannelDisableThreshold"] = strconv.FormatFloat(common.ChannelDisableThreshold, 'f', -1, 64)
 	common.OptionMap["SMTPServer"] = ""
 	common.OptionMap["SMTPServer"] = ""
 	common.OptionMap["SMTPFrom"] = ""
 	common.OptionMap["SMTPFrom"] = ""
@@ -64,6 +65,7 @@ func InitOptionMap() {
 	common.OptionMap["GroupRatio"] = common.GroupRatio2JSONString()
 	common.OptionMap["GroupRatio"] = common.GroupRatio2JSONString()
 	common.OptionMap["TopUpLink"] = common.TopUpLink
 	common.OptionMap["TopUpLink"] = common.TopUpLink
 	common.OptionMap["ChatLink"] = common.ChatLink
 	common.OptionMap["ChatLink"] = common.ChatLink
+	common.OptionMap["QuotaPerUnit"] = strconv.FormatFloat(common.QuotaPerUnit, 'f', -1, 64)
 	common.OptionMapRWMutex.Unlock()
 	common.OptionMapRWMutex.Unlock()
 	loadOptionsFromDatabase()
 	loadOptionsFromDatabase()
 }
 }
@@ -140,6 +142,8 @@ func updateOptionMap(key string, value string) (err error) {
 			common.AutomaticDisableChannelEnabled = boolValue
 			common.AutomaticDisableChannelEnabled = boolValue
 		case "LogConsumeEnabled":
 		case "LogConsumeEnabled":
 			common.LogConsumeEnabled = boolValue
 			common.LogConsumeEnabled = boolValue
+		case "DisplayInCurrencyEnabled":
+			common.DisplayInCurrencyEnabled = boolValue
 		}
 		}
 	}
 	}
 	switch key {
 	switch key {
@@ -196,6 +200,8 @@ func updateOptionMap(key string, value string) (err error) {
 		common.ChatLink = value
 		common.ChatLink = value
 	case "ChannelDisableThreshold":
 	case "ChannelDisableThreshold":
 		common.ChannelDisableThreshold, _ = strconv.ParseFloat(value, 64)
 		common.ChannelDisableThreshold, _ = strconv.ParseFloat(value, 64)
+	case "QuotaPerUnit":
+		common.QuotaPerUnit, _ = strconv.ParseFloat(value, 64)
 	}
 	}
 	return err
 	return err
 }
 }

+ 1 - 1
model/redemption.go

@@ -66,7 +66,7 @@ func Redeem(key string, userId int) (quota int, err error) {
 		if err != nil {
 		if err != nil {
 			common.SysError("更新兑换码状态失败:" + err.Error())
 			common.SysError("更新兑换码状态失败:" + err.Error())
 		}
 		}
-		RecordLog(userId, LogTypeTopup, fmt.Sprintf("通过兑换码充值 %d 点额度", redemption.Quota))
+		RecordLog(userId, LogTypeTopup, fmt.Sprintf("通过兑换码充值 %s", common.LogQuota(redemption.Quota)))
 	}()
 	}()
 	return redemption.Quota, nil
 	return redemption.Quota, nil
 }
 }

+ 8 - 3
model/user.go

@@ -93,16 +93,16 @@ func (user *User) Insert(inviterId int) error {
 		return result.Error
 		return result.Error
 	}
 	}
 	if common.QuotaForNewUser > 0 {
 	if common.QuotaForNewUser > 0 {
-		RecordLog(user.Id, LogTypeSystem, fmt.Sprintf("新用户注册赠送 %d 点额度", common.QuotaForNewUser))
+		RecordLog(user.Id, LogTypeSystem, fmt.Sprintf("新用户注册赠送 %s", common.LogQuota(common.QuotaForNewUser)))
 	}
 	}
 	if inviterId != 0 {
 	if inviterId != 0 {
 		if common.QuotaForInvitee > 0 {
 		if common.QuotaForInvitee > 0 {
 			_ = IncreaseUserQuota(user.Id, common.QuotaForInvitee)
 			_ = IncreaseUserQuota(user.Id, common.QuotaForInvitee)
-			RecordLog(user.Id, LogTypeSystem, fmt.Sprintf("使用邀请码赠送 %d 点额度", common.QuotaForInvitee))
+			RecordLog(user.Id, LogTypeSystem, fmt.Sprintf("使用邀请码赠送 %s", common.LogQuota(common.QuotaForInvitee)))
 		}
 		}
 		if common.QuotaForInviter > 0 {
 		if common.QuotaForInviter > 0 {
 			_ = IncreaseUserQuota(inviterId, common.QuotaForInviter)
 			_ = IncreaseUserQuota(inviterId, common.QuotaForInviter)
-			RecordLog(inviterId, LogTypeSystem, fmt.Sprintf("邀请用户赠送 %d 点额度", common.QuotaForInviter))
+			RecordLog(inviterId, LogTypeSystem, fmt.Sprintf("邀请用户赠送 %s", common.LogQuota(common.QuotaForInviter)))
 		}
 		}
 	}
 	}
 	return nil
 	return nil
@@ -256,6 +256,11 @@ func GetUserQuota(id int) (quota int, err error) {
 	return quota, err
 	return quota, err
 }
 }
 
 
+func GetUserUsedQuota(id int) (quota int, err error) {
+	err = DB.Model(&User{}).Where("id = ?", id).Select("used_quota").Find(&quota).Error
+	return quota, err
+}
+
 func GetUserEmail(id int) (email string, err error) {
 func GetUserEmail(id int) (email string, err error) {
 	err = DB.Model(&User{}).Where("id = ?", id).Select("email").Find(&email).Error
 	err = DB.Model(&User{}).Where("id = ?", id).Select("email").Find(&email).Error
 	return email, err
 	return email, err

+ 2 - 0
web/src/App.js

@@ -48,6 +48,8 @@ function App() {
       localStorage.setItem('system_name', data.system_name);
       localStorage.setItem('system_name', data.system_name);
       localStorage.setItem('logo', data.logo);
       localStorage.setItem('logo', data.logo);
       localStorage.setItem('footer_html', data.footer_html);
       localStorage.setItem('footer_html', data.footer_html);
+      localStorage.setItem('quota_per_unit', data.quota_per_unit);
+      localStorage.setItem('display_in_currency', data.display_in_currency);
       if (data.chat_link) {
       if (data.chat_link) {
         localStorage.setItem('chat_link', data.chat_link);
         localStorage.setItem('chat_link', data.chat_link);
       } else {
       } else {

+ 31 - 8
web/src/components/OperationSetting.js

@@ -13,9 +13,11 @@ const OperationSetting = () => {
     GroupRatio: '',
     GroupRatio: '',
     TopUpLink: '',
     TopUpLink: '',
     ChatLink: '',
     ChatLink: '',
+    QuotaPerUnit: 0,
     AutomaticDisableChannelEnabled: '',
     AutomaticDisableChannelEnabled: '',
     ChannelDisableThreshold: 0,
     ChannelDisableThreshold: 0,
-    LogConsumeEnabled: ''
+    LogConsumeEnabled: '',
+    DisplayInCurrencyEnabled: ''
   });
   });
   const [originInputs, setOriginInputs] = useState({});
   const [originInputs, setOriginInputs] = useState({});
   let [loading, setLoading] = useState(false);
   let [loading, setLoading] = useState(false);
@@ -118,6 +120,9 @@ const OperationSetting = () => {
         if (originInputs['ChatLink'] !== inputs.ChatLink) {
         if (originInputs['ChatLink'] !== inputs.ChatLink) {
           await updateOption('ChatLink', inputs.ChatLink);
           await updateOption('ChatLink', inputs.ChatLink);
         }
         }
+        if (originInputs['QuotaPerUnit'] !== inputs.QuotaPerUnit) {
+          await updateOption('QuotaPerUnit', inputs.QuotaPerUnit);
+        }
         break;
         break;
     }
     }
   };
   };
@@ -129,7 +134,7 @@ const OperationSetting = () => {
           <Header as='h3'>
           <Header as='h3'>
             通用设置
             通用设置
           </Header>
           </Header>
-          <Form.Group widths={2}>
+          <Form.Group widths={3}>
             <Form.Input
             <Form.Input
               label='充值链接'
               label='充值链接'
               name='TopUpLink'
               name='TopUpLink'
@@ -148,6 +153,30 @@ const OperationSetting = () => {
               type='link'
               type='link'
               placeholder='例如 ChatGPT Next Web 的部署地址'
               placeholder='例如 ChatGPT Next Web 的部署地址'
             />
             />
+            <Form.Input
+              label='额度汇率'
+              name='QuotaPerUnit'
+              onChange={handleInputChange}
+              autoComplete='new-password'
+              value={inputs.QuotaPerUnit}
+              type='number'
+              step='0.01'
+              placeholder='一单位货币能兑换的额度'
+            />
+          </Form.Group>
+          <Form.Group inline>
+            <Form.Checkbox
+              checked={inputs.LogConsumeEnabled === 'true'}
+              label='启用额度消费日志记录'
+              name='LogConsumeEnabled'
+              onChange={handleInputChange}
+            />
+            <Form.Checkbox
+              checked={inputs.DisplayInCurrencyEnabled === 'true'}
+              label='以货币形式显示额度'
+              name='DisplayInCurrencyEnabled'
+              onChange={handleInputChange}
+            />
           </Form.Group>
           </Form.Group>
           <Form.Button onClick={() => {
           <Form.Button onClick={() => {
             submitConfig('general').then();
             submitConfig('general').then();
@@ -264,12 +293,6 @@ const OperationSetting = () => {
               placeholder='为一个 JSON 文本,键为分组名称,值为倍率'
               placeholder='为一个 JSON 文本,键为分组名称,值为倍率'
             />
             />
           </Form.Group>
           </Form.Group>
-          <Form.Checkbox
-            checked={inputs.LogConsumeEnabled === 'true'}
-            label='启用额度消费日志记录'
-            name='LogConsumeEnabled'
-            onChange={handleInputChange}
-          />
           <Form.Button onClick={() => {
           <Form.Button onClick={() => {
             submitConfig('ratio').then();
             submitConfig('ratio').then();
           }}>保存倍率设置</Form.Button>
           }}>保存倍率设置</Form.Button>

+ 2 - 1
web/src/components/RedemptionsTable.js

@@ -4,6 +4,7 @@ import { Link } from 'react-router-dom';
 import { API, copy, showError, showInfo, showSuccess, showWarning, timestamp2string } from '../helpers';
 import { API, copy, showError, showInfo, showSuccess, showWarning, timestamp2string } from '../helpers';
 
 
 import { ITEMS_PER_PAGE } from '../constants';
 import { ITEMS_PER_PAGE } from '../constants';
+import { renderQuota } from '../helpers/render';
 
 
 function renderTimestamp(timestamp) {
 function renderTimestamp(timestamp) {
   return (
   return (
@@ -220,7 +221,7 @@ const RedemptionsTable = () => {
                   <Table.Cell>{redemption.id}</Table.Cell>
                   <Table.Cell>{redemption.id}</Table.Cell>
                   <Table.Cell>{redemption.name ? redemption.name : '无'}</Table.Cell>
                   <Table.Cell>{redemption.name ? redemption.name : '无'}</Table.Cell>
                   <Table.Cell>{renderStatus(redemption.status)}</Table.Cell>
                   <Table.Cell>{renderStatus(redemption.status)}</Table.Cell>
-                  <Table.Cell>{redemption.quota}</Table.Cell>
+                  <Table.Cell>{renderQuota(redemption.quota)}</Table.Cell>
                   <Table.Cell>{renderTimestamp(redemption.created_time)}</Table.Cell>
                   <Table.Cell>{renderTimestamp(redemption.created_time)}</Table.Cell>
                   <Table.Cell>{redemption.redeemed_time ? renderTimestamp(redemption.redeemed_time) : "尚未兑换"} </Table.Cell>
                   <Table.Cell>{redemption.redeemed_time ? renderTimestamp(redemption.redeemed_time) : "尚未兑换"} </Table.Cell>
                   <Table.Cell>
                   <Table.Cell>

+ 2 - 1
web/src/components/TokensTable.js

@@ -4,6 +4,7 @@ import { Link } from 'react-router-dom';
 import { API, copy, showError, showSuccess, showWarning, timestamp2string } from '../helpers';
 import { API, copy, showError, showSuccess, showWarning, timestamp2string } from '../helpers';
 
 
 import { ITEMS_PER_PAGE } from '../constants';
 import { ITEMS_PER_PAGE } from '../constants';
+import { renderQuota } from '../helpers/render';
 
 
 function renderTimestamp(timestamp) {
 function renderTimestamp(timestamp) {
   return (
   return (
@@ -220,7 +221,7 @@ const TokensTable = () => {
                 <Table.Row key={token.id}>
                 <Table.Row key={token.id}>
                   <Table.Cell>{token.name ? token.name : '无'}</Table.Cell>
                   <Table.Cell>{token.name ? token.name : '无'}</Table.Cell>
                   <Table.Cell>{renderStatus(token.status)}</Table.Cell>
                   <Table.Cell>{renderStatus(token.status)}</Table.Cell>
-                  <Table.Cell>{token.unlimited_quota ? '无限制' : token.remain_quota}</Table.Cell>
+                  <Table.Cell>{token.unlimited_quota ? '无限制' : renderQuota(token.remain_quota, 2)}</Table.Cell>
                   <Table.Cell>{renderTimestamp(token.created_time)}</Table.Cell>
                   <Table.Cell>{renderTimestamp(token.created_time)}</Table.Cell>
                   <Table.Cell>{token.expired_time === -1 ? '永不过期' : renderTimestamp(token.expired_time)}</Table.Cell>
                   <Table.Cell>{token.expired_time === -1 ? '永不过期' : renderTimestamp(token.expired_time)}</Table.Cell>
                   <Table.Cell>
                   <Table.Cell>

+ 3 - 3
web/src/components/UsersTable.js

@@ -4,7 +4,7 @@ import { Link } from 'react-router-dom';
 import { API, showError, showSuccess } from '../helpers';
 import { API, showError, showSuccess } from '../helpers';
 
 
 import { ITEMS_PER_PAGE } from '../constants';
 import { ITEMS_PER_PAGE } from '../constants';
-import { renderGroup, renderNumber, renderText } from '../helpers/render';
+import { renderGroup, renderNumber, renderQuota, renderText } from '../helpers/render';
 
 
 function renderRole(role) {
 function renderRole(role) {
   switch (role) {
   switch (role) {
@@ -244,8 +244,8 @@ const UsersTable = () => {
                     {user.email ? <Popup hoverable content={user.email} trigger={<span>{renderText(user.email, 24)}</span>} /> : '无'}
                     {user.email ? <Popup hoverable content={user.email} trigger={<span>{renderText(user.email, 24)}</span>} /> : '无'}
                   </Table.Cell>
                   </Table.Cell>
                   <Table.Cell>
                   <Table.Cell>
-                    <Popup content='剩余额度' trigger={<Label>{renderNumber(user.quota)}</Label>} />
-                    <Popup content='已用额度' trigger={<Label>{renderNumber(user.used_quota)}</Label>} />
+                    <Popup content='剩余额度' trigger={<Label>{renderQuota(user.quota)}</Label>} />
+                    <Popup content='已用额度' trigger={<Label>{renderQuota(user.used_quota)}</Label>} />
                     <Popup content='请求次数' trigger={<Label>{renderNumber(user.request_count)}</Label>} />
                     <Popup content='请求次数' trigger={<Label>{renderNumber(user.request_count)}</Label>} />
                   </Table.Cell>
                   </Table.Cell>
                   <Table.Cell>{renderRole(user.role)}</Table.Cell>
                   <Table.Cell>{renderRole(user.role)}</Table.Cell>

+ 11 - 0
web/src/helpers/render.js

@@ -35,4 +35,15 @@ export function renderNumber(num) {
   } else {
   } else {
     return num;
     return num;
   }
   }
+}
+
+export function renderQuota(quota, digits = 2) {
+  let quotaPerUnit = localStorage.getItem('quota_per_unit');
+  let displayInCurrency = localStorage.getItem('display_in_currency');
+  quotaPerUnit = parseFloat(quotaPerUnit);
+  displayInCurrency = displayInCurrency === 'true';
+  if (displayInCurrency) {
+    return '$' + (quota / quotaPerUnit).toFixed(digits);
+  }
+  return renderNumber(quota);
 }
 }

+ 2 - 1
web/src/pages/Redemption/EditRedemption.js

@@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react';
 import { Button, Form, Header, Segment } from 'semantic-ui-react';
 import { Button, Form, Header, Segment } from 'semantic-ui-react';
 import { useParams } from 'react-router-dom';
 import { useParams } from 'react-router-dom';
 import { API, downloadTextAsFile, showError, showSuccess } from '../../helpers';
 import { API, downloadTextAsFile, showError, showSuccess } from '../../helpers';
+import { renderQuota } from '../../helpers/render';
 
 
 const EditRedemption = () => {
 const EditRedemption = () => {
   const params = useParams();
   const params = useParams();
@@ -87,7 +88,7 @@ const EditRedemption = () => {
           </Form.Field>
           </Form.Field>
           <Form.Field>
           <Form.Field>
             <Form.Input
             <Form.Input
-              label='额度'
+              label={`额度(等价金额 ${renderQuota(quota)})`}
               name='quota'
               name='quota'
               placeholder={'请输入单个兑换码中包含的额度'}
               placeholder={'请输入单个兑换码中包含的额度'}
               onChange={handleInputChange}
               onChange={handleInputChange}

+ 2 - 1
web/src/pages/Token/EditToken.js

@@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react';
 import { Button, Form, Header, Message, Segment } from 'semantic-ui-react';
 import { Button, Form, Header, Message, Segment } from 'semantic-ui-react';
 import { useParams } from 'react-router-dom';
 import { useParams } from 'react-router-dom';
 import { API, showError, showSuccess, timestamp2string } from '../../helpers';
 import { API, showError, showSuccess, timestamp2string } from '../../helpers';
+import { renderQuota } from '../../helpers/render';
 
 
 const EditToken = () => {
 const EditToken = () => {
   const params = useParams();
   const params = useParams();
@@ -137,7 +138,7 @@ const EditToken = () => {
           <Message>注意,令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制。</Message>
           <Message>注意,令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制。</Message>
           <Form.Field>
           <Form.Field>
             <Form.Input
             <Form.Input
-              label='额度'
+              label={`额度(等价金额 ${renderQuota(remain_quota)})`}
               name='remain_quota'
               name='remain_quota'
               placeholder={'请输入额度'}
               placeholder={'请输入额度'}
               onChange={handleInputChange}
               onChange={handleInputChange}

+ 2 - 1
web/src/pages/TopUp/index.js

@@ -1,6 +1,7 @@
 import React, { useEffect, useState } from 'react';
 import React, { useEffect, useState } from 'react';
 import { Button, Form, Grid, Header, Segment, Statistic } from 'semantic-ui-react';
 import { Button, Form, Grid, Header, Segment, Statistic } from 'semantic-ui-react';
 import { API, showError, showInfo, showSuccess } from '../../helpers';
 import { API, showError, showInfo, showSuccess } from '../../helpers';
+import { renderQuota } from '../../helpers/render';
 
 
 const TopUp = () => {
 const TopUp = () => {
   const [redemptionCode, setRedemptionCode] = useState('');
   const [redemptionCode, setRedemptionCode] = useState('');
@@ -81,7 +82,7 @@ const TopUp = () => {
         <Grid.Column>
         <Grid.Column>
           <Statistic.Group widths='one'>
           <Statistic.Group widths='one'>
             <Statistic>
             <Statistic>
-              <Statistic.Value>{userQuota.toLocaleString()}</Statistic.Value>
+              <Statistic.Value>{renderQuota(userQuota)}</Statistic.Value>
               <Statistic.Label>剩余额度</Statistic.Label>
               <Statistic.Label>剩余额度</Statistic.Label>
             </Statistic>
             </Statistic>
           </Statistic.Group>
           </Statistic.Group>