feat: api-stats页面增加周限总限查询

This commit is contained in:
shaw
2025-09-21 14:22:34 +08:00
parent c5ce32e029
commit f9c397cc1f
6 changed files with 374 additions and 60 deletions

View File

@@ -17,10 +17,17 @@
--- ---
## ⭐ 如果觉得有用点个Star支持一下吧 ## 💎 Claude 拼车 - Claude Code 合租服务推荐
> 开源不易你的Star是我持续更新的动力 🚀 <div align="center">
> 欢迎加入 [Telegram 公告频道](https://t.me/claude_relay_service) 获取最新动态
| 平台 | 类型 | 介绍 |
|:---:|:---:|:---|
| **[PinCC.ai](https://pincc.ai/)** | 🏆 **官方运营** | 项目官方直营的Claude拼车服务<br>提供200刀 Claude Code Max 套餐共享服务 |
| **[CToK.ai](https://ctok.ai/)** | 🤝 合作伙伴 | 社区认可的Claude拼车服务稳定可靠 |
</div>
--- ---
@@ -42,11 +49,6 @@
如果有以上困惑,那这个项目可能适合你。 如果有以上困惑,那这个项目可能适合你。
> 💡 **Claude Code 拼车服务**
> 目前有两个稳定的 Claude Code Max 20X 200刀 拼车渠道:
> 1. **PinCC** - 项目官方运营的拼车服务:[https://pincc.ai/](https://pincc.ai/)
> 2. **CToK** - 社区认可的合作伙伴服务:[https://ctok.ai/](https://ctok.ai/)
### 适合的场景 ### 适合的场景
**找朋友拼车**: 三五好友一起分摊Claude Code Max订阅 **找朋友拼车**: 三五好友一起分摊Claude Code Max订阅
@@ -76,8 +78,6 @@
## 🚀 核心功能 ## 🚀 核心功能
> 📸 **[查看演示站点](https://demo.pincc.ai/admin-next/login)**
### 基础功能 ### 基础功能
-**多账户管理**: 可以添加多个Claude账户自动轮换 -**多账户管理**: 可以添加多个Claude账户自动轮换

View File

@@ -376,12 +376,14 @@ router.post('/api/user-stats', async (req, res) => {
rateLimitCost: parseFloat(fullKeyData.rateLimitCost) || 0, // 新增:费用限制 rateLimitCost: parseFloat(fullKeyData.rateLimitCost) || 0, // 新增:费用限制
dailyCostLimit: fullKeyData.dailyCostLimit || 0, dailyCostLimit: fullKeyData.dailyCostLimit || 0,
totalCostLimit: fullKeyData.totalCostLimit || 0, totalCostLimit: fullKeyData.totalCostLimit || 0,
weeklyOpusCostLimit: parseFloat(fullKeyData.weeklyOpusCostLimit) || 0, // Opus 周费用限制
// 当前使用量 // 当前使用量
currentWindowRequests, currentWindowRequests,
currentWindowTokens, currentWindowTokens,
currentWindowCost, // 新增:当前窗口费用 currentWindowCost, // 新增:当前窗口费用
currentDailyCost, currentDailyCost,
currentTotalCost: totalCost, currentTotalCost: totalCost,
weeklyOpusCost: (await redis.getWeeklyOpusCost(keyId)) || 0, // 当前 Opus 周费用
// 时间窗口信息 // 时间窗口信息
windowStartTime, windowStartTime,
windowEndTime, windowEndTime,

View File

@@ -135,6 +135,59 @@
</div> </div>
</div> </div>
<!-- 总费用限制 -->
<div>
<div class="mb-2 flex items-center justify-between">
<span class="text-sm font-medium text-gray-600 dark:text-gray-400 md:text-base"
>总费用限制</span
>
<span class="text-xs text-gray-500 dark:text-gray-400 md:text-sm">
<span v-if="statsData.limits.totalCostLimit > 0">
${{ statsData.limits.currentTotalCost.toFixed(4) }} / ${{
statsData.limits.totalCostLimit.toFixed(2)
}}
</span>
<span v-else class="flex items-center gap-1">
${{ statsData.limits.currentTotalCost.toFixed(4) }} / <i class="fas fa-infinity" />
</span>
</span>
</div>
<div
v-if="statsData.limits.totalCostLimit > 0"
class="h-2 w-full rounded-full bg-gray-200 dark:bg-gray-700"
>
<div
class="h-2 rounded-full transition-all duration-300"
:class="getTotalCostProgressColor()"
:style="{ width: getTotalCostProgress() + '%' }"
/>
</div>
<div v-else class="h-2 w-full rounded-full bg-gray-200">
<div class="h-2 rounded-full bg-blue-500" style="width: 0%" />
</div>
</div>
<!-- Opus 模型周费用限制 -->
<div v-if="statsData.limits.weeklyOpusCostLimit > 0">
<div class="mb-2 flex items-center justify-between">
<span class="text-sm font-medium text-gray-600 dark:text-gray-400 md:text-base"
>Opus 模型周费用限制</span
>
<span class="text-xs text-gray-500 dark:text-gray-400 md:text-sm">
${{ statsData.limits.weeklyOpusCost.toFixed(4) }} / ${{
statsData.limits.weeklyOpusCostLimit.toFixed(2)
}}
</span>
</div>
<div class="h-2 w-full rounded-full bg-gray-200 dark:bg-gray-700">
<div
class="h-2 rounded-full transition-all duration-300"
:class="getOpusWeeklyCostProgressColor()"
:style="{ width: getOpusWeeklyCostProgress() + '%' }"
/>
</div>
</div>
<!-- 时间窗口限制 --> <!-- 时间窗口限制 -->
<div <div
v-if=" v-if="
@@ -334,6 +387,43 @@ const getDailyCostProgressColor = () => {
return 'bg-green-500' return 'bg-green-500'
} }
// 获取总费用进度
const getTotalCostProgress = () => {
if (!statsData.value.limits.totalCostLimit || statsData.value.limits.totalCostLimit === 0)
return 0
const percentage =
(statsData.value.limits.currentTotalCost / statsData.value.limits.totalCostLimit) * 100
return Math.min(percentage, 100)
}
// 获取总费用进度条颜色
const getTotalCostProgressColor = () => {
const progress = getTotalCostProgress()
if (progress >= 100) return 'bg-red-500'
if (progress >= 80) return 'bg-yellow-500'
return 'bg-blue-500'
}
// 获取Opus周费用进度
const getOpusWeeklyCostProgress = () => {
if (
!statsData.value.limits.weeklyOpusCostLimit ||
statsData.value.limits.weeklyOpusCostLimit === 0
)
return 0
const percentage =
(statsData.value.limits.weeklyOpusCost / statsData.value.limits.weeklyOpusCostLimit) * 100
return Math.min(percentage, 100)
}
// 获取Opus周费用进度条颜色
const getOpusWeeklyCostProgressColor = () => {
const progress = getOpusWeeklyCostProgress()
if (progress >= 100) return 'bg-red-500'
if (progress >= 80) return 'bg-yellow-500'
return 'bg-indigo-500' // 使用紫色表示Opus模型
}
// 格式化数字 // 格式化数字
const formatNumber = (num) => { const formatNumber = (num) => {
if (typeof num !== 'number') { if (typeof num !== 'number') {

View File

@@ -260,6 +260,12 @@ export const useApiStatsStore = defineStore('apistats', () => {
if (result.success) { if (result.success) {
statsData.value = result.data statsData.value = result.data
// 调试:打印返回的限制数据
console.log('API Stats - Full response:', result.data)
console.log('API Stats - limits data:', result.data.limits)
console.log('API Stats - weeklyOpusCostLimit:', result.data.limits?.weeklyOpusCostLimit)
console.log('API Stats - weeklyOpusCost:', result.data.limits?.weeklyOpusCost)
// 同时加载今日和本月的统计数据 // 同时加载今日和本月的统计数据
await loadAllPeriodStats() await loadAllPeriodStats()

View File

@@ -430,16 +430,14 @@
<div <div
class="overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm" class="overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm"
> >
<div class="whitespace-nowrap text-gray-300">model_provider = "crs"</div> <div
<div class="whitespace-nowrap text-gray-300">model = "gpt-5-codex"</div> v-for="line in codexConfigContent.configToml"
<div class="whitespace-nowrap text-gray-300">model_reasoning_effort = "high"</div> :key="line"
<div class="whitespace-nowrap text-gray-300">disable_response_storage = true</div> class="whitespace-nowrap text-gray-300"
<div class="whitespace-nowrap text-gray-300">preferred_auth_method = "apikey"</div> :class="{ 'mt-2': line === '' }"
<div class="mt-2"></div> >
<div class="whitespace-nowrap text-gray-300">[model_providers.crs]</div> {{ line }}
<div class="whitespace-nowrap text-gray-300">name = "crs"</div> </div>
<div class="whitespace-nowrap text-gray-300">base_url = "{{ openaiBaseUrl }}"</div>
<div class="whitespace-nowrap text-gray-300">wire_api = "responses"</div>
</div> </div>
<p class="mt-3 text-sm text-yellow-700"> <p class="mt-3 text-sm text-yellow-700">
@@ -449,13 +447,59 @@
<div <div
class="mt-2 overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm" class="mt-2 overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm"
> >
<div class="whitespace-nowrap text-gray-300">{</div> <div
<div class="whitespace-nowrap text-gray-300">"OPENAI_API_KEY": "你的API密钥"</div> v-for="line in codexConfigContent.authJson"
<div class="whitespace-nowrap text-gray-300">}</div> :key="line"
class="whitespace-nowrap text-gray-300"
>
{{ line }}
</div>
</div>
<div class="mt-3 space-y-3 text-xs text-yellow-700 dark:text-yellow-300">
<!-- 描述文字 -->
<p>{{ codexConfigContent.authInstructions.description }}</p>
<!-- 标题 -->
<h6 class="text-sm font-medium text-yellow-800 dark:text-yellow-200">
{{ codexConfigContent.authInstructions.title }}
</h6>
<!-- 当前平台对应的环境变量设置 -->
<div class="space-y-2">
<p class="font-medium">
{{ codexConfigContent.authInstructions.platform.title }}:
</p>
<div
class="overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm"
>
<div class="whitespace-nowrap text-gray-300">
{{ codexConfigContent.authInstructions.platform.command }}
</div>
</div>
</div>
<!-- Shell 配置文件仅对于 macOS/Linux 显示 -->
<div v-if="codexConfigContent.authInstructions.persistent" class="space-y-2">
<p class="font-medium">
{{ codexConfigContent.authInstructions.persistent.title }}:
</p>
<p class="text-xs">
{{ codexConfigContent.authInstructions.persistent.description }}
</p>
<div
class="overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm"
>
<div
v-for="command in codexConfigContent.authInstructions.persistent.commands"
:key="command"
class="whitespace-nowrap text-gray-300"
:class="{ 'mt-2': command === '' }"
>
{{ command }}
</div>
</div>
</div>
</div> </div>
<p class="mt-2 text-xs text-yellow-700">
💡 使用与 Claude Code 相同的 API 密钥即可格式如 cr_xxxxxxxxxx
</p>
</div> </div>
</div> </div>
</div> </div>
@@ -917,16 +961,14 @@
<div <div
class="overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm" class="overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm"
> >
<div class="whitespace-nowrap text-gray-300">model_provider = "crs"</div> <div
<div class="whitespace-nowrap text-gray-300">model = "gpt-5-codex"</div> v-for="line in codexConfigContent.configToml"
<div class="whitespace-nowrap text-gray-300">model_reasoning_effort = "high"</div> :key="line"
<div class="whitespace-nowrap text-gray-300">disable_response_storage = true</div> class="whitespace-nowrap text-gray-300"
<div class="whitespace-nowrap text-gray-300">preferred_auth_method = "apikey"</div> :class="{ 'mt-2': line === '' }"
<div class="mt-2"></div> >
<div class="whitespace-nowrap text-gray-300">[model_providers.crs]</div> {{ line }}
<div class="whitespace-nowrap text-gray-300">name = "crs"</div> </div>
<div class="whitespace-nowrap text-gray-300">base_url = "{{ openaiBaseUrl }}"</div>
<div class="whitespace-nowrap text-gray-300">wire_api = "responses"</div>
</div> </div>
<p class="mt-3 text-sm text-yellow-700"> <p class="mt-3 text-sm text-yellow-700">
@@ -936,13 +978,59 @@
<div <div
class="mt-2 overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm" class="mt-2 overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm"
> >
<div class="whitespace-nowrap text-gray-300">{</div> <div
<div class="whitespace-nowrap text-gray-300">"OPENAI_API_KEY": "你的API密钥"</div> v-for="line in codexConfigContent.authJson"
<div class="whitespace-nowrap text-gray-300">}</div> :key="line"
class="whitespace-nowrap text-gray-300"
>
{{ line }}
</div>
</div>
<div class="mt-3 space-y-3 text-xs text-yellow-700 dark:text-yellow-300">
<!-- 描述文字 -->
<p>{{ codexConfigContent.authInstructions.description }}</p>
<!-- 标题 -->
<h6 class="text-sm font-medium text-yellow-800 dark:text-yellow-200">
{{ codexConfigContent.authInstructions.title }}
</h6>
<!-- 当前平台对应的环境变量设置 -->
<div class="space-y-2">
<p class="font-medium">
{{ codexConfigContent.authInstructions.platform.title }}:
</p>
<div
class="overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm"
>
<div class="whitespace-nowrap text-gray-300">
{{ codexConfigContent.authInstructions.platform.command }}
</div>
</div>
</div>
<!-- Shell 配置文件仅对于 macOS/Linux 显示 -->
<div v-if="codexConfigContent.authInstructions.persistent" class="space-y-2">
<p class="font-medium">
{{ codexConfigContent.authInstructions.persistent.title }}:
</p>
<p class="text-xs">
{{ codexConfigContent.authInstructions.persistent.description }}
</p>
<div
class="overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm"
>
<div
v-for="command in codexConfigContent.authInstructions.persistent.commands"
:key="command"
class="whitespace-nowrap text-gray-300"
:class="{ 'mt-2': command === '' }"
>
{{ command }}
</div>
</div>
</div>
</div> </div>
<p class="mt-2 text-xs text-yellow-700">
💡 使用与 Claude Code 相同的 API 密钥即可格式如 cr_xxxxxxxxxx
</p>
</div> </div>
</div> </div>
</div> </div>
@@ -1395,16 +1483,14 @@
<div <div
class="overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm" class="overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm"
> >
<div class="whitespace-nowrap text-gray-300">model_provider = "crs"</div> <div
<div class="whitespace-nowrap text-gray-300">model = "gpt-5-codex"</div> v-for="line in codexConfigContent.configToml"
<div class="whitespace-nowrap text-gray-300">model_reasoning_effort = "high"</div> :key="line"
<div class="whitespace-nowrap text-gray-300">disable_response_storage = true</div> class="whitespace-nowrap text-gray-300"
<div class="whitespace-nowrap text-gray-300">preferred_auth_method = "apikey"</div> :class="{ 'mt-2': line === '' }"
<div class="mt-2"></div> >
<div class="whitespace-nowrap text-gray-300">[model_providers.crs]</div> {{ line }}
<div class="whitespace-nowrap text-gray-300">name = "crs"</div> </div>
<div class="whitespace-nowrap text-gray-300">base_url = "{{ openaiBaseUrl }}"</div>
<div class="whitespace-nowrap text-gray-300">wire_api = "responses"</div>
</div> </div>
<p class="mt-3 text-sm text-yellow-700"> <p class="mt-3 text-sm text-yellow-700">
@@ -1414,13 +1500,59 @@
<div <div
class="mt-2 overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm" class="mt-2 overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm"
> >
<div class="whitespace-nowrap text-gray-300">{</div> <div
<div class="whitespace-nowrap text-gray-300">"OPENAI_API_KEY": "你的API密钥"</div> v-for="line in codexConfigContent.authJson"
<div class="whitespace-nowrap text-gray-300">}</div> :key="line"
class="whitespace-nowrap text-gray-300"
>
{{ line }}
</div>
</div>
<div class="mt-3 space-y-3 text-xs text-yellow-700 dark:text-yellow-300">
<!-- 描述文字 -->
<p>{{ codexConfigContent.authInstructions.description }}</p>
<!-- 标题 -->
<h6 class="text-sm font-medium text-yellow-800 dark:text-yellow-200">
{{ codexConfigContent.authInstructions.title }}
</h6>
<!-- 当前平台对应的环境变量设置 -->
<div class="space-y-2">
<p class="font-medium">
{{ codexConfigContent.authInstructions.platform.title }}:
</p>
<div
class="overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm"
>
<div class="whitespace-nowrap text-gray-300">
{{ codexConfigContent.authInstructions.platform.command }}
</div>
</div>
</div>
<!-- Shell 配置文件仅对于 macOS/Linux 显示 -->
<div v-if="codexConfigContent.authInstructions.persistent" class="space-y-2">
<p class="font-medium">
{{ codexConfigContent.authInstructions.persistent.title }}:
</p>
<p class="text-xs">
{{ codexConfigContent.authInstructions.persistent.description }}
</p>
<div
class="overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm"
>
<div
v-for="command in codexConfigContent.authInstructions.persistent.commands"
:key="command"
class="whitespace-nowrap text-gray-300"
:class="{ 'mt-2': command === '' }"
>
{{ command }}
</div>
</div>
</div>
</div> </div>
<p class="mt-2 text-xs text-yellow-700">
💡 使用与 Claude Code 相同的 API 密钥即可格式如 cr_xxxxxxxxxx
</p>
</div> </div>
</div> </div>
</div> </div>
@@ -1627,7 +1759,6 @@ const getBaseUrlPrefix = () => {
origin = currentUrl.substring(0, pathStart) origin = currentUrl.substring(0, pathStart)
} else { } else {
// 最后的降级方案,使用相对路径 // 最后的降级方案,使用相对路径
console.warn('无法获取完整的 origin将使用相对路径')
return '' return ''
} }
} }
@@ -1649,6 +1780,92 @@ const geminiBaseUrl = computed(() => {
const openaiBaseUrl = computed(() => { const openaiBaseUrl = computed(() => {
return getBaseUrlPrefix() + '/openai' return getBaseUrlPrefix() + '/openai'
}) })
// Codex 配置内容
const codexConfigContent = computed(() => {
// 根据当前激活的教程系统获取对应的环境变量设置说明
const getCurrentPlatformAuthInstructions = () => {
const baseInstructions = {
title: '环境变量设置方法',
description:
'💡 将 OPENAI_API_KEY 设置为 null然后设置环境变量 CRS_OAI_KEY 为您的 API 密钥(格式如 cr_xxxxxxxxxx。'
}
switch (activeTutorialSystem.value) {
case 'windows':
return {
...baseInstructions,
platform: {
title: 'Windows',
command: 'set CRS_OAI_KEY=cr_xxxxxxxxxx'
}
}
case 'macos':
return {
...baseInstructions,
platform: {
title: 'macOS',
command: 'export CRS_OAI_KEY=cr_xxxxxxxxxx'
},
persistent: {
title: 'Shell 配置文件(持久保存)',
description: '添加到您的 shell 配置文件中:',
commands: [
'# 对于 zsh (默认)',
'echo "export CRS_OAI_KEY=cr_xxxxxxxxxx" >> ~/.zshrc',
'source ~/.zshrc',
'',
'# 对于 bash',
'echo "export CRS_OAI_KEY=cr_xxxxxxxxxx" >> ~/.bash_profile',
'source ~/.bash_profile'
]
}
}
case 'linux':
return {
...baseInstructions,
platform: {
title: 'Linux',
command: 'export CRS_OAI_KEY=cr_xxxxxxxxxx'
},
persistent: {
title: 'Shell 配置文件(持久保存)',
description: '添加到您的 shell 配置文件中:',
commands: [
'# 对于 bash (默认)',
'echo "export CRS_OAI_KEY=cr_xxxxxxxxxx" >> ~/.bashrc',
'source ~/.bashrc',
'',
'# 对于 zsh',
'echo "export CRS_OAI_KEY=cr_xxxxxxxxxx" >> ~/.zshrc',
'source ~/.zshrc'
]
}
}
default:
return baseInstructions
}
}
return {
configToml: [
'model_provider = "crs"',
'model = "gpt-5-codex"',
'model_reasoning_effort = "high"',
'disable_response_storage = true',
'preferred_auth_method = "apikey"',
'',
'[model_providers.crs]',
'name = "crs"',
`base_url = "${openaiBaseUrl.value}"`,
'wire_api = "responses"',
'requires_openai_auth = true',
'env_key = "CRS_OAI_KEY"'
],
authJson: ['{', ' "OPENAI_API_KEY": null', '}'],
authInstructions: getCurrentPlatformAuthInstructions()
}
})
</script> </script>
<style scoped> <style scoped>

View File

@@ -388,7 +388,6 @@ const handleLogout = async () => {
showToast('Logged out successfully', 'success') showToast('Logged out successfully', 'success')
router.push('/user-login') router.push('/user-login')
} catch (error) { } catch (error) {
console.error('Logout error:', error)
showToast('Logout failed', 'error') showToast('Logout failed', 'error')
} }
} }