feat: 平台图标与计费修复
- fix(billing): 修复 OpenAI 兼容 API 缓存 token 重复计费问题 - fix(auth): 隐藏数据库错误详情,返回通用服务不可用错误 - feat(ui): 新增 PlatformIcon 组件,GroupBadge 支持平台颜色区分 - feat(ui): 账号管理新增重置状态按钮,重授权后自动清除错误 - feat(ui): 分组管理新增计费类型列,显示订阅限额信息 - ui: 首页 GPT 状态改为已支持
This commit is contained in:
@@ -23,6 +23,7 @@ var (
|
|||||||
ErrTokenExpired = errors.New("token has expired")
|
ErrTokenExpired = errors.New("token has expired")
|
||||||
ErrEmailVerifyRequired = errors.New("email verification is required")
|
ErrEmailVerifyRequired = errors.New("email verification is required")
|
||||||
ErrRegDisabled = errors.New("registration is currently disabled")
|
ErrRegDisabled = errors.New("registration is currently disabled")
|
||||||
|
ErrServiceUnavailable = errors.New("service temporarily unavailable")
|
||||||
)
|
)
|
||||||
|
|
||||||
// JWTClaims JWT载荷数据
|
// JWTClaims JWT载荷数据
|
||||||
@@ -90,7 +91,8 @@ func (s *AuthService) RegisterWithVerification(ctx context.Context, email, passw
|
|||||||
// 检查邮箱是否已存在
|
// 检查邮箱是否已存在
|
||||||
existsEmail, err := s.userRepo.ExistsByEmail(ctx, email)
|
existsEmail, err := s.userRepo.ExistsByEmail(ctx, email)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", nil, fmt.Errorf("check email exists: %w", err)
|
log.Printf("[Auth] Database error checking email exists: %v", err)
|
||||||
|
return "", nil, ErrServiceUnavailable
|
||||||
}
|
}
|
||||||
if existsEmail {
|
if existsEmail {
|
||||||
return "", nil, ErrEmailExists
|
return "", nil, ErrEmailExists
|
||||||
@@ -121,7 +123,8 @@ func (s *AuthService) RegisterWithVerification(ctx context.Context, email, passw
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := s.userRepo.Create(ctx, user); err != nil {
|
if err := s.userRepo.Create(ctx, user); err != nil {
|
||||||
return "", nil, fmt.Errorf("create user: %w", err)
|
log.Printf("[Auth] Database error creating user: %v", err)
|
||||||
|
return "", nil, ErrServiceUnavailable
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成token
|
// 生成token
|
||||||
@@ -148,7 +151,8 @@ func (s *AuthService) SendVerifyCode(ctx context.Context, email string) error {
|
|||||||
// 检查邮箱是否已存在
|
// 检查邮箱是否已存在
|
||||||
existsEmail, err := s.userRepo.ExistsByEmail(ctx, email)
|
existsEmail, err := s.userRepo.ExistsByEmail(ctx, email)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("check email exists: %w", err)
|
log.Printf("[Auth] Database error checking email exists: %v", err)
|
||||||
|
return ErrServiceUnavailable
|
||||||
}
|
}
|
||||||
if existsEmail {
|
if existsEmail {
|
||||||
return ErrEmailExists
|
return ErrEmailExists
|
||||||
@@ -181,8 +185,8 @@ func (s *AuthService) SendVerifyCodeAsync(ctx context.Context, email string) (*S
|
|||||||
// 检查邮箱是否已存在
|
// 检查邮箱是否已存在
|
||||||
existsEmail, err := s.userRepo.ExistsByEmail(ctx, email)
|
existsEmail, err := s.userRepo.ExistsByEmail(ctx, email)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[Auth] Error checking email exists: %v", err)
|
log.Printf("[Auth] Database error checking email exists: %v", err)
|
||||||
return nil, fmt.Errorf("check email exists: %w", err)
|
return nil, ErrServiceUnavailable
|
||||||
}
|
}
|
||||||
if existsEmail {
|
if existsEmail {
|
||||||
log.Printf("[Auth] Email already exists: %s", email)
|
log.Printf("[Auth] Email already exists: %s", email)
|
||||||
@@ -254,7 +258,9 @@ func (s *AuthService) Login(ctx context.Context, email, password string) (string
|
|||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return "", nil, ErrInvalidCredentials
|
return "", nil, ErrInvalidCredentials
|
||||||
}
|
}
|
||||||
return "", nil, fmt.Errorf("get user by email: %w", err)
|
// 记录数据库错误但不暴露给用户
|
||||||
|
log.Printf("[Auth] Database error during login: %v", err)
|
||||||
|
return "", nil, ErrServiceUnavailable
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证密码
|
// 验证密码
|
||||||
@@ -354,7 +360,8 @@ func (s *AuthService) RefreshToken(ctx context.Context, oldTokenString string) (
|
|||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return "", ErrInvalidToken
|
return "", ErrInvalidToken
|
||||||
}
|
}
|
||||||
return "", fmt.Errorf("get user: %w", err)
|
log.Printf("[Auth] Database error refreshing token: %v", err)
|
||||||
|
return "", ErrServiceUnavailable
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查用户状态
|
// 检查用户状态
|
||||||
|
|||||||
@@ -611,9 +611,16 @@ func (s *OpenAIGatewayService) RecordUsage(ctx context.Context, input *OpenAIRec
|
|||||||
account := input.Account
|
account := input.Account
|
||||||
subscription := input.Subscription
|
subscription := input.Subscription
|
||||||
|
|
||||||
|
// 计算实际的新输入token(减去缓存读取的token)
|
||||||
|
// 因为 input_tokens 包含了 cache_read_tokens,而缓存读取的token不应按输入价格计费
|
||||||
|
actualInputTokens := result.Usage.InputTokens - result.Usage.CacheReadInputTokens
|
||||||
|
if actualInputTokens < 0 {
|
||||||
|
actualInputTokens = 0
|
||||||
|
}
|
||||||
|
|
||||||
// Calculate cost
|
// Calculate cost
|
||||||
tokens := UsageTokens{
|
tokens := UsageTokens{
|
||||||
InputTokens: result.Usage.InputTokens,
|
InputTokens: actualInputTokens,
|
||||||
OutputTokens: result.Usage.OutputTokens,
|
OutputTokens: result.Usage.OutputTokens,
|
||||||
CacheCreationTokens: result.Usage.CacheCreationInputTokens,
|
CacheCreationTokens: result.Usage.CacheCreationInputTokens,
|
||||||
CacheReadTokens: result.Usage.CacheReadInputTokens,
|
CacheReadTokens: result.Usage.CacheReadInputTokens,
|
||||||
@@ -645,7 +652,7 @@ func (s *OpenAIGatewayService) RecordUsage(ctx context.Context, input *OpenAIRec
|
|||||||
AccountID: account.ID,
|
AccountID: account.ID,
|
||||||
RequestID: result.RequestID,
|
RequestID: result.RequestID,
|
||||||
Model: result.Model,
|
Model: result.Model,
|
||||||
InputTokens: result.Usage.InputTokens,
|
InputTokens: actualInputTokens,
|
||||||
OutputTokens: result.Usage.OutputTokens,
|
OutputTokens: result.Usage.OutputTokens,
|
||||||
CacheCreationTokens: result.Usage.CacheCreationInputTokens,
|
CacheCreationTokens: result.Usage.CacheCreationInputTokens,
|
||||||
CacheReadTokens: result.Usage.CacheReadInputTokens,
|
CacheReadTokens: result.Usage.CacheReadInputTokens,
|
||||||
|
|||||||
@@ -226,6 +226,9 @@ const handleExchangeCode = async () => {
|
|||||||
extra
|
extra
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Clear error status after successful re-authorization
|
||||||
|
await adminAPI.accounts.clearError(props.account.id)
|
||||||
|
|
||||||
appStore.showSuccess(t('admin.accounts.reAuthorizedSuccess'))
|
appStore.showSuccess(t('admin.accounts.reAuthorizedSuccess'))
|
||||||
emit('reauthorized')
|
emit('reauthorized')
|
||||||
handleClose()
|
handleClose()
|
||||||
@@ -262,6 +265,9 @@ const handleExchangeCode = async () => {
|
|||||||
extra
|
extra
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Clear error status after successful re-authorization
|
||||||
|
await adminAPI.accounts.clearError(props.account.id)
|
||||||
|
|
||||||
appStore.showSuccess(t('admin.accounts.reAuthorizedSuccess'))
|
appStore.showSuccess(t('admin.accounts.reAuthorizedSuccess'))
|
||||||
emit('reauthorized')
|
emit('reauthorized')
|
||||||
handleClose()
|
handleClose()
|
||||||
@@ -301,6 +307,9 @@ const handleCookieAuth = async (sessionKey: string) => {
|
|||||||
extra
|
extra
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Clear error status after successful re-authorization
|
||||||
|
await adminAPI.accounts.clearError(props.account.id)
|
||||||
|
|
||||||
appStore.showSuccess(t('admin.accounts.reAuthorizedSuccess'))
|
appStore.showSuccess(t('admin.accounts.reAuthorizedSuccess'))
|
||||||
emit('reauthorized')
|
emit('reauthorized')
|
||||||
handleClose()
|
handleClose()
|
||||||
|
|||||||
@@ -2,40 +2,32 @@
|
|||||||
<span
|
<span
|
||||||
:class="[
|
:class="[
|
||||||
'inline-flex items-center gap-1.5 px-2 py-0.5 rounded-md text-xs font-medium transition-colors',
|
'inline-flex items-center gap-1.5 px-2 py-0.5 rounded-md text-xs font-medium transition-colors',
|
||||||
isSubscription
|
badgeClass
|
||||||
? 'bg-violet-100 text-violet-700 dark:bg-violet-900/30 dark:text-violet-400'
|
|
||||||
: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400'
|
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
<!-- Subscription type icon (calendar) -->
|
<!-- Platform logo -->
|
||||||
<svg v-if="isSubscription" class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
|
<PlatformIcon v-if="platform" :platform="platform" size="sm" />
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5" />
|
<!-- Group name -->
|
||||||
</svg>
|
|
||||||
<!-- Standard type icon (wallet) -->
|
|
||||||
<svg v-else class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M21 12a2.25 2.25 0 00-2.25-2.25H15a3 3 0 11-6 0H5.25A2.25 2.25 0 003 12m18 0v6a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 18v-6m18 0V9M3 12V9m18 0a2.25 2.25 0 00-2.25-2.25H5.25A2.25 2.25 0 003 9m18 0V6a2.25 2.25 0 00-2.25-2.25H5.25A2.25 2.25 0 003 6v3" />
|
|
||||||
</svg>
|
|
||||||
<span class="truncate">{{ name }}</span>
|
<span class="truncate">{{ name }}</span>
|
||||||
|
<!-- Right side label: subscription shows "订阅", standard shows rate multiplier -->
|
||||||
<span
|
<span
|
||||||
v-if="showRate && rateMultiplier !== undefined"
|
v-if="showRate"
|
||||||
:class="[
|
:class="labelClass"
|
||||||
'px-1 py-0.5 rounded text-[10px] font-semibold',
|
|
||||||
isSubscription
|
|
||||||
? 'bg-violet-200/60 text-violet-800 dark:bg-violet-800/40 dark:text-violet-300'
|
|
||||||
: 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400'
|
|
||||||
]"
|
|
||||||
>
|
>
|
||||||
{{ rateMultiplier }}x
|
{{ labelText }}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import type { SubscriptionType } from '@/types'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import type { SubscriptionType, GroupPlatform } from '@/types'
|
||||||
|
import PlatformIcon from './PlatformIcon.vue'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
name: string
|
name: string
|
||||||
|
platform?: GroupPlatform
|
||||||
subscriptionType?: SubscriptionType
|
subscriptionType?: SubscriptionType
|
||||||
rateMultiplier?: number
|
rateMultiplier?: number
|
||||||
showRate?: boolean
|
showRate?: boolean
|
||||||
@@ -46,5 +38,50 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
showRate: true
|
showRate: true
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
const isSubscription = computed(() => props.subscriptionType === 'subscription')
|
const isSubscription = computed(() => props.subscriptionType === 'subscription')
|
||||||
|
|
||||||
|
// Label text: subscription shows localized text, standard shows rate
|
||||||
|
const labelText = computed(() => {
|
||||||
|
if (isSubscription.value) {
|
||||||
|
return t('groups.subscription')
|
||||||
|
}
|
||||||
|
return props.rateMultiplier !== undefined ? `${props.rateMultiplier}x` : ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// Label style based on type
|
||||||
|
const labelClass = computed(() => {
|
||||||
|
const base = 'px-1.5 py-0.5 rounded text-[10px] font-semibold'
|
||||||
|
if (isSubscription.value) {
|
||||||
|
// Subscription: more prominent style with border
|
||||||
|
if (props.platform === 'anthropic') {
|
||||||
|
return `${base} bg-orange-200/60 text-orange-800 dark:bg-orange-800/40 dark:text-orange-300`
|
||||||
|
} else if (props.platform === 'openai') {
|
||||||
|
return `${base} bg-emerald-200/60 text-emerald-800 dark:bg-emerald-800/40 dark:text-emerald-300`
|
||||||
|
}
|
||||||
|
return `${base} bg-violet-200/60 text-violet-800 dark:bg-violet-800/40 dark:text-violet-300`
|
||||||
|
}
|
||||||
|
// Standard: subtle background
|
||||||
|
return `${base} bg-black/10 dark:bg-white/10`
|
||||||
|
})
|
||||||
|
|
||||||
|
// Badge color based on platform and subscription type
|
||||||
|
const badgeClass = computed(() => {
|
||||||
|
if (props.platform === 'anthropic') {
|
||||||
|
// Claude: orange theme
|
||||||
|
return isSubscription.value
|
||||||
|
? 'bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400'
|
||||||
|
: 'bg-amber-50 text-amber-700 dark:bg-amber-900/20 dark:text-amber-400'
|
||||||
|
} else if (props.platform === 'openai') {
|
||||||
|
// OpenAI: green theme
|
||||||
|
return isSubscription.value
|
||||||
|
? 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400'
|
||||||
|
: 'bg-green-50 text-green-700 dark:bg-green-900/20 dark:text-green-400'
|
||||||
|
}
|
||||||
|
// Fallback: original colors
|
||||||
|
return isSubscription.value
|
||||||
|
? 'bg-violet-100 text-violet-700 dark:bg-violet-900/30 dark:text-violet-400'
|
||||||
|
: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400'
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
38
frontend/src/components/common/PlatformIcon.vue
Normal file
38
frontend/src/components/common/PlatformIcon.vue
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<template>
|
||||||
|
<!-- Claude/Anthropic logo -->
|
||||||
|
<svg v-if="platform === 'anthropic'" :class="sizeClass" viewBox="0 0 16 16" fill="currentColor">
|
||||||
|
<path d="m3.127 10.604 3.135-1.76.053-.153-.053-.085H6.11l-.525-.032-1.791-.048-1.554-.065-1.505-.08-.38-.081L0 7.832l.036-.234.32-.214.455.04 1.009.069 1.513.105 1.097.064 1.626.17h.259l.036-.105-.089-.065-.068-.064-1.566-1.062-1.695-1.121-.887-.646-.48-.327-.243-.306-.104-.67.435-.48.585.04.15.04.593.456 1.267.981 1.654 1.218.242.202.097-.068.012-.049-.109-.181-.9-1.626-.96-1.655-.428-.686-.113-.411a2 2 0 0 1-.068-.484l.496-.674L4.446 0l.662.089.279.242.411.94.666 1.48 1.033 2.014.302.597.162.553.06.17h.105v-.097l.085-1.134.157-1.392.154-1.792.052-.504.25-.605.497-.327.387.186.319.456-.045.294-.19 1.23-.37 1.93-.243 1.29h.142l.161-.16.654-.868 1.097-1.372.484-.545.565-.601.363-.287h.686l.505.751-.226.775-.707.895-.585.759-.839 1.13-.524.904.048.072.125-.012 1.897-.403 1.024-.186 1.223-.21.553.258.06.263-.218.536-1.307.323-1.533.307-2.284.54-.028.02.032.04 1.029.098.44.024h1.077l2.005.15.525.346.315.424-.053.323-.807.411-3.631-.863-.872-.218h-.12v.073l.726.71 1.331 1.202 1.667 1.55.084.383-.214.302-.226-.032-1.464-1.101-.565-.497-1.28-1.077h-.084v.113l.295.432 1.557 2.34.08.718-.112.234-.404.141-.444-.08-.911-1.28-.94-1.44-.759-1.291-.093.053-.448 4.821-.21.246-.484.186-.403-.307-.214-.496.214-.98.258-1.28.21-1.016.19-1.263.112-.42-.008-.028-.092.012-.953 1.307-1.448 1.957-1.146 1.227-.274.109-.477-.247.045-.44.266-.39 1.586-2.018.956-1.25.617-.723-.004-.105h-.036l-4.212 2.736-.75.096-.324-.302.04-.496.154-.162 1.267-.871z"/>
|
||||||
|
</svg>
|
||||||
|
<!-- OpenAI logo -->
|
||||||
|
<svg v-else-if="platform === 'openai'" :class="sizeClass" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path d="M22.282 9.821a5.985 5.985 0 0 0-.516-4.91 6.046 6.046 0 0 0-6.51-2.9A6.065 6.065 0 0 0 4.981 4.18a5.985 5.985 0 0 0-3.998 2.9 6.046 6.046 0 0 0 .743 7.097 5.98 5.98 0 0 0 .51 4.911 6.051 6.051 0 0 0 6.515 2.9A5.985 5.985 0 0 0 13.26 24a6.056 6.056 0 0 0 5.772-4.206 5.99 5.99 0 0 0 3.997-2.9 6.056 6.056 0 0 0-.747-7.073zM13.26 22.43a4.476 4.476 0 0 1-2.876-1.04l.141-.081 4.779-2.758a.795.795 0 0 0 .392-.681v-6.737l2.02 1.168a.071.071 0 0 1 .038.052v5.583a4.504 4.504 0 0 1-4.494 4.494zM3.6 18.304a4.47 4.47 0 0 1-.535-3.014l.142.085 4.783 2.759a.771.771 0 0 0 .78 0l5.843-3.369v2.332a.08.08 0 0 1-.033.062L9.74 19.95a4.5 4.5 0 0 1-6.14-1.646zM2.34 7.896a4.485 4.485 0 0 1 2.366-1.973V11.6a.766.766 0 0 0 .388.676l5.815 3.355-2.02 1.168a.076.076 0 0 1-.071 0l-4.83-2.786A4.504 4.504 0 0 1 2.34 7.872zm16.597 3.855l-5.833-3.387L15.119 7.2a.076.076 0 0 1 .071 0l4.83 2.791a4.494 4.494 0 0 1-.676 8.105v-5.678a.79.79 0 0 0-.407-.667zm2.01-3.023l-.141-.085-4.774-2.782a.776.776 0 0 0-.785 0L9.409 9.23V6.897a.066.066 0 0 1 .028-.061l4.83-2.787a4.5 4.5 0 0 1 6.68 4.66zm-12.64 4.135l-2.02-1.164a.08.08 0 0 1-.038-.057V6.075a4.5 4.5 0 0 1 7.375-3.453l-.142.08L8.704 5.46a.795.795 0 0 0-.393.681zm1.097-2.365l2.602-1.5 2.607 1.5v2.999l-2.597 1.5-2.607-1.5z"/>
|
||||||
|
</svg>
|
||||||
|
<!-- Fallback: generic platform icon -->
|
||||||
|
<svg v-else :class="sizeClass" fill="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import type { GroupPlatform } from '@/types'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
platform?: GroupPlatform
|
||||||
|
size?: 'xs' | 'sm' | 'md' | 'lg'
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
size: 'sm'
|
||||||
|
})
|
||||||
|
|
||||||
|
const sizeClass = computed(() => {
|
||||||
|
const sizes = {
|
||||||
|
xs: 'w-3 h-3',
|
||||||
|
sm: 'w-3.5 h-3.5',
|
||||||
|
md: 'w-4 h-4',
|
||||||
|
lg: 'w-5 h-5'
|
||||||
|
}
|
||||||
|
return sizes[props.size] + ' flex-shrink-0'
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -182,6 +182,11 @@ export default {
|
|||||||
addBalanceWithCode: 'Add balance with a code',
|
addBalanceWithCode: 'Add balance with a code',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Groups (shared)
|
||||||
|
groups: {
|
||||||
|
subscription: 'Sub',
|
||||||
|
},
|
||||||
|
|
||||||
// API Keys
|
// API Keys
|
||||||
keys: {
|
keys: {
|
||||||
title: 'API Keys',
|
title: 'API Keys',
|
||||||
@@ -515,6 +520,7 @@ export default {
|
|||||||
accounts: 'Accounts',
|
accounts: 'Accounts',
|
||||||
status: 'Status',
|
status: 'Status',
|
||||||
actions: 'Actions',
|
actions: 'Actions',
|
||||||
|
billingType: 'Billing Type',
|
||||||
},
|
},
|
||||||
accountsCount: '{count} accounts',
|
accountsCount: '{count} accounts',
|
||||||
form: {
|
form: {
|
||||||
@@ -527,12 +533,16 @@ export default {
|
|||||||
enterGroupName: 'Enter group name',
|
enterGroupName: 'Enter group name',
|
||||||
optionalDescription: 'Optional description',
|
optionalDescription: 'Optional description',
|
||||||
platformHint: 'Select the platform this group is associated with',
|
platformHint: 'Select the platform this group is associated with',
|
||||||
|
platformNotEditable: 'Platform cannot be changed after creation',
|
||||||
rateMultiplierHint: 'Cost multiplier for this group (e.g., 1.5 = 150% of base cost)',
|
rateMultiplierHint: 'Cost multiplier for this group (e.g., 1.5 = 150% of base cost)',
|
||||||
exclusiveHint: 'Exclusive (requires explicit user access)',
|
exclusiveHint: 'Exclusive (requires explicit user access)',
|
||||||
noGroupsYet: 'No groups yet',
|
noGroupsYet: 'No groups yet',
|
||||||
createFirstGroup: 'Create your first group to organize API keys.',
|
createFirstGroup: 'Create your first group to organize API keys.',
|
||||||
creating: 'Creating...',
|
creating: 'Creating...',
|
||||||
updating: 'Updating...',
|
updating: 'Updating...',
|
||||||
|
limitDay: 'd',
|
||||||
|
limitWeek: 'w',
|
||||||
|
limitMonth: 'mo',
|
||||||
groupCreated: 'Group created successfully',
|
groupCreated: 'Group created successfully',
|
||||||
groupUpdated: 'Group updated successfully',
|
groupUpdated: 'Group updated successfully',
|
||||||
groupDeleted: 'Group deleted successfully',
|
groupDeleted: 'Group deleted successfully',
|
||||||
@@ -661,6 +671,9 @@ export default {
|
|||||||
tokenRefreshed: 'Token refreshed successfully',
|
tokenRefreshed: 'Token refreshed successfully',
|
||||||
accountDeleted: 'Account deleted successfully',
|
accountDeleted: 'Account deleted successfully',
|
||||||
rateLimitCleared: 'Rate limit cleared successfully',
|
rateLimitCleared: 'Rate limit cleared successfully',
|
||||||
|
resetStatus: 'Reset Status',
|
||||||
|
statusReset: 'Account status reset successfully',
|
||||||
|
failedToResetStatus: 'Failed to reset account status',
|
||||||
failedToLoad: 'Failed to load accounts',
|
failedToLoad: 'Failed to load accounts',
|
||||||
failedToRefresh: 'Failed to refresh token',
|
failedToRefresh: 'Failed to refresh token',
|
||||||
failedToDelete: 'Failed to delete account',
|
failedToDelete: 'Failed to delete account',
|
||||||
|
|||||||
@@ -182,6 +182,11 @@ export default {
|
|||||||
addBalanceWithCode: '使用兑换码充值',
|
addBalanceWithCode: '使用兑换码充值',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Groups (shared)
|
||||||
|
groups: {
|
||||||
|
subscription: '订阅',
|
||||||
|
},
|
||||||
|
|
||||||
// API Keys
|
// API Keys
|
||||||
keys: {
|
keys: {
|
||||||
title: 'API 密钥',
|
title: 'API 密钥',
|
||||||
@@ -530,13 +535,16 @@ export default {
|
|||||||
deleteConfirmSubscription: "确定要删除订阅分组 '{name}' 吗?此操作会让所有绑定此订阅的用户的 API Key 失效,并删除所有相关的订阅记录。此操作无法撤销。",
|
deleteConfirmSubscription: "确定要删除订阅分组 '{name}' 吗?此操作会让所有绑定此订阅的用户的 API Key 失效,并删除所有相关的订阅记录。此操作无法撤销。",
|
||||||
columns: {
|
columns: {
|
||||||
name: '名称',
|
name: '名称',
|
||||||
|
platform: '平台',
|
||||||
rateMultiplier: '费率倍数',
|
rateMultiplier: '费率倍数',
|
||||||
exclusive: '独占',
|
exclusive: '独占',
|
||||||
platforms: '平台',
|
type: '类型',
|
||||||
priority: '优先级',
|
priority: '优先级',
|
||||||
apiKeys: 'API 密钥数',
|
apiKeys: 'API 密钥数',
|
||||||
|
accounts: '账号数',
|
||||||
status: '状态',
|
status: '状态',
|
||||||
actions: '操作',
|
actions: '操作',
|
||||||
|
billingType: '计费类型',
|
||||||
},
|
},
|
||||||
form: {
|
form: {
|
||||||
name: '名称',
|
name: '名称',
|
||||||
@@ -588,10 +596,14 @@ export default {
|
|||||||
enterGroupName: '请输入分组名称',
|
enterGroupName: '请输入分组名称',
|
||||||
optionalDescription: '可选描述',
|
optionalDescription: '可选描述',
|
||||||
platformHint: '选择此分组关联的平台',
|
platformHint: '选择此分组关联的平台',
|
||||||
|
platformNotEditable: '创建后不可更改平台',
|
||||||
noGroupsYet: '暂无分组',
|
noGroupsYet: '暂无分组',
|
||||||
createFirstGroup: '创建您的第一个分组来组织 API 密钥。',
|
createFirstGroup: '创建您的第一个分组来组织 API 密钥。',
|
||||||
creating: '创建中...',
|
creating: '创建中...',
|
||||||
updating: '更新中...',
|
updating: '更新中...',
|
||||||
|
limitDay: '日',
|
||||||
|
limitWeek: '周',
|
||||||
|
limitMonth: '月',
|
||||||
groupCreated: '分组创建成功',
|
groupCreated: '分组创建成功',
|
||||||
groupUpdated: '分组更新成功',
|
groupUpdated: '分组更新成功',
|
||||||
groupDeleted: '分组删除成功',
|
groupDeleted: '分组删除成功',
|
||||||
@@ -749,6 +761,9 @@ export default {
|
|||||||
accountCreatedSuccess: '账号添加成功',
|
accountCreatedSuccess: '账号添加成功',
|
||||||
accountUpdatedSuccess: '账号更新成功',
|
accountUpdatedSuccess: '账号更新成功',
|
||||||
accountDeletedSuccess: '账号删除成功',
|
accountDeletedSuccess: '账号删除成功',
|
||||||
|
resetStatus: '重置状态',
|
||||||
|
statusReset: '账号状态已重置',
|
||||||
|
failedToResetStatus: '重置账号状态失败',
|
||||||
cookieRefreshedSuccess: 'Cookie 刷新成功',
|
cookieRefreshedSuccess: 'Cookie 刷新成功',
|
||||||
testSuccess: '账号测试通过',
|
testSuccess: '账号测试通过',
|
||||||
testFailed: '账号测试失败',
|
testFailed: '账号测试失败',
|
||||||
|
|||||||
@@ -223,13 +223,13 @@
|
|||||||
<span class="text-sm font-medium text-gray-700 dark:text-dark-200">Claude</span>
|
<span class="text-sm font-medium text-gray-700 dark:text-dark-200">Claude</span>
|
||||||
<span class="px-1.5 py-0.5 text-[10px] font-medium bg-primary-100 dark:bg-primary-900/30 text-primary-600 dark:text-primary-400 rounded">{{ t('home.providers.supported') }}</span>
|
<span class="px-1.5 py-0.5 text-[10px] font-medium bg-primary-100 dark:bg-primary-900/30 text-primary-600 dark:text-primary-400 rounded">{{ t('home.providers.supported') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- GPT - Coming Soon -->
|
<!-- GPT - Supported -->
|
||||||
<div class="flex items-center gap-2 px-5 py-3 rounded-xl bg-white/40 dark:bg-dark-800/40 backdrop-blur-sm border border-gray-200/50 dark:border-dark-700/50 opacity-60">
|
<div class="flex items-center gap-2 px-5 py-3 rounded-xl bg-white/60 dark:bg-dark-800/60 backdrop-blur-sm border border-primary-200 dark:border-primary-800 ring-1 ring-primary-500/20">
|
||||||
<div class="w-8 h-8 rounded-lg bg-gradient-to-br from-green-500 to-green-600 flex items-center justify-center">
|
<div class="w-8 h-8 rounded-lg bg-gradient-to-br from-green-500 to-green-600 flex items-center justify-center">
|
||||||
<span class="text-white text-xs font-bold">G</span>
|
<span class="text-white text-xs font-bold">G</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="text-sm font-medium text-gray-700 dark:text-dark-200">GPT</span>
|
<span class="text-sm font-medium text-gray-700 dark:text-dark-200">GPT</span>
|
||||||
<span class="px-1.5 py-0.5 text-[10px] font-medium bg-gray-100 dark:bg-dark-700 text-gray-500 dark:text-dark-400 rounded">{{ t('home.providers.soon') }}</span>
|
<span class="px-1.5 py-0.5 text-[10px] font-medium bg-primary-100 dark:bg-primary-900/30 text-primary-600 dark:text-primary-400 rounded">{{ t('home.providers.supported') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- Gemini - Coming Soon -->
|
<!-- Gemini - Coming Soon -->
|
||||||
<div class="flex items-center gap-2 px-5 py-3 rounded-xl bg-white/40 dark:bg-dark-800/40 backdrop-blur-sm border border-gray-200/50 dark:border-dark-700/50 opacity-60">
|
<div class="flex items-center gap-2 px-5 py-3 rounded-xl bg-white/40 dark:bg-dark-800/40 backdrop-blur-sm border border-gray-200/50 dark:border-dark-700/50 opacity-60">
|
||||||
|
|||||||
@@ -139,6 +139,17 @@
|
|||||||
|
|
||||||
<template #cell-actions="{ row }">
|
<template #cell-actions="{ row }">
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
|
<!-- Reset Status button for error accounts -->
|
||||||
|
<button
|
||||||
|
v-if="row.status === 'error'"
|
||||||
|
@click="handleResetStatus(row)"
|
||||||
|
class="p-2 rounded-lg hover:bg-red-50 dark:hover:bg-red-900/20 text-red-500 hover:text-red-600 dark:hover:text-red-400 transition-colors"
|
||||||
|
:title="t('admin.accounts.resetStatus')"
|
||||||
|
>
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M9 15L3 9m0 0l6-6M3 9h12a6 6 0 010 12h-3" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
<!-- Clear Rate Limit button -->
|
<!-- Clear Rate Limit button -->
|
||||||
<button
|
<button
|
||||||
v-if="isRateLimited(row) || isOverloaded(row)"
|
v-if="isRateLimited(row) || isOverloaded(row)"
|
||||||
@@ -496,6 +507,23 @@ const handleClearRateLimit = async (account: Account) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset account status (clear error and rate limit)
|
||||||
|
const handleResetStatus = async (account: Account) => {
|
||||||
|
try {
|
||||||
|
// Clear error status
|
||||||
|
await adminAPI.accounts.clearError(account.id)
|
||||||
|
// Also clear rate limit if exists
|
||||||
|
if (isRateLimited(account) || isOverloaded(account)) {
|
||||||
|
await adminAPI.accounts.clearRateLimit(account.id)
|
||||||
|
}
|
||||||
|
appStore.showSuccess(t('admin.accounts.statusReset'))
|
||||||
|
loadAccounts()
|
||||||
|
} catch (error: any) {
|
||||||
|
appStore.showError(error.response?.data?.detail || t('admin.accounts.failedToResetStatus'))
|
||||||
|
console.error('Error resetting account status:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Toggle schedulable
|
// Toggle schedulable
|
||||||
const handleToggleSchedulable = async (account: Account) => {
|
const handleToggleSchedulable = async (account: Account) => {
|
||||||
togglingSchedulable.value = account.id
|
togglingSchedulable.value = account.id
|
||||||
|
|||||||
@@ -60,14 +60,46 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #cell-platform="{ value }">
|
<template #cell-platform="{ value }">
|
||||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-400">
|
<span
|
||||||
<svg class="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 24 24">
|
:class="[
|
||||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/>
|
'inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-xs font-medium',
|
||||||
</svg>
|
value === 'anthropic'
|
||||||
{{ value.charAt(0).toUpperCase() + value.slice(1) }}
|
? 'bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400'
|
||||||
|
: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400'
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<PlatformIcon :platform="value" size="xs" />
|
||||||
|
{{ value === 'anthropic' ? 'Anthropic' : 'OpenAI' }}
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template #cell-billing_type="{ row }">
|
||||||
|
<div class="space-y-1">
|
||||||
|
<!-- Type Badge -->
|
||||||
|
<span
|
||||||
|
:class="[
|
||||||
|
'inline-block px-2 py-0.5 rounded-full text-xs font-medium',
|
||||||
|
row.subscription_type === 'subscription'
|
||||||
|
? 'bg-violet-100 text-violet-700 dark:bg-violet-900/30 dark:text-violet-400'
|
||||||
|
: 'bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300'
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
{{ row.subscription_type === 'subscription' ? t('admin.groups.subscription.subscription') : t('admin.groups.subscription.standard') }}
|
||||||
|
</span>
|
||||||
|
<!-- Subscription Limits - compact single line -->
|
||||||
|
<div v-if="row.subscription_type === 'subscription'" class="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
<template v-if="row.daily_limit_usd || row.weekly_limit_usd || row.monthly_limit_usd">
|
||||||
|
<span v-if="row.daily_limit_usd">${{ row.daily_limit_usd }}/{{ t('admin.groups.limitDay') }}</span>
|
||||||
|
<span v-if="row.daily_limit_usd && (row.weekly_limit_usd || row.monthly_limit_usd)" class="mx-1 text-gray-300 dark:text-gray-600">·</span>
|
||||||
|
<span v-if="row.weekly_limit_usd">${{ row.weekly_limit_usd }}/{{ t('admin.groups.limitWeek') }}</span>
|
||||||
|
<span v-if="row.weekly_limit_usd && row.monthly_limit_usd" class="mx-1 text-gray-300 dark:text-gray-600">·</span>
|
||||||
|
<span v-if="row.monthly_limit_usd">${{ row.monthly_limit_usd }}/{{ t('admin.groups.limitMonth') }}</span>
|
||||||
|
</template>
|
||||||
|
<span v-else class="text-gray-400 dark:text-gray-500">{{ t('admin.groups.subscription.noLimit') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<template #cell-rate_multiplier="{ value }">
|
<template #cell-rate_multiplier="{ value }">
|
||||||
<span class="text-sm text-gray-700 dark:text-gray-300">{{ value }}x</span>
|
<span class="text-sm text-gray-700 dark:text-gray-300">{{ value }}x</span>
|
||||||
</template>
|
</template>
|
||||||
@@ -186,8 +218,8 @@
|
|||||||
<input
|
<input
|
||||||
v-model.number="createForm.rate_multiplier"
|
v-model.number="createForm.rate_multiplier"
|
||||||
type="number"
|
type="number"
|
||||||
step="0.1"
|
step="0.001"
|
||||||
min="0.1"
|
min="0.001"
|
||||||
required
|
required
|
||||||
class="input"
|
class="input"
|
||||||
/>
|
/>
|
||||||
@@ -323,15 +355,17 @@
|
|||||||
<Select
|
<Select
|
||||||
v-model="editForm.platform"
|
v-model="editForm.platform"
|
||||||
:options="platformOptions"
|
:options="platformOptions"
|
||||||
|
:disabled="true"
|
||||||
/>
|
/>
|
||||||
|
<p class="input-hint">{{ t('admin.groups.platformNotEditable') }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="editForm.subscription_type !== 'subscription'">
|
<div v-if="editForm.subscription_type !== 'subscription'">
|
||||||
<label class="input-label">{{ t('admin.groups.form.rateMultiplier') }}</label>
|
<label class="input-label">{{ t('admin.groups.form.rateMultiplier') }}</label>
|
||||||
<input
|
<input
|
||||||
v-model.number="editForm.rate_multiplier"
|
v-model.number="editForm.rate_multiplier"
|
||||||
type="number"
|
type="number"
|
||||||
step="0.1"
|
step="0.001"
|
||||||
min="0.1"
|
min="0.001"
|
||||||
required
|
required
|
||||||
class="input"
|
class="input"
|
||||||
/>
|
/>
|
||||||
@@ -472,6 +506,7 @@ import Modal from '@/components/common/Modal.vue'
|
|||||||
import ConfirmDialog from '@/components/common/ConfirmDialog.vue'
|
import ConfirmDialog from '@/components/common/ConfirmDialog.vue'
|
||||||
import EmptyState from '@/components/common/EmptyState.vue'
|
import EmptyState from '@/components/common/EmptyState.vue'
|
||||||
import Select from '@/components/common/Select.vue'
|
import Select from '@/components/common/Select.vue'
|
||||||
|
import PlatformIcon from '@/components/common/PlatformIcon.vue'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
@@ -479,6 +514,7 @@ const appStore = useAppStore()
|
|||||||
const columns = computed<Column[]>(() => [
|
const columns = computed<Column[]>(() => [
|
||||||
{ key: 'name', label: t('admin.groups.columns.name'), sortable: true },
|
{ key: 'name', label: t('admin.groups.columns.name'), sortable: true },
|
||||||
{ key: 'platform', label: t('admin.groups.columns.platform'), sortable: true },
|
{ key: 'platform', label: t('admin.groups.columns.platform'), sortable: true },
|
||||||
|
{ key: 'billing_type', label: t('admin.groups.columns.billingType'), sortable: true },
|
||||||
{ key: 'rate_multiplier', label: t('admin.groups.columns.rateMultiplier'), sortable: true },
|
{ key: 'rate_multiplier', label: t('admin.groups.columns.rateMultiplier'), sortable: true },
|
||||||
{ key: 'is_exclusive', label: t('admin.groups.columns.type'), sortable: true },
|
{ key: 'is_exclusive', label: t('admin.groups.columns.type'), sortable: true },
|
||||||
{ key: 'account_count', label: t('admin.groups.columns.accounts'), sortable: true },
|
{ key: 'account_count', label: t('admin.groups.columns.accounts'), sortable: true },
|
||||||
|
|||||||
@@ -71,6 +71,7 @@
|
|||||||
<GroupBadge
|
<GroupBadge
|
||||||
v-if="row.group"
|
v-if="row.group"
|
||||||
:name="row.group.name"
|
:name="row.group.name"
|
||||||
|
:platform="row.group.platform"
|
||||||
:subscription-type="row.group.subscription_type"
|
:subscription-type="row.group.subscription_type"
|
||||||
:rate-multiplier="row.group.rate_multiplier"
|
:rate-multiplier="row.group.rate_multiplier"
|
||||||
/>
|
/>
|
||||||
@@ -231,6 +232,7 @@
|
|||||||
<GroupBadge
|
<GroupBadge
|
||||||
v-if="option"
|
v-if="option"
|
||||||
:name="(option as unknown as GroupOption).label"
|
:name="(option as unknown as GroupOption).label"
|
||||||
|
:platform="(option as unknown as GroupOption).platform"
|
||||||
:subscription-type="(option as unknown as GroupOption).subscriptionType"
|
:subscription-type="(option as unknown as GroupOption).subscriptionType"
|
||||||
:rate-multiplier="(option as unknown as GroupOption).rate"
|
:rate-multiplier="(option as unknown as GroupOption).rate"
|
||||||
/>
|
/>
|
||||||
@@ -239,6 +241,7 @@
|
|||||||
<template #option="{ option }">
|
<template #option="{ option }">
|
||||||
<GroupBadge
|
<GroupBadge
|
||||||
:name="(option as unknown as GroupOption).label"
|
:name="(option as unknown as GroupOption).label"
|
||||||
|
:platform="(option as unknown as GroupOption).platform"
|
||||||
:subscription-type="(option as unknown as GroupOption).subscriptionType"
|
:subscription-type="(option as unknown as GroupOption).subscriptionType"
|
||||||
:rate-multiplier="(option as unknown as GroupOption).rate"
|
:rate-multiplier="(option as unknown as GroupOption).rate"
|
||||||
/>
|
/>
|
||||||
@@ -358,6 +361,7 @@
|
|||||||
>
|
>
|
||||||
<GroupBadge
|
<GroupBadge
|
||||||
:name="option.label"
|
:name="option.label"
|
||||||
|
:platform="option.platform"
|
||||||
:subscription-type="option.subscriptionType"
|
:subscription-type="option.subscriptionType"
|
||||||
:rate-multiplier="option.rate"
|
:rate-multiplier="option.rate"
|
||||||
/>
|
/>
|
||||||
@@ -394,7 +398,7 @@ import EmptyState from '@/components/common/EmptyState.vue'
|
|||||||
import Select from '@/components/common/Select.vue'
|
import Select from '@/components/common/Select.vue'
|
||||||
import UseKeyModal from '@/components/keys/UseKeyModal.vue'
|
import UseKeyModal from '@/components/keys/UseKeyModal.vue'
|
||||||
import GroupBadge from '@/components/common/GroupBadge.vue'
|
import GroupBadge from '@/components/common/GroupBadge.vue'
|
||||||
import type { ApiKey, Group, PublicSettings, SubscriptionType } from '@/types'
|
import type { ApiKey, Group, PublicSettings, SubscriptionType, GroupPlatform } from '@/types'
|
||||||
import type { Column } from '@/components/common/types'
|
import type { Column } from '@/components/common/types'
|
||||||
import type { BatchApiKeyUsageStats } from '@/api/usage'
|
import type { BatchApiKeyUsageStats } from '@/api/usage'
|
||||||
|
|
||||||
@@ -403,6 +407,7 @@ interface GroupOption {
|
|||||||
label: string
|
label: string
|
||||||
rate: number
|
rate: number
|
||||||
subscriptionType: SubscriptionType
|
subscriptionType: SubscriptionType
|
||||||
|
platform: GroupPlatform
|
||||||
}
|
}
|
||||||
|
|
||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
@@ -491,7 +496,8 @@ const groupOptions = computed(() =>
|
|||||||
value: group.id,
|
value: group.id,
|
||||||
label: group.name,
|
label: group.name,
|
||||||
rate: group.rate_multiplier,
|
rate: group.rate_multiplier,
|
||||||
subscriptionType: group.subscription_type
|
subscriptionType: group.subscription_type,
|
||||||
|
platform: group.platform
|
||||||
}))
|
}))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user