mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-23 00:53:33 +00:00
fix: 修复仪表盘和API统计页面的多个问题
- 修复仪表盘天粒度下7天/30天快捷选择无数据的问题 - 修复API Keys页面统计按钮链接路由错误(admin -> admin-next) - 改进统计页面限制展示,使用3个进度条更直观显示使用情况 - 后端API响应增加当前使用量数据(currentWindowRequests/Tokens/DailyCost) - 修复教程页面window.location.origin为空的兼容性问题 - 无限制时使用无穷符号(∞)展示,提升用户体验 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -269,6 +269,28 @@ router.post('/api/user-stats', async (req, res) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取当前使用量
|
||||||
|
let currentWindowRequests = 0;
|
||||||
|
let currentWindowTokens = 0;
|
||||||
|
let currentDailyCost = 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 获取当前时间窗口的请求次数和Token使用量
|
||||||
|
if (fullKeyData.rateLimitWindow > 0) {
|
||||||
|
const client = redis.getClientSafe();
|
||||||
|
const requestCountKey = `rate_limit:requests:${keyId}`;
|
||||||
|
const tokenCountKey = `rate_limit:tokens:${keyId}`;
|
||||||
|
|
||||||
|
currentWindowRequests = parseInt(await client.get(requestCountKey) || '0');
|
||||||
|
currentWindowTokens = parseInt(await client.get(tokenCountKey) || '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当日费用
|
||||||
|
currentDailyCost = await redis.getDailyCost(keyId) || 0;
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(`Failed to get current usage for key ${keyId}:`, error);
|
||||||
|
}
|
||||||
|
|
||||||
// 构建响应数据(只返回该API Key自己的信息,确保不泄露其他信息)
|
// 构建响应数据(只返回该API Key自己的信息,确保不泄露其他信息)
|
||||||
const responseData = {
|
const responseData = {
|
||||||
id: keyId,
|
id: keyId,
|
||||||
@@ -296,13 +318,17 @@ router.post('/api/user-stats', async (req, res) => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// 限制信息(只显示配置,不显示当前使用量)
|
// 限制信息(显示配置和当前使用量)
|
||||||
limits: {
|
limits: {
|
||||||
tokenLimit: fullKeyData.tokenLimit || 0,
|
tokenLimit: fullKeyData.tokenLimit || 0,
|
||||||
concurrencyLimit: fullKeyData.concurrencyLimit || 0,
|
concurrencyLimit: fullKeyData.concurrencyLimit || 0,
|
||||||
rateLimitWindow: fullKeyData.rateLimitWindow || 0,
|
rateLimitWindow: fullKeyData.rateLimitWindow || 0,
|
||||||
rateLimitRequests: fullKeyData.rateLimitRequests || 0,
|
rateLimitRequests: fullKeyData.rateLimitRequests || 0,
|
||||||
dailyCostLimit: fullKeyData.dailyCostLimit || 0
|
dailyCostLimit: fullKeyData.dailyCostLimit || 0,
|
||||||
|
// 当前使用量
|
||||||
|
currentWindowRequests: currentWindowRequests,
|
||||||
|
currentWindowTokens: currentWindowTokens,
|
||||||
|
currentDailyCost: currentDailyCost
|
||||||
},
|
},
|
||||||
|
|
||||||
// 绑定的账户信息(只显示ID,不显示敏感信息)
|
// 绑定的账户信息(只显示ID,不显示敏感信息)
|
||||||
|
|||||||
@@ -6,64 +6,131 @@
|
|||||||
<i class="fas fa-shield-alt mr-2 md:mr-3 text-red-500 text-sm md:text-base" />
|
<i class="fas fa-shield-alt mr-2 md:mr-3 text-red-500 text-sm md:text-base" />
|
||||||
限制配置
|
限制配置
|
||||||
</h3>
|
</h3>
|
||||||
<div class="space-y-2 md:space-y-3">
|
<div class="space-y-4 md:space-y-5">
|
||||||
<div class="flex justify-between items-center">
|
<!-- 每日费用限制 -->
|
||||||
<span class="text-gray-600 text-sm md:text-base">Token 限制</span>
|
<div>
|
||||||
<span class="font-medium text-gray-900 text-sm md:text-base">{{ statsData.limits.tokenLimit > 0 ? formatNumber(statsData.limits.tokenLimit) : '无限制' }}</span>
|
<div class="flex justify-between items-center mb-2">
|
||||||
</div>
|
<span class="text-gray-600 text-sm md:text-base font-medium">每日费用限制</span>
|
||||||
<div class="flex justify-between items-center">
|
<span class="text-xs md:text-sm text-gray-500">
|
||||||
<span class="text-gray-600 text-sm md:text-base">并发限制</span>
|
<span v-if="statsData.limits.dailyCostLimit > 0">
|
||||||
<span class="font-medium text-gray-900 text-sm md:text-base">{{ statsData.limits.concurrencyLimit > 0 ? statsData.limits.concurrencyLimit : '无限制' }}</span>
|
${{ statsData.limits.currentDailyCost.toFixed(4) }} / ${{ statsData.limits.dailyCostLimit.toFixed(2) }}
|
||||||
</div>
|
</span>
|
||||||
<div class="flex justify-between items-center">
|
<span v-else class="flex items-center gap-1">
|
||||||
<span class="text-gray-600 text-sm md:text-base">速率限制</span>
|
${{ statsData.limits.currentDailyCost.toFixed(4) }} / <i class="fas fa-infinity" />
|
||||||
<span class="font-medium text-gray-900 text-sm md:text-base">
|
</span>
|
||||||
{{ statsData.limits.rateLimitRequests > 0 && statsData.limits.rateLimitWindow > 0
|
|
||||||
? `${statsData.limits.rateLimitRequests}次/${statsData.limits.rateLimitWindow}分钟`
|
|
||||||
: '无限制' }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-between items-center">
|
|
||||||
<span class="text-gray-600 text-sm md:text-base">每日费用限制</span>
|
|
||||||
<span class="font-medium text-gray-900 text-sm md:text-base">{{ statsData.limits.dailyCostLimit > 0 ? '$' + statsData.limits.dailyCostLimit : '无限制' }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-between items-center">
|
|
||||||
<span class="text-gray-600 text-sm md:text-base">模型限制</span>
|
|
||||||
<span class="font-medium text-gray-900 text-sm md:text-base">
|
|
||||||
<span
|
|
||||||
v-if="statsData.restrictions.enableModelRestriction && statsData.restrictions.restrictedModels.length > 0"
|
|
||||||
class="text-orange-600"
|
|
||||||
>
|
|
||||||
<i class="fas fa-exclamation-triangle mr-1 text-xs md:text-sm" />
|
|
||||||
限制 {{ statsData.restrictions.restrictedModels.length }} 个模型
|
|
||||||
</span>
|
</span>
|
||||||
<span
|
</div>
|
||||||
v-else
|
<div v-if="statsData.limits.dailyCostLimit > 0" class="w-full bg-gray-200 rounded-full h-2">
|
||||||
class="text-green-600"
|
<div
|
||||||
>
|
:class="getDailyCostProgressColor()"
|
||||||
<i class="fas fa-check-circle mr-1 text-xs md:text-sm" />
|
class="h-2 rounded-full transition-all duration-300"
|
||||||
允许所有模型
|
:style="{ width: getDailyCostProgress() + '%' }"
|
||||||
</span>
|
/>
|
||||||
</span>
|
</div>
|
||||||
|
<div v-else class="w-full bg-gray-200 rounded-full h-2">
|
||||||
|
<div class="bg-green-500 h-2 rounded-full" style="width: 0%" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between items-center">
|
|
||||||
<span class="text-gray-600 text-sm md:text-base">客户端限制</span>
|
<!-- 时间窗口限制 -->
|
||||||
<span class="font-medium text-gray-900 text-sm md:text-base">
|
<div v-if="statsData.limits.rateLimitWindow > 0 && (statsData.limits.rateLimitRequests > 0 || statsData.limits.tokenLimit > 0)">
|
||||||
<span
|
<div class="flex justify-between items-center mb-2">
|
||||||
v-if="statsData.restrictions.enableClientRestriction && statsData.restrictions.allowedClients.length > 0"
|
<span class="text-gray-600 text-sm md:text-base font-medium">
|
||||||
class="text-orange-600"
|
时间窗口限制 ({{ statsData.limits.rateLimitWindow }}分钟)
|
||||||
>
|
|
||||||
<i class="fas fa-exclamation-triangle mr-1 text-xs md:text-sm" />
|
|
||||||
限制 {{ statsData.restrictions.allowedClients.length }} 个客户端
|
|
||||||
</span>
|
</span>
|
||||||
<span
|
</div>
|
||||||
v-else
|
|
||||||
class="text-green-600"
|
<!-- 请求次数限制 -->
|
||||||
>
|
<div v-if="statsData.limits.rateLimitRequests > 0" class="space-y-1.5 mb-3">
|
||||||
<i class="fas fa-check-circle mr-1 text-xs md:text-sm" />
|
<div class="flex justify-between items-center text-xs md:text-sm">
|
||||||
允许所有客户端
|
<span class="text-gray-500">请求次数</span>
|
||||||
|
<span class="text-gray-700">
|
||||||
|
{{ formatNumber(statsData.limits.currentWindowRequests) }} / {{ formatNumber(statsData.limits.rateLimitRequests) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="w-full bg-gray-200 rounded-full h-1.5">
|
||||||
|
<div
|
||||||
|
:class="getWindowRequestProgressColor()"
|
||||||
|
class="h-1.5 rounded-full transition-all duration-300"
|
||||||
|
:style="{ width: getWindowRequestProgress() + '%' }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Token使用量限制 -->
|
||||||
|
<div v-if="statsData.limits.tokenLimit > 0" class="space-y-1.5">
|
||||||
|
<div class="flex justify-between items-center text-xs md:text-sm">
|
||||||
|
<span class="text-gray-500">Token 使用量</span>
|
||||||
|
<span class="text-gray-700">
|
||||||
|
{{ formatNumber(statsData.limits.currentWindowTokens) }} / {{ formatNumber(statsData.limits.tokenLimit) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="w-full bg-gray-200 rounded-full h-1.5">
|
||||||
|
<div
|
||||||
|
:class="getWindowTokenProgressColor()"
|
||||||
|
class="h-1.5 rounded-full transition-all duration-300"
|
||||||
|
:style="{ width: getWindowTokenProgress() + '%' }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-2 text-xs text-gray-500">
|
||||||
|
<i class="fas fa-info-circle mr-1" />
|
||||||
|
请求次数和Token使用量为"或"的关系,任一达到限制即触发限流
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 其他限制信息 -->
|
||||||
|
<div class="pt-2 border-t border-gray-100 space-y-2">
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<span class="text-gray-600 text-sm md:text-base">并发限制</span>
|
||||||
|
<span class="font-medium text-gray-900 text-sm md:text-base">
|
||||||
|
<span v-if="statsData.limits.concurrencyLimit > 0">
|
||||||
|
{{ statsData.limits.concurrencyLimit }}
|
||||||
|
</span>
|
||||||
|
<span v-else class="flex items-center gap-1">
|
||||||
|
<i class="fas fa-infinity text-gray-400" />
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</div>
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<span class="text-gray-600 text-sm md:text-base">模型限制</span>
|
||||||
|
<span class="font-medium text-gray-900 text-sm md:text-base">
|
||||||
|
<span
|
||||||
|
v-if="statsData.restrictions.enableModelRestriction && statsData.restrictions.restrictedModels.length > 0"
|
||||||
|
class="text-orange-600"
|
||||||
|
>
|
||||||
|
<i class="fas fa-exclamation-triangle mr-1 text-xs md:text-sm" />
|
||||||
|
限制 {{ statsData.restrictions.restrictedModels.length }} 个模型
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-else
|
||||||
|
class="text-green-600"
|
||||||
|
>
|
||||||
|
<i class="fas fa-check-circle mr-1 text-xs md:text-sm" />
|
||||||
|
允许所有模型
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<span class="text-gray-600 text-sm md:text-base">客户端限制</span>
|
||||||
|
<span class="font-medium text-gray-900 text-sm md:text-base">
|
||||||
|
<span
|
||||||
|
v-if="statsData.restrictions.enableClientRestriction && statsData.restrictions.allowedClients.length > 0"
|
||||||
|
class="text-orange-600"
|
||||||
|
>
|
||||||
|
<i class="fas fa-exclamation-triangle mr-1 text-xs md:text-sm" />
|
||||||
|
限制 {{ statsData.restrictions.allowedClients.length }} 个客户端
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-else
|
||||||
|
class="text-green-600"
|
||||||
|
>
|
||||||
|
<i class="fas fa-check-circle mr-1 text-xs md:text-sm" />
|
||||||
|
允许所有客户端
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -158,6 +225,51 @@ const formatNumber = (num) => {
|
|||||||
return num.toLocaleString()
|
return num.toLocaleString()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取每日费用进度
|
||||||
|
const getDailyCostProgress = () => {
|
||||||
|
if (!statsData.value.limits.dailyCostLimit || statsData.value.limits.dailyCostLimit === 0) return 0
|
||||||
|
const percentage = (statsData.value.limits.currentDailyCost / statsData.value.limits.dailyCostLimit) * 100
|
||||||
|
return Math.min(percentage, 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取每日费用进度条颜色
|
||||||
|
const getDailyCostProgressColor = () => {
|
||||||
|
const progress = getDailyCostProgress()
|
||||||
|
if (progress >= 100) return 'bg-red-500'
|
||||||
|
if (progress >= 80) return 'bg-yellow-500'
|
||||||
|
return 'bg-green-500'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取窗口请求进度
|
||||||
|
const getWindowRequestProgress = () => {
|
||||||
|
if (!statsData.value.limits.rateLimitRequests || statsData.value.limits.rateLimitRequests === 0) return 0
|
||||||
|
const percentage = (statsData.value.limits.currentWindowRequests / statsData.value.limits.rateLimitRequests) * 100
|
||||||
|
return Math.min(percentage, 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取窗口请求进度条颜色
|
||||||
|
const getWindowRequestProgressColor = () => {
|
||||||
|
const progress = getWindowRequestProgress()
|
||||||
|
if (progress >= 100) return 'bg-red-500'
|
||||||
|
if (progress >= 80) return 'bg-yellow-500'
|
||||||
|
return 'bg-blue-500'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取窗口Token进度
|
||||||
|
const getWindowTokenProgress = () => {
|
||||||
|
if (!statsData.value.limits.tokenLimit || statsData.value.limits.tokenLimit === 0) return 0
|
||||||
|
const percentage = (statsData.value.limits.currentWindowTokens / statsData.value.limits.tokenLimit) * 100
|
||||||
|
return Math.min(percentage, 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取窗口Token进度条颜色
|
||||||
|
const getWindowTokenProgressColor = () => {
|
||||||
|
const progress = getWindowTokenProgress()
|
||||||
|
if (progress >= 100) return 'bg-red-500'
|
||||||
|
if (progress >= 80) return 'bg-yellow-500'
|
||||||
|
return 'bg-purple-500'
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -311,6 +311,28 @@ export const useDashboardStore = defineStore('dashboard', () => {
|
|||||||
url += `&startDate=${encodeURIComponent(startTime.toISOString())}`
|
url += `&startDate=${encodeURIComponent(startTime.toISOString())}`
|
||||||
url += `&endDate=${encodeURIComponent(endTime.toISOString())}`
|
url += `&endDate=${encodeURIComponent(endTime.toISOString())}`
|
||||||
}
|
}
|
||||||
|
} else if (dateFilter.value.type === 'preset' && trendGranularity.value === 'day') {
|
||||||
|
// 天粒度的预设时间范围,需要传递startDate和endDate参数
|
||||||
|
const now = new Date()
|
||||||
|
let startDate, endDate
|
||||||
|
|
||||||
|
const option = dateFilter.value.presetOptions.find(opt => opt.value === dateFilter.value.preset)
|
||||||
|
if (option) {
|
||||||
|
if (dateFilter.value.preset === 'today') {
|
||||||
|
// 今日:从系统时区的今天0点到23:59
|
||||||
|
startDate = getSystemTimezoneDay(now, true)
|
||||||
|
endDate = getSystemTimezoneDay(now, false)
|
||||||
|
} else {
|
||||||
|
// 7天或30天:从N天前的0点到今天的23:59
|
||||||
|
const daysAgo = new Date()
|
||||||
|
daysAgo.setDate(daysAgo.getDate() - (option.days - 1))
|
||||||
|
startDate = getSystemTimezoneDay(daysAgo, true)
|
||||||
|
endDate = getSystemTimezoneDay(now, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
url += `&startDate=${encodeURIComponent(startDate.toISOString())}`
|
||||||
|
url += `&endDate=${encodeURIComponent(endDate.toISOString())}`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await apiClient.get(url)
|
const response = await apiClient.get(url)
|
||||||
|
|||||||
@@ -1533,7 +1533,7 @@ const deleteApiKey = async (keyId) => {
|
|||||||
const copyApiStatsLink = (apiKey) => {
|
const copyApiStatsLink = (apiKey) => {
|
||||||
// 构建统计页面的完整URL
|
// 构建统计页面的完整URL
|
||||||
const baseUrl = window.location.origin
|
const baseUrl = window.location.origin
|
||||||
const statsUrl = `${baseUrl}/admin/api-stats?apiId=${apiKey.id}`
|
const statsUrl = `${baseUrl}/admin-next/api-stats?apiId=${apiKey.id}`
|
||||||
|
|
||||||
// 使用传统的textarea方法复制到剪贴板
|
// 使用传统的textarea方法复制到剪贴板
|
||||||
const textarea = document.createElement('textarea')
|
const textarea = document.createElement('textarea')
|
||||||
|
|||||||
@@ -1132,7 +1132,42 @@ const tutorialSystems = [
|
|||||||
|
|
||||||
// 当前基础URL
|
// 当前基础URL
|
||||||
const currentBaseUrl = computed(() => {
|
const currentBaseUrl = computed(() => {
|
||||||
return window.location.origin
|
// 更健壮的获取 origin 的方法,兼容旧版浏览器和特殊环境
|
||||||
|
let origin = ''
|
||||||
|
|
||||||
|
if (window.location.origin) {
|
||||||
|
// 现代浏览器直接支持 origin
|
||||||
|
origin = window.location.origin
|
||||||
|
} else {
|
||||||
|
// 旧版浏览器或特殊环境的兼容处理
|
||||||
|
const protocol = window.location.protocol
|
||||||
|
const hostname = window.location.hostname
|
||||||
|
const port = window.location.port
|
||||||
|
|
||||||
|
origin = protocol + '//' + hostname
|
||||||
|
|
||||||
|
// 只有在非默认端口时才添加端口号
|
||||||
|
if (port &&
|
||||||
|
((protocol === 'http:' && port !== '80') ||
|
||||||
|
(protocol === 'https:' && port !== '443'))) {
|
||||||
|
origin += ':' + port
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果还是获取不到,使用当前页面的 URL 推导
|
||||||
|
if (!origin) {
|
||||||
|
const currentUrl = window.location.href
|
||||||
|
const pathStart = currentUrl.indexOf('/', 8) // 跳过 http:// 或 https://
|
||||||
|
if (pathStart !== -1) {
|
||||||
|
origin = currentUrl.substring(0, pathStart)
|
||||||
|
} else {
|
||||||
|
// 最后的降级方案,使用相对路径
|
||||||
|
console.warn('无法获取完整的 origin,将使用相对路径')
|
||||||
|
return '/api'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return origin + '/api'
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user