feat: 平台图标与计费修复

- fix(billing): 修复 OpenAI 兼容 API 缓存 token 重复计费问题
- fix(auth): 隐藏数据库错误详情,返回通用服务不可用错误
- feat(ui): 新增 PlatformIcon 组件,GroupBadge 支持平台颜色区分
- feat(ui): 账号管理新增重置状态按钮,重授权后自动清除错误
- feat(ui): 分组管理新增计费类型列,显示订阅限额信息
- ui: 首页 GPT 状态改为已支持
This commit is contained in:
shaw
2025-12-23 10:01:58 +08:00
parent 6c469b42ed
commit fda753278c
11 changed files with 240 additions and 44 deletions

View File

@@ -223,13 +223,13 @@
<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>
</div>
<!-- GPT - 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">
<!-- GPT - Supported -->
<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">
<span class="text-white text-xs font-bold">G</span>
</div>
<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>
<!-- 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">

View File

@@ -139,6 +139,17 @@
<template #cell-actions="{ row }">
<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 -->
<button
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
const handleToggleSchedulable = async (account: Account) => {
togglingSchedulable.value = account.id

View File

@@ -60,14 +60,46 @@
</template>
<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">
<svg class="w-3 h-3 mr-1" 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>
{{ value.charAt(0).toUpperCase() + value.slice(1) }}
<span
:class="[
'inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-xs font-medium',
value === 'anthropic'
? '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>
</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 }">
<span class="text-sm text-gray-700 dark:text-gray-300">{{ value }}x</span>
</template>
@@ -186,8 +218,8 @@
<input
v-model.number="createForm.rate_multiplier"
type="number"
step="0.1"
min="0.1"
step="0.001"
min="0.001"
required
class="input"
/>
@@ -323,15 +355,17 @@
<Select
v-model="editForm.platform"
:options="platformOptions"
:disabled="true"
/>
<p class="input-hint">{{ t('admin.groups.platformNotEditable') }}</p>
</div>
<div v-if="editForm.subscription_type !== 'subscription'">
<label class="input-label">{{ t('admin.groups.form.rateMultiplier') }}</label>
<input
v-model.number="editForm.rate_multiplier"
type="number"
step="0.1"
min="0.1"
step="0.001"
min="0.001"
required
class="input"
/>
@@ -472,6 +506,7 @@ import Modal from '@/components/common/Modal.vue'
import ConfirmDialog from '@/components/common/ConfirmDialog.vue'
import EmptyState from '@/components/common/EmptyState.vue'
import Select from '@/components/common/Select.vue'
import PlatformIcon from '@/components/common/PlatformIcon.vue'
const { t } = useI18n()
const appStore = useAppStore()
@@ -479,6 +514,7 @@ const appStore = useAppStore()
const columns = computed<Column[]>(() => [
{ key: 'name', label: t('admin.groups.columns.name'), 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: 'is_exclusive', label: t('admin.groups.columns.type'), sortable: true },
{ key: 'account_count', label: t('admin.groups.columns.accounts'), sortable: true },

View File

@@ -71,6 +71,7 @@
<GroupBadge
v-if="row.group"
:name="row.group.name"
:platform="row.group.platform"
:subscription-type="row.group.subscription_type"
:rate-multiplier="row.group.rate_multiplier"
/>
@@ -231,6 +232,7 @@
<GroupBadge
v-if="option"
:name="(option as unknown as GroupOption).label"
:platform="(option as unknown as GroupOption).platform"
:subscription-type="(option as unknown as GroupOption).subscriptionType"
:rate-multiplier="(option as unknown as GroupOption).rate"
/>
@@ -239,6 +241,7 @@
<template #option="{ option }">
<GroupBadge
:name="(option as unknown as GroupOption).label"
:platform="(option as unknown as GroupOption).platform"
:subscription-type="(option as unknown as GroupOption).subscriptionType"
:rate-multiplier="(option as unknown as GroupOption).rate"
/>
@@ -358,6 +361,7 @@
>
<GroupBadge
:name="option.label"
:platform="option.platform"
:subscription-type="option.subscriptionType"
:rate-multiplier="option.rate"
/>
@@ -394,7 +398,7 @@ import EmptyState from '@/components/common/EmptyState.vue'
import Select from '@/components/common/Select.vue'
import UseKeyModal from '@/components/keys/UseKeyModal.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 { BatchApiKeyUsageStats } from '@/api/usage'
@@ -403,6 +407,7 @@ interface GroupOption {
label: string
rate: number
subscriptionType: SubscriptionType
platform: GroupPlatform
}
const appStore = useAppStore()
@@ -491,7 +496,8 @@ const groupOptions = computed(() =>
value: group.id,
label: group.name,
rate: group.rate_multiplier,
subscriptionType: group.subscription_type
subscriptionType: group.subscription_type,
platform: group.platform
}))
)