mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-09 02:47:45 +08:00
refactor(frontend): extract provider quota display components
This commit is contained in:
@@ -46,46 +46,12 @@
|
||||
|
||||
<div class="space-y-6 p-4 sm:p-6">
|
||||
<!-- 配额使用情况 -->
|
||||
<Card
|
||||
<ProviderMonthlyQuotaCard
|
||||
v-if="provider.billing_type === 'monthly_quota' && provider.monthly_quota_usd"
|
||||
class="p-4"
|
||||
>
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-sm font-semibold">
|
||||
{{ legacyT('订阅配额') }}
|
||||
</h3>
|
||||
<Badge
|
||||
variant="secondary"
|
||||
class="text-xs"
|
||||
>
|
||||
{{ ((provider.monthly_used_usd || 0) / provider.monthly_quota_usd * 100).toFixed(1) }}%
|
||||
</Badge>
|
||||
</div>
|
||||
<div class="relative w-full h-2 bg-border rounded-full overflow-hidden">
|
||||
<div
|
||||
class="absolute left-0 top-0 h-full transition-all duration-300"
|
||||
:class="{
|
||||
'bg-green-500': (provider.monthly_used_usd || 0) / provider.monthly_quota_usd < 0.7,
|
||||
'bg-yellow-500': (provider.monthly_used_usd || 0) / provider.monthly_quota_usd >= 0.7 && (provider.monthly_used_usd || 0) / provider.monthly_quota_usd < 0.9,
|
||||
'bg-red-500': (provider.monthly_used_usd || 0) / provider.monthly_quota_usd >= 0.9
|
||||
}"
|
||||
:style="{ width: `${Math.min((provider.monthly_used_usd || 0) / provider.monthly_quota_usd * 100, 100)}%` }"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center justify-between text-xs">
|
||||
<span class="font-semibold">
|
||||
${{ (provider.monthly_used_usd || 0).toFixed(2) }} / ${{ provider.monthly_quota_usd.toFixed(2) }}
|
||||
</span>
|
||||
<span
|
||||
v-if="provider.quota_reset_day"
|
||||
class="text-muted-foreground"
|
||||
>
|
||||
{{ legacyT('每月') }} {{ provider.quota_reset_day }} {{ legacyT('号重置') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
:used="provider.monthly_used_usd"
|
||||
:quota="provider.monthly_quota_usd"
|
||||
:reset-day="provider.quota_reset_day"
|
||||
/>
|
||||
|
||||
<!-- 密钥管理 -->
|
||||
<Card class="overflow-hidden">
|
||||
@@ -420,97 +386,60 @@
|
||||
v-if="hasCodexQuotaDisplayData(key)"
|
||||
class="mt-2 p-2 bg-muted/30 rounded-md"
|
||||
>
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<span class="text-[10px] text-muted-foreground">{{ legacyT('账号配额') }}</span>
|
||||
<div class="flex items-center gap-1">
|
||||
<RefreshCw
|
||||
v-if="refreshingQuota"
|
||||
class="w-3 h-3 text-muted-foreground/70 animate-spin"
|
||||
/>
|
||||
<span
|
||||
v-if="getCodexQuotaDisplay(key)?.updated_at"
|
||||
class="text-[9px] text-muted-foreground/70"
|
||||
>
|
||||
{{ formatCodexUpdatedAt(getCodexQuotaDisplay(key)?.updated_at || 0) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<ProviderQuotaSectionHeader
|
||||
:title="legacyT('账号配额')"
|
||||
:loading="refreshingQuota"
|
||||
:updated-text="getCodexQuotaDisplay(key)?.updated_at ? formatCodexUpdatedAt(getCodexQuotaDisplay(key)?.updated_at || 0) : null"
|
||||
/>
|
||||
<!-- 普通 Codex 限额并排显示:Team/Plus/Enterprise 账号 2列, Free 账号 1列 -->
|
||||
<div
|
||||
class="grid gap-3"
|
||||
:class="isCodexTeamPlan(key) ? 'grid-cols-2' : 'grid-cols-1'"
|
||||
>
|
||||
<!-- 周限额 -->
|
||||
<div v-if="getCodexQuotaDisplay(key)?.primary_used_percent !== undefined">
|
||||
<div class="flex items-center justify-between text-[10px] mb-0.5">
|
||||
<span class="text-muted-foreground">{{ legacyT('周限额') }}</span>
|
||||
<span :class="getQuotaRemainingClass(getCodexQuotaDisplay(key)?.primary_used_percent || 0)">
|
||||
{{ (100 - (getCodexQuotaDisplay(key)?.primary_used_percent || 0)).toFixed(1) }}%
|
||||
</span>
|
||||
</div>
|
||||
<div class="relative w-full h-1.5 bg-border rounded-full overflow-hidden">
|
||||
<div
|
||||
class="absolute left-0 top-0 h-full transition-all duration-300"
|
||||
:class="getQuotaRemainingBarColor(getCodexQuotaDisplay(key)?.primary_used_percent || 0)"
|
||||
:style="{ width: `${Math.max(100 - (getCodexQuotaDisplay(key)?.primary_used_percent || 0), 0)}%` }"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="(getCodexQuotaDisplay(key)?.primary_reset_at || getCodexQuotaDisplay(key)?.primary_reset_seconds) && shouldStartCodexResetCountdown(getCodexQuotaDisplay(key)?.primary_used_percent || 0)"
|
||||
class="text-[9px] mt-0.5 tabular-nums"
|
||||
:class="getResetCountdownClass(
|
||||
<ProviderQuotaProgressRow
|
||||
v-if="getCodexQuotaDisplay(key)?.primary_used_percent !== undefined"
|
||||
:label="legacyT('周限额')"
|
||||
:used-percent="getCodexQuotaDisplay(key)?.primary_used_percent || 0"
|
||||
:meter-class="getQuotaRemainingClass(getCodexQuotaDisplay(key)?.primary_used_percent || 0)"
|
||||
:bar-class="getQuotaRemainingBarColor(getCodexQuotaDisplay(key)?.primary_used_percent || 0)"
|
||||
:reset-text="(getCodexQuotaDisplay(key)?.primary_reset_at || getCodexQuotaDisplay(key)?.primary_reset_seconds) && shouldStartCodexResetCountdown(getCodexQuotaDisplay(key)?.primary_used_percent || 0)
|
||||
? getResetCountdownText(
|
||||
getCodexQuotaDisplay(key)?.primary_reset_at,
|
||||
getCodexQuotaDisplay(key)?.primary_reset_seconds,
|
||||
getCodexQuotaDisplay(key)?.updated_at,
|
||||
getCodexQuotaDisplay(key)?.primary_used_percent
|
||||
)"
|
||||
>
|
||||
{{ getResetCountdownText(
|
||||
getCodexQuotaDisplay(key)?.primary_reset_at,
|
||||
getCodexQuotaDisplay(key)?.primary_reset_seconds,
|
||||
getCodexQuotaDisplay(key)?.updated_at,
|
||||
getCodexQuotaDisplay(key)?.primary_used_percent
|
||||
) }}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
: null"
|
||||
:footer-class="getResetCountdownClass(
|
||||
getCodexQuotaDisplay(key)?.primary_reset_at,
|
||||
getCodexQuotaDisplay(key)?.primary_reset_seconds,
|
||||
getCodexQuotaDisplay(key)?.updated_at,
|
||||
getCodexQuotaDisplay(key)?.primary_used_percent
|
||||
)"
|
||||
/>
|
||||
<!-- 5H限额(仅 Team/Plus/Enterprise 显示) -->
|
||||
<div v-if="isCodexTeamPlan(key) && getCodexQuotaDisplay(key)?.secondary_used_percent !== undefined">
|
||||
<div class="flex items-center justify-between text-[10px] mb-0.5">
|
||||
<span class="text-muted-foreground">{{ legacyT('5H限额') }}</span>
|
||||
<span :class="getQuotaRemainingClass(getCodexQuotaDisplay(key)?.secondary_used_percent || 0)">
|
||||
{{ (100 - (getCodexQuotaDisplay(key)?.secondary_used_percent || 0)).toFixed(1) }}%
|
||||
</span>
|
||||
</div>
|
||||
<div class="relative w-full h-1.5 bg-border rounded-full overflow-hidden">
|
||||
<div
|
||||
class="absolute left-0 top-0 h-full transition-all duration-300"
|
||||
:class="getQuotaRemainingBarColor(getCodexQuotaDisplay(key)?.secondary_used_percent || 0)"
|
||||
:style="{ width: `${Math.max(100 - (getCodexQuotaDisplay(key)?.secondary_used_percent || 0), 0)}%` }"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="shouldStartCodexResetCountdown(getCodexQuotaDisplay(key)?.secondary_used_percent || 0)"
|
||||
class="text-[9px] mt-0.5 tabular-nums"
|
||||
:class="getResetCountdownClass(
|
||||
<ProviderQuotaProgressRow
|
||||
v-if="isCodexTeamPlan(key) && getCodexQuotaDisplay(key)?.secondary_used_percent !== undefined"
|
||||
:label="legacyT('5H限额')"
|
||||
:used-percent="getCodexQuotaDisplay(key)?.secondary_used_percent || 0"
|
||||
:meter-class="getQuotaRemainingClass(getCodexQuotaDisplay(key)?.secondary_used_percent || 0)"
|
||||
:bar-class="getQuotaRemainingBarColor(getCodexQuotaDisplay(key)?.secondary_used_percent || 0)"
|
||||
:reset-text="shouldStartCodexResetCountdown(getCodexQuotaDisplay(key)?.secondary_used_percent || 0)
|
||||
? getCodexQuotaResetText(
|
||||
getCodexQuotaDisplay(key)?.secondary_reset_at,
|
||||
getCodexQuotaDisplay(key)?.secondary_reset_seconds,
|
||||
getCodexQuotaDisplay(key)?.updated_at,
|
||||
getCodexQuotaDisplay(key)?.secondary_used_percent
|
||||
)"
|
||||
>
|
||||
<template v-if="getCodexQuotaDisplay(key)?.secondary_reset_at || getCodexQuotaDisplay(key)?.secondary_reset_seconds">
|
||||
{{ getResetCountdownText(
|
||||
getCodexQuotaDisplay(key)?.secondary_reset_at,
|
||||
getCodexQuotaDisplay(key)?.secondary_reset_seconds,
|
||||
getCodexQuotaDisplay(key)?.updated_at,
|
||||
getCodexQuotaDisplay(key)?.secondary_used_percent
|
||||
) }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ legacyT('已重置') }}
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
: null"
|
||||
:footer-class="getResetCountdownClass(
|
||||
getCodexQuotaDisplay(key)?.secondary_reset_at,
|
||||
getCodexQuotaDisplay(key)?.secondary_reset_seconds,
|
||||
getCodexQuotaDisplay(key)?.updated_at,
|
||||
getCodexQuotaDisplay(key)?.secondary_used_percent
|
||||
)"
|
||||
/>
|
||||
</div>
|
||||
<!-- Spark 限额独立一行展示,避免与普通 Codex 周/5H 混淆 -->
|
||||
<div
|
||||
@@ -521,80 +450,48 @@
|
||||
GPT-5.3 Codex Spark
|
||||
</div>
|
||||
<div class="grid gap-3 grid-cols-2">
|
||||
<div v-if="getCodexQuotaDisplay(key)?.spark_secondary_used_percent !== undefined">
|
||||
<div class="flex items-center justify-between text-[10px] mb-0.5">
|
||||
<span class="text-muted-foreground">{{ legacyT('Spark 周') }}</span>
|
||||
<span :class="getQuotaRemainingClass(getCodexQuotaDisplay(key)?.spark_secondary_used_percent || 0)">
|
||||
{{ (100 - (getCodexQuotaDisplay(key)?.spark_secondary_used_percent || 0)).toFixed(1) }}%
|
||||
</span>
|
||||
</div>
|
||||
<div class="relative w-full h-1.5 bg-border rounded-full overflow-hidden">
|
||||
<div
|
||||
class="absolute left-0 top-0 h-full transition-all duration-300"
|
||||
:class="getQuotaRemainingBarColor(getCodexQuotaDisplay(key)?.spark_secondary_used_percent || 0)"
|
||||
:style="{ width: `${Math.max(100 - (getCodexQuotaDisplay(key)?.spark_secondary_used_percent || 0), 0)}%` }"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="shouldStartCodexResetCountdown(getCodexQuotaDisplay(key)?.spark_secondary_used_percent || 0)"
|
||||
class="text-[9px] mt-0.5 tabular-nums"
|
||||
:class="getResetCountdownClass(
|
||||
<ProviderQuotaProgressRow
|
||||
v-if="getCodexQuotaDisplay(key)?.spark_secondary_used_percent !== undefined"
|
||||
:label="legacyT('Spark 周')"
|
||||
:used-percent="getCodexQuotaDisplay(key)?.spark_secondary_used_percent || 0"
|
||||
:meter-class="getQuotaRemainingClass(getCodexQuotaDisplay(key)?.spark_secondary_used_percent || 0)"
|
||||
:bar-class="getQuotaRemainingBarColor(getCodexQuotaDisplay(key)?.spark_secondary_used_percent || 0)"
|
||||
:reset-text="shouldStartCodexResetCountdown(getCodexQuotaDisplay(key)?.spark_secondary_used_percent || 0)
|
||||
? getCodexQuotaResetText(
|
||||
getCodexQuotaDisplay(key)?.spark_secondary_reset_at,
|
||||
getCodexQuotaDisplay(key)?.spark_secondary_reset_seconds,
|
||||
getCodexQuotaDisplay(key)?.updated_at,
|
||||
getCodexQuotaDisplay(key)?.spark_secondary_used_percent
|
||||
)"
|
||||
>
|
||||
<template v-if="getCodexQuotaDisplay(key)?.spark_secondary_reset_at || getCodexQuotaDisplay(key)?.spark_secondary_reset_seconds">
|
||||
{{ getResetCountdownText(
|
||||
getCodexQuotaDisplay(key)?.spark_secondary_reset_at,
|
||||
getCodexQuotaDisplay(key)?.spark_secondary_reset_seconds,
|
||||
getCodexQuotaDisplay(key)?.updated_at,
|
||||
getCodexQuotaDisplay(key)?.spark_secondary_used_percent
|
||||
) }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ legacyT('已重置') }}
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="getCodexQuotaDisplay(key)?.spark_primary_used_percent !== undefined">
|
||||
<div class="flex items-center justify-between text-[10px] mb-0.5">
|
||||
<span class="text-muted-foreground">{{ legacyT('Spark 5H') }}</span>
|
||||
<span :class="getQuotaRemainingClass(getCodexQuotaDisplay(key)?.spark_primary_used_percent || 0)">
|
||||
{{ (100 - (getCodexQuotaDisplay(key)?.spark_primary_used_percent || 0)).toFixed(1) }}%
|
||||
</span>
|
||||
</div>
|
||||
<div class="relative w-full h-1.5 bg-border rounded-full overflow-hidden">
|
||||
<div
|
||||
class="absolute left-0 top-0 h-full transition-all duration-300"
|
||||
:class="getQuotaRemainingBarColor(getCodexQuotaDisplay(key)?.spark_primary_used_percent || 0)"
|
||||
:style="{ width: `${Math.max(100 - (getCodexQuotaDisplay(key)?.spark_primary_used_percent || 0), 0)}%` }"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="shouldStartCodexResetCountdown(getCodexQuotaDisplay(key)?.spark_primary_used_percent || 0)"
|
||||
class="text-[9px] mt-0.5 tabular-nums"
|
||||
:class="getResetCountdownClass(
|
||||
)
|
||||
: null"
|
||||
:footer-class="getResetCountdownClass(
|
||||
getCodexQuotaDisplay(key)?.spark_secondary_reset_at,
|
||||
getCodexQuotaDisplay(key)?.spark_secondary_reset_seconds,
|
||||
getCodexQuotaDisplay(key)?.updated_at,
|
||||
getCodexQuotaDisplay(key)?.spark_secondary_used_percent
|
||||
)"
|
||||
/>
|
||||
<ProviderQuotaProgressRow
|
||||
v-if="getCodexQuotaDisplay(key)?.spark_primary_used_percent !== undefined"
|
||||
:label="legacyT('Spark 5H')"
|
||||
:used-percent="getCodexQuotaDisplay(key)?.spark_primary_used_percent || 0"
|
||||
:meter-class="getQuotaRemainingClass(getCodexQuotaDisplay(key)?.spark_primary_used_percent || 0)"
|
||||
:bar-class="getQuotaRemainingBarColor(getCodexQuotaDisplay(key)?.spark_primary_used_percent || 0)"
|
||||
:reset-text="shouldStartCodexResetCountdown(getCodexQuotaDisplay(key)?.spark_primary_used_percent || 0)
|
||||
? getCodexQuotaResetText(
|
||||
getCodexQuotaDisplay(key)?.spark_primary_reset_at,
|
||||
getCodexQuotaDisplay(key)?.spark_primary_reset_seconds,
|
||||
getCodexQuotaDisplay(key)?.updated_at,
|
||||
getCodexQuotaDisplay(key)?.spark_primary_used_percent
|
||||
)"
|
||||
>
|
||||
<template v-if="getCodexQuotaDisplay(key)?.spark_primary_reset_at || getCodexQuotaDisplay(key)?.spark_primary_reset_seconds">
|
||||
{{ getResetCountdownText(
|
||||
getCodexQuotaDisplay(key)?.spark_primary_reset_at,
|
||||
getCodexQuotaDisplay(key)?.spark_primary_reset_seconds,
|
||||
getCodexQuotaDisplay(key)?.updated_at,
|
||||
getCodexQuotaDisplay(key)?.spark_primary_used_percent
|
||||
) }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ legacyT('已重置') }}
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
: null"
|
||||
:footer-class="getResetCountdownClass(
|
||||
getCodexQuotaDisplay(key)?.spark_primary_reset_at,
|
||||
getCodexQuotaDisplay(key)?.spark_primary_reset_seconds,
|
||||
getCodexQuotaDisplay(key)?.updated_at,
|
||||
getCodexQuotaDisplay(key)?.spark_primary_used_percent
|
||||
)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -631,56 +528,38 @@
|
||||
</div>
|
||||
<!-- 正常配额显示 -->
|
||||
<template v-else>
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<span class="text-[10px] text-muted-foreground">{{ legacyT('模型配额') }}</span>
|
||||
<div class="flex items-center gap-1">
|
||||
<RefreshCw
|
||||
v-if="refreshingQuota"
|
||||
class="w-3 h-3 text-muted-foreground/70 animate-spin"
|
||||
/>
|
||||
<span
|
||||
v-if="getAntigravityQuotaUpdatedAt(key)"
|
||||
class="text-[9px] text-muted-foreground/70"
|
||||
>
|
||||
{{ formatAntigravityUpdatedAt(getAntigravityQuotaUpdatedAt(key) || 0) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<ProviderQuotaSectionHeader
|
||||
:title="legacyT('模型配额')"
|
||||
:loading="refreshingQuota"
|
||||
:updated-text="getAntigravityQuotaUpdatedAt(key) ? formatAntigravityUpdatedAt(getAntigravityQuotaUpdatedAt(key) || 0) : null"
|
||||
/>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div
|
||||
<ProviderQuotaProgressRow
|
||||
v-for="group in getAntigravityQuotaSummaryForKey(key)"
|
||||
:key="group.key"
|
||||
:label="group.label"
|
||||
:used-percent="group.usedPercent"
|
||||
:remaining-percent="group.remainingPercent"
|
||||
:meter-class="getQuotaRemainingClass(group.usedPercent)"
|
||||
:bar-class="getQuotaRemainingBarColor(group.usedPercent)"
|
||||
>
|
||||
<div class="flex items-center justify-between text-[10px] mb-0.5">
|
||||
<span class="text-muted-foreground truncate mr-2 min-w-0 flex-1">
|
||||
{{ group.label }}
|
||||
</span>
|
||||
<span :class="getQuotaRemainingClass(group.usedPercent)">
|
||||
{{ group.remainingPercent.toFixed(1) }}%
|
||||
</span>
|
||||
</div>
|
||||
<div class="relative w-full h-1.5 bg-border rounded-full overflow-hidden">
|
||||
<template #footer>
|
||||
<div
|
||||
class="absolute left-0 top-0 h-full transition-all duration-300"
|
||||
:class="getQuotaRemainingBarColor(group.usedPercent)"
|
||||
:style="{ width: `${Math.max(group.remainingPercent, 0)}%` }"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="group.resetSeconds !== null || group.usedPercent > 0"
|
||||
class="text-[9px] text-muted-foreground/70 mt-0.5"
|
||||
>
|
||||
<template v-if="group.resetSeconds !== null && group.resetSeconds > 0">
|
||||
{{ formatResetTime(group.resetSeconds) }}{{ legacyT('后重置') }}
|
||||
</template>
|
||||
<template v-else-if="group.resetSeconds !== null && group.resetSeconds <= 0">
|
||||
{{ legacyT('已重置') }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ legacyT('重置时间未知') }}
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
v-if="group.resetSeconds !== null || group.usedPercent > 0"
|
||||
class="text-[9px] text-muted-foreground/70 mt-0.5"
|
||||
>
|
||||
<template v-if="group.resetSeconds !== null && group.resetSeconds > 0">
|
||||
{{ formatResetTime(group.resetSeconds) }}{{ legacyT('后重置') }}
|
||||
</template>
|
||||
<template v-else-if="group.resetSeconds !== null && group.resetSeconds <= 0">
|
||||
{{ legacyT('已重置') }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ legacyT('重置时间未知') }}
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</ProviderQuotaProgressRow>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@@ -689,21 +568,11 @@
|
||||
v-if="provider.provider_type === 'gemini_cli' && hasGeminiCliQuotaDisplayData(key)"
|
||||
class="mt-2 p-2 rounded-md bg-muted/30"
|
||||
>
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<span class="text-[10px] text-muted-foreground">{{ legacyT('模型配额') }}</span>
|
||||
<div class="flex items-center gap-1">
|
||||
<RefreshCw
|
||||
v-if="refreshingQuota"
|
||||
class="w-3 h-3 text-muted-foreground/70 animate-spin"
|
||||
/>
|
||||
<span
|
||||
v-if="getGeminiCliQuotaUpdatedAt(key)"
|
||||
class="text-[9px] text-muted-foreground/70"
|
||||
>
|
||||
{{ formatUpdatedAt(getGeminiCliQuotaUpdatedAt(key) || 0) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<ProviderQuotaSectionHeader
|
||||
:title="legacyT('模型配额')"
|
||||
:loading="refreshingQuota"
|
||||
:updated-text="getGeminiCliQuotaUpdatedAt(key) ? formatUpdatedAt(getGeminiCliQuotaUpdatedAt(key) || 0) : null"
|
||||
/>
|
||||
<div
|
||||
v-if="getGeminiCliAccountCreditsText(key, 'gemini_cli')"
|
||||
class="mb-2 text-[10px] font-medium text-foreground/90"
|
||||
@@ -714,40 +583,30 @@
|
||||
v-if="getGeminiCliQuotaItems(key).length > 0"
|
||||
class="grid grid-cols-2 gap-3"
|
||||
>
|
||||
<div
|
||||
<ProviderQuotaProgressRow
|
||||
v-for="item in getGeminiCliQuotaItems(key)"
|
||||
:key="item.model"
|
||||
:label="item.label"
|
||||
:title="item.model"
|
||||
:used-percent="item.usedPercent"
|
||||
:remaining-percent="item.remainingPercent"
|
||||
:meter-class="getQuotaRemainingClass(item.usedPercent)"
|
||||
:bar-class="getQuotaRemainingBarColor(item.usedPercent)"
|
||||
>
|
||||
<div class="flex items-center justify-between text-[10px] mb-0.5">
|
||||
<span
|
||||
class="text-muted-foreground truncate mr-2 min-w-0 flex-1"
|
||||
:title="item.model"
|
||||
>
|
||||
{{ item.label }}
|
||||
</span>
|
||||
<span :class="getQuotaRemainingClass(item.usedPercent)">
|
||||
{{ item.remainingPercent.toFixed(1) }}%
|
||||
</span>
|
||||
</div>
|
||||
<div class="relative w-full h-1.5 bg-border rounded-full overflow-hidden">
|
||||
<template #footer>
|
||||
<div
|
||||
class="absolute left-0 top-0 h-full transition-all duration-300"
|
||||
:class="getQuotaRemainingBarColor(item.usedPercent)"
|
||||
:style="{ width: `${Math.max(item.remainingPercent, 0)}%` }"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="item.resetSeconds !== null && item.remainingPercent < 100"
|
||||
class="text-[9px] text-muted-foreground/70 mt-0.5"
|
||||
>
|
||||
<template v-if="item.resetSeconds > 0">
|
||||
{{ formatResetTime(item.resetSeconds) }}{{ legacyT('后重置') }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ legacyT('已重置') }}
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
v-if="item.resetSeconds !== null && item.remainingPercent < 100"
|
||||
class="text-[9px] text-muted-foreground/70 mt-0.5"
|
||||
>
|
||||
<template v-if="item.resetSeconds > 0">
|
||||
{{ formatResetTime(item.resetSeconds) }}{{ legacyT('后重置') }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ legacyT('已重置') }}
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</ProviderQuotaProgressRow>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Kiro 上游额度信息(仅当有元数据时显示) -->
|
||||
@@ -783,48 +642,32 @@
|
||||
</div>
|
||||
<!-- 正常配额显示 -->
|
||||
<template v-else>
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<span class="text-[10px] text-muted-foreground">{{ legacyT('账号配额') }}</span>
|
||||
<div class="flex items-center gap-1">
|
||||
<RefreshCw
|
||||
v-if="refreshingQuota"
|
||||
class="w-3 h-3 text-muted-foreground/70 animate-spin"
|
||||
/>
|
||||
<span
|
||||
v-if="getKiroQuotaDisplay(key)?.updated_at"
|
||||
class="text-[9px] text-muted-foreground/70"
|
||||
>
|
||||
{{ formatKiroUpdatedAt(getKiroQuotaDisplay(key)?.updated_at || 0) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<ProviderQuotaSectionHeader
|
||||
:title="legacyT('账号配额')"
|
||||
:loading="refreshingQuota"
|
||||
:updated-text="getKiroQuotaDisplay(key)?.updated_at ? formatKiroUpdatedAt(getKiroQuotaDisplay(key)?.updated_at || 0) : null"
|
||||
/>
|
||||
<!-- Kiro 额度显示:使用进度 -->
|
||||
<div>
|
||||
<!-- 使用额度进度条 -->
|
||||
<div>
|
||||
<div class="flex items-center justify-between text-[10px] mb-0.5">
|
||||
<span class="text-muted-foreground">{{ legacyT('使用额度') }}</span>
|
||||
<span :class="getQuotaRemainingClass(getKiroQuotaDisplay(key)?.usage_percentage || 0)">
|
||||
{{ (100 - (getKiroQuotaDisplay(key)?.usage_percentage || 0)).toFixed(1) }}%
|
||||
</span>
|
||||
</div>
|
||||
<div class="relative w-full h-1.5 bg-border rounded-full overflow-hidden">
|
||||
<div
|
||||
class="absolute left-0 top-0 h-full transition-all duration-300"
|
||||
:class="getQuotaRemainingBarColor(getKiroQuotaDisplay(key)?.usage_percentage || 0)"
|
||||
:style="{ width: `${Math.max(100 - (getKiroQuotaDisplay(key)?.usage_percentage || 0), 0)}%` }"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center justify-between text-[9px] text-muted-foreground/70 mt-0.5">
|
||||
<span>
|
||||
{{ formatKiroUsage(getKiroQuotaDisplay(key)?.current_usage) }} /
|
||||
{{ formatKiroUsage(getKiroQuotaDisplay(key)?.usage_limit) }}
|
||||
</span>
|
||||
<span v-if="getKiroQuotaDisplay(key)?.next_reset_at">
|
||||
{{ formatKiroResetTime(getKiroQuotaDisplay(key)?.next_reset_at) }}{{ legacyT('重置') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<ProviderQuotaProgressRow
|
||||
:label="legacyT('使用额度')"
|
||||
:used-percent="getKiroQuotaDisplay(key)?.usage_percentage || 0"
|
||||
:meter-class="getQuotaRemainingClass(getKiroQuotaDisplay(key)?.usage_percentage || 0)"
|
||||
:bar-class="getQuotaRemainingBarColor(getKiroQuotaDisplay(key)?.usage_percentage || 0)"
|
||||
>
|
||||
<template #footer>
|
||||
<div class="flex items-center justify-between text-[9px] text-muted-foreground/70 mt-0.5">
|
||||
<span>
|
||||
{{ formatKiroUsage(getKiroQuotaDisplay(key)?.current_usage) }} /
|
||||
{{ formatKiroUsage(getKiroQuotaDisplay(key)?.usage_limit) }}
|
||||
</span>
|
||||
<span v-if="getKiroQuotaDisplay(key)?.next_reset_at">
|
||||
{{ formatKiroResetTime(getKiroQuotaDisplay(key)?.next_reset_at) }}{{ legacyT('重置') }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</ProviderQuotaProgressRow>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@@ -871,64 +714,34 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<span class="text-[10px] text-muted-foreground">{{ legacyT('账号配额') }}</span>
|
||||
<div class="flex items-center gap-1">
|
||||
<RefreshCw
|
||||
v-if="refreshingQuota"
|
||||
class="w-3 h-3 text-muted-foreground/70 animate-spin"
|
||||
/>
|
||||
<span
|
||||
v-if="getWindsurfQuotaDisplay(key)?.updated_at"
|
||||
class="text-[9px] text-muted-foreground/70"
|
||||
>
|
||||
{{ formatKiroUpdatedAt(getWindsurfQuotaDisplay(key)?.updated_at || 0) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<ProviderQuotaSectionHeader
|
||||
:title="legacyT('账号配额')"
|
||||
:loading="refreshingQuota"
|
||||
:updated-text="getWindsurfQuotaDisplay(key)?.updated_at ? formatKiroUpdatedAt(getWindsurfQuotaDisplay(key)?.updated_at || 0) : null"
|
||||
/>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div v-if="getWindsurfQuotaDisplay(key)?.daily_remaining_percent !== undefined">
|
||||
<div class="flex items-center justify-between text-[10px] mb-0.5">
|
||||
<span class="text-muted-foreground">{{ legacyT('日额度') }}</span>
|
||||
<span :class="getQuotaRemainingClass(getWindsurfQuotaDisplay(key)?.daily_used_percent || 0)">
|
||||
{{ (getWindsurfQuotaDisplay(key)?.daily_remaining_percent || 0).toFixed(1) }}%
|
||||
</span>
|
||||
</div>
|
||||
<div class="relative w-full h-1.5 bg-border rounded-full overflow-hidden">
|
||||
<div
|
||||
class="absolute left-0 top-0 h-full transition-all duration-300"
|
||||
:class="getQuotaRemainingBarColor(getWindsurfQuotaDisplay(key)?.daily_used_percent || 0)"
|
||||
:style="{ width: `${Math.max(getWindsurfQuotaDisplay(key)?.daily_remaining_percent || 0, 0)}%` }"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="getWindsurfQuotaDisplay(key)?.daily_reset_at"
|
||||
class="text-[9px] text-muted-foreground/70 mt-0.5"
|
||||
>
|
||||
{{ formatKiroResetTime(getWindsurfQuotaDisplay(key)?.daily_reset_at || 0) }}{{ legacyT('重置') }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="getWindsurfQuotaDisplay(key)?.weekly_remaining_percent !== undefined">
|
||||
<div class="flex items-center justify-between text-[10px] mb-0.5">
|
||||
<span class="text-muted-foreground">{{ legacyT('周额度') }}</span>
|
||||
<span :class="getQuotaRemainingClass(getWindsurfQuotaDisplay(key)?.weekly_used_percent || 0)">
|
||||
{{ (getWindsurfQuotaDisplay(key)?.weekly_remaining_percent || 0).toFixed(1) }}%
|
||||
</span>
|
||||
</div>
|
||||
<div class="relative w-full h-1.5 bg-border rounded-full overflow-hidden">
|
||||
<div
|
||||
class="absolute left-0 top-0 h-full transition-all duration-300"
|
||||
:class="getQuotaRemainingBarColor(getWindsurfQuotaDisplay(key)?.weekly_used_percent || 0)"
|
||||
:style="{ width: `${Math.max(getWindsurfQuotaDisplay(key)?.weekly_remaining_percent || 0, 0)}%` }"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="getWindsurfQuotaDisplay(key)?.weekly_reset_at"
|
||||
class="text-[9px] text-muted-foreground/70 mt-0.5"
|
||||
>
|
||||
{{ formatKiroResetTime(getWindsurfQuotaDisplay(key)?.weekly_reset_at || 0) }}{{ legacyT('重置') }}
|
||||
</div>
|
||||
</div>
|
||||
<ProviderQuotaProgressRow
|
||||
v-if="getWindsurfQuotaDisplay(key)?.daily_remaining_percent !== undefined"
|
||||
:label="legacyT('日额度')"
|
||||
:used-percent="getWindsurfQuotaDisplay(key)?.daily_used_percent || 0"
|
||||
:remaining-percent="getWindsurfQuotaDisplay(key)?.daily_remaining_percent || 0"
|
||||
:meter-class="getQuotaRemainingClass(getWindsurfQuotaDisplay(key)?.daily_used_percent || 0)"
|
||||
:bar-class="getQuotaRemainingBarColor(getWindsurfQuotaDisplay(key)?.daily_used_percent || 0)"
|
||||
:reset-text="getWindsurfQuotaDisplay(key)?.daily_reset_at
|
||||
? `${formatKiroResetTime(getWindsurfQuotaDisplay(key)?.daily_reset_at || 0)}${legacyT('重置')}`
|
||||
: null"
|
||||
/>
|
||||
<ProviderQuotaProgressRow
|
||||
v-if="getWindsurfQuotaDisplay(key)?.weekly_remaining_percent !== undefined"
|
||||
:label="legacyT('周额度')"
|
||||
:used-percent="getWindsurfQuotaDisplay(key)?.weekly_used_percent || 0"
|
||||
:remaining-percent="getWindsurfQuotaDisplay(key)?.weekly_remaining_percent || 0"
|
||||
:meter-class="getQuotaRemainingClass(getWindsurfQuotaDisplay(key)?.weekly_used_percent || 0)"
|
||||
:bar-class="getQuotaRemainingBarColor(getWindsurfQuotaDisplay(key)?.weekly_used_percent || 0)"
|
||||
:reset-text="getWindsurfQuotaDisplay(key)?.weekly_reset_at
|
||||
? `${formatKiroResetTime(getWindsurfQuotaDisplay(key)?.weekly_reset_at || 0)}${legacyT('重置')}`
|
||||
: null"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="hasWindsurfPromptQuota(key) || hasWindsurfFlexQuota(key)"
|
||||
@@ -965,44 +778,31 @@
|
||||
v-if="provider.provider_type === 'chatgpt_web' && hasChatGPTWebQuotaDisplayData(key)"
|
||||
class="mt-2 p-2 rounded-md bg-muted/30"
|
||||
>
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<span class="text-[10px] text-muted-foreground">{{ legacyT('账号配额') }}</span>
|
||||
<div class="flex items-center gap-1">
|
||||
<RefreshCw
|
||||
v-if="refreshingQuota"
|
||||
class="w-3 h-3 text-muted-foreground/70 animate-spin"
|
||||
/>
|
||||
<span
|
||||
v-if="getChatGPTWebQuotaDisplay(key)?.updated_at"
|
||||
class="text-[9px] text-muted-foreground/70"
|
||||
>
|
||||
{{ formatKiroUpdatedAt(getChatGPTWebQuotaDisplay(key)?.updated_at || 0) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<ProviderQuotaSectionHeader
|
||||
:title="legacyT('账号配额')"
|
||||
:loading="refreshingQuota"
|
||||
:updated-text="getChatGPTWebQuotaDisplay(key)?.updated_at ? formatKiroUpdatedAt(getChatGPTWebQuotaDisplay(key)?.updated_at || 0) : null"
|
||||
/>
|
||||
<div>
|
||||
<div class="flex items-center justify-between text-[10px] mb-0.5">
|
||||
<span class="text-muted-foreground">{{ legacyT('剩余额度') }}</span>
|
||||
<span :class="getQuotaRemainingClass(getChatGPTWebQuotaUsedPercent(key))">
|
||||
{{ getChatGPTWebQuotaRemainingPercent(key).toFixed(1) }}%
|
||||
</span>
|
||||
</div>
|
||||
<div class="relative w-full h-1.5 bg-border rounded-full overflow-hidden">
|
||||
<div
|
||||
class="absolute left-0 top-0 h-full transition-all duration-300"
|
||||
:class="getQuotaRemainingBarColor(getChatGPTWebQuotaUsedPercent(key))"
|
||||
:style="{ width: `${Math.max(getChatGPTWebQuotaRemainingPercent(key), 0)}%` }"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center justify-between text-[9px] text-muted-foreground/70 mt-0.5">
|
||||
<span>
|
||||
{{ formatChatGPTWebUsage(getChatGPTWebQuotaDisplay(key)?.image_quota_remaining) }} /
|
||||
{{ formatChatGPTWebUsage(getChatGPTWebQuotaDisplay(key)?.image_quota_total) }}
|
||||
</span>
|
||||
<span v-if="getChatGPTWebQuotaDisplay(key)?.image_quota_reset_at">
|
||||
{{ formatKiroResetTime(getChatGPTWebQuotaDisplay(key)?.image_quota_reset_at) }}{{ legacyT('重置') }}
|
||||
</span>
|
||||
</div>
|
||||
<ProviderQuotaProgressRow
|
||||
:label="legacyT('剩余额度')"
|
||||
:used-percent="getChatGPTWebQuotaUsedPercent(key)"
|
||||
:remaining-percent="getChatGPTWebQuotaRemainingPercent(key)"
|
||||
:meter-class="getQuotaRemainingClass(getChatGPTWebQuotaUsedPercent(key))"
|
||||
:bar-class="getQuotaRemainingBarColor(getChatGPTWebQuotaUsedPercent(key))"
|
||||
>
|
||||
<template #footer>
|
||||
<div class="flex items-center justify-between text-[9px] text-muted-foreground/70 mt-0.5">
|
||||
<span>
|
||||
{{ formatChatGPTWebUsage(getChatGPTWebQuotaDisplay(key)?.image_quota_remaining) }} /
|
||||
{{ formatChatGPTWebUsage(getChatGPTWebQuotaDisplay(key)?.image_quota_total) }}
|
||||
</span>
|
||||
<span v-if="getChatGPTWebQuotaDisplay(key)?.image_quota_reset_at">
|
||||
{{ formatKiroResetTime(getChatGPTWebQuotaDisplay(key)?.image_quota_reset_at) }}{{ legacyT('重置') }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</ProviderQuotaProgressRow>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 第二行:优先级 + API 格式(展开显示) + 统计信息 -->
|
||||
@@ -1328,6 +1128,9 @@ import AlertDialog from '@/components/common/AlertDialog.vue'
|
||||
import AntigravityQuotaDialog from '@/features/providers/components/AntigravityQuotaDialog.vue'
|
||||
import FailoverRulesDialog from '@/features/providers/components/FailoverRulesDialog.vue'
|
||||
import ProviderDetailHeader from '@/features/providers/components/ProviderDetailHeader.vue'
|
||||
import ProviderMonthlyQuotaCard from '@/features/providers/components/ProviderMonthlyQuotaCard.vue'
|
||||
import ProviderQuotaProgressRow from '@/features/providers/components/ProviderQuotaProgressRow.vue'
|
||||
import ProviderQuotaSectionHeader from '@/features/providers/components/ProviderQuotaSectionHeader.vue'
|
||||
import ProxyNodeSelect from '@/features/providers/components/ProxyNodeSelect.vue'
|
||||
import { useProxyNodesStore } from '@/stores/proxy-nodes'
|
||||
import {
|
||||
@@ -3763,6 +3566,18 @@ function getResetCountdownText(
|
||||
return status.isExpired ? legacyT(status.text) : `${legacyT(status.text)} ${legacyT('后重置')}`
|
||||
}
|
||||
|
||||
function getCodexQuotaResetText(
|
||||
resetAt: number | null | undefined,
|
||||
resetSecs: number | null | undefined,
|
||||
updatedAt: number | null | undefined,
|
||||
usedPercent: number | null | undefined
|
||||
): string {
|
||||
if (resetAt || resetSecs) {
|
||||
return getResetCountdownText(resetAt, resetSecs, updatedAt, usedPercent)
|
||||
}
|
||||
return legacyT('已重置')
|
||||
}
|
||||
|
||||
function getResetCountdownClass(
|
||||
resetAt: number | null | undefined,
|
||||
resetSecs: number | null | undefined,
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<Card
|
||||
v-if="quota > 0"
|
||||
class="p-4"
|
||||
data-testid="provider-monthly-quota-card"
|
||||
>
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-sm font-semibold">
|
||||
{{ legacyT('订阅配额') }}
|
||||
</h3>
|
||||
<Badge
|
||||
variant="secondary"
|
||||
class="text-xs"
|
||||
data-testid="provider-monthly-quota-percent"
|
||||
>
|
||||
{{ usedPercent.toFixed(1) }}%
|
||||
</Badge>
|
||||
</div>
|
||||
<div class="relative w-full h-2 bg-border rounded-full overflow-hidden">
|
||||
<div
|
||||
class="absolute left-0 top-0 h-full transition-all duration-300"
|
||||
:class="barClass"
|
||||
:style="{ width: `${cappedUsedPercent}%` }"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center justify-between text-xs">
|
||||
<span
|
||||
class="font-semibold"
|
||||
data-testid="provider-monthly-quota-amount"
|
||||
>
|
||||
${{ used.toFixed(2) }} / ${{ quota.toFixed(2) }}
|
||||
</span>
|
||||
<span
|
||||
v-if="resetDay"
|
||||
class="text-muted-foreground"
|
||||
data-testid="provider-monthly-quota-reset"
|
||||
>
|
||||
{{ legacyT('每月') }} {{ resetDay }} {{ legacyT('号重置') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import Badge from '@/components/ui/badge.vue'
|
||||
import Card from '@/components/ui/card.vue'
|
||||
import { useI18n } from '@/i18n'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
used?: number | null
|
||||
quota?: number | null
|
||||
resetDay?: number | null
|
||||
}>(), {
|
||||
used: 0,
|
||||
quota: 0,
|
||||
resetDay: null,
|
||||
})
|
||||
|
||||
const { legacyT } = useI18n()
|
||||
|
||||
const used = computed(() => Number.isFinite(Number(props.used)) ? Number(props.used) : 0)
|
||||
const quota = computed(() => Number.isFinite(Number(props.quota)) ? Number(props.quota) : 0)
|
||||
const usedPercent = computed(() => quota.value > 0 ? (used.value / quota.value) * 100 : 0)
|
||||
const cappedUsedPercent = computed(() => Math.min(Math.max(usedPercent.value, 0), 100))
|
||||
const barClass = computed(() => {
|
||||
const ratio = quota.value > 0 ? used.value / quota.value : 0
|
||||
if (ratio >= 0.9) return 'bg-red-500'
|
||||
if (ratio >= 0.7) return 'bg-yellow-500'
|
||||
return 'bg-green-500'
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<div data-testid="provider-quota-progress-row">
|
||||
<div class="flex items-center justify-between text-[10px] mb-0.5">
|
||||
<span
|
||||
class="text-muted-foreground truncate mr-2 min-w-0 flex-1"
|
||||
:title="title || label"
|
||||
>
|
||||
{{ label }}
|
||||
</span>
|
||||
<span
|
||||
:class="meterClass"
|
||||
data-testid="provider-quota-progress-meter"
|
||||
>
|
||||
{{ normalizedRemainingPercent.toFixed(1) }}%
|
||||
</span>
|
||||
</div>
|
||||
<div class="relative w-full h-1.5 bg-border rounded-full overflow-hidden">
|
||||
<div
|
||||
class="absolute left-0 top-0 h-full transition-all duration-300"
|
||||
:class="barClass"
|
||||
:style="{ width: `${normalizedRemainingPercent}%` }"
|
||||
data-testid="provider-quota-progress-bar"
|
||||
/>
|
||||
</div>
|
||||
<slot name="footer">
|
||||
<div
|
||||
v-if="resetText"
|
||||
class="text-[9px] text-muted-foreground/70 mt-0.5"
|
||||
:class="footerClass"
|
||||
data-testid="provider-quota-progress-reset"
|
||||
>
|
||||
{{ resetText }}
|
||||
</div>
|
||||
</slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
label: string
|
||||
usedPercent?: number | null
|
||||
remainingPercent?: number | null
|
||||
title?: string | null
|
||||
meterClass?: string
|
||||
barClass?: string
|
||||
footerClass?: string
|
||||
resetText?: string | null
|
||||
}>(), {
|
||||
usedPercent: null,
|
||||
remainingPercent: null,
|
||||
title: null,
|
||||
meterClass: '',
|
||||
barClass: '',
|
||||
footerClass: '',
|
||||
resetText: null,
|
||||
})
|
||||
|
||||
function normalizePercent(value: number | null | undefined): number | null {
|
||||
const numeric = Number(value)
|
||||
if (!Number.isFinite(numeric)) return null
|
||||
return Math.min(Math.max(numeric, 0), 100)
|
||||
}
|
||||
|
||||
const normalizedRemainingPercent = computed(() => {
|
||||
const remaining = normalizePercent(props.remainingPercent)
|
||||
if (remaining !== null) return remaining
|
||||
|
||||
const used = normalizePercent(props.usedPercent)
|
||||
if (used !== null) return Math.max(100 - used, 0)
|
||||
|
||||
return 0
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<span class="text-[10px] text-muted-foreground">
|
||||
{{ title }}
|
||||
</span>
|
||||
<div class="flex items-center gap-1">
|
||||
<RefreshCw
|
||||
v-if="loading"
|
||||
class="w-3 h-3 text-muted-foreground/70 animate-spin"
|
||||
data-testid="provider-quota-header-loading"
|
||||
/>
|
||||
<span
|
||||
v-if="updatedText"
|
||||
class="text-[9px] text-muted-foreground/70"
|
||||
data-testid="provider-quota-header-updated"
|
||||
>
|
||||
{{ updatedText }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { RefreshCw } from 'lucide-vue-next'
|
||||
|
||||
withDefaults(defineProps<{
|
||||
title: string
|
||||
loading?: boolean
|
||||
updatedText?: string | null
|
||||
}>(), {
|
||||
loading: false,
|
||||
updatedText: null,
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,76 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { createApp, defineComponent, h } from 'vue'
|
||||
|
||||
import ProviderMonthlyQuotaCard from '@/features/providers/components/ProviderMonthlyQuotaCard.vue'
|
||||
import ProviderQuotaProgressRow from '@/features/providers/components/ProviderQuotaProgressRow.vue'
|
||||
import ProviderQuotaSectionHeader from '@/features/providers/components/ProviderQuotaSectionHeader.vue'
|
||||
import { createI18n } from '@/i18n'
|
||||
|
||||
function mount(component: Parameters<typeof createApp>[0], props?: Record<string, unknown>) {
|
||||
const root = document.createElement('div')
|
||||
document.body.appendChild(root)
|
||||
const app = createApp(component, props)
|
||||
app.use(createI18n())
|
||||
app.mount(root)
|
||||
|
||||
return {
|
||||
root,
|
||||
unmount: () => {
|
||||
app.unmount()
|
||||
root.remove()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
describe('provider quota display components', () => {
|
||||
it('renders monthly quota usage and reset day', () => {
|
||||
const { root, unmount } = mount(ProviderMonthlyQuotaCard, {
|
||||
used: 25,
|
||||
quota: 100,
|
||||
resetDay: 15,
|
||||
})
|
||||
|
||||
expect(root.querySelector('[data-testid="provider-monthly-quota-card"]')).toBeTruthy()
|
||||
expect(root.querySelector('[data-testid="provider-monthly-quota-percent"]')?.textContent).toContain('25.0%')
|
||||
expect(root.querySelector('[data-testid="provider-monthly-quota-amount"]')?.textContent).toContain('$25.00 / $100.00')
|
||||
expect(root.querySelector('[data-testid="provider-monthly-quota-reset"]')?.textContent).toContain('15')
|
||||
|
||||
unmount()
|
||||
})
|
||||
|
||||
it('normalizes quota progress and renders fallback footer text', () => {
|
||||
const { root, unmount } = mount(ProviderQuotaProgressRow, {
|
||||
label: 'Daily',
|
||||
remainingPercent: 120,
|
||||
meterClass: 'text-green-600',
|
||||
barClass: 'bg-green-500',
|
||||
resetText: '2h reset',
|
||||
})
|
||||
|
||||
expect(root.querySelector('[data-testid="provider-quota-progress-meter"]')?.textContent?.trim()).toBe('100.0%')
|
||||
expect((root.querySelector('[data-testid="provider-quota-progress-bar"]') as HTMLElement).style.width).toBe('100%')
|
||||
expect(root.querySelector('[data-testid="provider-quota-progress-reset"]')?.textContent).toBe('2h reset')
|
||||
|
||||
unmount()
|
||||
})
|
||||
|
||||
it('renders section loading and updated state', () => {
|
||||
const Probe = defineComponent({
|
||||
setup() {
|
||||
return () => h(ProviderQuotaSectionHeader, {
|
||||
title: 'Account quota',
|
||||
loading: true,
|
||||
updatedText: '10:30',
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
const { root, unmount } = mount(Probe)
|
||||
|
||||
expect(root.textContent).toContain('Account quota')
|
||||
expect(root.querySelector('[data-testid="provider-quota-header-loading"]')).toBeTruthy()
|
||||
expect(root.querySelector('[data-testid="provider-quota-header-updated"]')?.textContent).toBe('10:30')
|
||||
|
||||
unmount()
|
||||
})
|
||||
})
|
||||
@@ -1,4 +1,7 @@
|
||||
export { default as ProviderFormDialog } from './ProviderFormDialog.vue'
|
||||
export { default as ProviderMonthlyQuotaCard } from './ProviderMonthlyQuotaCard.vue'
|
||||
export { default as ProviderQuotaProgressRow } from './ProviderQuotaProgressRow.vue'
|
||||
export { default as ProviderQuotaSectionHeader } from './ProviderQuotaSectionHeader.vue'
|
||||
export { default as ProviderBatchActionDialog } from './ProviderBatchActionDialog.vue'
|
||||
export { default as EndpointFormDialog } from './EndpointFormDialog.vue'
|
||||
export { default as KeyFormDialog } from './KeyFormDialog.vue'
|
||||
|
||||
Reference in New Issue
Block a user