mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
feat: 优化 API Keys 页面的分组显示和使用统计展示
- 修复分组调度显示,正确展示分组名称 - 重新设计使用统计列,添加进度条显示每日费用和窗口限制 - 创建使用详情弹窗组件,展示完整统计信息 - 优化时间窗口限制显示,支持请求次数和Token双维度进度条 - 改进移动端自适应布局 - 修复 ESLint 警告,提升代码质量 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -183,7 +183,9 @@
|
||||
class="form-input flex-1"
|
||||
required
|
||||
>
|
||||
<option value="">请选择分组</option>
|
||||
<option value="">
|
||||
请选择分组
|
||||
</option>
|
||||
<option
|
||||
v-for="group in filteredGroups"
|
||||
:key="group.id"
|
||||
@@ -191,14 +193,19 @@
|
||||
>
|
||||
{{ group.name }} ({{ group.memberCount || 0 }} 个成员)
|
||||
</option>
|
||||
<option value="__new__">+ 新建分组</option>
|
||||
<option value="__new__">
|
||||
+ 新建分组
|
||||
</option>
|
||||
</select>
|
||||
<button
|
||||
type="button"
|
||||
class="px-3 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
@click="refreshGroups"
|
||||
>
|
||||
<i class="fas fa-sync-alt" :class="{ 'animate-spin': loadingGroups }" />
|
||||
<i
|
||||
class="fas fa-sync-alt"
|
||||
:class="{ 'animate-spin': loadingGroups }"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -561,7 +568,9 @@
|
||||
class="form-input flex-1"
|
||||
required
|
||||
>
|
||||
<option value="">请选择分组</option>
|
||||
<option value="">
|
||||
请选择分组
|
||||
</option>
|
||||
<option
|
||||
v-for="group in filteredGroups"
|
||||
:key="group.id"
|
||||
@@ -569,14 +578,19 @@
|
||||
>
|
||||
{{ group.name }} ({{ group.memberCount || 0 }} 个成员)
|
||||
</option>
|
||||
<option value="__new__">+ 新建分组</option>
|
||||
<option value="__new__">
|
||||
+ 新建分组
|
||||
</option>
|
||||
</select>
|
||||
<button
|
||||
type="button"
|
||||
class="px-3 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
@click="refreshGroups"
|
||||
>
|
||||
<i class="fas fa-sync-alt" :class="{ 'animate-spin': loadingGroups }" />
|
||||
<i
|
||||
class="fas fa-sync-alt"
|
||||
:class="{ 'animate-spin': loadingGroups }"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -38,7 +38,9 @@
|
||||
v-if="showCreateForm"
|
||||
class="mb-6 p-4 bg-blue-50 rounded-lg border border-blue-200"
|
||||
>
|
||||
<h4 class="text-lg font-semibold text-gray-900 mb-4">创建新分组</h4>
|
||||
<h4 class="text-lg font-semibold text-gray-900 mb-4">
|
||||
创建新分组
|
||||
</h4>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">分组名称 *</label>
|
||||
@@ -113,7 +115,9 @@
|
||||
class="text-center py-8"
|
||||
>
|
||||
<div class="loading-spinner-lg mx-auto mb-4" />
|
||||
<p class="text-gray-500">加载中...</p>
|
||||
<p class="text-gray-500">
|
||||
加载中...
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -121,7 +125,9 @@
|
||||
class="text-center py-8 bg-gray-50 rounded-lg"
|
||||
>
|
||||
<i class="fas fa-layer-group text-4xl text-gray-300 mb-4" />
|
||||
<p class="text-gray-500">暂无分组</p>
|
||||
<p class="text-gray-500">
|
||||
暂无分组
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -135,8 +141,12 @@
|
||||
>
|
||||
<div class="flex items-start justify-between mb-3">
|
||||
<div class="flex-1">
|
||||
<h4 class="font-semibold text-gray-900">{{ group.name }}</h4>
|
||||
<p class="text-sm text-gray-500 mt-1">{{ group.description || '暂无描述' }}</p>
|
||||
<h4 class="font-semibold text-gray-900">
|
||||
{{ group.name }}
|
||||
</h4>
|
||||
<p class="text-sm text-gray-500 mt-1">
|
||||
{{ group.description || '暂无描述' }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 ml-4">
|
||||
<span
|
||||
@@ -194,7 +204,9 @@
|
||||
>
|
||||
<div class="modal-content w-full max-w-lg p-4 sm:p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-bold text-gray-900">编辑分组</h3>
|
||||
<h3 class="text-lg font-bold text-gray-900">
|
||||
编辑分组
|
||||
</h3>
|
||||
<button
|
||||
class="text-gray-400 hover:text-gray-600 transition-colors"
|
||||
@click="cancelEdit"
|
||||
|
||||
360
web/admin-spa/src/components/apikeys/UsageDetailModal.vue
Normal file
360
web/admin-spa/src/components/apikeys/UsageDetailModal.vue
Normal file
@@ -0,0 +1,360 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="show"
|
||||
class="fixed inset-0 z-50 overflow-y-auto"
|
||||
@click.self="close"
|
||||
>
|
||||
<div class="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
||||
<!-- 背景遮罩 -->
|
||||
<div
|
||||
class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"
|
||||
@click="close"
|
||||
/>
|
||||
|
||||
<!-- 模态框 -->
|
||||
<div class="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-2xl sm:w-full">
|
||||
<!-- 标题栏 -->
|
||||
<div class="bg-gradient-to-r from-blue-500 to-blue-600 px-6 py-4">
|
||||
<h3 class="text-lg font-semibold text-white flex items-center">
|
||||
<i class="fas fa-chart-line mr-2" />
|
||||
使用统计详情 - {{ apiKey.name }}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<!-- 内容区 -->
|
||||
<div class="px-6 py-4 max-h-[70vh] overflow-y-auto">
|
||||
<!-- 总体统计卡片 -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
||||
<!-- 请求统计卡片 -->
|
||||
<div class="bg-gradient-to-br from-blue-50 to-blue-100 rounded-lg p-4 border border-blue-200">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<span class="text-sm font-medium text-gray-700">总请求数</span>
|
||||
<i class="fas fa-paper-plane text-blue-500" />
|
||||
</div>
|
||||
<div class="text-2xl font-bold text-gray-900">
|
||||
{{ formatNumber(totalRequests) }}
|
||||
</div>
|
||||
<div class="text-xs text-gray-600 mt-1">
|
||||
今日: {{ formatNumber(dailyRequests) }} 次
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Token统计卡片 -->
|
||||
<div class="bg-gradient-to-br from-green-50 to-green-100 rounded-lg p-4 border border-green-200">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<span class="text-sm font-medium text-gray-700">总Token数</span>
|
||||
<i class="fas fa-coins text-green-500" />
|
||||
</div>
|
||||
<div class="text-2xl font-bold text-gray-900">
|
||||
{{ formatNumber(totalTokens) }}
|
||||
</div>
|
||||
<div class="text-xs text-gray-600 mt-1">
|
||||
今日: {{ formatNumber(dailyTokens) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 费用统计卡片 -->
|
||||
<div class="bg-gradient-to-br from-yellow-50 to-yellow-100 rounded-lg p-4 border border-yellow-200">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<span class="text-sm font-medium text-gray-700">总费用</span>
|
||||
<i class="fas fa-dollar-sign text-yellow-600" />
|
||||
</div>
|
||||
<div class="text-2xl font-bold text-gray-900">
|
||||
${{ totalCost.toFixed(4) }}
|
||||
</div>
|
||||
<div class="text-xs text-gray-600 mt-1">
|
||||
今日: ${{ dailyCost.toFixed(4) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 平均统计卡片 -->
|
||||
<div class="bg-gradient-to-br from-purple-50 to-purple-100 rounded-lg p-4 border border-purple-200">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<span class="text-sm font-medium text-gray-700">平均速率</span>
|
||||
<i class="fas fa-tachometer-alt text-purple-500" />
|
||||
</div>
|
||||
<div class="text-sm space-y-1">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600">RPM:</span>
|
||||
<span class="font-semibold">{{ rpm }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600">TPM:</span>
|
||||
<span class="font-semibold">{{ tpm }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Token详细分布 -->
|
||||
<div class="mb-6">
|
||||
<h4 class="text-sm font-semibold text-gray-700 mb-3 flex items-center">
|
||||
<i class="fas fa-chart-pie text-indigo-500 mr-2" />
|
||||
Token 使用分布
|
||||
</h4>
|
||||
<div class="bg-gray-50 rounded-lg p-4 space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-arrow-down text-green-500 mr-2" />
|
||||
<span class="text-sm text-gray-600">输入 Token</span>
|
||||
</div>
|
||||
<span class="text-sm font-semibold text-gray-900">
|
||||
{{ formatNumber(inputTokens) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-arrow-up text-blue-500 mr-2" />
|
||||
<span class="text-sm text-gray-600">输出 Token</span>
|
||||
</div>
|
||||
<span class="text-sm font-semibold text-gray-900">
|
||||
{{ formatNumber(outputTokens) }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="cacheCreateTokens > 0"
|
||||
class="flex items-center justify-between"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-save text-purple-500 mr-2" />
|
||||
<span class="text-sm text-gray-600">缓存创建 Token</span>
|
||||
</div>
|
||||
<span class="text-sm font-semibold text-purple-600">
|
||||
{{ formatNumber(cacheCreateTokens) }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="cacheReadTokens > 0"
|
||||
class="flex items-center justify-between"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-download text-purple-500 mr-2" />
|
||||
<span class="text-sm text-gray-600">缓存读取 Token</span>
|
||||
</div>
|
||||
<span class="text-sm font-semibold text-purple-600">
|
||||
{{ formatNumber(cacheReadTokens) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 限制信息 -->
|
||||
<div
|
||||
v-if="hasLimits"
|
||||
class="mb-6"
|
||||
>
|
||||
<h4 class="text-sm font-semibold text-gray-700 mb-3 flex items-center">
|
||||
<i class="fas fa-shield-alt text-red-500 mr-2" />
|
||||
限制设置
|
||||
</h4>
|
||||
<div class="bg-gray-50 rounded-lg p-4 space-y-3">
|
||||
<div
|
||||
v-if="apiKey.dailyCostLimit > 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-gray-900">
|
||||
${{ apiKey.dailyCostLimit.toFixed(2) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 rounded-full h-2">
|
||||
<div
|
||||
class="h-2 rounded-full transition-all duration-300"
|
||||
:class="dailyCostPercentage >= 100 ? 'bg-red-500' : dailyCostPercentage >= 80 ? 'bg-yellow-500' : 'bg-green-500'"
|
||||
:style="{ width: Math.min(dailyCostPercentage, 100) + '%' }"
|
||||
/>
|
||||
</div>
|
||||
<div class="text-xs text-gray-500 text-right">
|
||||
已使用 {{ dailyCostPercentage.toFixed(1) }}%
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="apiKey.concurrencyLimit > 0"
|
||||
class="flex items-center justify-between text-sm"
|
||||
>
|
||||
<span class="text-gray-600">并发限制</span>
|
||||
<span class="font-semibold text-purple-600">
|
||||
{{ apiKey.currentConcurrency || 0 }} / {{ apiKey.concurrencyLimit }}
|
||||
</span>
|
||||
</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="w-full bg-gray-200 rounded-full h-2">
|
||||
<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="w-full bg-gray-200 rounded-full h-2">
|
||||
<div
|
||||
class="h-2 rounded-full transition-all duration-300"
|
||||
:class="windowTokenProgressColor"
|
||||
:style="{ width: windowTokenProgress + '%' }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部按钮 -->
|
||||
<div class="bg-gray-50 px-6 py-3 flex justify-end">
|
||||
<button
|
||||
type="button"
|
||||
class="px-4 py-2 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-gray-400"
|
||||
@click="close"
|
||||
>
|
||||
关闭
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
show: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
apiKey: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
// 计算属性
|
||||
const totalRequests = computed(() => (props.apiKey.usage?.total?.requests) || 0)
|
||||
const dailyRequests = computed(() => (props.apiKey.usage?.daily?.requests) || 0)
|
||||
const totalTokens = computed(() => (props.apiKey.usage?.total?.tokens) || 0)
|
||||
const dailyTokens = computed(() => (props.apiKey.usage?.daily?.tokens) || 0)
|
||||
const totalCost = computed(() => (props.apiKey.usage?.total?.cost) || 0)
|
||||
const dailyCost = computed(() => props.apiKey.dailyCost || 0)
|
||||
const inputTokens = computed(() => (props.apiKey.usage?.total?.inputTokens) || 0)
|
||||
const outputTokens = computed(() => (props.apiKey.usage?.total?.outputTokens) || 0)
|
||||
const cacheCreateTokens = computed(() => (props.apiKey.usage?.total?.cacheCreateTokens) || 0)
|
||||
const cacheReadTokens = computed(() => (props.apiKey.usage?.total?.cacheReadTokens) || 0)
|
||||
const rpm = computed(() => (props.apiKey.usage?.averages?.rpm) || 0)
|
||||
const tpm = computed(() => (props.apiKey.usage?.averages?.tpm) || 0)
|
||||
|
||||
const hasLimits = computed(() => {
|
||||
return props.apiKey.dailyCostLimit > 0 ||
|
||||
props.apiKey.concurrencyLimit > 0 ||
|
||||
props.apiKey.rateLimitWindow > 0 ||
|
||||
props.apiKey.tokenLimit > 0
|
||||
})
|
||||
|
||||
const dailyCostPercentage = computed(() => {
|
||||
if (!props.apiKey.dailyCostLimit || props.apiKey.dailyCostLimit === 0) return 0
|
||||
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'
|
||||
return num.toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
// 格式化Token数量(使用K/M单位)
|
||||
const formatTokenCount = (count) => {
|
||||
if (count >= 1000000) {
|
||||
return (count / 1000000).toFixed(1) + 'M'
|
||||
} else if (count >= 1000) {
|
||||
return (count / 1000).toFixed(1) + 'K'
|
||||
}
|
||||
return count.toString()
|
||||
}
|
||||
|
||||
const close = () => {
|
||||
emit('close')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 添加过渡动画 */
|
||||
.transform {
|
||||
animation: modalSlideIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes modalSlideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -38,8 +38,12 @@
|
||||
class="form-input px-3 py-2 text-sm w-full sm:w-auto"
|
||||
@change="filterByGroup"
|
||||
>
|
||||
<option value="all">所有账户</option>
|
||||
<option value="ungrouped">未分组账户</option>
|
||||
<option value="all">
|
||||
所有账户
|
||||
</option>
|
||||
<option value="ungrouped">
|
||||
未分组账户
|
||||
</option>
|
||||
<option
|
||||
v-for="group in accountGroups"
|
||||
:key="group.id"
|
||||
@@ -177,7 +181,10 @@
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="text-sm font-semibold text-gray-900 truncate" :title="account.name">
|
||||
<div
|
||||
class="text-sm font-semibold text-gray-900 truncate"
|
||||
:title="account.name"
|
||||
>
|
||||
{{ account.name }}
|
||||
</div>
|
||||
<span
|
||||
@@ -206,7 +213,10 @@
|
||||
<i class="fas fa-folder mr-1" />{{ account.groupInfo.name }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-xs text-gray-500 truncate" :title="account.id">
|
||||
<div
|
||||
class="text-xs text-gray-500 truncate"
|
||||
:title="account.id"
|
||||
>
|
||||
{{ account.id }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -221,7 +231,7 @@
|
||||
>
|
||||
<i class="fas fa-robot text-yellow-700 text-xs" />
|
||||
<span class="text-xs font-semibold text-yellow-800">Gemini</span>
|
||||
<span class="w-px h-4 bg-yellow-300 mx-1"></span>
|
||||
<span class="w-px h-4 bg-yellow-300 mx-1" />
|
||||
<span class="text-xs font-medium text-yellow-700">
|
||||
{{ (account.scopes && account.scopes.length > 0) ? 'OAuth' : '传统' }}
|
||||
</span>
|
||||
@@ -232,7 +242,7 @@
|
||||
>
|
||||
<i class="fas fa-terminal text-purple-700 text-xs" />
|
||||
<span class="text-xs font-semibold text-purple-800">Console</span>
|
||||
<span class="w-px h-4 bg-purple-300 mx-1"></span>
|
||||
<span class="w-px h-4 bg-purple-300 mx-1" />
|
||||
<span class="text-xs font-medium text-purple-700">API Key</span>
|
||||
</div>
|
||||
<div
|
||||
@@ -241,7 +251,7 @@
|
||||
>
|
||||
<i class="fas fa-brain text-indigo-700 text-xs" />
|
||||
<span class="text-xs font-semibold text-indigo-800">Claude</span>
|
||||
<span class="w-px h-4 bg-indigo-300 mx-1"></span>
|
||||
<span class="w-px h-4 bg-indigo-300 mx-1" />
|
||||
<span class="text-xs font-medium text-indigo-700">
|
||||
{{ (account.scopes && account.scopes.length > 0) ? 'OAuth' : '传统' }}
|
||||
</span>
|
||||
@@ -615,8 +625,8 @@
|
||||
:class="account.schedulable
|
||||
? 'text-gray-600 bg-gray-50 hover:bg-gray-100'
|
||||
: 'text-green-600 bg-green-50 hover:bg-green-100'"
|
||||
@click="toggleSchedulable(account)"
|
||||
:disabled="account.isTogglingSchedulable"
|
||||
@click="toggleSchedulable(account)"
|
||||
>
|
||||
<i :class="['fas', account.schedulable ? 'fa-pause' : 'fa-play']" />
|
||||
{{ account.schedulable ? '暂停' : '启用' }}
|
||||
|
||||
@@ -185,14 +185,23 @@
|
||||
<i class="fas fa-key text-white text-xs" />
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<div class="text-sm font-semibold text-gray-900 truncate" :title="key.name">
|
||||
<div
|
||||
class="text-sm font-semibold text-gray-900 truncate"
|
||||
:title="key.name"
|
||||
>
|
||||
{{ key.name }}
|
||||
</div>
|
||||
<div class="text-xs text-gray-500 truncate" :title="key.id">
|
||||
<div
|
||||
class="text-xs text-gray-500 truncate"
|
||||
:title="key.id"
|
||||
>
|
||||
{{ key.id }}
|
||||
</div>
|
||||
<div class="text-xs text-gray-500 mt-1 truncate">
|
||||
<span v-if="key.claudeAccountId" :title="`绑定: ${getBoundAccountName(key.claudeAccountId)}`">
|
||||
<span
|
||||
v-if="key.claudeAccountId"
|
||||
:title="`绑定: ${getBoundAccountName(key.claudeAccountId)}`"
|
||||
>
|
||||
<i class="fas fa-link mr-1" />
|
||||
{{ getBoundAccountName(key.claudeAccountId) }}
|
||||
</span>
|
||||
@@ -232,98 +241,100 @@
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-3 py-4">
|
||||
<div class="space-y-1">
|
||||
<!-- 请求统计 -->
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-gray-600">请求数:</span>
|
||||
<span class="font-medium text-gray-900">{{ formatNumber((key.usage && key.usage.total && key.usage.total.requests) || 0) }}</span>
|
||||
</div>
|
||||
<!-- Token统计 -->
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-gray-600">Token:</span>
|
||||
<span class="font-medium text-gray-900">{{ formatNumber((key.usage && key.usage.total && key.usage.total.tokens) || 0) }}</span>
|
||||
</div>
|
||||
<!-- 费用统计 -->
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-gray-600">费用:</span>
|
||||
<span class="font-medium text-green-600">{{ calculateApiKeyCost(key.usage) }}</span>
|
||||
</div>
|
||||
<!-- 每日费用限制 -->
|
||||
<div
|
||||
v-if="key.dailyCostLimit > 0"
|
||||
class="flex justify-between text-sm"
|
||||
>
|
||||
<span class="text-gray-600">今日费用:</span>
|
||||
<span :class="['font-medium', (key.dailyCost || 0) >= key.dailyCostLimit ? 'text-red-600' : 'text-blue-600']">
|
||||
${{ (key.dailyCost || 0).toFixed(2) }} / ${{ key.dailyCostLimit.toFixed(2) }}
|
||||
</span>
|
||||
</div>
|
||||
<!-- 并发限制 -->
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-gray-600">并发限制:</span>
|
||||
<span class="font-medium text-purple-600">{{ key.concurrencyLimit > 0 ? key.concurrencyLimit : '无限制' }}</span>
|
||||
</div>
|
||||
<!-- 当前并发数 -->
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-gray-600">当前并发:</span>
|
||||
<span :class="['font-medium', key.currentConcurrency > 0 ? 'text-orange-600' : 'text-gray-600']">
|
||||
{{ key.currentConcurrency || 0 }}
|
||||
<span
|
||||
v-if="key.concurrencyLimit > 0"
|
||||
class="text-xs text-gray-500"
|
||||
>/ {{ key.concurrencyLimit }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<!-- 时间窗口限流 -->
|
||||
<div
|
||||
v-if="key.rateLimitWindow > 0"
|
||||
class="flex justify-between text-sm"
|
||||
>
|
||||
<span class="text-gray-600">时间窗口:</span>
|
||||
<span class="font-medium text-indigo-600">{{ key.rateLimitWindow }} 分钟</span>
|
||||
</div>
|
||||
<!-- 请求次数限制 -->
|
||||
<div
|
||||
v-if="key.rateLimitRequests > 0"
|
||||
class="flex justify-between text-sm"
|
||||
>
|
||||
<span class="text-gray-600">请求限制:</span>
|
||||
<span class="font-medium text-indigo-600">{{ key.rateLimitRequests }} 次/窗口</span>
|
||||
</div>
|
||||
<!-- 输入/输出Token -->
|
||||
<div class="flex justify-between text-xs text-gray-500">
|
||||
<span>输入: {{ formatNumber((key.usage && key.usage.total && key.usage.total.inputTokens) || 0) }}</span>
|
||||
<span>输出: {{ formatNumber((key.usage && key.usage.total && key.usage.total.outputTokens) || 0) }}</span>
|
||||
</div>
|
||||
<!-- 缓存Token细节 -->
|
||||
<div
|
||||
v-if="((key.usage && key.usage.total && key.usage.total.cacheCreateTokens) || 0) > 0 || ((key.usage && key.usage.total && key.usage.total.cacheReadTokens) || 0) > 0"
|
||||
class="flex justify-between text-xs text-orange-500"
|
||||
>
|
||||
<span>缓存创建: {{ formatNumber((key.usage && key.usage.total && key.usage.total.cacheCreateTokens) || 0) }}</span>
|
||||
<span>缓存读取: {{ formatNumber((key.usage && key.usage.total && key.usage.total.cacheReadTokens) || 0) }}</span>
|
||||
</div>
|
||||
<!-- RPM/TPM -->
|
||||
<div class="flex justify-between text-xs text-blue-600">
|
||||
<span>RPM: {{ (key.usage && key.usage.averages && key.usage.averages.rpm) || 0 }}</span>
|
||||
<span>TPM: {{ (key.usage && key.usage.averages && key.usage.averages.tpm) || 0 }}</span>
|
||||
</div>
|
||||
<!-- 今日统计 -->
|
||||
<div class="pt-1 border-t border-gray-100">
|
||||
<div class="flex justify-between text-xs text-green-600">
|
||||
<span>今日: {{ formatNumber((key.usage && key.usage.daily && key.usage.daily.requests) || 0) }}次</span>
|
||||
<span>{{ formatNumber((key.usage && key.usage.daily && key.usage.daily.tokens) || 0) }}T</span>
|
||||
<div class="space-y-2">
|
||||
<!-- 今日使用统计 -->
|
||||
<div class="mb-2">
|
||||
<div class="flex justify-between items-center text-sm mb-1">
|
||||
<span class="text-gray-600">今日请求</span>
|
||||
<span class="font-semibold text-gray-900">{{ formatNumber((key.usage?.daily?.requests) || 0) }}次</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center text-sm">
|
||||
<span class="text-gray-600">今日费用</span>
|
||||
<span class="font-semibold text-green-600">${{ (key.dailyCost || 0).toFixed(4) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 模型分布按钮 -->
|
||||
<div class="pt-2">
|
||||
<button
|
||||
v-if="key && key.id"
|
||||
class="text-xs text-indigo-600 hover:text-indigo-800 font-medium"
|
||||
@click="toggleApiKeyModelStats(key.id)"
|
||||
|
||||
<!-- 每日费用限制进度条 -->
|
||||
<div
|
||||
v-if="key.dailyCostLimit > 0"
|
||||
class="space-y-1"
|
||||
>
|
||||
<div class="flex justify-between items-center text-xs">
|
||||
<span class="text-gray-500">费用限额</span>
|
||||
<span class="text-gray-700">
|
||||
${{ (key.dailyCost || 0).toFixed(2) }} / ${{ key.dailyCostLimit.toFixed(2) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 rounded-full h-1.5">
|
||||
<div
|
||||
:class="getDailyCostProgressColor(key)"
|
||||
class="h-1.5 rounded-full transition-all duration-300"
|
||||
:style="{ width: getDailyCostProgress(key) + '%' }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 时间窗口限制进度条 -->
|
||||
<div
|
||||
v-if="key.rateLimitWindow > 0"
|
||||
class="space-y-1"
|
||||
>
|
||||
<div class="flex justify-between items-center 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"
|
||||
>
|
||||
<i :class="['fas', expandedApiKeys[key.id] ? 'fa-chevron-up' : 'fa-chevron-down', 'mr-1']" />
|
||||
模型使用分布
|
||||
<div class="flex justify-between items-center text-xs">
|
||||
<span class="text-gray-400">请求</span>
|
||||
<span class="text-gray-600">
|
||||
{{ key.currentWindowRequests || 0 }}/{{ key.rateLimitRequests }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 rounded-full h-1">
|
||||
<div
|
||||
:class="getWindowRequestProgressColor(key)"
|
||||
class="h-1 rounded-full transition-all duration-300"
|
||||
:style="{ width: getWindowRequestProgress(key) + '%' }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Token使用量限制 -->
|
||||
<div
|
||||
v-if="key.tokenLimit > 0"
|
||||
class="space-y-0.5"
|
||||
>
|
||||
<div class="flex justify-between items-center 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="w-full bg-gray-200 rounded-full h-1">
|
||||
<div
|
||||
:class="getWindowTokenProgressColor(key)"
|
||||
class="h-1 rounded-full transition-all duration-300"
|
||||
:style="{ width: getWindowTokenProgress(key) + '%' }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 查看详情按钮 -->
|
||||
<div class="pt-1">
|
||||
<button
|
||||
class="text-xs text-blue-600 hover:text-blue-800 font-medium flex items-center gap-1 w-full justify-center py-1 hover:bg-blue-50 rounded transition-colors"
|
||||
@click="showUsageDetails(key)"
|
||||
>
|
||||
<i class="fas fa-chart-line" />
|
||||
查看详细统计
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -373,6 +384,15 @@
|
||||
</td>
|
||||
<td class="px-3 py-4 whitespace-nowrap text-sm">
|
||||
<div class="flex gap-1">
|
||||
<button
|
||||
v-if="key && key.id"
|
||||
class="text-indigo-600 hover:text-indigo-900 font-medium hover:bg-indigo-50 px-2 py-1 rounded transition-colors text-xs"
|
||||
title="模型使用分布"
|
||||
@click="toggleApiKeyModelStats(key.id)"
|
||||
>
|
||||
<i :class="['fas', expandedApiKeys[key.id] ? 'fa-chevron-up' : 'fa-chevron-down']" />
|
||||
<span class="hidden xl:inline ml-1">模型</span>
|
||||
</button>
|
||||
<button
|
||||
class="text-purple-600 hover:text-purple-900 font-medium hover:bg-purple-50 px-2 py-1 rounded transition-colors text-xs"
|
||||
title="复制统计页面链接"
|
||||
@@ -472,10 +492,10 @@
|
||||
:default-time="defaultTime"
|
||||
size="small"
|
||||
style="width: 280px;"
|
||||
@update:model-value="(value) => onApiKeyCustomDateRangeChange(key.id, value)"
|
||||
class="api-key-date-picker"
|
||||
:clearable="true"
|
||||
:unlink-panels="false"
|
||||
@update:model-value="(value) => onApiKeyCustomDateRangeChange(key.id, value)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -666,25 +686,104 @@
|
||||
</div>
|
||||
|
||||
<!-- 统计信息 -->
|
||||
<div class="grid grid-cols-2 gap-3 mb-3">
|
||||
<div>
|
||||
<p class="text-xs text-gray-500">
|
||||
使用量
|
||||
</p>
|
||||
<p class="text-sm font-semibold text-gray-900">
|
||||
{{ formatNumber((key.usage && key.usage.total && key.usage.total.requests) || 0) }} 次
|
||||
</p>
|
||||
<p class="text-xs text-gray-500 mt-0.5">
|
||||
{{ formatNumber((key.usage && key.usage.total && key.usage.total.tokens) || 0) }} tokens
|
||||
</p>
|
||||
<div class="space-y-2 mb-3">
|
||||
<!-- 今日使用 -->
|
||||
<div class="bg-gray-50 rounded-lg p-3">
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<span class="text-xs text-gray-600">今日使用</span>
|
||||
<button
|
||||
class="text-xs text-blue-600 hover:text-blue-800"
|
||||
@click="showUsageDetails(key)"
|
||||
>
|
||||
<i class="fas fa-chart-line mr-1" />详情
|
||||
</button>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<p class="text-sm font-semibold text-gray-900">
|
||||
{{ formatNumber((key.usage?.daily?.requests) || 0) }} 次
|
||||
</p>
|
||||
<p class="text-xs text-gray-500">
|
||||
请求
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-semibold text-green-600">
|
||||
${{ (key.dailyCost || 0).toFixed(4) }}
|
||||
</p>
|
||||
<p class="text-xs text-gray-500">
|
||||
费用
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-gray-500">
|
||||
费用
|
||||
</p>
|
||||
<p class="text-sm font-semibold text-green-600">
|
||||
{{ calculateApiKeyCost(key.usage) }}
|
||||
</p>
|
||||
|
||||
<!-- 限制进度 -->
|
||||
<div
|
||||
v-if="key.dailyCostLimit > 0"
|
||||
class="space-y-1"
|
||||
>
|
||||
<div class="flex justify-between items-center text-xs">
|
||||
<span class="text-gray-500">每日费用限额</span>
|
||||
<span class="text-gray-700">
|
||||
${{ (key.dailyCost || 0).toFixed(2) }} / ${{ key.dailyCostLimit.toFixed(2) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 rounded-full h-2">
|
||||
<div
|
||||
:class="getDailyCostProgressColor(key)"
|
||||
class="h-2 rounded-full transition-all duration-300"
|
||||
:style="{ width: getDailyCostProgress(key) + '%' }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 移动端时间窗口限制 -->
|
||||
<div
|
||||
v-if="key.rateLimitWindow > 0 && (key.rateLimitRequests > 0 || key.tokenLimit > 0)"
|
||||
class="space-y-1"
|
||||
>
|
||||
<div class="text-xs text-gray-500 mb-1">
|
||||
窗口限制 ({{ key.rateLimitWindow }}分钟)
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="key.rateLimitRequests > 0"
|
||||
class="flex items-center gap-2"
|
||||
>
|
||||
<span class="text-xs text-gray-500 w-10">请求</span>
|
||||
<div class="flex-1">
|
||||
<div class="w-full bg-gray-200 rounded-full h-1.5">
|
||||
<div
|
||||
:class="getWindowRequestProgressColor(key)"
|
||||
class="h-1.5 rounded-full transition-all duration-300"
|
||||
:style="{ width: getWindowRequestProgress(key) + '%' }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-xs text-gray-600 w-16 text-right">
|
||||
{{ key.currentWindowRequests || 0 }}/{{ key.rateLimitRequests }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="key.tokenLimit > 0"
|
||||
class="flex items-center gap-2"
|
||||
>
|
||||
<span class="text-xs text-gray-500 w-10">Token</span>
|
||||
<div class="flex-1">
|
||||
<div class="w-full bg-gray-200 rounded-full h-1.5">
|
||||
<div
|
||||
:class="getWindowTokenProgressColor(key)"
|
||||
class="h-1.5 rounded-full transition-all duration-300"
|
||||
:style="{ width: getWindowTokenProgress(key) + '%' }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-xs text-gray-600 w-16 text-right">
|
||||
{{ formatTokenCount(key.currentWindowTokens || 0) }}/{{ formatTokenCount(key.tokenLimit) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -720,10 +819,10 @@
|
||||
<div class="flex gap-2 mt-3 pt-3 border-t border-gray-100">
|
||||
<button
|
||||
class="flex-1 px-3 py-2 text-xs text-blue-600 bg-blue-50 rounded-lg hover:bg-blue-100 transition-colors flex items-center justify-center gap-1"
|
||||
@click="toggleExpanded(key.id)"
|
||||
@click="showUsageDetails(key)"
|
||||
>
|
||||
<i :class="['fas', expandedKeys.includes(key.id) ? 'fa-chevron-up' : 'fa-chevron-down']" />
|
||||
{{ expandedKeys.includes(key.id) ? '收起' : '详情' }}
|
||||
<i class="fas fa-chart-line" />
|
||||
查看详情
|
||||
</button>
|
||||
<button
|
||||
class="flex-1 px-3 py-2 text-xs text-gray-600 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors"
|
||||
@@ -747,77 +846,6 @@
|
||||
<i class="fas fa-trash" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 展开的详细统计 -->
|
||||
<div
|
||||
v-if="expandedKeys.includes(key.id)"
|
||||
class="mt-3 pt-3 border-t border-gray-100"
|
||||
>
|
||||
<h5 class="text-xs font-semibold text-gray-700 mb-2">
|
||||
详细信息
|
||||
</h5>
|
||||
|
||||
<!-- 更多统计数据 -->
|
||||
<div class="space-y-2 text-xs">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600">并发限制:</span>
|
||||
<span class="font-medium text-purple-600">{{ key.concurrencyLimit > 0 ? key.concurrencyLimit : '无限制' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600">当前并发:</span>
|
||||
<span :class="['font-medium', key.currentConcurrency > 0 ? 'text-orange-600' : 'text-gray-600']">
|
||||
{{ key.currentConcurrency || 0 }}
|
||||
<span v-if="key.concurrencyLimit > 0" class="text-xs text-gray-500">/ {{ key.concurrencyLimit }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="key.dailyCostLimit > 0" class="flex justify-between">
|
||||
<span class="text-gray-600">今日费用:</span>
|
||||
<span :class="['font-medium', (key.dailyCost || 0) >= key.dailyCostLimit ? 'text-red-600' : 'text-blue-600']">
|
||||
${{ (key.dailyCost || 0).toFixed(2) }} / ${{ key.dailyCostLimit.toFixed(2) }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="key.rateLimitWindow > 0" class="flex justify-between">
|
||||
<span class="text-gray-600">时间窗口:</span>
|
||||
<span class="font-medium text-indigo-600">{{ key.rateLimitWindow }} 分钟</span>
|
||||
</div>
|
||||
<div v-if="key.rateLimitRequests > 0" class="flex justify-between">
|
||||
<span class="text-gray-600">请求限制:</span>
|
||||
<span class="font-medium text-indigo-600">{{ key.rateLimitRequests }} 次/窗口</span>
|
||||
</div>
|
||||
|
||||
<!-- Token 细节 -->
|
||||
<div class="pt-2 mt-2 border-t border-gray-100">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600">输入 Token:</span>
|
||||
<span class="font-medium">{{ formatNumber((key.usage && key.usage.total && key.usage.total.inputTokens) || 0) }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600">输出 Token:</span>
|
||||
<span class="font-medium">{{ formatNumber((key.usage && key.usage.total && key.usage.total.outputTokens) || 0) }}</span>
|
||||
</div>
|
||||
<div v-if="((key.usage && key.usage.total && key.usage.total.cacheCreateTokens) || 0) > 0" class="flex justify-between">
|
||||
<span class="text-gray-600">缓存创建:</span>
|
||||
<span class="font-medium text-purple-600">{{ formatNumber((key.usage && key.usage.total && key.usage.total.cacheCreateTokens) || 0) }}</span>
|
||||
</div>
|
||||
<div v-if="((key.usage && key.usage.total && key.usage.total.cacheReadTokens) || 0) > 0" class="flex justify-between">
|
||||
<span class="text-gray-600">缓存读取:</span>
|
||||
<span class="font-medium text-purple-600">{{ formatNumber((key.usage && key.usage.total && key.usage.total.cacheReadTokens) || 0) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 今日统计 -->
|
||||
<div class="pt-2 mt-2 border-t border-gray-100">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600">今日请求:</span>
|
||||
<span class="font-medium text-green-600">{{ formatNumber((key.usage && key.usage.daily && key.usage.daily.requests) || 0) }} 次</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600">今日 Token:</span>
|
||||
<span class="font-medium text-green-600">{{ formatNumber((key.usage && key.usage.daily && key.usage.daily.tokens) || 0) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -959,6 +987,13 @@
|
||||
@close="closeExpiryEdit"
|
||||
@save="handleSaveExpiry"
|
||||
/>
|
||||
|
||||
<!-- 使用详情弹窗 -->
|
||||
<UsageDetailModal
|
||||
:show="showUsageDetailModal"
|
||||
:api-key="selectedApiKeyForDetail || {}"
|
||||
@close="showUsageDetailModal = false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -973,6 +1008,7 @@ import RenewApiKeyModal from '@/components/apikeys/RenewApiKeyModal.vue'
|
||||
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'
|
||||
|
||||
// 响应式数据
|
||||
const clientsStore = useClientsStore()
|
||||
@@ -988,13 +1024,13 @@ const defaultTime = ref([new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 2, 1, 23,
|
||||
const accounts = ref({ claude: [], gemini: [], claudeGroups: [], geminiGroups: [] })
|
||||
const editingExpiryKey = ref(null)
|
||||
const expiryEditModalRef = ref(null)
|
||||
const showUsageDetailModal = ref(false)
|
||||
const selectedApiKeyForDetail = ref(null)
|
||||
|
||||
// 标签相关
|
||||
const selectedTagFilter = ref('')
|
||||
const availableTags = ref([])
|
||||
|
||||
// 移动端展开状态
|
||||
const expandedKeys = ref([])
|
||||
|
||||
// 分页相关
|
||||
const currentPage = ref(1)
|
||||
@@ -1183,16 +1219,36 @@ const calculateApiKeyCost = (usage) => {
|
||||
const getBoundAccountName = (accountId) => {
|
||||
if (!accountId) return '未知账户'
|
||||
|
||||
// 检查是否是分组
|
||||
if (accountId.startsWith('group:')) {
|
||||
const groupId = accountId.substring(6) // 移除 'group:' 前缀
|
||||
|
||||
// 从Claude分组中查找
|
||||
const claudeGroup = accounts.value.claudeGroups.find(g => g.id === groupId)
|
||||
if (claudeGroup) {
|
||||
return `分组-${claudeGroup.name}`
|
||||
}
|
||||
|
||||
// 从Gemini分组中查找
|
||||
const geminiGroup = accounts.value.geminiGroups.find(g => g.id === groupId)
|
||||
if (geminiGroup) {
|
||||
return `分组-${geminiGroup.name}`
|
||||
}
|
||||
|
||||
// 如果找不到分组,返回分组ID的前8位
|
||||
return `分组-${groupId.substring(0, 8)}`
|
||||
}
|
||||
|
||||
// 从Claude账户列表中查找
|
||||
const claudeAccount = accounts.value.claude.find(acc => acc.id === accountId)
|
||||
if (claudeAccount) {
|
||||
return claudeAccount.name
|
||||
return `账户-${claudeAccount.name}`
|
||||
}
|
||||
|
||||
// 从Gemini账户列表中查找
|
||||
const geminiAccount = accounts.value.gemini.find(acc => acc.id === accountId)
|
||||
if (geminiAccount) {
|
||||
return geminiAccount.name
|
||||
return `账户-${geminiAccount.name}`
|
||||
}
|
||||
|
||||
// 如果找不到,返回账户ID的前8位
|
||||
@@ -1548,15 +1604,6 @@ const handleSaveExpiry = async ({ keyId, expiresAt }) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 切换移动端卡片展开状态
|
||||
const toggleExpanded = (keyId) => {
|
||||
const index = expandedKeys.value.indexOf(keyId)
|
||||
if (index > -1) {
|
||||
expandedKeys.value.splice(index, 1)
|
||||
} else {
|
||||
expandedKeys.value.push(keyId)
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化日期时间
|
||||
const formatDate = (dateString) => {
|
||||
@@ -1571,26 +1618,68 @@ const formatDate = (dateString) => {
|
||||
}).replace(/\//g, '-')
|
||||
}
|
||||
|
||||
// 显示API Key详情
|
||||
const showApiKey = async (apiKey) => {
|
||||
try {
|
||||
// 重新获取API Key的完整信息(包含实际的key值)
|
||||
const response = await apiClient.get(`/admin/api-keys/${apiKey.id}`)
|
||||
if (response.success && response.data) {
|
||||
newApiKeyData.value = {
|
||||
...response.data,
|
||||
key: response.data.key || response.data.apiKey // 兼容不同的字段名
|
||||
}
|
||||
showNewApiKeyModal.value = true
|
||||
} else {
|
||||
showToast('获取API Key信息失败', 'error')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching API key:', error)
|
||||
showToast('获取API Key信息失败', 'error')
|
||||
}
|
||||
// 获取每日费用进度
|
||||
const getDailyCostProgress = (key) => {
|
||||
if (!key.dailyCostLimit || key.dailyCostLimit === 0) return 0
|
||||
const percentage = ((key.dailyCost || 0) / key.dailyCostLimit) * 100
|
||||
return Math.min(percentage, 100)
|
||||
}
|
||||
|
||||
// 获取每日费用进度条颜色
|
||||
const getDailyCostProgressColor = (key) => {
|
||||
const progress = getDailyCostProgress(key)
|
||||
if (progress >= 100) return 'bg-red-500'
|
||||
if (progress >= 80) return 'bg-yellow-500'
|
||||
return 'bg-green-500'
|
||||
}
|
||||
|
||||
// 显示使用详情
|
||||
const showUsageDetails = (apiKey) => {
|
||||
selectedApiKeyForDetail.value = apiKey
|
||||
showUsageDetailModal.value = true
|
||||
}
|
||||
|
||||
// 格式化Token数量(使用K/M单位)
|
||||
const formatTokenCount = (count) => {
|
||||
if (count >= 1000000) {
|
||||
return (count / 1000000).toFixed(1) + 'M'
|
||||
} else if (count >= 1000) {
|
||||
return (count / 1000).toFixed(1) + 'K'
|
||||
}
|
||||
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
|
||||
|
||||
@@ -180,8 +180,12 @@
|
||||
<i class="fas fa-font text-white text-sm" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold text-gray-900">网站名称</h4>
|
||||
<p class="text-xs text-gray-500">品牌标识</p>
|
||||
<h4 class="text-sm font-semibold text-gray-900">
|
||||
网站名称
|
||||
</h4>
|
||||
<p class="text-xs text-gray-500">
|
||||
品牌标识
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
@@ -205,8 +209,12 @@
|
||||
<i class="fas fa-image text-white text-sm" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold text-gray-900">网站图标</h4>
|
||||
<p class="text-xs text-gray-500">Favicon</p>
|
||||
<h4 class="text-sm font-semibold text-gray-900">
|
||||
网站图标
|
||||
</h4>
|
||||
<p class="text-xs text-gray-500">
|
||||
Favicon
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
|
||||
Reference in New Issue
Block a user