refactor(frontend): 优化 Gemini 配额显示,参考 Antigravity 样式
- 简化标签:将 "RPD Pro/Flash" 改为 "Pro/Flash",避免文字截断 - 添加账号类型徽章(Free/Pro/Ultra),带颜色区分 - 添加帮助图标(?),悬停显示限流政策和官方文档链接 - 重构显示布局:账号类型 + 两行配额(Pro/Flash) - 移除冗余的 AccountQuotaInfo 组件调用
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -77,6 +77,7 @@ temp/
|
|||||||
*.temp
|
*.temp
|
||||||
*.log
|
*.log
|
||||||
*.bak
|
*.bak
|
||||||
|
.cache/
|
||||||
|
|
||||||
# ===================
|
# ===================
|
||||||
# 构建产物
|
# 构建产物
|
||||||
|
|||||||
@@ -202,22 +202,25 @@ func (s *AccountUsageService) GetUsage(ctx context.Context, accountID int64) (*U
|
|||||||
|
|
||||||
func (s *AccountUsageService) getGeminiUsage(ctx context.Context, account *Account) (*UsageInfo, error) {
|
func (s *AccountUsageService) getGeminiUsage(ctx context.Context, account *Account) (*UsageInfo, error) {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
start := geminiDailyWindowStart(now)
|
|
||||||
|
|
||||||
stats, err := s.usageLogRepo.GetModelStatsWithFilters(ctx, start, now, 0, 0, account.ID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("get gemini usage stats failed: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
usage := &UsageInfo{
|
usage := &UsageInfo{
|
||||||
UpdatedAt: &now,
|
UpdatedAt: &now,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if s.geminiQuotaService == nil || s.usageLogRepo == nil {
|
||||||
|
return usage, nil
|
||||||
|
}
|
||||||
|
|
||||||
quota, ok := s.geminiQuotaService.QuotaForAccount(ctx, account)
|
quota, ok := s.geminiQuotaService.QuotaForAccount(ctx, account)
|
||||||
if !ok {
|
if !ok {
|
||||||
return usage, nil
|
return usage, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
start := geminiDailyWindowStart(now)
|
||||||
|
stats, err := s.usageLogRepo.GetModelStatsWithFilters(ctx, start, now, 0, 0, account.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("get gemini usage stats failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
totals := geminiAggregateUsage(stats)
|
totals := geminiAggregateUsage(stats)
|
||||||
resetAt := geminiDailyResetTime(now)
|
resetAt := geminiDailyResetTime(now)
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Wei-Shaw/sub2api/internal/config"
|
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||||
@@ -17,8 +18,18 @@ type RateLimitService struct {
|
|||||||
usageRepo UsageLogRepository
|
usageRepo UsageLogRepository
|
||||||
cfg *config.Config
|
cfg *config.Config
|
||||||
geminiQuotaService *GeminiQuotaService
|
geminiQuotaService *GeminiQuotaService
|
||||||
|
usageCacheMu sync.Mutex
|
||||||
|
usageCache map[int64]*geminiUsageCacheEntry
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type geminiUsageCacheEntry struct {
|
||||||
|
windowStart time.Time
|
||||||
|
cachedAt time.Time
|
||||||
|
totals GeminiUsageTotals
|
||||||
|
}
|
||||||
|
|
||||||
|
const geminiPrecheckCacheTTL = time.Minute
|
||||||
|
|
||||||
// NewRateLimitService 创建RateLimitService实例
|
// NewRateLimitService 创建RateLimitService实例
|
||||||
func NewRateLimitService(accountRepo AccountRepository, usageRepo UsageLogRepository, cfg *config.Config, geminiQuotaService *GeminiQuotaService) *RateLimitService {
|
func NewRateLimitService(accountRepo AccountRepository, usageRepo UsageLogRepository, cfg *config.Config, geminiQuotaService *GeminiQuotaService) *RateLimitService {
|
||||||
return &RateLimitService{
|
return &RateLimitService{
|
||||||
@@ -26,6 +37,7 @@ func NewRateLimitService(accountRepo AccountRepository, usageRepo UsageLogReposi
|
|||||||
usageRepo: usageRepo,
|
usageRepo: usageRepo,
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
geminiQuotaService: geminiQuotaService,
|
geminiQuotaService: geminiQuotaService,
|
||||||
|
usageCache: make(map[int64]*geminiUsageCacheEntry),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,7 +85,7 @@ func (s *RateLimitService) PreCheckUsage(ctx context.Context, account *Account,
|
|||||||
if account == nil || !account.IsGeminiCodeAssist() || strings.TrimSpace(requestedModel) == "" {
|
if account == nil || !account.IsGeminiCodeAssist() || strings.TrimSpace(requestedModel) == "" {
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
if s.usageRepo == nil {
|
if s.usageRepo == nil || s.geminiQuotaService == nil {
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,11 +107,15 @@ func (s *RateLimitService) PreCheckUsage(ctx context.Context, account *Account,
|
|||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
start := geminiDailyWindowStart(now)
|
start := geminiDailyWindowStart(now)
|
||||||
stats, err := s.usageRepo.GetModelStatsWithFilters(ctx, start, now, 0, 0, account.ID)
|
totals, ok := s.getGeminiUsageTotals(account.ID, start, now)
|
||||||
if err != nil {
|
if !ok {
|
||||||
return true, err
|
stats, err := s.usageRepo.GetModelStatsWithFilters(ctx, start, now, 0, 0, account.ID)
|
||||||
|
if err != nil {
|
||||||
|
return true, err
|
||||||
|
}
|
||||||
|
totals = geminiAggregateUsage(stats)
|
||||||
|
s.setGeminiUsageTotals(account.ID, start, now, totals)
|
||||||
}
|
}
|
||||||
totals := geminiAggregateUsage(stats)
|
|
||||||
|
|
||||||
var used int64
|
var used int64
|
||||||
switch geminiModelClassFromName(requestedModel) {
|
switch geminiModelClassFromName(requestedModel) {
|
||||||
@@ -121,6 +137,40 @@ func (s *RateLimitService) PreCheckUsage(ctx context.Context, account *Account,
|
|||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *RateLimitService) getGeminiUsageTotals(accountID int64, windowStart, now time.Time) (GeminiUsageTotals, bool) {
|
||||||
|
s.usageCacheMu.Lock()
|
||||||
|
defer s.usageCacheMu.Unlock()
|
||||||
|
|
||||||
|
if s.usageCache == nil {
|
||||||
|
return GeminiUsageTotals{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
entry, ok := s.usageCache[accountID]
|
||||||
|
if !ok || entry == nil {
|
||||||
|
return GeminiUsageTotals{}, false
|
||||||
|
}
|
||||||
|
if !entry.windowStart.Equal(windowStart) {
|
||||||
|
return GeminiUsageTotals{}, false
|
||||||
|
}
|
||||||
|
if now.Sub(entry.cachedAt) >= geminiPrecheckCacheTTL {
|
||||||
|
return GeminiUsageTotals{}, false
|
||||||
|
}
|
||||||
|
return entry.totals, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *RateLimitService) setGeminiUsageTotals(accountID int64, windowStart, now time.Time, totals GeminiUsageTotals) {
|
||||||
|
s.usageCacheMu.Lock()
|
||||||
|
defer s.usageCacheMu.Unlock()
|
||||||
|
if s.usageCache == nil {
|
||||||
|
s.usageCache = make(map[int64]*geminiUsageCacheEntry)
|
||||||
|
}
|
||||||
|
s.usageCache[accountID] = &geminiUsageCacheEntry{
|
||||||
|
windowStart: windowStart,
|
||||||
|
cachedAt: now,
|
||||||
|
totals: totals,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// GeminiCooldown returns the fallback cooldown duration for Gemini 429s based on tier.
|
// GeminiCooldown returns the fallback cooldown duration for Gemini 429s based on tier.
|
||||||
func (s *RateLimitService) GeminiCooldown(ctx context.Context, account *Account) time.Duration {
|
func (s *RateLimitService) GeminiCooldown(ctx context.Context, account *Account) time.Duration {
|
||||||
if account == nil {
|
if account == nil {
|
||||||
|
|||||||
@@ -26,5 +26,10 @@ UPDATE accounts
|
|||||||
SET credentials = credentials - 'tier_id'
|
SET credentials = credentials - 'tier_id'
|
||||||
WHERE platform = 'gemini'
|
WHERE platform = 'gemini'
|
||||||
AND type = 'oauth'
|
AND type = 'oauth'
|
||||||
AND credentials->>'oauth_type' = 'code_assist';
|
AND jsonb_typeof(credentials) = 'object'
|
||||||
|
AND credentials->>'tier_id' = 'LEGACY'
|
||||||
|
AND (
|
||||||
|
credentials->>'oauth_type' = 'code_assist'
|
||||||
|
OR (credentials->>'oauth_type' IS NULL AND credentials->>'project_id' IS NOT NULL)
|
||||||
|
);
|
||||||
-- +goose StatementEnd
|
-- +goose StatementEnd
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
v-if="!isRateLimited"
|
v-if="!isRateLimited"
|
||||||
class="text-xs text-gray-400 dark:text-gray-500"
|
class="text-xs text-gray-400 dark:text-gray-500"
|
||||||
>
|
>
|
||||||
未限流
|
{{ t('admin.accounts.gemini.rateLimit.ok') }}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
v-else
|
v-else
|
||||||
@@ -21,19 +21,22 @@
|
|||||||
: 'text-amber-600 dark:text-amber-400'
|
: 'text-amber-600 dark:text-amber-400'
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
限流 {{ resetCountdown }}
|
{{ t('admin.accounts.gemini.rateLimit.limited', { time: resetCountdown }) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, watch, onUnmounted } from 'vue'
|
import { computed, ref, watch, onUnmounted } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
import type { Account, GeminiCredentials } from '@/types'
|
import type { Account, GeminiCredentials } from '@/types'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
account: Account
|
account: Account
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
const now = ref(new Date())
|
const now = ref(new Date())
|
||||||
let timer: ReturnType<typeof setInterval> | null = null
|
let timer: ReturnType<typeof setInterval> | null = null
|
||||||
|
|
||||||
@@ -98,7 +101,7 @@ const resetCountdown = computed(() => {
|
|||||||
if (Number.isNaN(resetTime)) return '-'
|
if (Number.isNaN(resetTime)) return '-'
|
||||||
|
|
||||||
const diffMs = resetTime - now.value.getTime()
|
const diffMs = resetTime - now.value.getTime()
|
||||||
if (diffMs <= 0) return 'now'
|
if (diffMs <= 0) return t('admin.accounts.gemini.rateLimit.now')
|
||||||
|
|
||||||
const diffSeconds = Math.floor(diffMs / 1000)
|
const diffSeconds = Math.floor(diffMs / 1000)
|
||||||
const diffMinutes = Math.floor(diffSeconds / 60)
|
const diffMinutes = Math.floor(diffSeconds / 60)
|
||||||
|
|||||||
@@ -171,8 +171,50 @@
|
|||||||
|
|
||||||
<!-- Gemini platform: show quota + local usage window -->
|
<!-- Gemini platform: show quota + local usage window -->
|
||||||
<template v-else-if="account.platform === 'gemini'">
|
<template v-else-if="account.platform === 'gemini'">
|
||||||
|
<!-- 账户类型徽章 -->
|
||||||
|
<div v-if="geminiTierLabel" class="mb-1 flex items-center gap-1">
|
||||||
|
<span
|
||||||
|
:class="[
|
||||||
|
'inline-block rounded px-1.5 py-0.5 text-[10px] font-medium',
|
||||||
|
geminiTierClass
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
{{ geminiTierLabel }}
|
||||||
|
</span>
|
||||||
|
<!-- 帮助图标 -->
|
||||||
|
<span
|
||||||
|
class="group relative cursor-help"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="h-3.5 w-3.5 text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-8-3a1 1 0 00-.867.5 1 1 0 11-1.731-1A3 3 0 0113 8a3.001 3.001 0 01-2 2.83V11a1 1 0 11-2 0v-1a1 1 0 011-1 1 1 0 100-2zm0 8a1 1 0 100-2 1 1 0 000 2z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span
|
||||||
|
class="pointer-events-none absolute left-0 top-full z-50 mt-1 w-80 whitespace-normal break-words rounded bg-gray-900 px-3 py-2 text-xs leading-relaxed text-white opacity-0 shadow-lg transition-opacity group-hover:opacity-100 dark:bg-gray-700"
|
||||||
|
>
|
||||||
|
<div class="font-semibold mb-1">{{ t('admin.accounts.gemini.quotaPolicy.title') }}</div>
|
||||||
|
<div class="mb-2 text-gray-300">{{ t('admin.accounts.gemini.quotaPolicy.note') }}</div>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<div><strong>{{ geminiQuotaPolicyChannel }}:</strong></div>
|
||||||
|
<div class="pl-2">• {{ geminiQuotaPolicyLimits }}</div>
|
||||||
|
<div class="mt-2">
|
||||||
|
<a :href="geminiQuotaPolicyDocsUrl" target="_blank" class="text-blue-400 hover:text-blue-300 underline">
|
||||||
|
{{ t('admin.accounts.gemini.quotaPolicy.columns.docs') }} →
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
<AccountQuotaInfo :account="account" />
|
|
||||||
<div v-if="loading" class="space-y-1">
|
<div v-if="loading" class="space-y-1">
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<div class="h-3 w-[32px] animate-pulse rounded bg-gray-200 dark:bg-gray-700"></div>
|
<div class="h-3 w-[32px] animate-pulse rounded bg-gray-200 dark:bg-gray-700"></div>
|
||||||
@@ -227,7 +269,7 @@
|
|||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { adminAPI } from '@/api/admin'
|
import { adminAPI } from '@/api/admin'
|
||||||
import type { Account, AccountUsageInfo } from '@/types'
|
import type { Account, AccountUsageInfo, GeminiCredentials } from '@/types'
|
||||||
import UsageProgressBar from './UsageProgressBar.vue'
|
import UsageProgressBar from './UsageProgressBar.vue'
|
||||||
import AccountQuotaInfo from './AccountQuotaInfo.vue'
|
import AccountQuotaInfo from './AccountQuotaInfo.vue'
|
||||||
|
|
||||||
@@ -509,6 +551,71 @@ const antigravityTier = computed(() => {
|
|||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Gemini 账户类型(从 credentials 中提取)
|
||||||
|
const geminiTier = computed(() => {
|
||||||
|
if (props.account.platform !== 'gemini') return null
|
||||||
|
const creds = props.account.credentials as GeminiCredentials | undefined
|
||||||
|
return creds?.tier_id || null
|
||||||
|
})
|
||||||
|
|
||||||
|
// Gemini 是否为 Code Assist OAuth
|
||||||
|
const isGeminiCodeAssist = computed(() => {
|
||||||
|
if (props.account.platform !== 'gemini') return false
|
||||||
|
const creds = props.account.credentials as GeminiCredentials | undefined
|
||||||
|
return creds?.oauth_type === 'code_assist' || (!creds?.oauth_type && !!creds?.project_id)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Gemini 账户类型显示标签
|
||||||
|
const geminiTierLabel = computed(() => {
|
||||||
|
if (!geminiTier.value) return null
|
||||||
|
const tierMap: Record<string, string> = {
|
||||||
|
LEGACY: t('admin.accounts.tier.free'),
|
||||||
|
PRO: t('admin.accounts.tier.pro'),
|
||||||
|
ULTRA: t('admin.accounts.tier.ultra')
|
||||||
|
}
|
||||||
|
return tierMap[geminiTier.value] || null
|
||||||
|
})
|
||||||
|
|
||||||
|
// Gemini 账户类型徽章样式
|
||||||
|
const geminiTierClass = computed(() => {
|
||||||
|
switch (geminiTier.value) {
|
||||||
|
case 'LEGACY':
|
||||||
|
return 'bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300'
|
||||||
|
case 'PRO':
|
||||||
|
return 'bg-blue-100 text-blue-600 dark:bg-blue-900/40 dark:text-blue-300'
|
||||||
|
case 'ULTRA':
|
||||||
|
return 'bg-purple-100 text-purple-600 dark:bg-purple-900/40 dark:text-purple-300'
|
||||||
|
default:
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Gemini 配额政策信息
|
||||||
|
const geminiQuotaPolicyChannel = computed(() => {
|
||||||
|
if (isGeminiCodeAssist.value) {
|
||||||
|
return t('admin.accounts.gemini.quotaPolicy.rows.cli.channel')
|
||||||
|
}
|
||||||
|
return t('admin.accounts.gemini.quotaPolicy.rows.aiStudio.channel')
|
||||||
|
})
|
||||||
|
|
||||||
|
const geminiQuotaPolicyLimits = computed(() => {
|
||||||
|
if (isGeminiCodeAssist.value) {
|
||||||
|
if (geminiTier.value === 'PRO' || geminiTier.value === 'ULTRA') {
|
||||||
|
return t('admin.accounts.gemini.quotaPolicy.rows.cli.limitsPremium')
|
||||||
|
}
|
||||||
|
return t('admin.accounts.gemini.quotaPolicy.rows.cli.limitsFree')
|
||||||
|
}
|
||||||
|
// AI Studio - 默认显示免费层限制
|
||||||
|
return t('admin.accounts.gemini.quotaPolicy.rows.aiStudio.limitsFree')
|
||||||
|
})
|
||||||
|
|
||||||
|
const geminiQuotaPolicyDocsUrl = computed(() => {
|
||||||
|
if (isGeminiCodeAssist.value) {
|
||||||
|
return 'https://cloud.google.com/products/gemini/code-assist#pricing'
|
||||||
|
}
|
||||||
|
return 'https://ai.google.dev/pricing'
|
||||||
|
})
|
||||||
|
|
||||||
// 账户类型显示标签
|
// 账户类型显示标签
|
||||||
const antigravityTierLabel = computed(() => {
|
const antigravityTierLabel = computed(() => {
|
||||||
switch (antigravityTier.value) {
|
switch (antigravityTier.value) {
|
||||||
|
|||||||
@@ -1208,6 +1208,11 @@ export default {
|
|||||||
limitsPaid: 'RPD unlimited; RPM 1000+ (project quota)'
|
limitsPaid: 'RPD unlimited; RPM 1000+ (project quota)'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
rateLimit: {
|
||||||
|
ok: 'Not rate limited',
|
||||||
|
limited: 'Rate limited {time}',
|
||||||
|
now: 'now'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// Re-Auth Modal
|
// Re-Auth Modal
|
||||||
@@ -1275,8 +1280,8 @@ export default {
|
|||||||
usageWindow: {
|
usageWindow: {
|
||||||
statsTitle: '5-Hour Window Usage Statistics',
|
statsTitle: '5-Hour Window Usage Statistics',
|
||||||
statsTitleDaily: 'Daily Usage Statistics',
|
statsTitleDaily: 'Daily Usage Statistics',
|
||||||
geminiProDaily: 'RPD Pro',
|
geminiProDaily: 'Pro',
|
||||||
geminiFlashDaily: 'RPD Flash',
|
geminiFlashDaily: 'Flash',
|
||||||
gemini3Pro: 'G3P',
|
gemini3Pro: 'G3P',
|
||||||
gemini3Flash: 'G3F',
|
gemini3Flash: 'G3F',
|
||||||
gemini3Image: 'G3I',
|
gemini3Image: 'G3I',
|
||||||
|
|||||||
@@ -986,8 +986,8 @@ export default {
|
|||||||
usageWindow: {
|
usageWindow: {
|
||||||
statsTitle: '5小时窗口用量统计',
|
statsTitle: '5小时窗口用量统计',
|
||||||
statsTitleDaily: '每日用量统计',
|
statsTitleDaily: '每日用量统计',
|
||||||
geminiProDaily: 'RPD Pro',
|
geminiProDaily: 'Pro',
|
||||||
geminiFlashDaily: 'RPD Flash',
|
geminiFlashDaily: 'Flash',
|
||||||
gemini3Pro: 'G3P',
|
gemini3Pro: 'G3P',
|
||||||
gemini3Flash: 'G3F',
|
gemini3Flash: 'G3F',
|
||||||
gemini3Image: 'G3I',
|
gemini3Image: 'G3I',
|
||||||
@@ -1342,6 +1342,11 @@ export default {
|
|||||||
limitsPaid: 'RPD 不限;RPM 1000+(项目配额)'
|
limitsPaid: 'RPD 不限;RPM 1000+(项目配额)'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
rateLimit: {
|
||||||
|
ok: '未限流',
|
||||||
|
limited: '限流 {time}',
|
||||||
|
now: '现在'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// Re-Auth Modal
|
// Re-Auth Modal
|
||||||
|
|||||||
Reference in New Issue
Block a user