恢复并保存本地修改:仪表盘服务账户分类显示、WindowCountdown组件等功能

This commit is contained in:
shaw
2025-08-08 11:56:24 +08:00
parent 5bed33cd9c
commit 4adc8d9695
8 changed files with 329 additions and 214 deletions

View File

@@ -1828,15 +1828,15 @@ const handleGroupRefresh = async () => {
// 监听平台变化,重置表单
watch(
() => form.value.platform,
(newPlatform, oldPlatform) => {
(newPlatform) => {
// 处理添加方式的自动切换
if (newPlatform === 'claude-console' || newPlatform === 'bedrock') {
form.value.addType = 'manual' // Claude Console 和 Bedrock 只支持手动模式
} else if (
oldPlatform === 'claude-console' &&
(newPlatform === 'claude' || newPlatform === 'gemini')
) {
// 从 Claude Console 切换到其他平台时,恢复为 OAuth
} else if (newPlatform === 'claude') {
// 切换到 Claude 时,使用 Setup Token 作为默认方式
form.value.addType = 'setup-token'
} else if (newPlatform === 'gemini') {
// 切换到 Gemini 时,使用 OAuth 作为默认方式
form.value.addType = 'oauth'
}

View File

@@ -183,47 +183,23 @@
</div>
<div v-if="apiKey.rateLimitWindow > 0" class="space-y-2">
<div class="flex items-center justify-between text-sm">
<span class="text-gray-600">时间窗口</span>
<span class="font-semibold text-indigo-600">
{{ apiKey.rateLimitWindow }} 分钟
</span>
</div>
<!-- 请求次数限制 -->
<div v-if="apiKey.rateLimitRequests > 0" class="space-y-1">
<div class="flex items-center justify-between text-sm">
<span class="text-gray-600">请求限制</span>
<span class="font-semibold text-gray-900">
{{ apiKey.currentWindowRequests || 0 }} / {{ apiKey.rateLimitRequests }}
</span>
</div>
<div class="h-2 w-full rounded-full bg-gray-200">
<div
class="h-2 rounded-full transition-all duration-300"
:class="windowRequestProgressColor"
:style="{ width: windowRequestProgress + '%' }"
/>
</div>
</div>
<!-- Token使用量限制 -->
<div v-if="apiKey.tokenLimit > 0" class="space-y-1">
<div class="flex items-center justify-between text-sm">
<span class="text-gray-600">Token限制</span>
<span class="font-semibold text-gray-900">
{{ formatTokenCount(apiKey.currentWindowTokens || 0) }} /
{{ formatTokenCount(apiKey.tokenLimit) }}
</span>
</div>
<div class="h-2 w-full rounded-full bg-gray-200">
<div
class="h-2 rounded-full transition-all duration-300"
:class="windowTokenProgressColor"
:style="{ width: windowTokenProgress + '%' }"
/>
</div>
</div>
<h5 class="text-sm font-medium text-gray-700">
<i class="fas fa-clock mr-1 text-blue-500" />
时间窗口限制
</h5>
<WindowCountdown
:current-requests="apiKey.currentWindowRequests"
:current-tokens="apiKey.currentWindowTokens"
label="窗口状态"
:rate-limit-window="apiKey.rateLimitWindow"
:request-limit="apiKey.rateLimitRequests"
:show-progress="true"
:show-tooltip="true"
:token-limit="apiKey.tokenLimit"
:window-end-time="apiKey.windowEndTime"
:window-remaining-seconds="apiKey.windowRemainingSeconds"
:window-start-time="apiKey.windowStartTime"
/>
</div>
</div>
</div>
@@ -242,6 +218,7 @@
<script setup>
import { computed } from 'vue'
import WindowCountdown from './WindowCountdown.vue'
const props = defineProps({
show: {
@@ -284,35 +261,6 @@ const dailyCostPercentage = computed(() => {
return (dailyCost.value / props.apiKey.dailyCostLimit) * 100
})
// 窗口请求进度
const windowRequestProgress = computed(() => {
if (!props.apiKey.rateLimitRequests || props.apiKey.rateLimitRequests === 0) return 0
const percentage =
((props.apiKey.currentWindowRequests || 0) / props.apiKey.rateLimitRequests) * 100
return Math.min(percentage, 100)
})
const windowRequestProgressColor = computed(() => {
const progress = windowRequestProgress.value
if (progress >= 100) return 'bg-red-500'
if (progress >= 80) return 'bg-yellow-500'
return 'bg-blue-500'
})
// 窗口Token进度
const windowTokenProgress = computed(() => {
if (!props.apiKey.tokenLimit || props.apiKey.tokenLimit === 0) return 0
const percentage = ((props.apiKey.currentWindowTokens || 0) / props.apiKey.tokenLimit) * 100
return Math.min(percentage, 100)
})
const windowTokenProgressColor = computed(() => {
const progress = windowTokenProgress.value
if (progress >= 100) return 'bg-red-500'
if (progress >= 80) return 'bg-yellow-500'
return 'bg-purple-500'
})
// 方法
const formatNumber = (num) => {
if (!num && num !== 0) return '0'

View File

@@ -10,8 +10,17 @@ export const useDashboardStore = defineStore('dashboard', () => {
totalApiKeys: 0,
activeApiKeys: 0,
totalAccounts: 0,
activeAccounts: 0,
normalAccounts: 0,
abnormalAccounts: 0,
pausedAccounts: 0,
activeAccounts: 0, // 保留兼容
rateLimitedAccounts: 0,
accountsByPlatform: {
claude: { total: 0, normal: 0, abnormal: 0, paused: 0, rateLimited: 0 },
'claude-console': { total: 0, normal: 0, abnormal: 0, paused: 0, rateLimited: 0 },
gemini: { total: 0, normal: 0, abnormal: 0, paused: 0, rateLimited: 0 },
bedrock: { total: 0, normal: 0, abnormal: 0, paused: 0, rateLimited: 0 }
},
todayRequests: 0,
totalRequests: 0,
todayTokens: 0,
@@ -152,9 +161,21 @@ export const useDashboardStore = defineStore('dashboard', () => {
dashboardData.value = {
totalApiKeys: overview.totalApiKeys || 0,
activeApiKeys: overview.activeApiKeys || 0,
totalAccounts: overview.totalClaudeAccounts || 0,
activeAccounts: overview.activeClaudeAccounts || 0,
rateLimitedAccounts: overview.rateLimitedClaudeAccounts || 0,
// 使用新的统一统计字段
totalAccounts: overview.totalAccounts || overview.totalClaudeAccounts || 0,
normalAccounts: overview.normalAccounts || 0,
abnormalAccounts: overview.abnormalAccounts || 0,
pausedAccounts: overview.pausedAccounts || 0,
activeAccounts: overview.activeAccounts || overview.activeClaudeAccounts || 0, // 兼容
rateLimitedAccounts:
overview.rateLimitedAccounts || overview.rateLimitedClaudeAccounts || 0,
// 各平台详细统计
accountsByPlatform: overview.accountsByPlatform || {
claude: { total: 0, normal: 0, abnormal: 0, paused: 0, rateLimited: 0 },
'claude-console': { total: 0, normal: 0, abnormal: 0, paused: 0, rateLimited: 0 },
gemini: { total: 0, normal: 0, abnormal: 0, paused: 0, rateLimited: 0 },
bedrock: { total: 0, normal: 0, abnormal: 0, paused: 0, rateLimited: 0 }
},
todayRequests: recentActivity.requestsToday || 0,
totalRequests: overview.totalRequestsUsed || 0,
todayTokens: recentActivity.tokensToday || 0,

View File

@@ -254,48 +254,19 @@
</div>
<!-- 时间窗口限制进度条 -->
<div v-if="key.rateLimitWindow > 0" class="space-y-1">
<div class="flex items-center justify-between text-xs">
<span class="text-gray-500">窗口限制</span>
<span class="text-gray-700"> {{ key.rateLimitWindow }}分钟 </span>
</div>
<!-- 请求次数限制 -->
<div v-if="key.rateLimitRequests > 0" class="space-y-0.5">
<div class="flex items-center justify-between text-xs">
<span class="text-gray-400">请求</span>
<span class="text-gray-600">
{{ key.currentWindowRequests || 0 }}/{{ key.rateLimitRequests }}
</span>
</div>
<div class="h-1 w-full rounded-full bg-gray-200">
<div
class="h-1 rounded-full transition-all duration-300"
:class="getWindowRequestProgressColor(key)"
:style="{ width: getWindowRequestProgress(key) + '%' }"
/>
</div>
</div>
<!-- Token使用量限制 -->
<div v-if="key.tokenLimit > 0" class="space-y-0.5">
<div class="flex items-center justify-between text-xs">
<span class="text-gray-400">Token</span>
<span class="text-gray-600">
{{ formatTokenCount(key.currentWindowTokens || 0) }}/{{
formatTokenCount(key.tokenLimit)
}}
</span>
</div>
<div class="h-1 w-full rounded-full bg-gray-200">
<div
class="h-1 rounded-full transition-all duration-300"
:class="getWindowTokenProgressColor(key)"
:style="{ width: getWindowTokenProgress(key) + '%' }"
/>
</div>
</div>
</div>
<WindowCountdown
v-if="key.rateLimitWindow > 0"
:current-requests="key.currentWindowRequests"
:current-tokens="key.currentWindowTokens"
:rate-limit-window="key.rateLimitWindow"
:request-limit="key.rateLimitRequests"
:show-progress="true"
:show-tooltip="false"
:token-limit="key.tokenLimit"
:window-end-time="key.windowEndTime"
:window-remaining-seconds="key.windowRemainingSeconds"
:window-start-time="key.windowStartTime"
/>
<!-- 查看详情按钮 -->
<div class="pt-1">
@@ -743,46 +714,19 @@
</div>
<!-- 移动端时间窗口限制 -->
<div
<WindowCountdown
v-if="key.rateLimitWindow > 0 && (key.rateLimitRequests > 0 || key.tokenLimit > 0)"
class="space-y-1"
>
<div class="mb-1 text-xs text-gray-500">窗口限制 ({{ key.rateLimitWindow }}分钟)</div>
<div v-if="key.rateLimitRequests > 0" class="flex items-center gap-2">
<span class="w-10 text-xs text-gray-500">请求</span>
<div class="flex-1">
<div class="h-1.5 w-full rounded-full bg-gray-200">
<div
class="h-1.5 rounded-full transition-all duration-300"
:class="getWindowRequestProgressColor(key)"
:style="{ width: getWindowRequestProgress(key) + '%' }"
/>
</div>
</div>
<span class="w-16 text-right text-xs text-gray-600">
{{ key.currentWindowRequests || 0 }}/{{ key.rateLimitRequests }}
</span>
</div>
<div v-if="key.tokenLimit > 0" class="flex items-center gap-2">
<span class="w-10 text-xs text-gray-500">Token</span>
<div class="flex-1">
<div class="h-1.5 w-full rounded-full bg-gray-200">
<div
class="h-1.5 rounded-full transition-all duration-300"
:class="getWindowTokenProgressColor(key)"
:style="{ width: getWindowTokenProgress(key) + '%' }"
/>
</div>
</div>
<span class="w-16 text-right text-xs text-gray-600">
{{ formatTokenCount(key.currentWindowTokens || 0) }}/{{
formatTokenCount(key.tokenLimit)
}}
</span>
</div>
</div>
:current-requests="key.currentWindowRequests"
:current-tokens="key.currentWindowTokens"
:rate-limit-window="key.rateLimitWindow"
:request-limit="key.rateLimitRequests"
:show-progress="true"
:show-tooltip="false"
:token-limit="key.tokenLimit"
:window-end-time="key.windowEndTime"
:window-remaining-seconds="key.windowRemainingSeconds"
:window-start-time="key.windowStartTime"
/>
</div>
<!-- 时间信息 -->
@@ -1019,6 +963,7 @@ import NewApiKeyModal from '@/components/apikeys/NewApiKeyModal.vue'
import BatchApiKeyModal from '@/components/apikeys/BatchApiKeyModal.vue'
import ExpiryEditModal from '@/components/apikeys/ExpiryEditModal.vue'
import UsageDetailModal from '@/components/apikeys/UsageDetailModal.vue'
import WindowCountdown from '@/components/apikeys/WindowCountdown.vue'
// 响应式数据
const clientsStore = useClientsStore()
@@ -1662,36 +1607,6 @@ const formatTokenCount = (count) => {
return count.toString()
}
// 获取窗口请求进度
const getWindowRequestProgress = (key) => {
if (!key.rateLimitRequests || key.rateLimitRequests === 0) return 0
const percentage = ((key.currentWindowRequests || 0) / key.rateLimitRequests) * 100
return Math.min(percentage, 100)
}
// 获取窗口请求进度条颜色
const getWindowRequestProgressColor = (key) => {
const progress = getWindowRequestProgress(key)
if (progress >= 100) return 'bg-red-500'
if (progress >= 80) return 'bg-yellow-500'
return 'bg-blue-500'
}
// 获取窗口Token进度
const getWindowTokenProgress = (key) => {
if (!key.tokenLimit || key.tokenLimit === 0) return 0
const percentage = ((key.currentWindowTokens || 0) / key.tokenLimit) * 100
return Math.min(percentage, 100)
}
// 获取窗口Token进度条颜色
const getWindowTokenProgressColor = (key) => {
const progress = getWindowTokenProgress(key)
if (progress >= 100) return 'bg-red-500'
if (progress >= 80) return 'bg-yellow-500'
return 'bg-purple-500'
}
// 监听筛选条件变化,重置页码
watch([selectedTagFilter, apiKeyStatsTimeRange], () => {
currentPage.value = 1

View File

@@ -21,19 +21,86 @@
<div class="stat-card">
<div class="flex items-center justify-between">
<div>
<div class="flex-1">
<p class="mb-1 text-xs font-semibold text-gray-600 sm:text-sm">服务账户</p>
<p class="text-2xl font-bold text-gray-900 sm:text-3xl">
{{ dashboardData.totalAccounts }}
</p>
<div class="flex flex-wrap items-baseline gap-x-2">
<p class="text-2xl font-bold text-gray-900 sm:text-3xl">
{{ dashboardData.totalAccounts }}
</p>
<!-- 各平台账户数量展示 -->
<div v-if="dashboardData.accountsByPlatform" class="flex items-center gap-2">
<!-- Claude账户 -->
<div
v-if="
dashboardData.accountsByPlatform.claude &&
dashboardData.accountsByPlatform.claude.total > 0
"
class="inline-flex items-center gap-0.5"
:title="`Claude: ${dashboardData.accountsByPlatform.claude.total} 个 (正常: ${dashboardData.accountsByPlatform.claude.normal})`"
>
<i class="fas fa-brain text-xs text-indigo-600" />
<span class="text-xs font-medium text-gray-700">{{
dashboardData.accountsByPlatform.claude.total
}}</span>
</div>
<!-- Claude Console账户 -->
<div
v-if="
dashboardData.accountsByPlatform['claude-console'] &&
dashboardData.accountsByPlatform['claude-console'].total > 0
"
class="inline-flex items-center gap-0.5"
:title="`Console: ${dashboardData.accountsByPlatform['claude-console'].total} 个 (正常: ${dashboardData.accountsByPlatform['claude-console'].normal})`"
>
<i class="fas fa-terminal text-xs text-purple-600" />
<span class="text-xs font-medium text-gray-700">{{
dashboardData.accountsByPlatform['claude-console'].total
}}</span>
</div>
<!-- Gemini账户 -->
<div
v-if="
dashboardData.accountsByPlatform.gemini &&
dashboardData.accountsByPlatform.gemini.total > 0
"
class="inline-flex items-center gap-0.5"
:title="`Gemini: ${dashboardData.accountsByPlatform.gemini.total} 个 (正常: ${dashboardData.accountsByPlatform.gemini.normal})`"
>
<i class="fas fa-robot text-xs text-yellow-600" />
<span class="text-xs font-medium text-gray-700">{{
dashboardData.accountsByPlatform.gemini.total
}}</span>
</div>
<!-- Bedrock账户 -->
<div
v-if="
dashboardData.accountsByPlatform.bedrock &&
dashboardData.accountsByPlatform.bedrock.total > 0
"
class="inline-flex items-center gap-0.5"
:title="`Bedrock: ${dashboardData.accountsByPlatform.bedrock.total} 个 (正常: ${dashboardData.accountsByPlatform.bedrock.normal})`"
>
<i class="fab fa-aws text-xs text-orange-600" />
<span class="text-xs font-medium text-gray-700">{{
dashboardData.accountsByPlatform.bedrock.total
}}</span>
</div>
</div>
</div>
<p class="mt-1 text-xs text-gray-500">
活跃: {{ dashboardData.activeAccounts || 0 }}
正常: {{ dashboardData.normalAccounts || 0 }}
<span v-if="dashboardData.abnormalAccounts > 0" class="text-red-600">
| 异常: {{ dashboardData.abnormalAccounts }}
</span>
<span v-if="dashboardData.pausedAccounts > 0" class="text-gray-600">
| 停止调度: {{ dashboardData.pausedAccounts }}
</span>
<span v-if="dashboardData.rateLimitedAccounts > 0" class="text-yellow-600">
| 限流: {{ dashboardData.rateLimitedAccounts }}
</span>
</p>
</div>
<div class="stat-icon flex-shrink-0 bg-gradient-to-br from-green-500 to-green-600">
<div class="stat-icon ml-2 flex-shrink-0 bg-gradient-to-br from-green-500 to-green-600">
<i class="fas fa-user-circle" />
</div>
</div>