mirror of
https://gitee.com/wanwujie/sub2api
synced 2026-04-03 06:52:13 +08:00
feat: apikey限额支持查询重置时间
This commit is contained in:
@@ -153,6 +153,7 @@ export default {
|
||||
todayExpires: '(expires today)',
|
||||
daysLeft: '({days} days)',
|
||||
usedQuota: 'Used Quota',
|
||||
resetNow: 'Resetting soon',
|
||||
subscriptionType: 'Subscription Type',
|
||||
subscriptionExpires: 'Subscription Expires',
|
||||
// Usage stat cells
|
||||
@@ -660,6 +661,7 @@ export default {
|
||||
resetRateLimitConfirmMessage: 'Are you sure you want to reset the rate limit usage for key "{name}"? All time window usage will be reset to zero. This action cannot be undone.',
|
||||
rateLimitResetSuccess: 'Rate limit usage reset successfully',
|
||||
failedToResetRateLimit: 'Failed to reset rate limit usage',
|
||||
resetNow: 'Resetting soon',
|
||||
expiration: 'Expiration',
|
||||
expiresInDays: '{days} days',
|
||||
extendDays: '+{days} days',
|
||||
|
||||
@@ -153,6 +153,7 @@ export default {
|
||||
todayExpires: '(今日到期)',
|
||||
daysLeft: '({days} 天)',
|
||||
usedQuota: '已用额度',
|
||||
resetNow: '即将重置',
|
||||
subscriptionType: '订阅类型',
|
||||
subscriptionExpires: '订阅到期',
|
||||
// Usage stat cells
|
||||
@@ -665,6 +666,7 @@ export default {
|
||||
resetRateLimitConfirmMessage: '确定要重置密钥 "{name}" 的速率限制用量吗?所有时间窗口的已用额度将归零。此操作不可撤销。',
|
||||
rateLimitResetSuccess: '速率限制已重置',
|
||||
failedToResetRateLimit: '重置速率限制失败',
|
||||
resetNow: '即将重置',
|
||||
expiration: '密钥有效期',
|
||||
expiresInDays: '{days} 天',
|
||||
extendDays: '+{days} 天',
|
||||
|
||||
@@ -441,6 +441,9 @@ export interface ApiKey {
|
||||
window_5h_start: string | null
|
||||
window_1d_start: string | null
|
||||
window_7d_start: string | null
|
||||
reset_5h_at: string | null
|
||||
reset_1d_at: string | null
|
||||
reset_7d_at: string | null
|
||||
}
|
||||
|
||||
export interface CreateApiKeyRequest {
|
||||
|
||||
@@ -226,6 +226,9 @@
|
||||
class="text-sm font-semibold mt-1 tabular-nums"
|
||||
:style="{ color: RING_GRADIENTS[i % 4].from }"
|
||||
>{{ ring.amount }}</span>
|
||||
<p v-if="ring.resetAt && formatResetTime(ring.resetAt)" class="text-xs text-gray-400 dark:text-gray-500 mt-0.5 tabular-nums">
|
||||
⟳ {{ formatResetTime(ring.resetAt) }}
|
||||
</p>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
@@ -358,7 +361,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, nextTick } from 'vue'
|
||||
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useAppStore } from '@/stores'
|
||||
import LocaleSwitcher from '@/components/common/LocaleSwitcher.vue'
|
||||
@@ -396,6 +399,8 @@ const showLoading = ref(false)
|
||||
const showDatePicker = ref(false)
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const resultData = ref<any>(null)
|
||||
const now = ref(new Date())
|
||||
let resetTimer: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
// ==================== Date Range State ====================
|
||||
|
||||
@@ -461,6 +466,7 @@ interface RingItem {
|
||||
amount: string
|
||||
isBalance?: boolean
|
||||
iconType: 'clock' | 'calendar' | 'dollar'
|
||||
resetAt?: string | null
|
||||
}
|
||||
|
||||
function getRingOffset(ring: RingItem): number {
|
||||
@@ -544,6 +550,7 @@ const ringItems = computed<RingItem[]>(() => {
|
||||
pct,
|
||||
amount: `${usd(rl.used)} / ${usd(rl.limit)}`,
|
||||
iconType: windowIcons[rl.window] || 'clock',
|
||||
resetAt: rl.reset_at,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -627,10 +634,15 @@ const detailRows = computed<DetailRow[]>(() => {
|
||||
const windowMap: Record<string, string> = { '5h': '5H', '1d': locale.value === 'zh' ? '日' : 'D', '7d': '7D' }
|
||||
for (const rl of data.rate_limits) {
|
||||
const pct = rl.limit > 0 ? (rl.used / rl.limit) * 100 : 0
|
||||
let valueStr = `${usd(rl.used)} / ${usd(rl.limit)}`
|
||||
const resetStr = formatResetTime(rl.reset_at)
|
||||
if (resetStr) {
|
||||
valueStr += ` (⟳ ${resetStr})`
|
||||
}
|
||||
rows.push({
|
||||
iconBg: 'bg-primary-500/10', iconColor: 'text-primary-500', iconSvg: ICON_DOLLAR,
|
||||
label: `${t('keyUsage.usedQuota')} (${windowMap[rl.window] || rl.window})`,
|
||||
value: `${usd(rl.used)} / ${usd(rl.limit)}`,
|
||||
value: valueStr,
|
||||
valueClass: getUsageColor(pct),
|
||||
})
|
||||
}
|
||||
@@ -798,11 +810,28 @@ function initTheme() {
|
||||
}
|
||||
}
|
||||
|
||||
function formatResetTime(resetAt: string | null | undefined): string {
|
||||
if (!resetAt) return ''
|
||||
const diff = new Date(resetAt).getTime() - now.value.getTime()
|
||||
if (diff <= 0) return t('keyUsage.resetNow')
|
||||
const days = Math.floor(diff / 86400000)
|
||||
const hours = Math.floor((diff % 86400000) / 3600000)
|
||||
const mins = Math.floor((diff % 3600000) / 60000)
|
||||
if (days > 0) return `${days}d ${hours}h`
|
||||
if (hours > 0) return `${hours}h ${mins}m`
|
||||
return `${mins}m`
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initTheme()
|
||||
if (!appStore.publicSettingsLoaded) {
|
||||
appStore.fetchPublicSettings()
|
||||
}
|
||||
resetTimer = setInterval(() => { now.value = new Date() }, 60000)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (resetTimer) clearInterval(resetTimer)
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -187,6 +187,9 @@
|
||||
:style="{ width: Math.min((row.usage_5h / row.rate_limit_5h) * 100, 100) + '%' }"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="row.reset_5h_at && formatResetTime(row.reset_5h_at)" class="text-[10px] text-gray-400 dark:text-gray-500 tabular-nums">
|
||||
⟳ {{ formatResetTime(row.reset_5h_at) }}
|
||||
</div>
|
||||
</div>
|
||||
<!-- 1d window -->
|
||||
<div v-if="row.rate_limit_1d > 0">
|
||||
@@ -212,6 +215,9 @@
|
||||
:style="{ width: Math.min((row.usage_1d / row.rate_limit_1d) * 100, 100) + '%' }"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="row.reset_1d_at && formatResetTime(row.reset_1d_at)" class="text-[10px] text-gray-400 dark:text-gray-500 tabular-nums">
|
||||
⟳ {{ formatResetTime(row.reset_1d_at) }}
|
||||
</div>
|
||||
</div>
|
||||
<!-- 7d window -->
|
||||
<div v-if="row.rate_limit_7d > 0">
|
||||
@@ -237,6 +243,9 @@
|
||||
:style="{ width: Math.min((row.usage_7d / row.rate_limit_7d) * 100, 100) + '%' }"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="row.reset_7d_at && formatResetTime(row.reset_7d_at)" class="text-[10px] text-gray-400 dark:text-gray-500 tabular-nums">
|
||||
⟳ {{ formatResetTime(row.reset_7d_at) }}
|
||||
</div>
|
||||
</div>
|
||||
<!-- Reset button -->
|
||||
<button
|
||||
@@ -1085,6 +1094,8 @@ const apiKeys = ref<ApiKey[]>([])
|
||||
const groups = ref<Group[]>([])
|
||||
const loading = ref(false)
|
||||
const submitting = ref(false)
|
||||
const now = ref(new Date())
|
||||
let resetTimer: ReturnType<typeof setInterval> | null = null
|
||||
const usageStats = ref<Record<string, BatchApiKeyUsageStats>>({})
|
||||
const userGroupRates = ref<Record<number, number>>({})
|
||||
|
||||
@@ -1745,15 +1756,29 @@ const closeCcsClientSelect = () => {
|
||||
pendingCcsRow.value = null
|
||||
}
|
||||
|
||||
function formatResetTime(resetAt: string | null): string {
|
||||
if (!resetAt) return ''
|
||||
const diff = new Date(resetAt).getTime() - now.value.getTime()
|
||||
if (diff <= 0) return t('keys.resetNow')
|
||||
const days = Math.floor(diff / 86400000)
|
||||
const hours = Math.floor((diff % 86400000) / 3600000)
|
||||
const mins = Math.floor((diff % 3600000) / 60000)
|
||||
if (days > 0) return `${days}d ${hours}h`
|
||||
if (hours > 0) return `${hours}h ${mins}m`
|
||||
return `${mins}m`
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadApiKeys()
|
||||
loadGroups()
|
||||
loadUserGroupRates()
|
||||
loadPublicSettings()
|
||||
document.addEventListener('click', closeGroupSelector)
|
||||
resetTimer = setInterval(() => { now.value = new Date() }, 60000)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('click', closeGroupSelector)
|
||||
if (resetTimer) clearInterval(resetTimer)
|
||||
})
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user