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

@@ -135,6 +135,59 @@
</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
v-if="
@@ -334,6 +387,43 @@ const getDailyCostProgressColor = () => {
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) => {
if (typeof num !== 'number') {

View File

@@ -260,6 +260,12 @@ export const useApiStatsStore = defineStore('apistats', () => {
if (result.success) {
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()

View File

@@ -430,16 +430,14 @@
<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">model_provider = "crs"</div>
<div class="whitespace-nowrap text-gray-300">model = "gpt-5-codex"</div>
<div class="whitespace-nowrap text-gray-300">model_reasoning_effort = "high"</div>
<div class="whitespace-nowrap text-gray-300">disable_response_storage = true</div>
<div class="whitespace-nowrap text-gray-300">preferred_auth_method = "apikey"</div>
<div class="mt-2"></div>
<div class="whitespace-nowrap text-gray-300">[model_providers.crs]</div>
<div class="whitespace-nowrap text-gray-300">name = "crs"</div>
<div class="whitespace-nowrap text-gray-300">base_url = "{{ openaiBaseUrl }}"</div>
<div class="whitespace-nowrap text-gray-300">wire_api = "responses"</div>
<div
v-for="line in codexConfigContent.configToml"
:key="line"
class="whitespace-nowrap text-gray-300"
:class="{ 'mt-2': line === '' }"
>
{{ line }}
</div>
</div>
<p class="mt-3 text-sm text-yellow-700">
@@ -449,13 +447,59 @@
<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"
>
<div class="whitespace-nowrap text-gray-300">{</div>
<div class="whitespace-nowrap text-gray-300">"OPENAI_API_KEY": "你的API密钥"</div>
<div class="whitespace-nowrap text-gray-300">}</div>
<div
v-for="line in codexConfigContent.authJson"
: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>
<p class="mt-2 text-xs text-yellow-700">
💡 使用与 Claude Code 相同的 API 密钥即可格式如 cr_xxxxxxxxxx
</p>
</div>
</div>
</div>
@@ -917,16 +961,14 @@
<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">model_provider = "crs"</div>
<div class="whitespace-nowrap text-gray-300">model = "gpt-5-codex"</div>
<div class="whitespace-nowrap text-gray-300">model_reasoning_effort = "high"</div>
<div class="whitespace-nowrap text-gray-300">disable_response_storage = true</div>
<div class="whitespace-nowrap text-gray-300">preferred_auth_method = "apikey"</div>
<div class="mt-2"></div>
<div class="whitespace-nowrap text-gray-300">[model_providers.crs]</div>
<div class="whitespace-nowrap text-gray-300">name = "crs"</div>
<div class="whitespace-nowrap text-gray-300">base_url = "{{ openaiBaseUrl }}"</div>
<div class="whitespace-nowrap text-gray-300">wire_api = "responses"</div>
<div
v-for="line in codexConfigContent.configToml"
:key="line"
class="whitespace-nowrap text-gray-300"
:class="{ 'mt-2': line === '' }"
>
{{ line }}
</div>
</div>
<p class="mt-3 text-sm text-yellow-700">
@@ -936,13 +978,59 @@
<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"
>
<div class="whitespace-nowrap text-gray-300">{</div>
<div class="whitespace-nowrap text-gray-300">"OPENAI_API_KEY": "你的API密钥"</div>
<div class="whitespace-nowrap text-gray-300">}</div>
<div
v-for="line in codexConfigContent.authJson"
: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>
<p class="mt-2 text-xs text-yellow-700">
💡 使用与 Claude Code 相同的 API 密钥即可格式如 cr_xxxxxxxxxx
</p>
</div>
</div>
</div>
@@ -1395,16 +1483,14 @@
<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">model_provider = "crs"</div>
<div class="whitespace-nowrap text-gray-300">model = "gpt-5-codex"</div>
<div class="whitespace-nowrap text-gray-300">model_reasoning_effort = "high"</div>
<div class="whitespace-nowrap text-gray-300">disable_response_storage = true</div>
<div class="whitespace-nowrap text-gray-300">preferred_auth_method = "apikey"</div>
<div class="mt-2"></div>
<div class="whitespace-nowrap text-gray-300">[model_providers.crs]</div>
<div class="whitespace-nowrap text-gray-300">name = "crs"</div>
<div class="whitespace-nowrap text-gray-300">base_url = "{{ openaiBaseUrl }}"</div>
<div class="whitespace-nowrap text-gray-300">wire_api = "responses"</div>
<div
v-for="line in codexConfigContent.configToml"
:key="line"
class="whitespace-nowrap text-gray-300"
:class="{ 'mt-2': line === '' }"
>
{{ line }}
</div>
</div>
<p class="mt-3 text-sm text-yellow-700">
@@ -1414,13 +1500,59 @@
<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"
>
<div class="whitespace-nowrap text-gray-300">{</div>
<div class="whitespace-nowrap text-gray-300">"OPENAI_API_KEY": "你的API密钥"</div>
<div class="whitespace-nowrap text-gray-300">}</div>
<div
v-for="line in codexConfigContent.authJson"
: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>
<p class="mt-2 text-xs text-yellow-700">
💡 使用与 Claude Code 相同的 API 密钥即可格式如 cr_xxxxxxxxxx
</p>
</div>
</div>
</div>
@@ -1627,7 +1759,6 @@ const getBaseUrlPrefix = () => {
origin = currentUrl.substring(0, pathStart)
} else {
// 最后的降级方案,使用相对路径
console.warn('无法获取完整的 origin将使用相对路径')
return ''
}
}
@@ -1649,6 +1780,92 @@ const geminiBaseUrl = computed(() => {
const openaiBaseUrl = computed(() => {
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>
<style scoped>

View File

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