mirror of
				https://github.com/songquanpeng/one-api.git
				synced 2025-11-04 15:53:42 +08:00 
			
		
		
		
	feat: able to display quota in dollar
This commit is contained in:
		@@ -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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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),
 | 
							SoftLimitUSD:       amount,
 | 
				
			||||||
		HardLimitUSD:       float64(quota),
 | 
							HardLimitUSD:       amount,
 | 
				
			||||||
		SystemHardLimitUSD: float64(quota),
 | 
							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")
 | 
						userId := c.GetInt("id")
 | 
				
			||||||
	// TODO: get usage from database
 | 
						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
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,21 +14,23 @@ func GetStatus(c *gin.Context) {
 | 
				
			|||||||
		"success": true,
 | 
							"success": true,
 | 
				
			||||||
		"message": "",
 | 
							"message": "",
 | 
				
			||||||
		"data": gin.H{
 | 
							"data": gin.H{
 | 
				
			||||||
			"version":            common.Version,
 | 
								"version":             common.Version,
 | 
				
			||||||
			"start_time":         common.StartTime,
 | 
								"start_time":          common.StartTime,
 | 
				
			||||||
			"email_verification": common.EmailVerificationEnabled,
 | 
								"email_verification":  common.EmailVerificationEnabled,
 | 
				
			||||||
			"github_oauth":       common.GitHubOAuthEnabled,
 | 
								"github_oauth":        common.GitHubOAuthEnabled,
 | 
				
			||||||
			"github_client_id":   common.GitHubClientId,
 | 
								"github_client_id":    common.GitHubClientId,
 | 
				
			||||||
			"system_name":        common.SystemName,
 | 
								"system_name":         common.SystemName,
 | 
				
			||||||
			"logo":               common.Logo,
 | 
								"logo":                common.Logo,
 | 
				
			||||||
			"footer_html":        common.Footer,
 | 
								"footer_html":         common.Footer,
 | 
				
			||||||
			"wechat_qrcode":      common.WeChatAccountQRCodeImageURL,
 | 
								"wechat_qrcode":       common.WeChatAccountQRCodeImageURL,
 | 
				
			||||||
			"wechat_login":       common.WeChatAuthEnabled,
 | 
								"wechat_login":        common.WeChatAuthEnabled,
 | 
				
			||||||
			"server_address":     common.ServerAddress,
 | 
								"server_address":      common.ServerAddress,
 | 
				
			||||||
			"turnstile_check":    common.TurnstileCheckEnabled,
 | 
								"turnstile_check":     common.TurnstileCheckEnabled,
 | 
				
			||||||
			"turnstile_site_key": common.TurnstileSiteKey,
 | 
								"turnstile_site_key":  common.TurnstileSiteKey,
 | 
				
			||||||
			"top_up_link":        common.TopUpLink,
 | 
								"top_up_link":         common.TopUpLink,
 | 
				
			||||||
			"chat_link":          common.ChatLink,
 | 
								"chat_link":           common.ChatLink,
 | 
				
			||||||
 | 
								"quota_per_unit":      common.QuotaPerUnit,
 | 
				
			||||||
 | 
								"display_in_currency": common.DisplayInCurrencyEnabled,
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
	return
 | 
						return
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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("a).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
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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 {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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>{renderQuota(user.quota)}</Label>} />
 | 
				
			||||||
                    <Popup content='已用额度' trigger={<Label>{renderNumber(user.used_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>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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,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,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}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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>
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user