恢复并保存本地修改:仪表盘服务账户分类显示、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

@@ -63,13 +63,6 @@ npm run service:status # 查看服务状态
npm run service:logs # 查看日志 npm run service:logs # 查看日志
npm run service:stop # 停止服务 npm run service:stop # 停止服务
# CLI管理工具
npm run cli admin # 管理员操作
npm run cli keys # API Key管理
npm run cli accounts # Claude账户管理
npm run cli status # 系统状态
```
### 开发环境配置 ### 开发环境配置
必须配置的环境变量: 必须配置的环境变量:
- `JWT_SECRET`: JWT密钥32字符以上随机字符串 - `JWT_SECRET`: JWT密钥32字符以上随机字符串

View File

@@ -2300,6 +2300,7 @@ router.get('/dashboard', authenticateAdmin, async (req, res) => {
claudeAccounts, claudeAccounts,
claudeConsoleAccounts, claudeConsoleAccounts,
geminiAccounts, geminiAccounts,
bedrockAccountsResult,
todayStats, todayStats,
systemAverages, systemAverages,
realtimeMetrics realtimeMetrics
@@ -2309,11 +2310,15 @@ router.get('/dashboard', authenticateAdmin, async (req, res) => {
claudeAccountService.getAllAccounts(), claudeAccountService.getAllAccounts(),
claudeConsoleAccountService.getAllAccounts(), claudeConsoleAccountService.getAllAccounts(),
geminiAccountService.getAllAccounts(), geminiAccountService.getAllAccounts(),
bedrockAccountService.getAllAccounts(),
redis.getTodayStats(), redis.getTodayStats(),
redis.getSystemAverages(), redis.getSystemAverages(),
redis.getRealtimeSystemMetrics() redis.getRealtimeSystemMetrics()
]) ])
// 处理Bedrock账户数据
const bedrockAccounts = bedrockAccountsResult.success ? bedrockAccountsResult.data : []
// 计算使用统计统一使用allTokens // 计算使用统计统一使用allTokens
const totalTokensUsed = apiKeys.reduce( const totalTokensUsed = apiKeys.reduce(
(sum, key) => sum + (key.usage?.total?.allTokens || 0), (sum, key) => sum + (key.usage?.total?.allTokens || 0),
@@ -2345,34 +2350,167 @@ router.get('/dashboard', authenticateAdmin, async (req, res) => {
) )
const activeApiKeys = apiKeys.filter((key) => key.isActive).length const activeApiKeys = apiKeys.filter((key) => key.isActive).length
const activeClaudeAccounts = claudeAccounts.filter(
(acc) => acc.isActive && acc.status === 'active' // Claude账户统计 - 根据账户管理页面的判断逻辑
const normalClaudeAccounts = claudeAccounts.filter(
(acc) =>
acc.isActive &&
acc.status !== 'blocked' &&
acc.status !== 'unauthorized' &&
acc.schedulable !== false
).length
const abnormalClaudeAccounts = claudeAccounts.filter(
(acc) => !acc.isActive || acc.status === 'blocked' || acc.status === 'unauthorized'
).length
const pausedClaudeAccounts = claudeAccounts.filter(
(acc) =>
acc.schedulable === false &&
acc.isActive &&
acc.status !== 'blocked' &&
acc.status !== 'unauthorized'
).length ).length
const rateLimitedClaudeAccounts = claudeAccounts.filter( const rateLimitedClaudeAccounts = claudeAccounts.filter(
(acc) => acc.rateLimitStatus && acc.rateLimitStatus.isRateLimited (acc) => acc.rateLimitStatus && acc.rateLimitStatus.isRateLimited
).length ).length
const activeClaudeConsoleAccounts = claudeConsoleAccounts.filter(
(acc) => acc.isActive && acc.status === 'active' // Claude Console账户统计
const normalClaudeConsoleAccounts = claudeConsoleAccounts.filter(
(acc) =>
acc.isActive &&
acc.status !== 'blocked' &&
acc.status !== 'unauthorized' &&
acc.schedulable !== false
).length
const abnormalClaudeConsoleAccounts = claudeConsoleAccounts.filter(
(acc) => !acc.isActive || acc.status === 'blocked' || acc.status === 'unauthorized'
).length
const pausedClaudeConsoleAccounts = claudeConsoleAccounts.filter(
(acc) =>
acc.schedulable === false &&
acc.isActive &&
acc.status !== 'blocked' &&
acc.status !== 'unauthorized'
).length ).length
const rateLimitedClaudeConsoleAccounts = claudeConsoleAccounts.filter( const rateLimitedClaudeConsoleAccounts = claudeConsoleAccounts.filter(
(acc) => acc.rateLimitStatus && acc.rateLimitStatus.isRateLimited (acc) => acc.rateLimitStatus && acc.rateLimitStatus.isRateLimited
).length ).length
const activeGeminiAccounts = geminiAccounts.filter(
(acc) => acc.isActive && acc.status === 'active' // Gemini账户统计
const normalGeminiAccounts = geminiAccounts.filter(
(acc) =>
acc.isActive &&
acc.status !== 'blocked' &&
acc.status !== 'unauthorized' &&
acc.schedulable !== false
).length
const abnormalGeminiAccounts = geminiAccounts.filter(
(acc) => !acc.isActive || acc.status === 'blocked' || acc.status === 'unauthorized'
).length
const pausedGeminiAccounts = geminiAccounts.filter(
(acc) =>
acc.schedulable === false &&
acc.isActive &&
acc.status !== 'blocked' &&
acc.status !== 'unauthorized'
).length ).length
const rateLimitedGeminiAccounts = geminiAccounts.filter( const rateLimitedGeminiAccounts = geminiAccounts.filter(
(acc) => acc.rateLimitStatus === 'limited' (acc) => acc.rateLimitStatus === 'limited'
).length ).length
// Bedrock账户统计
const normalBedrockAccounts = bedrockAccounts.filter(
(acc) =>
acc.isActive &&
acc.status !== 'blocked' &&
acc.status !== 'unauthorized' &&
acc.schedulable !== false
).length
const abnormalBedrockAccounts = bedrockAccounts.filter(
(acc) => !acc.isActive || acc.status === 'blocked' || acc.status === 'unauthorized'
).length
const pausedBedrockAccounts = bedrockAccounts.filter(
(acc) =>
acc.schedulable === false &&
acc.isActive &&
acc.status !== 'blocked' &&
acc.status !== 'unauthorized'
).length
const rateLimitedBedrockAccounts = bedrockAccounts.filter(
(acc) => acc.rateLimitStatus && acc.rateLimitStatus.isRateLimited
).length
const dashboard = { const dashboard = {
overview: { overview: {
totalApiKeys: apiKeys.length, totalApiKeys: apiKeys.length,
activeApiKeys, activeApiKeys,
// 总账户统计(所有平台)
totalAccounts:
claudeAccounts.length +
claudeConsoleAccounts.length +
geminiAccounts.length +
bedrockAccounts.length,
normalAccounts:
normalClaudeAccounts +
normalClaudeConsoleAccounts +
normalGeminiAccounts +
normalBedrockAccounts,
abnormalAccounts:
abnormalClaudeAccounts +
abnormalClaudeConsoleAccounts +
abnormalGeminiAccounts +
abnormalBedrockAccounts,
pausedAccounts:
pausedClaudeAccounts +
pausedClaudeConsoleAccounts +
pausedGeminiAccounts +
pausedBedrockAccounts,
rateLimitedAccounts:
rateLimitedClaudeAccounts +
rateLimitedClaudeConsoleAccounts +
rateLimitedGeminiAccounts +
rateLimitedBedrockAccounts,
// 各平台详细统计
accountsByPlatform: {
claude: {
total: claudeAccounts.length,
normal: normalClaudeAccounts,
abnormal: abnormalClaudeAccounts,
paused: pausedClaudeAccounts,
rateLimited: rateLimitedClaudeAccounts
},
'claude-console': {
total: claudeConsoleAccounts.length,
normal: normalClaudeConsoleAccounts,
abnormal: abnormalClaudeConsoleAccounts,
paused: pausedClaudeConsoleAccounts,
rateLimited: rateLimitedClaudeConsoleAccounts
},
gemini: {
total: geminiAccounts.length,
normal: normalGeminiAccounts,
abnormal: abnormalGeminiAccounts,
paused: pausedGeminiAccounts,
rateLimited: rateLimitedGeminiAccounts
},
bedrock: {
total: bedrockAccounts.length,
normal: normalBedrockAccounts,
abnormal: abnormalBedrockAccounts,
paused: pausedBedrockAccounts,
rateLimited: rateLimitedBedrockAccounts
}
},
// 保留旧字段以兼容
activeAccounts:
normalClaudeAccounts +
normalClaudeConsoleAccounts +
normalGeminiAccounts +
normalBedrockAccounts,
totalClaudeAccounts: claudeAccounts.length + claudeConsoleAccounts.length, totalClaudeAccounts: claudeAccounts.length + claudeConsoleAccounts.length,
activeClaudeAccounts: activeClaudeAccounts + activeClaudeConsoleAccounts, activeClaudeAccounts: normalClaudeAccounts + normalClaudeConsoleAccounts,
rateLimitedClaudeAccounts: rateLimitedClaudeAccounts + rateLimitedClaudeConsoleAccounts, rateLimitedClaudeAccounts: rateLimitedClaudeAccounts + rateLimitedClaudeConsoleAccounts,
totalGeminiAccounts: geminiAccounts.length, totalGeminiAccounts: geminiAccounts.length,
activeGeminiAccounts, activeGeminiAccounts: normalGeminiAccounts,
rateLimitedGeminiAccounts, rateLimitedGeminiAccounts,
totalTokensUsed, totalTokensUsed,
totalRequestsUsed, totalRequestsUsed,
@@ -2403,8 +2541,8 @@ router.get('/dashboard', authenticateAdmin, async (req, res) => {
}, },
systemHealth: { systemHealth: {
redisConnected: redis.isConnected, redisConnected: redis.isConnected,
claudeAccountsHealthy: activeClaudeAccounts + activeClaudeConsoleAccounts > 0, claudeAccountsHealthy: normalClaudeAccounts + normalClaudeConsoleAccounts > 0,
geminiAccountsHealthy: activeGeminiAccounts > 0, geminiAccountsHealthy: normalGeminiAccounts > 0,
uptime: process.uptime() uptime: process.uptime()
}, },
systemTimezone: config.system.timezoneOffset || 8 systemTimezone: config.system.timezoneOffset || 8

View File

@@ -213,12 +213,45 @@ class ApiKeyService {
if (key.rateLimitWindow > 0) { if (key.rateLimitWindow > 0) {
const requestCountKey = `rate_limit:requests:${key.id}` const requestCountKey = `rate_limit:requests:${key.id}`
const tokenCountKey = `rate_limit:tokens:${key.id}` const tokenCountKey = `rate_limit:tokens:${key.id}`
const windowStartKey = `rate_limit:window_start:${key.id}`
key.currentWindowRequests = parseInt((await client.get(requestCountKey)) || '0') key.currentWindowRequests = parseInt((await client.get(requestCountKey)) || '0')
key.currentWindowTokens = parseInt((await client.get(tokenCountKey)) || '0') key.currentWindowTokens = parseInt((await client.get(tokenCountKey)) || '0')
// 获取窗口开始时间和计算剩余时间
const windowStart = await client.get(windowStartKey)
if (windowStart) {
const now = Date.now()
const windowStartTime = parseInt(windowStart)
const windowDuration = key.rateLimitWindow * 60 * 1000 // 转换为毫秒
const windowEndTime = windowStartTime + windowDuration
// 如果窗口还有效
if (now < windowEndTime) {
key.windowStartTime = windowStartTime
key.windowEndTime = windowEndTime
key.windowRemainingSeconds = Math.max(0, Math.floor((windowEndTime - now) / 1000))
} else {
// 窗口已过期,下次请求会重置
key.windowStartTime = null
key.windowEndTime = null
key.windowRemainingSeconds = 0
// 重置计数为0因为窗口已过期
key.currentWindowRequests = 0
key.currentWindowTokens = 0
}
} else {
// 窗口还未开始(没有任何请求)
key.windowStartTime = null
key.windowEndTime = null
key.windowRemainingSeconds = null
}
} else { } else {
key.currentWindowRequests = 0 key.currentWindowRequests = 0
key.currentWindowTokens = 0 key.currentWindowTokens = 0
key.windowStartTime = null
key.windowEndTime = null
key.windowRemainingSeconds = null
} }
try { try {

View File

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

View File

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

View File

@@ -10,8 +10,17 @@ export const useDashboardStore = defineStore('dashboard', () => {
totalApiKeys: 0, totalApiKeys: 0,
activeApiKeys: 0, activeApiKeys: 0,
totalAccounts: 0, totalAccounts: 0,
activeAccounts: 0, normalAccounts: 0,
abnormalAccounts: 0,
pausedAccounts: 0,
activeAccounts: 0, // 保留兼容
rateLimitedAccounts: 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, todayRequests: 0,
totalRequests: 0, totalRequests: 0,
todayTokens: 0, todayTokens: 0,
@@ -152,9 +161,21 @@ export const useDashboardStore = defineStore('dashboard', () => {
dashboardData.value = { dashboardData.value = {
totalApiKeys: overview.totalApiKeys || 0, totalApiKeys: overview.totalApiKeys || 0,
activeApiKeys: overview.activeApiKeys || 0, activeApiKeys: overview.activeApiKeys || 0,
totalAccounts: overview.totalClaudeAccounts || 0, // 使用新的统一统计字段
activeAccounts: overview.activeClaudeAccounts || 0, totalAccounts: overview.totalAccounts || overview.totalClaudeAccounts || 0,
rateLimitedAccounts: overview.rateLimitedClaudeAccounts || 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, todayRequests: recentActivity.requestsToday || 0,
totalRequests: overview.totalRequestsUsed || 0, totalRequests: overview.totalRequestsUsed || 0,
todayTokens: recentActivity.tokensToday || 0, todayTokens: recentActivity.tokensToday || 0,

View File

@@ -254,48 +254,19 @@
</div> </div>
<!-- 时间窗口限制进度条 --> <!-- 时间窗口限制进度条 -->
<div v-if="key.rateLimitWindow > 0" class="space-y-1"> <WindowCountdown
<div class="flex items-center justify-between text-xs"> v-if="key.rateLimitWindow > 0"
<span class="text-gray-500">窗口限制</span> :current-requests="key.currentWindowRequests"
<span class="text-gray-700"> {{ key.rateLimitWindow }}分钟 </span> :current-tokens="key.currentWindowTokens"
</div> :rate-limit-window="key.rateLimitWindow"
:request-limit="key.rateLimitRequests"
<!-- 请求次数限制 --> :show-progress="true"
<div v-if="key.rateLimitRequests > 0" class="space-y-0.5"> :show-tooltip="false"
<div class="flex items-center justify-between text-xs"> :token-limit="key.tokenLimit"
<span class="text-gray-400">请求</span> :window-end-time="key.windowEndTime"
<span class="text-gray-600"> :window-remaining-seconds="key.windowRemainingSeconds"
{{ key.currentWindowRequests || 0 }}/{{ key.rateLimitRequests }} :window-start-time="key.windowStartTime"
</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>
<!-- 查看详情按钮 --> <!-- 查看详情按钮 -->
<div class="pt-1"> <div class="pt-1">
@@ -743,46 +714,19 @@
</div> </div>
<!-- 移动端时间窗口限制 --> <!-- 移动端时间窗口限制 -->
<div <WindowCountdown
v-if="key.rateLimitWindow > 0 && (key.rateLimitRequests > 0 || key.tokenLimit > 0)" v-if="key.rateLimitWindow > 0 && (key.rateLimitRequests > 0 || key.tokenLimit > 0)"
class="space-y-1" :current-requests="key.currentWindowRequests"
> :current-tokens="key.currentWindowTokens"
<div class="mb-1 text-xs text-gray-500">窗口限制 ({{ key.rateLimitWindow }}分钟)</div> :rate-limit-window="key.rateLimitWindow"
:request-limit="key.rateLimitRequests"
<div v-if="key.rateLimitRequests > 0" class="flex items-center gap-2"> :show-progress="true"
<span class="w-10 text-xs text-gray-500">请求</span> :show-tooltip="false"
<div class="flex-1"> :token-limit="key.tokenLimit"
<div class="h-1.5 w-full rounded-full bg-gray-200"> :window-end-time="key.windowEndTime"
<div :window-remaining-seconds="key.windowRemainingSeconds"
class="h-1.5 rounded-full transition-all duration-300" :window-start-time="key.windowStartTime"
: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>
</div> </div>
<!-- 时间信息 --> <!-- 时间信息 -->
@@ -1019,6 +963,7 @@ import NewApiKeyModal from '@/components/apikeys/NewApiKeyModal.vue'
import BatchApiKeyModal from '@/components/apikeys/BatchApiKeyModal.vue' import BatchApiKeyModal from '@/components/apikeys/BatchApiKeyModal.vue'
import ExpiryEditModal from '@/components/apikeys/ExpiryEditModal.vue' import ExpiryEditModal from '@/components/apikeys/ExpiryEditModal.vue'
import UsageDetailModal from '@/components/apikeys/UsageDetailModal.vue' import UsageDetailModal from '@/components/apikeys/UsageDetailModal.vue'
import WindowCountdown from '@/components/apikeys/WindowCountdown.vue'
// 响应式数据 // 响应式数据
const clientsStore = useClientsStore() const clientsStore = useClientsStore()
@@ -1662,36 +1607,6 @@ const formatTokenCount = (count) => {
return count.toString() 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], () => { watch([selectedTagFilter, apiKeyStatsTimeRange], () => {
currentPage.value = 1 currentPage.value = 1

View File

@@ -21,19 +21,86 @@
<div class="stat-card"> <div class="stat-card">
<div class="flex items-center justify-between"> <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="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"> <div class="flex flex-wrap items-baseline gap-x-2">
{{ dashboardData.totalAccounts }} <p class="text-2xl font-bold text-gray-900 sm:text-3xl">
</p> {{ 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"> <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"> <span v-if="dashboardData.rateLimitedAccounts > 0" class="text-yellow-600">
| 限流: {{ dashboardData.rateLimitedAccounts }} | 限流: {{ dashboardData.rateLimitedAccounts }}
</span> </span>
</p> </p>
</div> </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" /> <i class="fas fa-user-circle" />
</div> </div>
</div> </div>