feat(notify): add global toggles, percentage threshold, and visibility control
- Add global toggle for account quota notification in admin settings - Add percentage-based threshold type for per-account quota alerts - Hide balance notify card on user profile when global toggle is off - Expose balance_low_notify_enabled and account_quota_notify_enabled in PublicSettings - Add threshold type (fixed/percentage) to QuotaNotifyToggle with $ / % switcher
This commit is contained in:
@@ -1432,6 +1432,14 @@ func (a *Account) getExtraString(key string) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getExtraStringDefault 从 Extra 中读取指定 key 的字符串值,不存在时返回 defaultVal
|
||||||
|
func (a *Account) getExtraStringDefault(key, defaultVal string) string {
|
||||||
|
if v := a.getExtraString(key); v != "" {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return defaultVal
|
||||||
|
}
|
||||||
|
|
||||||
// getExtraInt 从 Extra 中读取指定 key 的 int 值
|
// getExtraInt 从 Extra 中读取指定 key 的 int 值
|
||||||
func (a *Account) getExtraInt(key string) int {
|
func (a *Account) getExtraInt(key string) int {
|
||||||
if a.Extra == nil {
|
if a.Extra == nil {
|
||||||
@@ -1498,6 +1506,10 @@ func (a *Account) GetQuotaNotifyDailyThreshold() float64 {
|
|||||||
return a.getExtraFloat64("quota_notify_daily_threshold")
|
return a.getExtraFloat64("quota_notify_daily_threshold")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *Account) GetQuotaNotifyDailyThresholdType() string {
|
||||||
|
return a.getExtraStringDefault("quota_notify_daily_threshold_type", "fixed")
|
||||||
|
}
|
||||||
|
|
||||||
func (a *Account) GetQuotaNotifyWeeklyEnabled() bool {
|
func (a *Account) GetQuotaNotifyWeeklyEnabled() bool {
|
||||||
return a.getExtraBool("quota_notify_weekly_enabled")
|
return a.getExtraBool("quota_notify_weekly_enabled")
|
||||||
}
|
}
|
||||||
@@ -1506,6 +1518,10 @@ func (a *Account) GetQuotaNotifyWeeklyThreshold() float64 {
|
|||||||
return a.getExtraFloat64("quota_notify_weekly_threshold")
|
return a.getExtraFloat64("quota_notify_weekly_threshold")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *Account) GetQuotaNotifyWeeklyThresholdType() string {
|
||||||
|
return a.getExtraStringDefault("quota_notify_weekly_threshold_type", "fixed")
|
||||||
|
}
|
||||||
|
|
||||||
func (a *Account) GetQuotaNotifyTotalEnabled() bool {
|
func (a *Account) GetQuotaNotifyTotalEnabled() bool {
|
||||||
return a.getExtraBool("quota_notify_total_enabled")
|
return a.getExtraBool("quota_notify_total_enabled")
|
||||||
}
|
}
|
||||||
@@ -1514,6 +1530,10 @@ func (a *Account) GetQuotaNotifyTotalThreshold() float64 {
|
|||||||
return a.getExtraFloat64("quota_notify_total_threshold")
|
return a.getExtraFloat64("quota_notify_total_threshold")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *Account) GetQuotaNotifyTotalThresholdType() string {
|
||||||
|
return a.getExtraStringDefault("quota_notify_total_threshold_type", "fixed")
|
||||||
|
}
|
||||||
|
|
||||||
// nextFixedDailyReset 计算在 after 之后的下一个每日固定重置时间点
|
// nextFixedDailyReset 计算在 after 之后的下一个每日固定重置时间点
|
||||||
func nextFixedDailyReset(hour int, tz *time.Location, after time.Time) time.Time {
|
func nextFixedDailyReset(hour int, tz *time.Location, after time.Time) time.Time {
|
||||||
t := after.In(tz)
|
t := after.In(tz)
|
||||||
|
|||||||
@@ -85,16 +85,26 @@ type quotaDim struct {
|
|||||||
name string
|
name string
|
||||||
enabled bool
|
enabled bool
|
||||||
threshold float64
|
threshold float64
|
||||||
|
thresholdType string // "fixed" (default) or "percentage"
|
||||||
oldUsed float64
|
oldUsed float64
|
||||||
limit float64
|
limit float64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// resolvedThreshold returns the effective threshold value.
|
||||||
|
// For percentage type, it computes threshold = limit * percentage / 100.
|
||||||
|
func (d quotaDim) resolvedThreshold() float64 {
|
||||||
|
if d.thresholdType == "percentage" && d.limit > 0 {
|
||||||
|
return d.limit * d.threshold / 100
|
||||||
|
}
|
||||||
|
return d.threshold
|
||||||
|
}
|
||||||
|
|
||||||
// buildQuotaDims returns the three quota dimensions for notification checking.
|
// buildQuotaDims returns the three quota dimensions for notification checking.
|
||||||
func buildQuotaDims(account *Account) []quotaDim {
|
func buildQuotaDims(account *Account) []quotaDim {
|
||||||
return []quotaDim{
|
return []quotaDim{
|
||||||
{quotaDimDaily, account.GetQuotaNotifyDailyEnabled(), account.GetQuotaNotifyDailyThreshold(), account.GetQuotaDailyUsed(), account.GetQuotaDailyLimit()},
|
{quotaDimDaily, account.GetQuotaNotifyDailyEnabled(), account.GetQuotaNotifyDailyThreshold(), account.GetQuotaNotifyDailyThresholdType(), account.GetQuotaDailyUsed(), account.GetQuotaDailyLimit()},
|
||||||
{quotaDimWeekly, account.GetQuotaNotifyWeeklyEnabled(), account.GetQuotaNotifyWeeklyThreshold(), account.GetQuotaWeeklyUsed(), account.GetQuotaWeeklyLimit()},
|
{quotaDimWeekly, account.GetQuotaNotifyWeeklyEnabled(), account.GetQuotaNotifyWeeklyThreshold(), account.GetQuotaNotifyWeeklyThresholdType(), account.GetQuotaWeeklyUsed(), account.GetQuotaWeeklyLimit()},
|
||||||
{quotaDimTotal, account.GetQuotaNotifyTotalEnabled(), account.GetQuotaNotifyTotalThreshold(), account.GetQuotaUsed(), account.GetQuotaLimit()},
|
{quotaDimTotal, account.GetQuotaNotifyTotalEnabled(), account.GetQuotaNotifyTotalThreshold(), account.GetQuotaNotifyTotalThresholdType(), account.GetQuotaUsed(), account.GetQuotaLimit()},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,6 +114,9 @@ func (s *BalanceNotifyService) CheckAccountQuotaAfterIncrement(ctx context.Conte
|
|||||||
if account == nil || s.emailService == nil || s.settingRepo == nil || cost <= 0 {
|
if account == nil || s.emailService == nil || s.settingRepo == nil || cost <= 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if !s.isAccountQuotaNotifyEnabled(ctx) {
|
||||||
|
return
|
||||||
|
}
|
||||||
adminEmails := s.getAccountQuotaNotifyEmails(ctx)
|
adminEmails := s.getAccountQuotaNotifyEmails(ctx)
|
||||||
if len(adminEmails) == 0 {
|
if len(adminEmails) == 0 {
|
||||||
return
|
return
|
||||||
@@ -114,22 +127,26 @@ func (s *BalanceNotifyService) CheckAccountQuotaAfterIncrement(ctx context.Conte
|
|||||||
if !dim.enabled || dim.threshold <= 0 {
|
if !dim.enabled || dim.threshold <= 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
effectiveThreshold := dim.resolvedThreshold()
|
||||||
|
if effectiveThreshold <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
newUsed := dim.oldUsed + cost
|
newUsed := dim.oldUsed + cost
|
||||||
if dim.oldUsed < dim.threshold && newUsed >= dim.threshold {
|
if dim.oldUsed < effectiveThreshold && newUsed >= effectiveThreshold {
|
||||||
s.asyncSendQuotaAlert(adminEmails, account.Name, dim, newUsed, siteName)
|
s.asyncSendQuotaAlert(adminEmails, account.Name, dim, newUsed, effectiveThreshold, siteName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// asyncSendQuotaAlert sends quota alert email in a goroutine with panic recovery.
|
// asyncSendQuotaAlert sends quota alert email in a goroutine with panic recovery.
|
||||||
func (s *BalanceNotifyService) asyncSendQuotaAlert(adminEmails []string, accountName string, dim quotaDim, newUsed float64, siteName string) {
|
func (s *BalanceNotifyService) asyncSendQuotaAlert(adminEmails []string, accountName string, dim quotaDim, newUsed, effectiveThreshold float64, siteName string) {
|
||||||
go func() {
|
go func() {
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
slog.Error("panic in quota notification", "recover", r)
|
slog.Error("panic in quota notification", "recover", r)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
s.sendQuotaAlertEmails(adminEmails, accountName, dim.name, newUsed, dim.limit, dim.threshold, siteName)
|
s.sendQuotaAlertEmails(adminEmails, accountName, dim.name, newUsed, dim.limit, effectiveThreshold, siteName)
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,6 +166,15 @@ func (s *BalanceNotifyService) getBalanceNotifyConfig(ctx context.Context) (enab
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isAccountQuotaNotifyEnabled checks the global account quota notification toggle.
|
||||||
|
func (s *BalanceNotifyService) isAccountQuotaNotifyEnabled(ctx context.Context) bool {
|
||||||
|
val, err := s.settingRepo.GetValue(ctx, SettingKeyAccountQuotaNotifyEnabled)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return val == "true"
|
||||||
|
}
|
||||||
|
|
||||||
// getAccountQuotaNotifyEmails reads admin notification emails from settings.
|
// getAccountQuotaNotifyEmails reads admin notification emails from settings.
|
||||||
func (s *BalanceNotifyService) getAccountQuotaNotifyEmails(ctx context.Context) []string {
|
func (s *BalanceNotifyService) getAccountQuotaNotifyEmails(ctx context.Context) []string {
|
||||||
raw, err := s.settingRepo.GetValue(ctx, SettingKeyAccountQuotaNotifyEmails)
|
raw, err := s.settingRepo.GetValue(ctx, SettingKeyAccountQuotaNotifyEmails)
|
||||||
|
|||||||
@@ -255,6 +255,7 @@ const (
|
|||||||
SettingKeyBalanceLowNotifyThreshold = "balance_low_notify_threshold" // 默认阈值(USD)
|
SettingKeyBalanceLowNotifyThreshold = "balance_low_notify_threshold" // 默认阈值(USD)
|
||||||
|
|
||||||
// Account Quota Notification
|
// Account Quota Notification
|
||||||
|
SettingKeyAccountQuotaNotifyEnabled = "account_quota_notify_enabled" // 全局开关
|
||||||
SettingKeyAccountQuotaNotifyEmails = "account_quota_notify_emails" // 管理员通知邮箱列表(JSON 数组)
|
SettingKeyAccountQuotaNotifyEmails = "account_quota_notify_emails" // 管理员通知邮箱列表(JSON 数组)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -182,6 +182,8 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings
|
|||||||
SettingPaymentEnabled,
|
SettingPaymentEnabled,
|
||||||
SettingKeyOIDCConnectEnabled,
|
SettingKeyOIDCConnectEnabled,
|
||||||
SettingKeyOIDCConnectProviderName,
|
SettingKeyOIDCConnectProviderName,
|
||||||
|
SettingKeyBalanceLowNotifyEnabled,
|
||||||
|
SettingKeyAccountQuotaNotifyEnabled,
|
||||||
}
|
}
|
||||||
|
|
||||||
settings, err := s.settingRepo.GetMultiple(ctx, keys)
|
settings, err := s.settingRepo.GetMultiple(ctx, keys)
|
||||||
@@ -249,6 +251,8 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings
|
|||||||
PaymentEnabled: settings[SettingPaymentEnabled] == "true",
|
PaymentEnabled: settings[SettingPaymentEnabled] == "true",
|
||||||
OIDCOAuthEnabled: oidcEnabled,
|
OIDCOAuthEnabled: oidcEnabled,
|
||||||
OIDCOAuthProviderName: oidcProviderName,
|
OIDCOAuthProviderName: oidcProviderName,
|
||||||
|
BalanceLowNotifyEnabled: settings[SettingKeyBalanceLowNotifyEnabled] == "true",
|
||||||
|
AccountQuotaNotifyEnabled: settings[SettingKeyAccountQuotaNotifyEnabled] == "true",
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -302,6 +306,8 @@ func (s *SettingService) GetPublicSettingsForInjection(ctx context.Context) (any
|
|||||||
OIDCOAuthEnabled bool `json:"oidc_oauth_enabled"`
|
OIDCOAuthEnabled bool `json:"oidc_oauth_enabled"`
|
||||||
OIDCOAuthProviderName string `json:"oidc_oauth_provider_name"`
|
OIDCOAuthProviderName string `json:"oidc_oauth_provider_name"`
|
||||||
Version string `json:"version,omitempty"`
|
Version string `json:"version,omitempty"`
|
||||||
|
BalanceLowNotifyEnabled bool `json:"balance_low_notify_enabled"`
|
||||||
|
AccountQuotaNotifyEnabled bool `json:"account_quota_notify_enabled"`
|
||||||
}{
|
}{
|
||||||
RegistrationEnabled: settings.RegistrationEnabled,
|
RegistrationEnabled: settings.RegistrationEnabled,
|
||||||
EmailVerifyEnabled: settings.EmailVerifyEnabled,
|
EmailVerifyEnabled: settings.EmailVerifyEnabled,
|
||||||
@@ -332,6 +338,8 @@ func (s *SettingService) GetPublicSettingsForInjection(ctx context.Context) (any
|
|||||||
OIDCOAuthEnabled: settings.OIDCOAuthEnabled,
|
OIDCOAuthEnabled: settings.OIDCOAuthEnabled,
|
||||||
OIDCOAuthProviderName: settings.OIDCOAuthProviderName,
|
OIDCOAuthProviderName: settings.OIDCOAuthProviderName,
|
||||||
Version: s.version,
|
Version: s.version,
|
||||||
|
BalanceLowNotifyEnabled: settings.BalanceLowNotifyEnabled,
|
||||||
|
AccountQuotaNotifyEnabled: settings.AccountQuotaNotifyEnabled,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -609,6 +617,7 @@ func (s *SettingService) UpdateSettings(ctx context.Context, settings *SystemSet
|
|||||||
// Balance low notification
|
// Balance low notification
|
||||||
updates[SettingKeyBalanceLowNotifyEnabled] = strconv.FormatBool(settings.BalanceLowNotifyEnabled)
|
updates[SettingKeyBalanceLowNotifyEnabled] = strconv.FormatBool(settings.BalanceLowNotifyEnabled)
|
||||||
updates[SettingKeyBalanceLowNotifyThreshold] = strconv.FormatFloat(settings.BalanceLowNotifyThreshold, 'f', 8, 64)
|
updates[SettingKeyBalanceLowNotifyThreshold] = strconv.FormatFloat(settings.BalanceLowNotifyThreshold, 'f', 8, 64)
|
||||||
|
updates[SettingKeyAccountQuotaNotifyEnabled] = strconv.FormatBool(settings.AccountQuotaNotifyEnabled)
|
||||||
accountQuotaNotifyEmailsJSON, err := json.Marshal(settings.AccountQuotaNotifyEmails)
|
accountQuotaNotifyEmailsJSON, err := json.Marshal(settings.AccountQuotaNotifyEmails)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("marshal account quota notify emails: %w", err)
|
return fmt.Errorf("marshal account quota notify emails: %w", err)
|
||||||
@@ -1251,7 +1260,8 @@ func (s *SettingService) parseSettings(settings map[string]string) *SystemSettin
|
|||||||
result.BalanceLowNotifyThreshold = v
|
result.BalanceLowNotifyThreshold = v
|
||||||
}
|
}
|
||||||
|
|
||||||
// Account quota notification emails
|
// Account quota notification
|
||||||
|
result.AccountQuotaNotifyEnabled = settings[SettingKeyAccountQuotaNotifyEnabled] == "true"
|
||||||
if raw := strings.TrimSpace(settings[SettingKeyAccountQuotaNotifyEmails]); raw != "" {
|
if raw := strings.TrimSpace(settings[SettingKeyAccountQuotaNotifyEmails]); raw != "" {
|
||||||
var emails []string
|
var emails []string
|
||||||
if err := json.Unmarshal([]byte(raw), &emails); err == nil {
|
if err := json.Unmarshal([]byte(raw), &emails); err == nil {
|
||||||
|
|||||||
@@ -112,6 +112,7 @@ type SystemSettings struct {
|
|||||||
BalanceLowNotifyThreshold float64
|
BalanceLowNotifyThreshold float64
|
||||||
|
|
||||||
// Account quota notification
|
// Account quota notification
|
||||||
|
AccountQuotaNotifyEnabled bool
|
||||||
AccountQuotaNotifyEmails []string
|
AccountQuotaNotifyEmails []string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,6 +153,9 @@ type PublicSettings struct {
|
|||||||
OIDCOAuthEnabled bool
|
OIDCOAuthEnabled bool
|
||||||
OIDCOAuthProviderName string
|
OIDCOAuthProviderName string
|
||||||
Version string
|
Version string
|
||||||
|
|
||||||
|
BalanceLowNotifyEnabled bool
|
||||||
|
AccountQuotaNotifyEnabled bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// StreamTimeoutSettings 流超时处理配置(仅控制超时后的处理方式,超时判定由网关配置控制)
|
// StreamTimeoutSettings 流超时处理配置(仅控制超时后的处理方式,超时判定由网关配置控制)
|
||||||
|
|||||||
@@ -138,6 +138,7 @@ export interface SystemSettings {
|
|||||||
// Balance & quota notification
|
// Balance & quota notification
|
||||||
balance_low_notify_enabled: boolean
|
balance_low_notify_enabled: boolean
|
||||||
balance_low_notify_threshold: number
|
balance_low_notify_threshold: number
|
||||||
|
account_quota_notify_enabled: boolean
|
||||||
account_quota_notify_emails: string[]
|
account_quota_notify_emails: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,6 +242,7 @@ export interface UpdateSettingsRequest {
|
|||||||
// Balance & quota notification
|
// Balance & quota notification
|
||||||
balance_low_notify_enabled?: boolean
|
balance_low_notify_enabled?: boolean
|
||||||
balance_low_notify_threshold?: number
|
balance_low_notify_threshold?: number
|
||||||
|
account_quota_notify_enabled?: boolean
|
||||||
account_quota_notify_emails?: string[]
|
account_quota_notify_emails?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1188,10 +1188,13 @@
|
|||||||
:resetTimezone="editResetTimezone"
|
:resetTimezone="editResetTimezone"
|
||||||
:quotaNotifyDailyEnabled="editQuotaNotifyDailyEnabled"
|
:quotaNotifyDailyEnabled="editQuotaNotifyDailyEnabled"
|
||||||
:quotaNotifyDailyThreshold="editQuotaNotifyDailyThreshold"
|
:quotaNotifyDailyThreshold="editQuotaNotifyDailyThreshold"
|
||||||
|
:quotaNotifyDailyThresholdType="editQuotaNotifyDailyThresholdType"
|
||||||
:quotaNotifyWeeklyEnabled="editQuotaNotifyWeeklyEnabled"
|
:quotaNotifyWeeklyEnabled="editQuotaNotifyWeeklyEnabled"
|
||||||
:quotaNotifyWeeklyThreshold="editQuotaNotifyWeeklyThreshold"
|
:quotaNotifyWeeklyThreshold="editQuotaNotifyWeeklyThreshold"
|
||||||
|
:quotaNotifyWeeklyThresholdType="editQuotaNotifyWeeklyThresholdType"
|
||||||
:quotaNotifyTotalEnabled="editQuotaNotifyTotalEnabled"
|
:quotaNotifyTotalEnabled="editQuotaNotifyTotalEnabled"
|
||||||
:quotaNotifyTotalThreshold="editQuotaNotifyTotalThreshold"
|
:quotaNotifyTotalThreshold="editQuotaNotifyTotalThreshold"
|
||||||
|
:quotaNotifyTotalThresholdType="editQuotaNotifyTotalThresholdType"
|
||||||
@update:totalLimit="editQuotaLimit = $event"
|
@update:totalLimit="editQuotaLimit = $event"
|
||||||
@update:dailyLimit="editQuotaDailyLimit = $event"
|
@update:dailyLimit="editQuotaDailyLimit = $event"
|
||||||
@update:weeklyLimit="editQuotaWeeklyLimit = $event"
|
@update:weeklyLimit="editQuotaWeeklyLimit = $event"
|
||||||
@@ -1203,10 +1206,13 @@
|
|||||||
@update:resetTimezone="editResetTimezone = $event"
|
@update:resetTimezone="editResetTimezone = $event"
|
||||||
@update:quotaNotifyDailyEnabled="editQuotaNotifyDailyEnabled = $event"
|
@update:quotaNotifyDailyEnabled="editQuotaNotifyDailyEnabled = $event"
|
||||||
@update:quotaNotifyDailyThreshold="editQuotaNotifyDailyThreshold = $event"
|
@update:quotaNotifyDailyThreshold="editQuotaNotifyDailyThreshold = $event"
|
||||||
|
@update:quotaNotifyDailyThresholdType="editQuotaNotifyDailyThresholdType = $event"
|
||||||
@update:quotaNotifyWeeklyEnabled="editQuotaNotifyWeeklyEnabled = $event"
|
@update:quotaNotifyWeeklyEnabled="editQuotaNotifyWeeklyEnabled = $event"
|
||||||
@update:quotaNotifyWeeklyThreshold="editQuotaNotifyWeeklyThreshold = $event"
|
@update:quotaNotifyWeeklyThreshold="editQuotaNotifyWeeklyThreshold = $event"
|
||||||
|
@update:quotaNotifyWeeklyThresholdType="editQuotaNotifyWeeklyThresholdType = $event"
|
||||||
@update:quotaNotifyTotalEnabled="editQuotaNotifyTotalEnabled = $event"
|
@update:quotaNotifyTotalEnabled="editQuotaNotifyTotalEnabled = $event"
|
||||||
@update:quotaNotifyTotalThreshold="editQuotaNotifyTotalThreshold = $event"
|
@update:quotaNotifyTotalThreshold="editQuotaNotifyTotalThreshold = $event"
|
||||||
|
@update:quotaNotifyTotalThresholdType="editQuotaNotifyTotalThresholdType = $event"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<!-- 配额控制 (非 Anthropic apikey/bedrock) -->
|
<!-- 配额控制 (非 Anthropic apikey/bedrock) -->
|
||||||
@@ -1232,10 +1238,13 @@
|
|||||||
:resetTimezone="editResetTimezone"
|
:resetTimezone="editResetTimezone"
|
||||||
:quotaNotifyDailyEnabled="editQuotaNotifyDailyEnabled"
|
:quotaNotifyDailyEnabled="editQuotaNotifyDailyEnabled"
|
||||||
:quotaNotifyDailyThreshold="editQuotaNotifyDailyThreshold"
|
:quotaNotifyDailyThreshold="editQuotaNotifyDailyThreshold"
|
||||||
|
:quotaNotifyDailyThresholdType="editQuotaNotifyDailyThresholdType"
|
||||||
:quotaNotifyWeeklyEnabled="editQuotaNotifyWeeklyEnabled"
|
:quotaNotifyWeeklyEnabled="editQuotaNotifyWeeklyEnabled"
|
||||||
:quotaNotifyWeeklyThreshold="editQuotaNotifyWeeklyThreshold"
|
:quotaNotifyWeeklyThreshold="editQuotaNotifyWeeklyThreshold"
|
||||||
|
:quotaNotifyWeeklyThresholdType="editQuotaNotifyWeeklyThresholdType"
|
||||||
:quotaNotifyTotalEnabled="editQuotaNotifyTotalEnabled"
|
:quotaNotifyTotalEnabled="editQuotaNotifyTotalEnabled"
|
||||||
:quotaNotifyTotalThreshold="editQuotaNotifyTotalThreshold"
|
:quotaNotifyTotalThreshold="editQuotaNotifyTotalThreshold"
|
||||||
|
:quotaNotifyTotalThresholdType="editQuotaNotifyTotalThresholdType"
|
||||||
@update:totalLimit="editQuotaLimit = $event"
|
@update:totalLimit="editQuotaLimit = $event"
|
||||||
@update:dailyLimit="editQuotaDailyLimit = $event"
|
@update:dailyLimit="editQuotaDailyLimit = $event"
|
||||||
@update:weeklyLimit="editQuotaWeeklyLimit = $event"
|
@update:weeklyLimit="editQuotaWeeklyLimit = $event"
|
||||||
@@ -1247,10 +1256,13 @@
|
|||||||
@update:resetTimezone="editResetTimezone = $event"
|
@update:resetTimezone="editResetTimezone = $event"
|
||||||
@update:quotaNotifyDailyEnabled="editQuotaNotifyDailyEnabled = $event"
|
@update:quotaNotifyDailyEnabled="editQuotaNotifyDailyEnabled = $event"
|
||||||
@update:quotaNotifyDailyThreshold="editQuotaNotifyDailyThreshold = $event"
|
@update:quotaNotifyDailyThreshold="editQuotaNotifyDailyThreshold = $event"
|
||||||
|
@update:quotaNotifyDailyThresholdType="editQuotaNotifyDailyThresholdType = $event"
|
||||||
@update:quotaNotifyWeeklyEnabled="editQuotaNotifyWeeklyEnabled = $event"
|
@update:quotaNotifyWeeklyEnabled="editQuotaNotifyWeeklyEnabled = $event"
|
||||||
@update:quotaNotifyWeeklyThreshold="editQuotaNotifyWeeklyThreshold = $event"
|
@update:quotaNotifyWeeklyThreshold="editQuotaNotifyWeeklyThreshold = $event"
|
||||||
|
@update:quotaNotifyWeeklyThresholdType="editQuotaNotifyWeeklyThresholdType = $event"
|
||||||
@update:quotaNotifyTotalEnabled="editQuotaNotifyTotalEnabled = $event"
|
@update:quotaNotifyTotalEnabled="editQuotaNotifyTotalEnabled = $event"
|
||||||
@update:quotaNotifyTotalThreshold="editQuotaNotifyTotalThreshold = $event"
|
@update:quotaNotifyTotalThreshold="editQuotaNotifyTotalThreshold = $event"
|
||||||
|
@update:quotaNotifyTotalThresholdType="editQuotaNotifyTotalThresholdType = $event"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1992,10 +2004,13 @@ const editWeeklyResetHour = ref<number | null>(null)
|
|||||||
const editResetTimezone = ref<string | null>(null)
|
const editResetTimezone = ref<string | null>(null)
|
||||||
const editQuotaNotifyDailyEnabled = ref<boolean | null>(null)
|
const editQuotaNotifyDailyEnabled = ref<boolean | null>(null)
|
||||||
const editQuotaNotifyDailyThreshold = ref<number | null>(null)
|
const editQuotaNotifyDailyThreshold = ref<number | null>(null)
|
||||||
|
const editQuotaNotifyDailyThresholdType = ref<string | null>(null)
|
||||||
const editQuotaNotifyWeeklyEnabled = ref<boolean | null>(null)
|
const editQuotaNotifyWeeklyEnabled = ref<boolean | null>(null)
|
||||||
const editQuotaNotifyWeeklyThreshold = ref<number | null>(null)
|
const editQuotaNotifyWeeklyThreshold = ref<number | null>(null)
|
||||||
|
const editQuotaNotifyWeeklyThresholdType = ref<string | null>(null)
|
||||||
const editQuotaNotifyTotalEnabled = ref<boolean | null>(null)
|
const editQuotaNotifyTotalEnabled = ref<boolean | null>(null)
|
||||||
const editQuotaNotifyTotalThreshold = ref<number | null>(null)
|
const editQuotaNotifyTotalThreshold = ref<number | null>(null)
|
||||||
|
const editQuotaNotifyTotalThresholdType = ref<string | null>(null)
|
||||||
const openAIWSModeOptions = computed(() => [
|
const openAIWSModeOptions = computed(() => [
|
||||||
{ value: OPENAI_WS_MODE_OFF, label: t('admin.accounts.openai.wsModeOff') },
|
{ value: OPENAI_WS_MODE_OFF, label: t('admin.accounts.openai.wsModeOff') },
|
||||||
// TODO: ctx_pool 选项暂时隐藏,待测试完成后恢复
|
// TODO: ctx_pool 选项暂时隐藏,待测试完成后恢复
|
||||||
@@ -2198,10 +2213,13 @@ const syncFormFromAccount = (newAccount: Account | null) => {
|
|||||||
// Load quota notify config
|
// Load quota notify config
|
||||||
editQuotaNotifyDailyEnabled.value = (extra?.quota_notify_daily_enabled as boolean) ?? null
|
editQuotaNotifyDailyEnabled.value = (extra?.quota_notify_daily_enabled as boolean) ?? null
|
||||||
editQuotaNotifyDailyThreshold.value = (extra?.quota_notify_daily_threshold as number) ?? null
|
editQuotaNotifyDailyThreshold.value = (extra?.quota_notify_daily_threshold as number) ?? null
|
||||||
|
editQuotaNotifyDailyThresholdType.value = (extra?.quota_notify_daily_threshold_type as string) ?? null
|
||||||
editQuotaNotifyWeeklyEnabled.value = (extra?.quota_notify_weekly_enabled as boolean) ?? null
|
editQuotaNotifyWeeklyEnabled.value = (extra?.quota_notify_weekly_enabled as boolean) ?? null
|
||||||
editQuotaNotifyWeeklyThreshold.value = (extra?.quota_notify_weekly_threshold as number) ?? null
|
editQuotaNotifyWeeklyThreshold.value = (extra?.quota_notify_weekly_threshold as number) ?? null
|
||||||
|
editQuotaNotifyWeeklyThresholdType.value = (extra?.quota_notify_weekly_threshold_type as string) ?? null
|
||||||
editQuotaNotifyTotalEnabled.value = (extra?.quota_notify_total_enabled as boolean) ?? null
|
editQuotaNotifyTotalEnabled.value = (extra?.quota_notify_total_enabled as boolean) ?? null
|
||||||
editQuotaNotifyTotalThreshold.value = (extra?.quota_notify_total_threshold as number) ?? null
|
editQuotaNotifyTotalThreshold.value = (extra?.quota_notify_total_threshold as number) ?? null
|
||||||
|
editQuotaNotifyTotalThresholdType.value = (extra?.quota_notify_total_threshold_type as string) ?? null
|
||||||
} else {
|
} else {
|
||||||
editQuotaLimit.value = null
|
editQuotaLimit.value = null
|
||||||
editQuotaDailyLimit.value = null
|
editQuotaDailyLimit.value = null
|
||||||
@@ -3262,9 +3280,11 @@ const handleSubmit = async () => {
|
|||||||
} else {
|
} else {
|
||||||
delete newExtra.quota_notify_daily_threshold
|
delete newExtra.quota_notify_daily_threshold
|
||||||
}
|
}
|
||||||
|
newExtra.quota_notify_daily_threshold_type = editQuotaNotifyDailyThresholdType.value || 'fixed'
|
||||||
} else {
|
} else {
|
||||||
delete newExtra.quota_notify_daily_enabled
|
delete newExtra.quota_notify_daily_enabled
|
||||||
delete newExtra.quota_notify_daily_threshold
|
delete newExtra.quota_notify_daily_threshold
|
||||||
|
delete newExtra.quota_notify_daily_threshold_type
|
||||||
}
|
}
|
||||||
if (editQuotaNotifyWeeklyEnabled.value) {
|
if (editQuotaNotifyWeeklyEnabled.value) {
|
||||||
newExtra.quota_notify_weekly_enabled = true
|
newExtra.quota_notify_weekly_enabled = true
|
||||||
@@ -3273,9 +3293,11 @@ const handleSubmit = async () => {
|
|||||||
} else {
|
} else {
|
||||||
delete newExtra.quota_notify_weekly_threshold
|
delete newExtra.quota_notify_weekly_threshold
|
||||||
}
|
}
|
||||||
|
newExtra.quota_notify_weekly_threshold_type = editQuotaNotifyWeeklyThresholdType.value || 'fixed'
|
||||||
} else {
|
} else {
|
||||||
delete newExtra.quota_notify_weekly_enabled
|
delete newExtra.quota_notify_weekly_enabled
|
||||||
delete newExtra.quota_notify_weekly_threshold
|
delete newExtra.quota_notify_weekly_threshold
|
||||||
|
delete newExtra.quota_notify_weekly_threshold_type
|
||||||
}
|
}
|
||||||
if (editQuotaNotifyTotalEnabled.value) {
|
if (editQuotaNotifyTotalEnabled.value) {
|
||||||
newExtra.quota_notify_total_enabled = true
|
newExtra.quota_notify_total_enabled = true
|
||||||
@@ -3284,9 +3306,11 @@ const handleSubmit = async () => {
|
|||||||
} else {
|
} else {
|
||||||
delete newExtra.quota_notify_total_threshold
|
delete newExtra.quota_notify_total_threshold
|
||||||
}
|
}
|
||||||
|
newExtra.quota_notify_total_threshold_type = editQuotaNotifyTotalThresholdType.value || 'fixed'
|
||||||
} else {
|
} else {
|
||||||
delete newExtra.quota_notify_total_enabled
|
delete newExtra.quota_notify_total_enabled
|
||||||
delete newExtra.quota_notify_total_threshold
|
delete newExtra.quota_notify_total_threshold
|
||||||
|
delete newExtra.quota_notify_total_threshold_type
|
||||||
}
|
}
|
||||||
updatePayload.extra = newExtra
|
updatePayload.extra = newExtra
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,17 +17,23 @@ const props = withDefaults(defineProps<{
|
|||||||
resetTimezone: string | null
|
resetTimezone: string | null
|
||||||
quotaNotifyDailyEnabled?: boolean | null
|
quotaNotifyDailyEnabled?: boolean | null
|
||||||
quotaNotifyDailyThreshold?: number | null
|
quotaNotifyDailyThreshold?: number | null
|
||||||
|
quotaNotifyDailyThresholdType?: string | null
|
||||||
quotaNotifyWeeklyEnabled?: boolean | null
|
quotaNotifyWeeklyEnabled?: boolean | null
|
||||||
quotaNotifyWeeklyThreshold?: number | null
|
quotaNotifyWeeklyThreshold?: number | null
|
||||||
|
quotaNotifyWeeklyThresholdType?: string | null
|
||||||
quotaNotifyTotalEnabled?: boolean | null
|
quotaNotifyTotalEnabled?: boolean | null
|
||||||
quotaNotifyTotalThreshold?: number | null
|
quotaNotifyTotalThreshold?: number | null
|
||||||
|
quotaNotifyTotalThresholdType?: string | null
|
||||||
}>(), {
|
}>(), {
|
||||||
quotaNotifyDailyEnabled: null,
|
quotaNotifyDailyEnabled: null,
|
||||||
quotaNotifyDailyThreshold: null,
|
quotaNotifyDailyThreshold: null,
|
||||||
|
quotaNotifyDailyThresholdType: null,
|
||||||
quotaNotifyWeeklyEnabled: null,
|
quotaNotifyWeeklyEnabled: null,
|
||||||
quotaNotifyWeeklyThreshold: null,
|
quotaNotifyWeeklyThreshold: null,
|
||||||
|
quotaNotifyWeeklyThresholdType: null,
|
||||||
quotaNotifyTotalEnabled: null,
|
quotaNotifyTotalEnabled: null,
|
||||||
quotaNotifyTotalThreshold: null,
|
quotaNotifyTotalThreshold: null,
|
||||||
|
quotaNotifyTotalThresholdType: null,
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -42,10 +48,13 @@ const emit = defineEmits<{
|
|||||||
'update:resetTimezone': [value: string | null]
|
'update:resetTimezone': [value: string | null]
|
||||||
'update:quotaNotifyDailyEnabled': [value: boolean | null]
|
'update:quotaNotifyDailyEnabled': [value: boolean | null]
|
||||||
'update:quotaNotifyDailyThreshold': [value: number | null]
|
'update:quotaNotifyDailyThreshold': [value: number | null]
|
||||||
|
'update:quotaNotifyDailyThresholdType': [value: string | null]
|
||||||
'update:quotaNotifyWeeklyEnabled': [value: boolean | null]
|
'update:quotaNotifyWeeklyEnabled': [value: boolean | null]
|
||||||
'update:quotaNotifyWeeklyThreshold': [value: number | null]
|
'update:quotaNotifyWeeklyThreshold': [value: number | null]
|
||||||
|
'update:quotaNotifyWeeklyThresholdType': [value: string | null]
|
||||||
'update:quotaNotifyTotalEnabled': [value: boolean | null]
|
'update:quotaNotifyTotalEnabled': [value: boolean | null]
|
||||||
'update:quotaNotifyTotalThreshold': [value: number | null]
|
'update:quotaNotifyTotalThreshold': [value: number | null]
|
||||||
|
'update:quotaNotifyTotalThresholdType': [value: string | null]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const enabled = computed(() =>
|
const enabled = computed(() =>
|
||||||
@@ -228,8 +237,10 @@ const onWeeklyModeChange = (e: Event) => {
|
|||||||
v-if="dailyLimit && dailyLimit > 0"
|
v-if="dailyLimit && dailyLimit > 0"
|
||||||
:enabled="props.quotaNotifyDailyEnabled"
|
:enabled="props.quotaNotifyDailyEnabled"
|
||||||
:threshold="props.quotaNotifyDailyThreshold"
|
:threshold="props.quotaNotifyDailyThreshold"
|
||||||
|
:threshold-type="props.quotaNotifyDailyThresholdType"
|
||||||
@update:enabled="emit('update:quotaNotifyDailyEnabled', $event)"
|
@update:enabled="emit('update:quotaNotifyDailyEnabled', $event)"
|
||||||
@update:threshold="emit('update:quotaNotifyDailyThreshold', $event)"
|
@update:threshold="emit('update:quotaNotifyDailyThreshold', $event)"
|
||||||
|
@update:threshold-type="emit('update:quotaNotifyDailyThresholdType', $event)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -292,8 +303,10 @@ const onWeeklyModeChange = (e: Event) => {
|
|||||||
v-if="weeklyLimit && weeklyLimit > 0"
|
v-if="weeklyLimit && weeklyLimit > 0"
|
||||||
:enabled="props.quotaNotifyWeeklyEnabled"
|
:enabled="props.quotaNotifyWeeklyEnabled"
|
||||||
:threshold="props.quotaNotifyWeeklyThreshold"
|
:threshold="props.quotaNotifyWeeklyThreshold"
|
||||||
|
:threshold-type="props.quotaNotifyWeeklyThresholdType"
|
||||||
@update:enabled="emit('update:quotaNotifyWeeklyEnabled', $event)"
|
@update:enabled="emit('update:quotaNotifyWeeklyEnabled', $event)"
|
||||||
@update:threshold="emit('update:quotaNotifyWeeklyThreshold', $event)"
|
@update:threshold="emit('update:quotaNotifyWeeklyThreshold', $event)"
|
||||||
|
@update:threshold-type="emit('update:quotaNotifyWeeklyThresholdType', $event)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -330,8 +343,10 @@ const onWeeklyModeChange = (e: Event) => {
|
|||||||
v-if="totalLimit && totalLimit > 0"
|
v-if="totalLimit && totalLimit > 0"
|
||||||
:enabled="props.quotaNotifyTotalEnabled"
|
:enabled="props.quotaNotifyTotalEnabled"
|
||||||
:threshold="props.quotaNotifyTotalThreshold"
|
:threshold="props.quotaNotifyTotalThreshold"
|
||||||
|
:threshold-type="props.quotaNotifyTotalThresholdType"
|
||||||
@update:enabled="emit('update:quotaNotifyTotalEnabled', $event)"
|
@update:enabled="emit('update:quotaNotifyTotalEnabled', $event)"
|
||||||
@update:threshold="emit('update:quotaNotifyTotalThreshold', $event)"
|
@update:threshold="emit('update:quotaNotifyTotalThreshold', $event)"
|
||||||
|
@update:threshold-type="emit('update:quotaNotifyTotalThresholdType', $event)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,12 +6,18 @@ const { t } = useI18n()
|
|||||||
defineProps<{
|
defineProps<{
|
||||||
enabled: boolean | null
|
enabled: boolean | null
|
||||||
threshold: number | null
|
threshold: number | null
|
||||||
|
thresholdType: string | null // "fixed" (default) or "percentage"
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
'update:enabled': [value: boolean | null]
|
'update:enabled': [value: boolean | null]
|
||||||
'update:threshold': [value: number | null]
|
'update:threshold': [value: number | null]
|
||||||
|
'update:thresholdType': [value: string | null]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
function toggleType(current: string | null) {
|
||||||
|
emit('update:thresholdType', current === 'percentage' ? 'fixed' : 'percentage')
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -32,15 +38,32 @@ const emit = defineEmits<{
|
|||||||
]"
|
]"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<div v-if="enabled" class="relative flex-1">
|
<div v-if="enabled" class="flex items-center gap-1 flex-1">
|
||||||
<span class="absolute left-2 top-1/2 -translate-y-1/2 text-gray-400 text-sm">$</span>
|
<button
|
||||||
|
type="button"
|
||||||
|
class="px-1.5 py-0.5 text-xs font-medium rounded border transition-colors"
|
||||||
|
:class="(!thresholdType || thresholdType === 'fixed') ? 'bg-primary-100 text-primary-700 border-primary-300 dark:bg-primary-900/30 dark:text-primary-400 dark:border-primary-700' : 'bg-gray-100 text-gray-500 border-gray-200 dark:bg-dark-600 dark:text-gray-400 dark:border-dark-500'"
|
||||||
|
@click="toggleType(thresholdType)"
|
||||||
|
>
|
||||||
|
$
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="px-1.5 py-0.5 text-xs font-medium rounded border transition-colors"
|
||||||
|
:class="thresholdType === 'percentage' ? 'bg-primary-100 text-primary-700 border-primary-300 dark:bg-primary-900/30 dark:text-primary-400 dark:border-primary-700' : 'bg-gray-100 text-gray-500 border-gray-200 dark:bg-dark-600 dark:text-gray-400 dark:border-dark-500'"
|
||||||
|
@click="toggleType(thresholdType)"
|
||||||
|
>
|
||||||
|
%
|
||||||
|
</button>
|
||||||
<input
|
<input
|
||||||
:value="threshold"
|
:value="threshold"
|
||||||
@input="emit('update:threshold', parseFloat(($event.target as HTMLInputElement).value) || null)"
|
@input="emit('update:threshold', parseFloat(($event.target as HTMLInputElement).value) || null)"
|
||||||
type="number"
|
type="number"
|
||||||
min="0"
|
min="0"
|
||||||
step="0.01"
|
:max="thresholdType === 'percentage' ? 100 : undefined"
|
||||||
class="input pl-6 py-1 text-sm"
|
:step="thresholdType === 'percentage' ? 1 : 0.01"
|
||||||
|
class="input py-1 text-sm flex-1"
|
||||||
|
:placeholder="thresholdType === 'percentage' ? t('admin.accounts.quotaNotify.thresholdPlaceholder') : t('admin.accounts.quotaNotify.threshold')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2257,7 +2257,7 @@ export default {
|
|||||||
alert: 'Alert Threshold',
|
alert: 'Alert Threshold',
|
||||||
enabled: 'Enable Alert',
|
enabled: 'Enable Alert',
|
||||||
threshold: 'Alert Amount',
|
threshold: 'Alert Amount',
|
||||||
thresholdPlaceholder: 'Enter alert amount',
|
thresholdPlaceholder: 'Enter percentage',
|
||||||
},
|
},
|
||||||
testConnection: 'Test Connection',
|
testConnection: 'Test Connection',
|
||||||
reAuthorize: 'Re-Authorize',
|
reAuthorize: 'Re-Authorize',
|
||||||
@@ -4640,6 +4640,7 @@ export default {
|
|||||||
quotaNotify: {
|
quotaNotify: {
|
||||||
title: 'Account Quota Notification',
|
title: 'Account Quota Notification',
|
||||||
description: 'Notify admins when account quota usage reaches alert threshold',
|
description: 'Notify admins when account quota usage reaches alert threshold',
|
||||||
|
enabled: 'Enable Account Quota Notification',
|
||||||
emails: 'Notification Emails',
|
emails: 'Notification Emails',
|
||||||
emailsHint: 'Leave empty to disable notifications',
|
emailsHint: 'Leave empty to disable notifications',
|
||||||
addEmail: 'Add Email',
|
addEmail: 'Add Email',
|
||||||
|
|||||||
@@ -2255,7 +2255,7 @@ export default {
|
|||||||
alert: '告警阈值',
|
alert: '告警阈值',
|
||||||
enabled: '启用告警',
|
enabled: '启用告警',
|
||||||
threshold: '告警金额',
|
threshold: '告警金额',
|
||||||
thresholdPlaceholder: '输入告警金额',
|
thresholdPlaceholder: '输入百分比',
|
||||||
},
|
},
|
||||||
testConnection: '测试连接',
|
testConnection: '测试连接',
|
||||||
reAuthorize: '重新授权',
|
reAuthorize: '重新授权',
|
||||||
@@ -4804,6 +4804,7 @@ export default {
|
|||||||
quotaNotify: {
|
quotaNotify: {
|
||||||
title: '账号限额通知',
|
title: '账号限额通知',
|
||||||
description: '当账号配额用量达到告警阈值时通知管理员',
|
description: '当账号配额用量达到告警阈值时通知管理员',
|
||||||
|
enabled: '启用账号限额通知',
|
||||||
emails: '通知邮箱',
|
emails: '通知邮箱',
|
||||||
emailsHint: '留空则不发送通知',
|
emailsHint: '留空则不发送通知',
|
||||||
addEmail: '添加邮箱',
|
addEmail: '添加邮箱',
|
||||||
|
|||||||
@@ -339,7 +339,9 @@ export const useAppStore = defineStore('app', () => {
|
|||||||
oidc_oauth_enabled: false,
|
oidc_oauth_enabled: false,
|
||||||
oidc_oauth_provider_name: 'OIDC',
|
oidc_oauth_provider_name: 'OIDC',
|
||||||
backend_mode_enabled: false,
|
backend_mode_enabled: false,
|
||||||
version: siteVersion.value
|
version: siteVersion.value,
|
||||||
|
balance_low_notify_enabled: false,
|
||||||
|
account_quota_notify_enabled: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -117,6 +117,8 @@ export interface PublicSettings {
|
|||||||
oidc_oauth_provider_name: string
|
oidc_oauth_provider_name: string
|
||||||
backend_mode_enabled: boolean
|
backend_mode_enabled: boolean
|
||||||
version: string
|
version: string
|
||||||
|
balance_low_notify_enabled: boolean
|
||||||
|
account_quota_notify_enabled: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AuthResponse {
|
export interface AuthResponse {
|
||||||
|
|||||||
@@ -2718,11 +2718,15 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="px-6 py-6 space-y-4">
|
<div class="px-6 py-6 space-y-4">
|
||||||
<div>
|
<div class="flex items-center justify-between">
|
||||||
|
<label class="mb-0 block text-sm font-medium text-gray-700 dark:text-gray-300">{{ t('admin.settings.quotaNotify.enabled') }}</label>
|
||||||
|
<Toggle v-model="form.account_quota_notify_enabled" />
|
||||||
|
</div>
|
||||||
|
<div v-if="form.account_quota_notify_enabled">
|
||||||
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">{{ t('admin.settings.quotaNotify.emails') }}</label>
|
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">{{ t('admin.settings.quotaNotify.emails') }}</label>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<div v-for="(_, index) in (form.account_quota_notify_emails || [])" :key="index" class="flex items-center gap-2">
|
<div v-for="(_, index) in (form.account_quota_notify_emails || [])" :key="index" class="flex items-center gap-2">
|
||||||
<input v-model="form.account_quota_notify_emails[index]" type="email" class="input flex-1" />
|
<input v-model="form.account_quota_notify_emails[index]" type="email" class="input flex-1" :placeholder="t('admin.settings.quotaNotify.emailPlaceholder')" />
|
||||||
<button @click="form.account_quota_notify_emails.splice(index, 1)" class="btn btn-secondary px-2" type="button">
|
<button @click="form.account_quota_notify_emails.splice(index, 1)" class="btn btn-secondary px-2" type="button">
|
||||||
<Icon name="x" size="xs" class="h-4 w-4" />
|
<Icon name="x" size="xs" class="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
@@ -3018,6 +3022,7 @@ const form = reactive<SettingsForm>({
|
|||||||
// Balance & quota notification
|
// Balance & quota notification
|
||||||
balance_low_notify_enabled: false,
|
balance_low_notify_enabled: false,
|
||||||
balance_low_notify_threshold: 0,
|
balance_low_notify_threshold: 0,
|
||||||
|
account_quota_notify_enabled: false,
|
||||||
account_quota_notify_emails: [] as string[]
|
account_quota_notify_emails: [] as string[]
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -3588,6 +3593,7 @@ async function saveSettings() {
|
|||||||
// Balance & quota notification
|
// Balance & quota notification
|
||||||
balance_low_notify_enabled: form.balance_low_notify_enabled,
|
balance_low_notify_enabled: form.balance_low_notify_enabled,
|
||||||
balance_low_notify_threshold: Number(form.balance_low_notify_threshold) || 0,
|
balance_low_notify_threshold: Number(form.balance_low_notify_threshold) || 0,
|
||||||
|
account_quota_notify_enabled: form.account_quota_notify_enabled,
|
||||||
account_quota_notify_emails: (form.account_quota_notify_emails || []).filter((e: string) => e.trim() !== ''),
|
account_quota_notify_emails: (form.account_quota_notify_emails || []).filter((e: string) => e.trim() !== ''),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<ProfileEditForm :initial-username="user?.username || ''" />
|
<ProfileEditForm :initial-username="user?.username || ''" />
|
||||||
<ProfileBalanceNotifyCard
|
<ProfileBalanceNotifyCard
|
||||||
v-if="user"
|
v-if="user && balanceLowNotifyEnabled"
|
||||||
:enabled="user.balance_notify_enabled ?? true"
|
:enabled="user.balance_notify_enabled ?? true"
|
||||||
:threshold="user.balance_notify_threshold"
|
:threshold="user.balance_notify_threshold"
|
||||||
:extra-emails="user.balance_notify_extra_emails ?? []"
|
:extra-emails="user.balance_notify_extra_emails ?? []"
|
||||||
@@ -40,11 +40,12 @@ import { Icon } from '@/components/icons'
|
|||||||
|
|
||||||
const { t } = useI18n(); const authStore = useAuthStore(); const user = computed(() => authStore.user)
|
const { t } = useI18n(); const authStore = useAuthStore(); const user = computed(() => authStore.user)
|
||||||
const contactInfo = ref('')
|
const contactInfo = ref('')
|
||||||
|
const balanceLowNotifyEnabled = ref(false)
|
||||||
|
|
||||||
const WalletIcon = { render: () => h('svg', { fill: 'none', viewBox: '0 0 24 24', stroke: 'currentColor', 'stroke-width': '1.5' }, [h('path', { d: 'M21 12a2.25 2.25 0 00-2.25-2.25H15a3 3 0 11-6 0H5.25A2.25 2.25 0 003 12' })]) }
|
const WalletIcon = { render: () => h('svg', { fill: 'none', viewBox: '0 0 24 24', stroke: 'currentColor', 'stroke-width': '1.5' }, [h('path', { d: 'M21 12a2.25 2.25 0 00-2.25-2.25H15a3 3 0 11-6 0H5.25A2.25 2.25 0 003 12' })]) }
|
||||||
const BoltIcon = { render: () => h('svg', { fill: 'none', viewBox: '0 0 24 24', stroke: 'currentColor', 'stroke-width': '1.5' }, [h('path', { d: 'm3.75 13.5 10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z' })]) }
|
const BoltIcon = { render: () => h('svg', { fill: 'none', viewBox: '0 0 24 24', stroke: 'currentColor', 'stroke-width': '1.5' }, [h('path', { d: 'm3.75 13.5 10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z' })]) }
|
||||||
const CalendarIcon = { render: () => h('svg', { fill: 'none', viewBox: '0 0 24 24', stroke: 'currentColor', 'stroke-width': '1.5' }, [h('path', { d: 'M6.75 3v2.25M17.25 3v2.25' })]) }
|
const CalendarIcon = { render: () => h('svg', { fill: 'none', viewBox: '0 0 24 24', stroke: 'currentColor', 'stroke-width': '1.5' }, [h('path', { d: 'M6.75 3v2.25M17.25 3v2.25' })]) }
|
||||||
|
|
||||||
onMounted(async () => { try { const s = await authAPI.getPublicSettings(); contactInfo.value = s.contact_info || '' } catch (error) { console.error('Failed to load contact info:', error) } })
|
onMounted(async () => { try { const s = await authAPI.getPublicSettings(); contactInfo.value = s.contact_info || ''; balanceLowNotifyEnabled.value = s.balance_low_notify_enabled ?? false } catch (error) { console.error('Failed to load contact info:', error) } })
|
||||||
const formatCurrency = (v: number) => `$${v.toFixed(2)}`
|
const formatCurrency = (v: number) => `$${v.toFixed(2)}`
|
||||||
</script>
|
</script>
|
||||||
Reference in New Issue
Block a user