style(frontend): 优化 Components 代码风格和结构
- 统一移除语句末尾分号,规范代码格式 - 优化组件类型定义和 props 声明 - 改进组件文档和示例代码 - 提升代码可读性和一致性
This commit is contained in:
@@ -1,17 +1,22 @@
|
||||
<template>
|
||||
<Modal
|
||||
:show="show"
|
||||
:title="t('admin.accounts.usageStatistics')"
|
||||
size="2xl"
|
||||
@close="handleClose"
|
||||
>
|
||||
<Modal :show="show" :title="t('admin.accounts.usageStatistics')" size="2xl" @close="handleClose">
|
||||
<div class="space-y-6">
|
||||
<!-- Account Info Header -->
|
||||
<div v-if="account" class="flex items-center justify-between p-3 bg-gradient-to-r from-primary-50 to-primary-100 dark:from-primary-900/20 dark:to-primary-800/20 rounded-xl border border-primary-200 dark:border-primary-700/50">
|
||||
<div
|
||||
v-if="account"
|
||||
class="flex items-center justify-between rounded-xl border border-primary-200 bg-gradient-to-r from-primary-50 to-primary-100 p-3 dark:border-primary-700/50 dark:from-primary-900/20 dark:to-primary-800/20"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 rounded-lg bg-gradient-to-br from-primary-500 to-primary-600 flex items-center justify-center">
|
||||
<svg class="w-5 h-5 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
||||
<div
|
||||
class="flex h-10 w-10 items-center justify-center rounded-lg bg-gradient-to-br from-primary-500 to-primary-600"
|
||||
>
|
||||
<svg class="h-5 w-5 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
@@ -23,7 +28,7 @@
|
||||
</div>
|
||||
<span
|
||||
:class="[
|
||||
'px-2.5 py-1 text-xs font-semibold rounded-full',
|
||||
'rounded-full px-2.5 py-1 text-xs font-semibold',
|
||||
account.status === 'active'
|
||||
? 'bg-green-100 text-green-700 dark:bg-green-500/20 dark:text-green-400'
|
||||
: 'bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-400'
|
||||
@@ -42,62 +47,140 @@
|
||||
<!-- Row 1: Main Stats Cards -->
|
||||
<div class="grid grid-cols-2 gap-4 lg:grid-cols-4">
|
||||
<!-- 30-Day Total Cost -->
|
||||
<div class="card p-4 bg-gradient-to-br from-emerald-50 to-white dark:from-emerald-900/10 dark:to-dark-700 border-emerald-200 dark:border-emerald-800/30">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<span class="text-xs font-medium text-gray-500 dark:text-gray-400">{{ t('admin.accounts.stats.totalCost') }}</span>
|
||||
<div class="p-1.5 rounded-lg bg-emerald-100 dark:bg-emerald-900/30">
|
||||
<svg class="w-4 h-4 text-emerald-600 dark:text-emerald-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
<div
|
||||
class="card border-emerald-200 bg-gradient-to-br from-emerald-50 to-white p-4 dark:border-emerald-800/30 dark:from-emerald-900/10 dark:to-dark-700"
|
||||
>
|
||||
<div class="mb-2 flex items-center justify-between">
|
||||
<span class="text-xs font-medium text-gray-500 dark:text-gray-400">{{
|
||||
t('admin.accounts.stats.totalCost')
|
||||
}}</span>
|
||||
<div class="rounded-lg bg-emerald-100 p-1.5 dark:bg-emerald-900/30">
|
||||
<svg
|
||||
class="h-4 w-4 text-emerald-600 dark:text-emerald-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-2xl font-bold text-gray-900 dark:text-white">${{ formatCost(stats.summary.total_cost) }}</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
<p class="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
${{ formatCost(stats.summary.total_cost) }}
|
||||
</p>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.accounts.stats.accumulatedCost') }}
|
||||
<span class="text-gray-400 dark:text-gray-500">({{ t('admin.accounts.stats.standardCost') }}: ${{ formatCost(stats.summary.total_standard_cost) }})</span>
|
||||
<span class="text-gray-400 dark:text-gray-500"
|
||||
>({{ t('admin.accounts.stats.standardCost') }}: ${{
|
||||
formatCost(stats.summary.total_standard_cost)
|
||||
}})</span
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 30-Day Total Requests -->
|
||||
<div class="card p-4 bg-gradient-to-br from-blue-50 to-white dark:from-blue-900/10 dark:to-dark-700 border-blue-200 dark:border-blue-800/30">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<span class="text-xs font-medium text-gray-500 dark:text-gray-400">{{ t('admin.accounts.stats.totalRequests') }}</span>
|
||||
<div class="p-1.5 rounded-lg bg-blue-100 dark:bg-blue-900/30">
|
||||
<svg class="w-4 h-4 text-blue-600 dark:text-blue-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
<div
|
||||
class="card border-blue-200 bg-gradient-to-br from-blue-50 to-white p-4 dark:border-blue-800/30 dark:from-blue-900/10 dark:to-dark-700"
|
||||
>
|
||||
<div class="mb-2 flex items-center justify-between">
|
||||
<span class="text-xs font-medium text-gray-500 dark:text-gray-400">{{
|
||||
t('admin.accounts.stats.totalRequests')
|
||||
}}</span>
|
||||
<div class="rounded-lg bg-blue-100 p-1.5 dark:bg-blue-900/30">
|
||||
<svg
|
||||
class="h-4 w-4 text-blue-600 dark:text-blue-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M13 10V3L4 14h7v7l9-11h-7z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-2xl font-bold text-gray-900 dark:text-white">{{ formatNumber(stats.summary.total_requests) }}</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">{{ t('admin.accounts.stats.totalCalls') }}</p>
|
||||
<p class="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
{{ formatNumber(stats.summary.total_requests) }}
|
||||
</p>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.accounts.stats.totalCalls') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Daily Average Cost -->
|
||||
<div class="card p-4 bg-gradient-to-br from-amber-50 to-white dark:from-amber-900/10 dark:to-dark-700 border-amber-200 dark:border-amber-800/30">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<span class="text-xs font-medium text-gray-500 dark:text-gray-400">{{ t('admin.accounts.stats.avgDailyCost') }}</span>
|
||||
<div class="p-1.5 rounded-lg bg-amber-100 dark:bg-amber-900/30">
|
||||
<svg class="w-4 h-4 text-amber-600 dark:text-amber-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 7h6m0 10v-3m-3 3h.01M9 17h.01M9 14h.01M12 14h.01M15 11h.01M12 11h.01M9 11h.01M7 21h10a2 2 0 002-2V5a2 2 0 00-2-2H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
|
||||
<div
|
||||
class="card border-amber-200 bg-gradient-to-br from-amber-50 to-white p-4 dark:border-amber-800/30 dark:from-amber-900/10 dark:to-dark-700"
|
||||
>
|
||||
<div class="mb-2 flex items-center justify-between">
|
||||
<span class="text-xs font-medium text-gray-500 dark:text-gray-400">{{
|
||||
t('admin.accounts.stats.avgDailyCost')
|
||||
}}</span>
|
||||
<div class="rounded-lg bg-amber-100 p-1.5 dark:bg-amber-900/30">
|
||||
<svg
|
||||
class="h-4 w-4 text-amber-600 dark:text-amber-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 7h6m0 10v-3m-3 3h.01M9 17h.01M9 14h.01M12 14h.01M15 11h.01M12 11h.01M9 11h.01M7 21h10a2 2 0 002-2V5a2 2 0 00-2-2H7a2 2 0 00-2 2v14a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-2xl font-bold text-gray-900 dark:text-white">${{ formatCost(stats.summary.avg_daily_cost) }}</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">{{ t('admin.accounts.stats.basedOnActualDays', { days: stats.summary.actual_days_used }) }}</p>
|
||||
<p class="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
${{ formatCost(stats.summary.avg_daily_cost) }}
|
||||
</p>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{
|
||||
t('admin.accounts.stats.basedOnActualDays', {
|
||||
days: stats.summary.actual_days_used
|
||||
})
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Daily Average Requests -->
|
||||
<div class="card p-4 bg-gradient-to-br from-purple-50 to-white dark:from-purple-900/10 dark:to-dark-700 border-purple-200 dark:border-purple-800/30">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<span class="text-xs font-medium text-gray-500 dark:text-gray-400">{{ t('admin.accounts.stats.avgDailyRequests') }}</span>
|
||||
<div class="p-1.5 rounded-lg bg-purple-100 dark:bg-purple-900/30">
|
||||
<svg class="w-4 h-4 text-purple-600 dark:text-purple-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 12l3-3 3 3 4-4M8 21l4-4 4 4M3 4h18M4 4h16v12a1 1 0 01-1 1H5a1 1 0 01-1-1V4z" />
|
||||
<div
|
||||
class="card border-purple-200 bg-gradient-to-br from-purple-50 to-white p-4 dark:border-purple-800/30 dark:from-purple-900/10 dark:to-dark-700"
|
||||
>
|
||||
<div class="mb-2 flex items-center justify-between">
|
||||
<span class="text-xs font-medium text-gray-500 dark:text-gray-400">{{
|
||||
t('admin.accounts.stats.avgDailyRequests')
|
||||
}}</span>
|
||||
<div class="rounded-lg bg-purple-100 p-1.5 dark:bg-purple-900/30">
|
||||
<svg
|
||||
class="h-4 w-4 text-purple-600 dark:text-purple-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M7 12l3-3 3 3 4-4M8 21l4-4 4 4M3 4h18M4 4h16v12a1 1 0 01-1 1H5a1 1 0 01-1-1V4z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-2xl font-bold text-gray-900 dark:text-white">{{ formatNumber(Math.round(stats.summary.avg_daily_requests)) }}</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">{{ t('admin.accounts.stats.avgDailyUsage') }}</p>
|
||||
<p class="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
{{ formatNumber(Math.round(stats.summary.avg_daily_requests)) }}
|
||||
</p>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.accounts.stats.avgDailyUsage') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -105,78 +188,148 @@
|
||||
<div class="grid grid-cols-1 gap-4 lg:grid-cols-3">
|
||||
<!-- Today Overview -->
|
||||
<div class="card p-4">
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<div class="p-1.5 rounded-lg bg-cyan-100 dark:bg-cyan-900/30">
|
||||
<svg class="w-4 h-4 text-cyan-600 dark:text-cyan-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
<div class="mb-3 flex items-center gap-2">
|
||||
<div class="rounded-lg bg-cyan-100 p-1.5 dark:bg-cyan-900/30">
|
||||
<svg
|
||||
class="h-4 w-4 text-cyan-600 dark:text-cyan-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="text-sm font-semibold text-gray-900 dark:text-white">{{ t('admin.accounts.stats.todayOverview') }}</span>
|
||||
<span class="text-sm font-semibold text-gray-900 dark:text-white">{{
|
||||
t('admin.accounts.stats.todayOverview')
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">{{ t('admin.accounts.stats.cost') }}</span>
|
||||
<span class="text-sm font-semibold text-gray-900 dark:text-white">${{ formatCost(stats.summary.today?.cost || 0) }}</span>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">{{
|
||||
t('admin.accounts.stats.cost')
|
||||
}}</span>
|
||||
<span class="text-sm font-semibold text-gray-900 dark:text-white"
|
||||
>${{ formatCost(stats.summary.today?.cost || 0) }}</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">{{ t('admin.accounts.stats.requests') }}</span>
|
||||
<span class="text-sm font-semibold text-gray-900 dark:text-white">{{ formatNumber(stats.summary.today?.requests || 0) }}</span>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">{{
|
||||
t('admin.accounts.stats.requests')
|
||||
}}</span>
|
||||
<span class="text-sm font-semibold text-gray-900 dark:text-white">{{
|
||||
formatNumber(stats.summary.today?.requests || 0)
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">Tokens</span>
|
||||
<span class="text-sm font-semibold text-gray-900 dark:text-white">{{ formatTokens(stats.summary.today?.tokens || 0) }}</span>
|
||||
<span class="text-sm font-semibold text-gray-900 dark:text-white">{{
|
||||
formatTokens(stats.summary.today?.tokens || 0)
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Highest Cost Day -->
|
||||
<div class="card p-4">
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<div class="p-1.5 rounded-lg bg-orange-100 dark:bg-orange-900/30">
|
||||
<svg class="w-4 h-4 text-orange-600 dark:text-orange-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 18.657A8 8 0 016.343 7.343S7 9 9 10c0-2 .5-5 2.986-7C14 5 16.09 5.777 17.656 7.343A7.975 7.975 0 0120 13a7.975 7.975 0 01-2.343 5.657z" />
|
||||
<div class="mb-3 flex items-center gap-2">
|
||||
<div class="rounded-lg bg-orange-100 p-1.5 dark:bg-orange-900/30">
|
||||
<svg
|
||||
class="h-4 w-4 text-orange-600 dark:text-orange-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M17.657 18.657A8 8 0 016.343 7.343S7 9 9 10c0-2 .5-5 2.986-7C14 5 16.09 5.777 17.656 7.343A7.975 7.975 0 0120 13a7.975 7.975 0 01-2.343 5.657z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="text-sm font-semibold text-gray-900 dark:text-white">{{ t('admin.accounts.stats.highestCostDay') }}</span>
|
||||
<span class="text-sm font-semibold text-gray-900 dark:text-white">{{
|
||||
t('admin.accounts.stats.highestCostDay')
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">{{ t('admin.accounts.stats.date') }}</span>
|
||||
<span class="text-sm font-semibold text-gray-900 dark:text-white">{{ stats.summary.highest_cost_day?.label || '-' }}</span>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">{{
|
||||
t('admin.accounts.stats.date')
|
||||
}}</span>
|
||||
<span class="text-sm font-semibold text-gray-900 dark:text-white">{{
|
||||
stats.summary.highest_cost_day?.label || '-'
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">{{ t('admin.accounts.stats.cost') }}</span>
|
||||
<span class="text-sm font-semibold text-orange-600 dark:text-orange-400">${{ formatCost(stats.summary.highest_cost_day?.cost || 0) }}</span>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">{{
|
||||
t('admin.accounts.stats.cost')
|
||||
}}</span>
|
||||
<span class="text-sm font-semibold text-orange-600 dark:text-orange-400"
|
||||
>${{ formatCost(stats.summary.highest_cost_day?.cost || 0) }}</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">{{ t('admin.accounts.stats.requests') }}</span>
|
||||
<span class="text-sm font-semibold text-gray-900 dark:text-white">{{ formatNumber(stats.summary.highest_cost_day?.requests || 0) }}</span>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">{{
|
||||
t('admin.accounts.stats.requests')
|
||||
}}</span>
|
||||
<span class="text-sm font-semibold text-gray-900 dark:text-white">{{
|
||||
formatNumber(stats.summary.highest_cost_day?.requests || 0)
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Highest Request Day -->
|
||||
<div class="card p-4">
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<div class="p-1.5 rounded-lg bg-indigo-100 dark:bg-indigo-900/30">
|
||||
<svg class="w-4 h-4 text-indigo-600 dark:text-indigo-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6" />
|
||||
<div class="mb-3 flex items-center gap-2">
|
||||
<div class="rounded-lg bg-indigo-100 p-1.5 dark:bg-indigo-900/30">
|
||||
<svg
|
||||
class="h-4 w-4 text-indigo-600 dark:text-indigo-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="text-sm font-semibold text-gray-900 dark:text-white">{{ t('admin.accounts.stats.highestRequestDay') }}</span>
|
||||
<span class="text-sm font-semibold text-gray-900 dark:text-white">{{
|
||||
t('admin.accounts.stats.highestRequestDay')
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">{{ t('admin.accounts.stats.date') }}</span>
|
||||
<span class="text-sm font-semibold text-gray-900 dark:text-white">{{ stats.summary.highest_request_day?.label || '-' }}</span>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">{{
|
||||
t('admin.accounts.stats.date')
|
||||
}}</span>
|
||||
<span class="text-sm font-semibold text-gray-900 dark:text-white">{{
|
||||
stats.summary.highest_request_day?.label || '-'
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">{{ t('admin.accounts.stats.requests') }}</span>
|
||||
<span class="text-sm font-semibold text-indigo-600 dark:text-indigo-400">{{ formatNumber(stats.summary.highest_request_day?.requests || 0) }}</span>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">{{
|
||||
t('admin.accounts.stats.requests')
|
||||
}}</span>
|
||||
<span class="text-sm font-semibold text-indigo-600 dark:text-indigo-400">{{
|
||||
formatNumber(stats.summary.highest_request_day?.requests || 0)
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">{{ t('admin.accounts.stats.cost') }}</span>
|
||||
<span class="text-sm font-semibold text-gray-900 dark:text-white">${{ formatCost(stats.summary.highest_request_day?.cost || 0) }}</span>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">{{
|
||||
t('admin.accounts.stats.cost')
|
||||
}}</span>
|
||||
<span class="text-sm font-semibold text-gray-900 dark:text-white"
|
||||
>${{ formatCost(stats.summary.highest_request_day?.cost || 0) }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -186,70 +339,134 @@
|
||||
<div class="grid grid-cols-1 gap-4 lg:grid-cols-3">
|
||||
<!-- Accumulated Tokens -->
|
||||
<div class="card p-4">
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<div class="p-1.5 rounded-lg bg-teal-100 dark:bg-teal-900/30">
|
||||
<svg class="w-4 h-4 text-teal-600 dark:text-teal-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
|
||||
<div class="mb-3 flex items-center gap-2">
|
||||
<div class="rounded-lg bg-teal-100 p-1.5 dark:bg-teal-900/30">
|
||||
<svg
|
||||
class="h-4 w-4 text-teal-600 dark:text-teal-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="text-sm font-semibold text-gray-900 dark:text-white">{{ t('admin.accounts.stats.accumulatedTokens') }}</span>
|
||||
<span class="text-sm font-semibold text-gray-900 dark:text-white">{{
|
||||
t('admin.accounts.stats.accumulatedTokens')
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">{{ t('admin.accounts.stats.totalTokens') }}</span>
|
||||
<span class="text-sm font-semibold text-gray-900 dark:text-white">{{ formatTokens(stats.summary.total_tokens) }}</span>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">{{
|
||||
t('admin.accounts.stats.totalTokens')
|
||||
}}</span>
|
||||
<span class="text-sm font-semibold text-gray-900 dark:text-white">{{
|
||||
formatTokens(stats.summary.total_tokens)
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">{{ t('admin.accounts.stats.dailyAvgTokens') }}</span>
|
||||
<span class="text-sm font-semibold text-gray-900 dark:text-white">{{ formatTokens(Math.round(stats.summary.avg_daily_tokens)) }}</span>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">{{
|
||||
t('admin.accounts.stats.dailyAvgTokens')
|
||||
}}</span>
|
||||
<span class="text-sm font-semibold text-gray-900 dark:text-white">{{
|
||||
formatTokens(Math.round(stats.summary.avg_daily_tokens))
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Performance -->
|
||||
<div class="card p-4">
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<div class="p-1.5 rounded-lg bg-rose-100 dark:bg-rose-900/30">
|
||||
<svg class="w-4 h-4 text-rose-600 dark:text-rose-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
<div class="mb-3 flex items-center gap-2">
|
||||
<div class="rounded-lg bg-rose-100 p-1.5 dark:bg-rose-900/30">
|
||||
<svg
|
||||
class="h-4 w-4 text-rose-600 dark:text-rose-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M13 10V3L4 14h7v7l9-11h-7z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="text-sm font-semibold text-gray-900 dark:text-white">{{ t('admin.accounts.stats.performance') }}</span>
|
||||
<span class="text-sm font-semibold text-gray-900 dark:text-white">{{
|
||||
t('admin.accounts.stats.performance')
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">{{ t('admin.accounts.stats.avgResponseTime') }}</span>
|
||||
<span class="text-sm font-semibold text-gray-900 dark:text-white">{{ formatDuration(stats.summary.avg_duration_ms) }}</span>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">{{
|
||||
t('admin.accounts.stats.avgResponseTime')
|
||||
}}</span>
|
||||
<span class="text-sm font-semibold text-gray-900 dark:text-white">{{
|
||||
formatDuration(stats.summary.avg_duration_ms)
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">{{ t('admin.accounts.stats.daysActive') }}</span>
|
||||
<span class="text-sm font-semibold text-gray-900 dark:text-white">{{ stats.summary.actual_days_used }} / {{ stats.summary.days }}</span>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">{{
|
||||
t('admin.accounts.stats.daysActive')
|
||||
}}</span>
|
||||
<span class="text-sm font-semibold text-gray-900 dark:text-white"
|
||||
>{{ stats.summary.actual_days_used }} / {{ stats.summary.days }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Activity -->
|
||||
<div class="card p-4">
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<div class="p-1.5 rounded-lg bg-lime-100 dark:bg-lime-900/30">
|
||||
<svg class="w-4 h-4 text-lime-600 dark:text-lime-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
|
||||
<div class="mb-3 flex items-center gap-2">
|
||||
<div class="rounded-lg bg-lime-100 p-1.5 dark:bg-lime-900/30">
|
||||
<svg
|
||||
class="h-4 w-4 text-lime-600 dark:text-lime-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="text-sm font-semibold text-gray-900 dark:text-white">{{ t('admin.accounts.stats.recentActivity') }}</span>
|
||||
<span class="text-sm font-semibold text-gray-900 dark:text-white">{{
|
||||
t('admin.accounts.stats.recentActivity')
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">{{ t('admin.accounts.stats.todayRequests') }}</span>
|
||||
<span class="text-sm font-semibold text-gray-900 dark:text-white">{{ formatNumber(stats.summary.today?.requests || 0) }}</span>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">{{
|
||||
t('admin.accounts.stats.todayRequests')
|
||||
}}</span>
|
||||
<span class="text-sm font-semibold text-gray-900 dark:text-white">{{
|
||||
formatNumber(stats.summary.today?.requests || 0)
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">{{ t('admin.accounts.stats.todayTokens') }}</span>
|
||||
<span class="text-sm font-semibold text-gray-900 dark:text-white">{{ formatTokens(stats.summary.today?.tokens || 0) }}</span>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">{{
|
||||
t('admin.accounts.stats.todayTokens')
|
||||
}}</span>
|
||||
<span class="text-sm font-semibold text-gray-900 dark:text-white">{{
|
||||
formatTokens(stats.summary.today?.tokens || 0)
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">{{ t('admin.accounts.stats.todayCost') }}</span>
|
||||
<span class="text-sm font-semibold text-gray-900 dark:text-white">${{ formatCost(stats.summary.today?.cost || 0) }}</span>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">{{
|
||||
t('admin.accounts.stats.todayCost')
|
||||
}}</span>
|
||||
<span class="text-sm font-semibold text-gray-900 dark:text-white"
|
||||
>${{ formatCost(stats.summary.today?.cost || 0) }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -257,26 +474,36 @@
|
||||
|
||||
<!-- Usage Trend Chart -->
|
||||
<div class="card p-4">
|
||||
<h3 class="text-sm font-semibold text-gray-900 dark:text-white mb-4">{{ t('admin.accounts.stats.usageTrend') }}</h3>
|
||||
<h3 class="mb-4 text-sm font-semibold text-gray-900 dark:text-white">
|
||||
{{ t('admin.accounts.stats.usageTrend') }}
|
||||
</h3>
|
||||
<div class="h-64">
|
||||
<Line v-if="trendChartData" :data="trendChartData" :options="lineChartOptions" />
|
||||
<div v-else class="flex items-center justify-center h-full text-gray-500 dark:text-gray-400 text-sm">
|
||||
<div
|
||||
v-else
|
||||
class="flex h-full items-center justify-center text-sm text-gray-500 dark:text-gray-400"
|
||||
>
|
||||
{{ t('admin.dashboard.noDataAvailable') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Model Distribution -->
|
||||
<ModelDistributionChart
|
||||
:model-stats="stats.models"
|
||||
:loading="false"
|
||||
/>
|
||||
<ModelDistributionChart :model-stats="stats.models" :loading="false" />
|
||||
</template>
|
||||
|
||||
<!-- No Data State -->
|
||||
<div v-else-if="!loading" class="flex flex-col items-center justify-center py-12 text-gray-500 dark:text-gray-400">
|
||||
<svg class="w-12 h-12 mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
||||
<div
|
||||
v-else-if="!loading"
|
||||
class="flex flex-col items-center justify-center py-12 text-gray-500 dark:text-gray-400"
|
||||
>
|
||||
<svg class="mb-4 h-12 w-12" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1.5"
|
||||
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"
|
||||
/>
|
||||
</svg>
|
||||
<p class="text-sm">{{ t('admin.accounts.stats.noData') }}</p>
|
||||
</div>
|
||||
@@ -286,7 +513,7 @@
|
||||
<div class="flex justify-end">
|
||||
<button
|
||||
@click="handleClose"
|
||||
class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-dark-600 hover:bg-gray-200 dark:hover:bg-dark-500 rounded-lg transition-colors"
|
||||
class="rounded-lg bg-gray-100 px-4 py-2 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-200 dark:bg-dark-600 dark:text-gray-300 dark:hover:bg-dark-500"
|
||||
>
|
||||
{{ t('common.close') }}
|
||||
</button>
|
||||
@@ -349,7 +576,7 @@ const isDarkMode = computed(() => {
|
||||
// Chart colors
|
||||
const chartColors = computed(() => ({
|
||||
text: isDarkMode.value ? '#e5e7eb' : '#374151',
|
||||
grid: isDarkMode.value ? '#374151' : '#e5e7eb',
|
||||
grid: isDarkMode.value ? '#374151' : '#e5e7eb'
|
||||
}))
|
||||
|
||||
// Line chart data
|
||||
@@ -357,27 +584,27 @@ const trendChartData = computed(() => {
|
||||
if (!stats.value?.history?.length) return null
|
||||
|
||||
return {
|
||||
labels: stats.value.history.map(h => h.label),
|
||||
labels: stats.value.history.map((h) => h.label),
|
||||
datasets: [
|
||||
{
|
||||
label: t('admin.accounts.stats.cost') + ' (USD)',
|
||||
data: stats.value.history.map(h => h.cost),
|
||||
data: stats.value.history.map((h) => h.cost),
|
||||
borderColor: '#3b82f6',
|
||||
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
||||
fill: true,
|
||||
tension: 0.3,
|
||||
yAxisID: 'y',
|
||||
yAxisID: 'y'
|
||||
},
|
||||
{
|
||||
label: t('admin.accounts.stats.requests'),
|
||||
data: stats.value.history.map(h => h.requests),
|
||||
data: stats.value.history.map((h) => h.requests),
|
||||
borderColor: '#f97316',
|
||||
backgroundColor: 'rgba(249, 115, 22, 0.1)',
|
||||
fill: false,
|
||||
tension: 0.3,
|
||||
yAxisID: 'y1',
|
||||
},
|
||||
],
|
||||
yAxisID: 'y1'
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
@@ -387,7 +614,7 @@ const lineChartOptions = computed(() => ({
|
||||
maintainAspectRatio: false,
|
||||
interaction: {
|
||||
intersect: false,
|
||||
mode: 'index' as const,
|
||||
mode: 'index' as const
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
@@ -398,9 +625,9 @@ const lineChartOptions = computed(() => ({
|
||||
pointStyle: 'circle',
|
||||
padding: 15,
|
||||
font: {
|
||||
size: 11,
|
||||
},
|
||||
},
|
||||
size: 11
|
||||
}
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
@@ -411,81 +638,84 @@ const lineChartOptions = computed(() => ({
|
||||
return `${label}: $${formatCost(value)}`
|
||||
}
|
||||
return `${label}: ${formatNumber(value)}`
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: {
|
||||
color: chartColors.value.grid,
|
||||
color: chartColors.value.grid
|
||||
},
|
||||
ticks: {
|
||||
color: chartColors.value.text,
|
||||
font: {
|
||||
size: 10,
|
||||
size: 10
|
||||
},
|
||||
maxRotation: 45,
|
||||
minRotation: 0,
|
||||
},
|
||||
minRotation: 0
|
||||
}
|
||||
},
|
||||
y: {
|
||||
type: 'linear' as const,
|
||||
display: true,
|
||||
position: 'left' as const,
|
||||
grid: {
|
||||
color: chartColors.value.grid,
|
||||
color: chartColors.value.grid
|
||||
},
|
||||
ticks: {
|
||||
color: '#3b82f6',
|
||||
font: {
|
||||
size: 10,
|
||||
size: 10
|
||||
},
|
||||
callback: (value: string | number) => '$' + formatCost(Number(value)),
|
||||
callback: (value: string | number) => '$' + formatCost(Number(value))
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: t('admin.accounts.stats.cost') + ' (USD)',
|
||||
color: '#3b82f6',
|
||||
font: {
|
||||
size: 11,
|
||||
},
|
||||
},
|
||||
size: 11
|
||||
}
|
||||
}
|
||||
},
|
||||
y1: {
|
||||
type: 'linear' as const,
|
||||
display: true,
|
||||
position: 'right' as const,
|
||||
grid: {
|
||||
drawOnChartArea: false,
|
||||
drawOnChartArea: false
|
||||
},
|
||||
ticks: {
|
||||
color: '#f97316',
|
||||
font: {
|
||||
size: 10,
|
||||
size: 10
|
||||
},
|
||||
callback: (value: string | number) => formatNumber(Number(value)),
|
||||
callback: (value: string | number) => formatNumber(Number(value))
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: t('admin.accounts.stats.requests'),
|
||||
color: '#f97316',
|
||||
font: {
|
||||
size: 11,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
size: 11
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
// Load stats when modal opens
|
||||
watch(() => props.show, async (newVal) => {
|
||||
if (newVal && props.account) {
|
||||
await loadStats()
|
||||
} else {
|
||||
stats.value = null
|
||||
watch(
|
||||
() => props.show,
|
||||
async (newVal) => {
|
||||
if (newVal && props.account) {
|
||||
await loadStats()
|
||||
} else {
|
||||
stats.value = null
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
const loadStats = async () => {
|
||||
if (!props.account) return
|
||||
|
||||
@@ -1,55 +1,86 @@
|
||||
<template>
|
||||
<div class="flex items-center gap-2">
|
||||
<!-- Main Status Badge -->
|
||||
<span
|
||||
:class="[
|
||||
'badge text-xs',
|
||||
statusClass
|
||||
]"
|
||||
>
|
||||
<span :class="['badge text-xs', statusClass]">
|
||||
{{ statusText }}
|
||||
</span>
|
||||
|
||||
<!-- Error Info Indicator -->
|
||||
<div v-if="hasError && account.error_message" class="relative group/error">
|
||||
<svg class="w-4 h-4 text-red-500 dark:text-red-400 cursor-help hover:text-red-600 dark:hover:text-red-300 transition-colors" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9 5.25h.008v.008H12v-.008z" />
|
||||
<div v-if="hasError && account.error_message" class="group/error relative">
|
||||
<svg
|
||||
class="h-4 w-4 cursor-help text-red-500 transition-colors hover:text-red-600 dark:text-red-400 dark:hover:text-red-300"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9 5.25h.008v.008H12v-.008z"
|
||||
/>
|
||||
</svg>
|
||||
<!-- Tooltip - 向下显示 -->
|
||||
<div class="absolute top-full left-0 mt-1.5 px-3 py-2 bg-gray-800 dark:bg-gray-900 text-white text-xs rounded-lg shadow-xl opacity-0 invisible group-hover/error:opacity-100 group-hover/error:visible transition-all duration-200 z-[100] min-w-[200px] max-w-[300px]">
|
||||
<div class="text-gray-300 break-words whitespace-pre-wrap leading-relaxed">{{ account.error_message }}</div>
|
||||
<div
|
||||
class="invisible absolute left-0 top-full z-[100] mt-1.5 min-w-[200px] max-w-[300px] rounded-lg bg-gray-800 px-3 py-2 text-xs text-white opacity-0 shadow-xl transition-all duration-200 group-hover/error:visible group-hover/error:opacity-100 dark:bg-gray-900"
|
||||
>
|
||||
<div class="whitespace-pre-wrap break-words leading-relaxed text-gray-300">
|
||||
{{ account.error_message }}
|
||||
</div>
|
||||
<!-- 上方小三角 -->
|
||||
<div class="absolute bottom-full left-3 border-[6px] border-transparent border-b-gray-800 dark:border-b-gray-900"></div>
|
||||
<div
|
||||
class="absolute bottom-full left-3 border-[6px] border-transparent border-b-gray-800 dark:border-b-gray-900"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rate Limit Indicator (429) -->
|
||||
<div v-if="isRateLimited" class="relative group">
|
||||
<span class="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs font-medium bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400">
|
||||
<svg class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
<div v-if="isRateLimited" class="group relative">
|
||||
<span
|
||||
class="inline-flex items-center gap-1 rounded bg-amber-100 px-1.5 py-0.5 text-xs font-medium text-amber-700 dark:bg-amber-900/30 dark:text-amber-400"
|
||||
>
|
||||
<svg class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
||||
/>
|
||||
</svg>
|
||||
429
|
||||
</span>
|
||||
<!-- Tooltip -->
|
||||
<div class="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-2 py-1 bg-gray-900 dark:bg-gray-700 text-white text-xs rounded whitespace-nowrap opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none z-50">
|
||||
<div
|
||||
class="pointer-events-none absolute bottom-full left-1/2 z-50 mb-2 -translate-x-1/2 whitespace-nowrap rounded bg-gray-900 px-2 py-1 text-xs text-white opacity-0 transition-opacity group-hover:opacity-100 dark:bg-gray-700"
|
||||
>
|
||||
Rate limited until {{ formatTime(account.rate_limit_reset_at) }}
|
||||
<div class="absolute top-full left-1/2 -translate-x-1/2 border-4 border-transparent border-t-gray-900 dark:border-t-gray-700"></div>
|
||||
<div
|
||||
class="absolute left-1/2 top-full -translate-x-1/2 border-4 border-transparent border-t-gray-900 dark:border-t-gray-700"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Overload Indicator (529) -->
|
||||
<div v-if="isOverloaded" class="relative group">
|
||||
<span class="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs font-medium bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400">
|
||||
<svg class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
<div v-if="isOverloaded" class="group relative">
|
||||
<span
|
||||
class="inline-flex items-center gap-1 rounded bg-red-100 px-1.5 py-0.5 text-xs font-medium text-red-700 dark:bg-red-900/30 dark:text-red-400"
|
||||
>
|
||||
<svg class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
||||
/>
|
||||
</svg>
|
||||
529
|
||||
</span>
|
||||
<!-- Tooltip -->
|
||||
<div class="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-2 py-1 bg-gray-900 dark:bg-gray-700 text-white text-xs rounded whitespace-nowrap opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none z-50">
|
||||
<div
|
||||
class="pointer-events-none absolute bottom-full left-1/2 z-50 mb-2 -translate-x-1/2 whitespace-nowrap rounded bg-gray-900 px-2 py-1 text-xs text-white opacity-0 transition-opacity group-hover:opacity-100 dark:bg-gray-700"
|
||||
>
|
||||
Overloaded until {{ formatTime(account.overload_until) }}
|
||||
<div class="absolute top-full left-1/2 -translate-x-1/2 border-4 border-transparent border-t-gray-900 dark:border-t-gray-700"></div>
|
||||
<div
|
||||
class="absolute left-1/2 top-full -translate-x-1/2 border-4 border-transparent border-t-gray-900 dark:border-t-gray-700"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,17 +7,29 @@
|
||||
>
|
||||
<div class="space-y-4">
|
||||
<!-- Account Info Card -->
|
||||
<div v-if="account" class="flex items-center justify-between p-3 bg-gradient-to-r from-gray-50 to-gray-100 dark:from-dark-700 dark:to-dark-600 rounded-xl border border-gray-200 dark:border-dark-500">
|
||||
<div
|
||||
v-if="account"
|
||||
class="flex items-center justify-between rounded-xl border border-gray-200 bg-gradient-to-r from-gray-50 to-gray-100 p-3 dark:border-dark-500 dark:from-dark-700 dark:to-dark-600"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 rounded-lg bg-gradient-to-br from-primary-500 to-primary-600 flex items-center justify-center">
|
||||
<svg class="w-5 h-5 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5.121 17.804A13.937 13.937 0 0112 16c2.5 0 4.847.655 6.879 1.804M15 10a3 3 0 11-6 0 3 3 0 016 0zm6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
<div
|
||||
class="flex h-10 w-10 items-center justify-center rounded-lg bg-gradient-to-br from-primary-500 to-primary-600"
|
||||
>
|
||||
<svg class="h-5 w-5 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M5.121 17.804A13.937 13.937 0 0112 16c2.5 0 4.847.655 6.879 1.804M15 10a3 3 0 11-6 0 3 3 0 016 0zm6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-semibold text-gray-900 dark:text-gray-100">{{ account.name }}</div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400 flex items-center gap-1.5">
|
||||
<span class="px-1.5 py-0.5 bg-gray-200 dark:bg-dark-500 rounded text-[10px] font-medium uppercase">
|
||||
<div class="flex items-center gap-1.5 text-xs text-gray-500 dark:text-gray-400">
|
||||
<span
|
||||
class="rounded bg-gray-200 px-1.5 py-0.5 text-[10px] font-medium uppercase dark:bg-dark-500"
|
||||
>
|
||||
{{ account.type }}
|
||||
</span>
|
||||
<span>{{ t('admin.accounts.account') }}</span>
|
||||
@@ -26,7 +38,7 @@
|
||||
</div>
|
||||
<span
|
||||
:class="[
|
||||
'px-2.5 py-1 text-xs font-semibold rounded-full',
|
||||
'rounded-full px-2.5 py-1 text-xs font-semibold',
|
||||
account.status === 'active'
|
||||
? 'bg-green-100 text-green-700 dark:bg-green-500/20 dark:text-green-400'
|
||||
: 'bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-400'
|
||||
@@ -44,7 +56,7 @@
|
||||
<select
|
||||
v-model="selectedModelId"
|
||||
:disabled="loadingModels || status === 'connecting'"
|
||||
class="w-full px-3 py-2 text-sm rounded-lg border border-gray-300 dark:border-dark-500 bg-white dark:bg-dark-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-primary-500 focus:border-primary-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
class="w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 focus:border-primary-500 focus:ring-2 focus:ring-primary-500 disabled:cursor-not-allowed disabled:opacity-50 dark:border-dark-500 dark:bg-dark-700 dark:text-gray-100"
|
||||
>
|
||||
<option v-if="loadingModels" value="">{{ t('common.loading') }}...</option>
|
||||
<option v-for="model in availableModels" :key="model.id" :value="model.id">
|
||||
@@ -54,22 +66,38 @@
|
||||
</div>
|
||||
|
||||
<!-- Terminal Output -->
|
||||
<div class="relative group">
|
||||
<div class="group relative">
|
||||
<div
|
||||
ref="terminalRef"
|
||||
class="bg-gray-900 dark:bg-black rounded-xl p-4 min-h-[120px] max-h-[240px] overflow-y-auto font-mono text-sm border border-gray-700 dark:border-gray-800"
|
||||
class="max-h-[240px] min-h-[120px] overflow-y-auto rounded-xl border border-gray-700 bg-gray-900 p-4 font-mono text-sm dark:border-gray-800 dark:bg-black"
|
||||
>
|
||||
<!-- Status Line -->
|
||||
<div v-if="status === 'idle'" class="text-gray-500 flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
<div v-if="status === 'idle'" class="flex items-center gap-2 text-gray-500">
|
||||
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M13 10V3L4 14h7v7l9-11h-7z"
|
||||
/>
|
||||
</svg>
|
||||
<span>{{ t('admin.accounts.readyToTest') }}</span>
|
||||
</div>
|
||||
<div v-else-if="status === 'connecting'" class="text-yellow-400 flex items-center gap-2">
|
||||
<svg class="animate-spin w-4 h-4" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
<div v-else-if="status === 'connecting'" class="flex items-center gap-2 text-yellow-400">
|
||||
<svg class="h-4 w-4 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||
<circle
|
||||
class="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
stroke-width="4"
|
||||
></circle>
|
||||
<path
|
||||
class="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
></path>
|
||||
</svg>
|
||||
<span>{{ t('admin.accounts.connectingToApi') }}</span>
|
||||
</div>
|
||||
@@ -85,15 +113,31 @@
|
||||
</div>
|
||||
|
||||
<!-- Result Status -->
|
||||
<div v-if="status === 'success'" class="text-green-400 mt-3 pt-3 border-t border-gray-700 flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
<div
|
||||
v-if="status === 'success'"
|
||||
class="mt-3 flex items-center gap-2 border-t border-gray-700 pt-3 text-green-400"
|
||||
>
|
||||
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<span>{{ t('admin.accounts.testCompleted') }}</span>
|
||||
</div>
|
||||
<div v-else-if="status === 'error'" class="text-red-400 mt-3 pt-3 border-t border-gray-700 flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
<div
|
||||
v-else-if="status === 'error'"
|
||||
class="mt-3 flex items-center gap-2 border-t border-gray-700 pt-3 text-red-400"
|
||||
>
|
||||
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<span>{{ errorMessage }}</span>
|
||||
</div>
|
||||
@@ -103,28 +147,43 @@
|
||||
<button
|
||||
v-if="outputLines.length > 0"
|
||||
@click="copyOutput"
|
||||
class="absolute top-2 right-2 p-1.5 text-gray-400 hover:text-white bg-gray-800/80 hover:bg-gray-700 rounded-lg transition-all opacity-0 group-hover:opacity-100"
|
||||
class="absolute right-2 top-2 rounded-lg bg-gray-800/80 p-1.5 text-gray-400 opacity-0 transition-all hover:bg-gray-700 hover:text-white group-hover:opacity-100"
|
||||
:title="t('admin.accounts.copyOutput')"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
||||
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Test Info -->
|
||||
<div class="flex items-center justify-between text-xs text-gray-500 dark:text-gray-400 px-1">
|
||||
<div class="flex items-center justify-between px-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="flex items-center gap-1">
|
||||
<svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z" />
|
||||
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z"
|
||||
/>
|
||||
</svg>
|
||||
{{ t('admin.accounts.testModel') }}
|
||||
</span>
|
||||
</div>
|
||||
<span class="flex items-center gap-1">
|
||||
<svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z" />
|
||||
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z"
|
||||
/>
|
||||
</svg>
|
||||
{{ t('admin.accounts.testPrompt') }}
|
||||
</span>
|
||||
@@ -135,7 +194,7 @@
|
||||
<div class="flex justify-end gap-3">
|
||||
<button
|
||||
@click="handleClose"
|
||||
class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-dark-600 hover:bg-gray-200 dark:hover:bg-dark-500 rounded-lg transition-colors"
|
||||
class="rounded-lg bg-gray-100 px-4 py-2 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-200 dark:bg-dark-600 dark:text-gray-300 dark:hover:bg-dark-500"
|
||||
:disabled="status === 'connecting'"
|
||||
>
|
||||
{{ t('common.close') }}
|
||||
@@ -144,29 +203,72 @@
|
||||
@click="startTest"
|
||||
:disabled="status === 'connecting' || !selectedModelId"
|
||||
:class="[
|
||||
'px-4 py-2 text-sm font-medium rounded-lg transition-all flex items-center gap-2',
|
||||
'flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium transition-all',
|
||||
status === 'connecting' || !selectedModelId
|
||||
? 'bg-primary-400 text-white cursor-not-allowed'
|
||||
? 'cursor-not-allowed bg-primary-400 text-white'
|
||||
: status === 'success'
|
||||
? 'bg-green-500 hover:bg-green-600 text-white'
|
||||
? 'bg-green-500 text-white hover:bg-green-600'
|
||||
: status === 'error'
|
||||
? 'bg-orange-500 hover:bg-orange-600 text-white'
|
||||
: 'bg-primary-500 hover:bg-primary-600 text-white'
|
||||
? 'bg-orange-500 text-white hover:bg-orange-600'
|
||||
: 'bg-primary-500 text-white hover:bg-primary-600'
|
||||
]"
|
||||
>
|
||||
<svg v-if="status === 'connecting'" class="animate-spin h-4 w-4" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
<svg
|
||||
v-if="status === 'connecting'"
|
||||
class="h-4 w-4 animate-spin"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle
|
||||
class="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
stroke-width="4"
|
||||
></circle>
|
||||
<path
|
||||
class="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
></path>
|
||||
</svg>
|
||||
<svg v-else-if="status === 'idle'" class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
<svg
|
||||
v-else-if="status === 'idle'"
|
||||
class="h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"
|
||||
/>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<svg v-else class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
<svg v-else class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
|
||||
/>
|
||||
</svg>
|
||||
<span>
|
||||
{{ status === 'connecting' ? t('admin.accounts.testing') : status === 'idle' ? t('admin.accounts.startTest') : t('admin.accounts.retry') }}
|
||||
{{
|
||||
status === 'connecting'
|
||||
? t('admin.accounts.testing')
|
||||
: status === 'idle'
|
||||
? t('admin.accounts.startTest')
|
||||
: t('admin.accounts.retry')
|
||||
}}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -208,14 +310,17 @@ const loadingModels = ref(false)
|
||||
let eventSource: EventSource | null = null
|
||||
|
||||
// Load available models when modal opens
|
||||
watch(() => props.show, async (newVal) => {
|
||||
if (newVal && props.account) {
|
||||
resetState()
|
||||
await loadAvailableModels()
|
||||
} else {
|
||||
closeEventSource()
|
||||
watch(
|
||||
() => props.show,
|
||||
async (newVal) => {
|
||||
if (newVal && props.account) {
|
||||
resetState()
|
||||
await loadAvailableModels()
|
||||
} else {
|
||||
closeEventSource()
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
const loadAvailableModels = async () => {
|
||||
if (!props.account) return
|
||||
@@ -227,7 +332,7 @@ const loadAvailableModels = async () => {
|
||||
// Default to first model (usually Sonnet)
|
||||
if (availableModels.value.length > 0) {
|
||||
// Try to select Sonnet as default, otherwise use first model
|
||||
const sonnetModel = availableModels.value.find(m => m.id.includes('sonnet'))
|
||||
const sonnetModel = availableModels.value.find((m) => m.id.includes('sonnet'))
|
||||
selectedModelId.value = sonnetModel?.id || availableModels.value[0].id
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -290,7 +395,7 @@ const startTest = async () => {
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`,
|
||||
Authorization: `Bearer ${localStorage.getItem('auth_token')}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ model_id: selectedModelId.value })
|
||||
@@ -337,7 +442,13 @@ const startTest = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleEvent = (event: { type: string; text?: string; model?: string; success?: boolean; error?: string }) => {
|
||||
const handleEvent = (event: {
|
||||
type: string
|
||||
text?: string
|
||||
model?: string
|
||||
success?: boolean
|
||||
error?: string
|
||||
}) => {
|
||||
switch (event.type) {
|
||||
case 'test_start':
|
||||
addLine(t('admin.accounts.connectedToApi'), 'text-green-400')
|
||||
@@ -382,7 +493,7 @@ const handleEvent = (event: { type: string; text?: string; model?: string; succe
|
||||
}
|
||||
|
||||
const copyOutput = () => {
|
||||
const text = outputLines.value.map(l => l.text).join('\n')
|
||||
const text = outputLines.value.map((l) => l.text).join('\n')
|
||||
navigator.clipboard.writeText(text)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
<div>
|
||||
<!-- Loading state -->
|
||||
<div v-if="loading" class="space-y-0.5">
|
||||
<div class="h-3 w-12 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
|
||||
<div class="h-3 w-16 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
|
||||
<div class="h-3 w-10 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
|
||||
<div class="h-3 w-12 animate-pulse rounded bg-gray-200 dark:bg-gray-700"></div>
|
||||
<div class="h-3 w-16 animate-pulse rounded bg-gray-200 dark:bg-gray-700"></div>
|
||||
<div class="h-3 w-10 animate-pulse rounded bg-gray-200 dark:bg-gray-700"></div>
|
||||
</div>
|
||||
|
||||
<!-- Error state -->
|
||||
@@ -17,24 +17,28 @@
|
||||
<!-- Requests -->
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="text-gray-500 dark:text-gray-400">Req:</span>
|
||||
<span class="font-medium text-gray-700 dark:text-gray-300">{{ formatNumber(stats.requests) }}</span>
|
||||
<span class="font-medium text-gray-700 dark:text-gray-300">{{
|
||||
formatNumber(stats.requests)
|
||||
}}</span>
|
||||
</div>
|
||||
<!-- Tokens -->
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="text-gray-500 dark:text-gray-400">Tok:</span>
|
||||
<span class="font-medium text-gray-700 dark:text-gray-300">{{ formatTokens(stats.tokens) }}</span>
|
||||
<span class="font-medium text-gray-700 dark:text-gray-300">{{
|
||||
formatTokens(stats.tokens)
|
||||
}}</span>
|
||||
</div>
|
||||
<!-- Cost -->
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="text-gray-500 dark:text-gray-400">Cost:</span>
|
||||
<span class="font-medium text-emerald-600 dark:text-emerald-400">{{ formatCurrency(stats.cost) }}</span>
|
||||
<span class="font-medium text-emerald-600 dark:text-emerald-400">{{
|
||||
formatCurrency(stats.cost)
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- No data -->
|
||||
<div v-else class="text-xs text-gray-400">
|
||||
-
|
||||
</div>
|
||||
<div v-else class="text-xs text-gray-400">-</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,25 +1,30 @@
|
||||
<template>
|
||||
<div v-if="showUsageWindows">
|
||||
<!-- Anthropic OAuth and Setup Token accounts: fetch real usage data -->
|
||||
<template v-if="account.platform === 'anthropic' && (account.type === 'oauth' || account.type === 'setup-token')">
|
||||
<template
|
||||
v-if="
|
||||
account.platform === 'anthropic' &&
|
||||
(account.type === 'oauth' || account.type === 'setup-token')
|
||||
"
|
||||
>
|
||||
<!-- Loading state -->
|
||||
<div v-if="loading" class="space-y-1.5">
|
||||
<!-- OAuth: 3 rows, Setup Token: 1 row -->
|
||||
<div class="flex items-center gap-1">
|
||||
<div class="w-[32px] h-3 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
|
||||
<div class="w-8 h-1.5 bg-gray-200 dark:bg-gray-700 rounded-full animate-pulse"></div>
|
||||
<div class="w-[32px] h-3 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
|
||||
<div class="h-3 w-[32px] animate-pulse rounded bg-gray-200 dark:bg-gray-700"></div>
|
||||
<div class="h-1.5 w-8 animate-pulse rounded-full bg-gray-200 dark:bg-gray-700"></div>
|
||||
<div class="h-3 w-[32px] animate-pulse rounded bg-gray-200 dark:bg-gray-700"></div>
|
||||
</div>
|
||||
<template v-if="account.type === 'oauth'">
|
||||
<div class="flex items-center gap-1">
|
||||
<div class="w-[32px] h-3 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
|
||||
<div class="w-8 h-1.5 bg-gray-200 dark:bg-gray-700 rounded-full animate-pulse"></div>
|
||||
<div class="w-[32px] h-3 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
|
||||
<div class="h-3 w-[32px] animate-pulse rounded bg-gray-200 dark:bg-gray-700"></div>
|
||||
<div class="h-1.5 w-8 animate-pulse rounded-full bg-gray-200 dark:bg-gray-700"></div>
|
||||
<div class="h-3 w-[32px] animate-pulse rounded bg-gray-200 dark:bg-gray-700"></div>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<div class="w-[32px] h-3 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
|
||||
<div class="w-8 h-1.5 bg-gray-200 dark:bg-gray-700 rounded-full animate-pulse"></div>
|
||||
<div class="w-[32px] h-3 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
|
||||
<div class="h-3 w-[32px] animate-pulse rounded bg-gray-200 dark:bg-gray-700"></div>
|
||||
<div class="h-1.5 w-8 animate-pulse rounded-full bg-gray-200 dark:bg-gray-700"></div>
|
||||
<div class="h-3 w-[32px] animate-pulse rounded bg-gray-200 dark:bg-gray-700"></div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@@ -61,9 +66,7 @@
|
||||
</div>
|
||||
|
||||
<!-- No data yet -->
|
||||
<div v-else class="text-xs text-gray-400">
|
||||
-
|
||||
</div>
|
||||
<div v-else class="text-xs text-gray-400">-</div>
|
||||
</template>
|
||||
|
||||
<!-- OpenAI OAuth accounts: show Codex usage from extra field -->
|
||||
@@ -97,9 +100,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Non-OAuth/Setup-Token accounts -->
|
||||
<div v-else class="text-xs text-gray-400">
|
||||
-
|
||||
</div>
|
||||
<div v-else class="text-xs text-gray-400">-</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -117,20 +118,21 @@ const error = ref<string | null>(null)
|
||||
const usageInfo = ref<AccountUsageInfo | null>(null)
|
||||
|
||||
// Show usage windows for OAuth and Setup Token accounts
|
||||
const showUsageWindows = computed(() =>
|
||||
props.account.type === 'oauth' || props.account.type === 'setup-token'
|
||||
const showUsageWindows = computed(
|
||||
() => props.account.type === 'oauth' || props.account.type === 'setup-token'
|
||||
)
|
||||
|
||||
// OpenAI Codex usage computed properties
|
||||
const hasCodexUsage = computed(() => {
|
||||
const extra = props.account.extra
|
||||
return extra && (
|
||||
return (
|
||||
extra &&
|
||||
// Check for new canonical fields first
|
||||
extra.codex_5h_used_percent !== undefined ||
|
||||
extra.codex_7d_used_percent !== undefined ||
|
||||
// Fallback to legacy fields
|
||||
extra.codex_primary_used_percent !== undefined ||
|
||||
extra.codex_secondary_used_percent !== undefined
|
||||
(extra.codex_5h_used_percent !== undefined ||
|
||||
extra.codex_7d_used_percent !== undefined ||
|
||||
// Fallback to legacy fields
|
||||
extra.codex_primary_used_percent !== undefined ||
|
||||
extra.codex_secondary_used_percent !== undefined)
|
||||
)
|
||||
})
|
||||
|
||||
@@ -145,10 +147,16 @@ const codex5hUsedPercent = computed(() => {
|
||||
}
|
||||
|
||||
// Fallback: detect from legacy fields using window_minutes
|
||||
if (extra.codex_primary_window_minutes !== undefined && extra.codex_primary_window_minutes <= 360) {
|
||||
if (
|
||||
extra.codex_primary_window_minutes !== undefined &&
|
||||
extra.codex_primary_window_minutes <= 360
|
||||
) {
|
||||
return extra.codex_primary_used_percent ?? null
|
||||
}
|
||||
if (extra.codex_secondary_window_minutes !== undefined && extra.codex_secondary_window_minutes <= 360) {
|
||||
if (
|
||||
extra.codex_secondary_window_minutes !== undefined &&
|
||||
extra.codex_secondary_window_minutes <= 360
|
||||
) {
|
||||
return extra.codex_secondary_used_percent ?? null
|
||||
}
|
||||
|
||||
@@ -167,13 +175,19 @@ const codex5hResetAt = computed(() => {
|
||||
}
|
||||
|
||||
// Fallback: detect from legacy fields using window_minutes
|
||||
if (extra.codex_primary_window_minutes !== undefined && extra.codex_primary_window_minutes <= 360) {
|
||||
if (
|
||||
extra.codex_primary_window_minutes !== undefined &&
|
||||
extra.codex_primary_window_minutes <= 360
|
||||
) {
|
||||
if (extra.codex_primary_reset_after_seconds !== undefined) {
|
||||
const resetTime = new Date(Date.now() + extra.codex_primary_reset_after_seconds * 1000)
|
||||
return resetTime.toISOString()
|
||||
}
|
||||
}
|
||||
if (extra.codex_secondary_window_minutes !== undefined && extra.codex_secondary_window_minutes <= 360) {
|
||||
if (
|
||||
extra.codex_secondary_window_minutes !== undefined &&
|
||||
extra.codex_secondary_window_minutes <= 360
|
||||
) {
|
||||
if (extra.codex_secondary_reset_after_seconds !== undefined) {
|
||||
const resetTime = new Date(Date.now() + extra.codex_secondary_reset_after_seconds * 1000)
|
||||
return resetTime.toISOString()
|
||||
@@ -200,10 +214,16 @@ const codex7dUsedPercent = computed(() => {
|
||||
}
|
||||
|
||||
// Fallback: detect from legacy fields using window_minutes
|
||||
if (extra.codex_primary_window_minutes !== undefined && extra.codex_primary_window_minutes >= 10000) {
|
||||
if (
|
||||
extra.codex_primary_window_minutes !== undefined &&
|
||||
extra.codex_primary_window_minutes >= 10000
|
||||
) {
|
||||
return extra.codex_primary_used_percent ?? null
|
||||
}
|
||||
if (extra.codex_secondary_window_minutes !== undefined && extra.codex_secondary_window_minutes >= 10000) {
|
||||
if (
|
||||
extra.codex_secondary_window_minutes !== undefined &&
|
||||
extra.codex_secondary_window_minutes >= 10000
|
||||
) {
|
||||
return extra.codex_secondary_used_percent ?? null
|
||||
}
|
||||
|
||||
@@ -222,13 +242,19 @@ const codex7dResetAt = computed(() => {
|
||||
}
|
||||
|
||||
// Fallback: detect from legacy fields using window_minutes
|
||||
if (extra.codex_primary_window_minutes !== undefined && extra.codex_primary_window_minutes >= 10000) {
|
||||
if (
|
||||
extra.codex_primary_window_minutes !== undefined &&
|
||||
extra.codex_primary_window_minutes >= 10000
|
||||
) {
|
||||
if (extra.codex_primary_reset_after_seconds !== undefined) {
|
||||
const resetTime = new Date(Date.now() + extra.codex_primary_reset_after_seconds * 1000)
|
||||
return resetTime.toISOString()
|
||||
}
|
||||
}
|
||||
if (extra.codex_secondary_window_minutes !== undefined && extra.codex_secondary_window_minutes >= 10000) {
|
||||
if (
|
||||
extra.codex_secondary_window_minutes !== undefined &&
|
||||
extra.codex_secondary_window_minutes >= 10000
|
||||
) {
|
||||
if (extra.codex_secondary_reset_after_seconds !== undefined) {
|
||||
const resetTime = new Date(Date.now() + extra.codex_secondary_reset_after_seconds * 1000)
|
||||
return resetTime.toISOString()
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
<Modal :show="show" :title="t('admin.accounts.bulkEdit.title')" size="lg" @close="handleClose">
|
||||
<form class="space-y-5" @submit.prevent="handleSubmit">
|
||||
<!-- Info -->
|
||||
<div class="rounded-lg bg-blue-50 dark:bg-blue-900/20 p-4">
|
||||
<div class="rounded-lg bg-blue-50 p-4 dark:bg-blue-900/20">
|
||||
<p class="text-sm text-blue-700 dark:text-blue-400">
|
||||
<svg class="w-5 h-5 inline mr-1.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<svg class="mr-1.5 inline h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
@@ -17,8 +17,8 @@
|
||||
</div>
|
||||
|
||||
<!-- Base URL (API Key only) -->
|
||||
<div class="border-t border-gray-200 dark:border-dark-600 pt-4">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div class="border-t border-gray-200 pt-4 dark:border-dark-600">
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<label class="input-label mb-0">{{ t('admin.accounts.baseUrl') }}</label>
|
||||
<input
|
||||
v-model="enableBaseUrl"
|
||||
@@ -31,7 +31,7 @@
|
||||
type="text"
|
||||
:disabled="!enableBaseUrl"
|
||||
class="input"
|
||||
:class="!enableBaseUrl && 'opacity-50 cursor-not-allowed'"
|
||||
:class="!enableBaseUrl && 'cursor-not-allowed opacity-50'"
|
||||
:placeholder="t('admin.accounts.bulkEdit.baseUrlPlaceholder')"
|
||||
/>
|
||||
<p class="input-hint">
|
||||
@@ -40,8 +40,8 @@
|
||||
</div>
|
||||
|
||||
<!-- Model restriction -->
|
||||
<div class="border-t border-gray-200 dark:border-dark-600 pt-4">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div class="border-t border-gray-200 pt-4 dark:border-dark-600">
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<label class="input-label mb-0">{{ t('admin.accounts.modelRestriction') }}</label>
|
||||
<input
|
||||
v-model="enableModelRestriction"
|
||||
@@ -50,21 +50,21 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div :class="!enableModelRestriction && 'opacity-50 pointer-events-none'">
|
||||
<div :class="!enableModelRestriction && 'pointer-events-none opacity-50'">
|
||||
<!-- Mode Toggle -->
|
||||
<div class="flex gap-2 mb-4">
|
||||
<div class="mb-4 flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
:class="[
|
||||
'flex-1 rounded-lg px-4 py-2 text-sm font-medium transition-all',
|
||||
modelRestrictionMode === 'whitelist'
|
||||
? 'bg-primary-100 text-primary-700 dark:bg-primary-900/30 dark:text-primary-400'
|
||||
: 'bg-gray-100 text-gray-600 hover:bg-gray-200 dark:bg-dark-600 dark:text-gray-400 dark:hover:bg-dark-500',
|
||||
: 'bg-gray-100 text-gray-600 hover:bg-gray-200 dark:bg-dark-600 dark:text-gray-400 dark:hover:bg-dark-500'
|
||||
]"
|
||||
@click="modelRestrictionMode = 'whitelist'"
|
||||
>
|
||||
<svg
|
||||
class="w-4 h-4 inline mr-1.5"
|
||||
class="mr-1.5 inline h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
@@ -84,12 +84,12 @@
|
||||
'flex-1 rounded-lg px-4 py-2 text-sm font-medium transition-all',
|
||||
modelRestrictionMode === 'mapping'
|
||||
? 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400'
|
||||
: 'bg-gray-100 text-gray-600 hover:bg-gray-200 dark:bg-dark-600 dark:text-gray-400 dark:hover:bg-dark-500',
|
||||
: 'bg-gray-100 text-gray-600 hover:bg-gray-200 dark:bg-dark-600 dark:text-gray-400 dark:hover:bg-dark-500'
|
||||
]"
|
||||
@click="modelRestrictionMode = 'mapping'"
|
||||
>
|
||||
<svg
|
||||
class="w-4 h-4 inline mr-1.5"
|
||||
class="mr-1.5 inline h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
@@ -107,10 +107,10 @@
|
||||
|
||||
<!-- Whitelist Mode -->
|
||||
<div v-if="modelRestrictionMode === 'whitelist'">
|
||||
<div class="mb-3 rounded-lg bg-blue-50 dark:bg-blue-900/20 p-3">
|
||||
<div class="mb-3 rounded-lg bg-blue-50 p-3 dark:bg-blue-900/20">
|
||||
<p class="text-xs text-blue-700 dark:text-blue-400">
|
||||
<svg
|
||||
class="w-4 h-4 inline mr-1"
|
||||
class="mr-1 inline h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
@@ -127,7 +127,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Model Checkbox List -->
|
||||
<div class="grid grid-cols-2 gap-2 mb-3">
|
||||
<div class="mb-3 grid grid-cols-2 gap-2">
|
||||
<label
|
||||
v-for="model in allModels"
|
||||
:key="model.value"
|
||||
@@ -158,10 +158,10 @@
|
||||
|
||||
<!-- Mapping Mode -->
|
||||
<div v-else>
|
||||
<div class="mb-3 rounded-lg bg-purple-50 dark:bg-purple-900/20 p-3">
|
||||
<div class="mb-3 rounded-lg bg-purple-50 p-3 dark:bg-purple-900/20">
|
||||
<p class="text-xs text-purple-700 dark:text-purple-400">
|
||||
<svg
|
||||
class="w-4 h-4 inline mr-1"
|
||||
class="mr-1 inline h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
@@ -178,7 +178,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Model Mapping List -->
|
||||
<div v-if="modelMappings.length > 0" class="space-y-2 mb-3">
|
||||
<div v-if="modelMappings.length > 0" class="mb-3 space-y-2">
|
||||
<div
|
||||
v-for="(mapping, index) in modelMappings"
|
||||
:key="index"
|
||||
@@ -191,7 +191,7 @@
|
||||
:placeholder="t('admin.accounts.requestModel')"
|
||||
/>
|
||||
<svg
|
||||
class="w-4 h-4 text-gray-400 flex-shrink-0"
|
||||
class="h-4 w-4 flex-shrink-0 text-gray-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
@@ -211,10 +211,10 @@
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="p-2 text-red-500 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-lg transition-colors"
|
||||
class="rounded-lg p-2 text-red-500 transition-colors hover:bg-red-50 hover:text-red-600 dark:hover:bg-red-900/20"
|
||||
@click="removeModelMapping(index)"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
@@ -228,11 +228,11 @@
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="w-full rounded-lg border-2 border-dashed border-gray-300 dark:border-dark-500 px-4 py-2 text-gray-600 dark:text-gray-400 transition-colors hover:border-gray-400 hover:text-gray-700 dark:hover:border-dark-400 dark:hover:text-gray-300 mb-3"
|
||||
class="mb-3 w-full rounded-lg border-2 border-dashed border-gray-300 px-4 py-2 text-gray-600 transition-colors hover:border-gray-400 hover:text-gray-700 dark:border-dark-500 dark:text-gray-400 dark:hover:border-dark-400 dark:hover:text-gray-300"
|
||||
@click="addModelMapping"
|
||||
>
|
||||
<svg
|
||||
class="w-4 h-4 inline mr-1"
|
||||
class="mr-1 inline h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
@@ -264,11 +264,11 @@
|
||||
</div>
|
||||
|
||||
<!-- Custom error codes -->
|
||||
<div class="border-t border-gray-200 dark:border-dark-600 pt-4">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div class="border-t border-gray-200 pt-4 dark:border-dark-600">
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<div>
|
||||
<label class="input-label mb-0">{{ t('admin.accounts.customErrorCodes') }}</label>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.accounts.customErrorCodesHint') }}
|
||||
</p>
|
||||
</div>
|
||||
@@ -280,10 +280,10 @@
|
||||
</div>
|
||||
|
||||
<div v-if="enableCustomErrorCodes" class="space-y-3">
|
||||
<div class="rounded-lg bg-amber-50 dark:bg-amber-900/20 p-3">
|
||||
<div class="rounded-lg bg-amber-50 p-3 dark:bg-amber-900/20">
|
||||
<p class="text-xs text-amber-700 dark:text-amber-400">
|
||||
<svg
|
||||
class="w-4 h-4 inline mr-1"
|
||||
class="mr-1 inline h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
@@ -308,8 +308,8 @@
|
||||
:class="[
|
||||
'rounded-lg px-3 py-1.5 text-sm font-medium transition-colors',
|
||||
selectedErrorCodes.includes(code.value)
|
||||
? 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400 ring-1 ring-red-500'
|
||||
: 'bg-gray-100 text-gray-600 hover:bg-gray-200 dark:bg-dark-600 dark:text-gray-400 dark:hover:bg-dark-500',
|
||||
? 'bg-red-100 text-red-700 ring-1 ring-red-500 dark:bg-red-900/30 dark:text-red-400'
|
||||
: 'bg-gray-100 text-gray-600 hover:bg-gray-200 dark:bg-dark-600 dark:text-gray-400 dark:hover:bg-dark-500'
|
||||
]"
|
||||
@click="toggleErrorCode(code.value)"
|
||||
>
|
||||
@@ -329,7 +329,7 @@
|
||||
@keyup.enter="addCustomErrorCode"
|
||||
/>
|
||||
<button type="button" class="btn btn-secondary px-3" @click="addCustomErrorCode">
|
||||
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
@@ -345,7 +345,7 @@
|
||||
<span
|
||||
v-for="code in selectedErrorCodes.sort((a, b) => a - b)"
|
||||
:key="code"
|
||||
class="inline-flex items-center gap-1 rounded-full bg-red-100 dark:bg-red-900/30 px-2.5 py-0.5 text-sm font-medium text-red-700 dark:text-red-400"
|
||||
class="inline-flex items-center gap-1 rounded-full bg-red-100 px-2.5 py-0.5 text-sm font-medium text-red-700 dark:bg-red-900/30 dark:text-red-400"
|
||||
>
|
||||
{{ code }}
|
||||
<button
|
||||
@@ -353,7 +353,7 @@
|
||||
class="hover:text-red-900 dark:hover:text-red-300"
|
||||
@click="removeErrorCode(code)"
|
||||
>
|
||||
<svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
@@ -371,13 +371,13 @@
|
||||
</div>
|
||||
|
||||
<!-- Intercept warmup requests (Anthropic only) -->
|
||||
<div class="border-t border-gray-200 dark:border-dark-600 pt-4">
|
||||
<div class="border-t border-gray-200 pt-4 dark:border-dark-600">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex-1 pr-4">
|
||||
<label class="input-label mb-0">{{
|
||||
t('admin.accounts.interceptWarmupRequests')
|
||||
}}</label>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.accounts.interceptWarmupRequestsDesc') }}
|
||||
</p>
|
||||
</div>
|
||||
@@ -392,14 +392,14 @@
|
||||
type="button"
|
||||
:class="[
|
||||
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2',
|
||||
interceptWarmupRequests ? 'bg-primary-600' : 'bg-gray-200 dark:bg-dark-600',
|
||||
interceptWarmupRequests ? 'bg-primary-600' : 'bg-gray-200 dark:bg-dark-600'
|
||||
]"
|
||||
@click="interceptWarmupRequests = !interceptWarmupRequests"
|
||||
>
|
||||
<span
|
||||
:class="[
|
||||
'pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out',
|
||||
interceptWarmupRequests ? 'translate-x-5' : 'translate-x-0',
|
||||
interceptWarmupRequests ? 'translate-x-5' : 'translate-x-0'
|
||||
]"
|
||||
/>
|
||||
</button>
|
||||
@@ -407,8 +407,8 @@
|
||||
</div>
|
||||
|
||||
<!-- Proxy -->
|
||||
<div class="border-t border-gray-200 dark:border-dark-600 pt-4">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div class="border-t border-gray-200 pt-4 dark:border-dark-600">
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<label class="input-label mb-0">{{ t('admin.accounts.proxy') }}</label>
|
||||
<input
|
||||
v-model="enableProxy"
|
||||
@@ -416,15 +416,15 @@
|
||||
class="rounded border-gray-300 text-primary-600 focus:ring-primary-500"
|
||||
/>
|
||||
</div>
|
||||
<div :class="!enableProxy && 'opacity-50 pointer-events-none'">
|
||||
<div :class="!enableProxy && 'pointer-events-none opacity-50'">
|
||||
<ProxySelector v-model="proxyId" :proxies="proxies" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Concurrency & Priority -->
|
||||
<div class="grid grid-cols-2 gap-4 border-t border-gray-200 dark:border-dark-600 pt-4">
|
||||
<div class="grid grid-cols-2 gap-4 border-t border-gray-200 pt-4 dark:border-dark-600">
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<label class="input-label mb-0">{{ t('admin.accounts.concurrency') }}</label>
|
||||
<input
|
||||
v-model="enableConcurrency"
|
||||
@@ -438,11 +438,11 @@
|
||||
min="1"
|
||||
:disabled="!enableConcurrency"
|
||||
class="input"
|
||||
:class="!enableConcurrency && 'opacity-50 cursor-not-allowed'"
|
||||
:class="!enableConcurrency && 'cursor-not-allowed opacity-50'"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<label class="input-label mb-0">{{ t('admin.accounts.priority') }}</label>
|
||||
<input
|
||||
v-model="enablePriority"
|
||||
@@ -456,14 +456,14 @@
|
||||
min="1"
|
||||
:disabled="!enablePriority"
|
||||
class="input"
|
||||
:class="!enablePriority && 'opacity-50 cursor-not-allowed'"
|
||||
:class="!enablePriority && 'cursor-not-allowed opacity-50'"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status -->
|
||||
<div class="border-t border-gray-200 dark:border-dark-600 pt-4">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div class="border-t border-gray-200 pt-4 dark:border-dark-600">
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<label class="input-label mb-0">{{ t('common.status') }}</label>
|
||||
<input
|
||||
v-model="enableStatus"
|
||||
@@ -471,14 +471,14 @@
|
||||
class="rounded border-gray-300 text-primary-600 focus:ring-primary-500"
|
||||
/>
|
||||
</div>
|
||||
<div :class="!enableStatus && 'opacity-50 pointer-events-none'">
|
||||
<div :class="!enableStatus && 'pointer-events-none opacity-50'">
|
||||
<Select v-model="status" :options="statusOptions" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Groups -->
|
||||
<div class="border-t border-gray-200 dark:border-dark-600 pt-4">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div class="border-t border-gray-200 pt-4 dark:border-dark-600">
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<label class="input-label mb-0">{{ t('nav.groups') }}</label>
|
||||
<input
|
||||
v-model="enableGroups"
|
||||
@@ -486,7 +486,7 @@
|
||||
class="rounded border-gray-300 text-primary-600 focus:ring-primary-500"
|
||||
/>
|
||||
</div>
|
||||
<div :class="!enableGroups && 'opacity-50 pointer-events-none'">
|
||||
<div :class="!enableGroups && 'pointer-events-none opacity-50'">
|
||||
<GroupSelector v-model="groupIds" :groups="groups" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -499,7 +499,7 @@
|
||||
<button type="submit" :disabled="submitting" class="btn btn-primary">
|
||||
<svg
|
||||
v-if="submitting"
|
||||
class="animate-spin -ml-1 mr-2 h-4 w-4"
|
||||
class="-ml-1 mr-2 h-4 w-4 animate-spin"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
@@ -601,7 +601,7 @@ const allModels = [
|
||||
{ value: 'gpt-5.1-codex', label: 'GPT-5.1 Codex' },
|
||||
{ value: 'gpt-5.1-2025-11-13', label: 'GPT-5.1' },
|
||||
{ value: 'gpt-5.1-codex-mini', label: 'GPT-5.1 Codex Mini' },
|
||||
{ value: 'gpt-5-2025-08-07', label: 'GPT-5' },
|
||||
{ value: 'gpt-5-2025-08-07', label: 'GPT-5' }
|
||||
]
|
||||
|
||||
// Preset mappings (combined Anthropic + OpenAI)
|
||||
@@ -610,48 +610,46 @@ const presetMappings = [
|
||||
label: 'Sonnet 4',
|
||||
from: 'claude-sonnet-4-20250514',
|
||||
to: 'claude-sonnet-4-20250514',
|
||||
color: 'bg-blue-100 text-blue-700 hover:bg-blue-200 dark:bg-blue-900/30 dark:text-blue-400',
|
||||
color: 'bg-blue-100 text-blue-700 hover:bg-blue-200 dark:bg-blue-900/30 dark:text-blue-400'
|
||||
},
|
||||
{
|
||||
label: 'Sonnet 4.5',
|
||||
from: 'claude-sonnet-4-5-20250929',
|
||||
to: 'claude-sonnet-4-5-20250929',
|
||||
color:
|
||||
'bg-indigo-100 text-indigo-700 hover:bg-indigo-200 dark:bg-indigo-900/30 dark:text-indigo-400',
|
||||
'bg-indigo-100 text-indigo-700 hover:bg-indigo-200 dark:bg-indigo-900/30 dark:text-indigo-400'
|
||||
},
|
||||
{
|
||||
label: 'Opus 4.5',
|
||||
from: 'claude-opus-4-5-20251101',
|
||||
to: 'claude-opus-4-5-20251101',
|
||||
color:
|
||||
'bg-purple-100 text-purple-700 hover:bg-purple-200 dark:bg-purple-900/30 dark:text-purple-400',
|
||||
'bg-purple-100 text-purple-700 hover:bg-purple-200 dark:bg-purple-900/30 dark:text-purple-400'
|
||||
},
|
||||
{
|
||||
label: 'Opus->Sonnet',
|
||||
from: 'claude-opus-4-5-20251101',
|
||||
to: 'claude-sonnet-4-5-20250929',
|
||||
color:
|
||||
'bg-amber-100 text-amber-700 hover:bg-amber-200 dark:bg-amber-900/30 dark:text-amber-400',
|
||||
color: 'bg-amber-100 text-amber-700 hover:bg-amber-200 dark:bg-amber-900/30 dark:text-amber-400'
|
||||
},
|
||||
{
|
||||
label: 'GPT-5.2',
|
||||
from: 'gpt-5.2-2025-12-11',
|
||||
to: 'gpt-5.2-2025-12-11',
|
||||
color:
|
||||
'bg-green-100 text-green-700 hover:bg-green-200 dark:bg-green-900/30 dark:text-green-400',
|
||||
color: 'bg-green-100 text-green-700 hover:bg-green-200 dark:bg-green-900/30 dark:text-green-400'
|
||||
},
|
||||
{
|
||||
label: 'GPT-5.2 Codex',
|
||||
from: 'gpt-5.2-codex',
|
||||
to: 'gpt-5.2-codex',
|
||||
color: 'bg-blue-100 text-blue-700 hover:bg-blue-200 dark:bg-blue-900/30 dark:text-blue-400',
|
||||
color: 'bg-blue-100 text-blue-700 hover:bg-blue-200 dark:bg-blue-900/30 dark:text-blue-400'
|
||||
},
|
||||
{
|
||||
label: 'Max->Codex',
|
||||
from: 'gpt-5.1-codex-max',
|
||||
to: 'gpt-5.1-codex',
|
||||
color: 'bg-pink-100 text-pink-700 hover:bg-pink-200 dark:bg-pink-900/30 dark:text-pink-400',
|
||||
},
|
||||
color: 'bg-pink-100 text-pink-700 hover:bg-pink-200 dark:bg-pink-900/30 dark:text-pink-400'
|
||||
}
|
||||
]
|
||||
|
||||
// Common HTTP error codes
|
||||
@@ -662,12 +660,12 @@ const commonErrorCodes = [
|
||||
{ value: 500, label: 'Server Error' },
|
||||
{ value: 502, label: 'Bad Gateway' },
|
||||
{ value: 503, label: 'Unavailable' },
|
||||
{ value: 529, label: 'Overloaded' },
|
||||
{ value: 529, label: 'Overloaded' }
|
||||
]
|
||||
|
||||
const statusOptions = computed(() => [
|
||||
{ value: 'active', label: t('common.active') },
|
||||
{ value: 'inactive', label: t('common.inactive') },
|
||||
{ value: 'inactive', label: t('common.inactive') }
|
||||
])
|
||||
|
||||
// Model mapping helpers
|
||||
|
||||
@@ -10,10 +10,15 @@
|
||||
<div class="text-sm text-gray-600 dark:text-dark-300">
|
||||
{{ t('admin.accounts.syncFromCrsDesc') }}
|
||||
</div>
|
||||
<div class="text-xs text-gray-500 dark:text-dark-400 bg-gray-50 dark:bg-dark-700/60 rounded-lg p-3">
|
||||
已有账号仅同步 CRS 返回的字段,缺失字段保持原值;凭据按键合并,不会清空未下发的键;未勾选"同步代理"时保留原有代理。
|
||||
<div
|
||||
class="rounded-lg bg-gray-50 p-3 text-xs text-gray-500 dark:bg-dark-700/60 dark:text-dark-400"
|
||||
>
|
||||
已有账号仅同步 CRS
|
||||
返回的字段,缺失字段保持原值;凭据按键合并,不会清空未下发的键;未勾选"同步代理"时保留原有代理。
|
||||
</div>
|
||||
<div class="text-xs text-amber-600 dark:text-amber-400 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg p-3">
|
||||
<div
|
||||
class="rounded-lg border border-amber-200 bg-amber-50 p-3 text-xs text-amber-600 dark:border-amber-800 dark:bg-amber-900/20 dark:text-amber-400"
|
||||
>
|
||||
{{ t('admin.accounts.crsVersionRequirement') }}
|
||||
</div>
|
||||
|
||||
@@ -31,12 +36,7 @@
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<label class="input-label">{{ t('admin.accounts.crsUsername') }}</label>
|
||||
<input
|
||||
v-model="form.username"
|
||||
type="text"
|
||||
class="input"
|
||||
autocomplete="username"
|
||||
/>
|
||||
<input v-model="form.username" type="text" class="input" autocomplete="username" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="input-label">{{ t('admin.accounts.crsPassword') }}</label>
|
||||
@@ -50,12 +50,19 @@
|
||||
</div>
|
||||
|
||||
<label class="flex items-center gap-2 text-sm text-gray-700 dark:text-dark-300">
|
||||
<input v-model="form.sync_proxies" type="checkbox" class="rounded border-gray-300 dark:border-dark-600" />
|
||||
<input
|
||||
v-model="form.sync_proxies"
|
||||
type="checkbox"
|
||||
class="rounded border-gray-300 dark:border-dark-600"
|
||||
/>
|
||||
{{ t('admin.accounts.syncProxies') }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div v-if="result" class="rounded-xl border border-gray-200 dark:border-dark-700 p-4 space-y-2">
|
||||
<div
|
||||
v-if="result"
|
||||
class="space-y-2 rounded-xl border border-gray-200 p-4 dark:border-dark-700"
|
||||
>
|
||||
<div class="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{{ t('admin.accounts.syncResult') }}
|
||||
</div>
|
||||
@@ -67,9 +74,12 @@
|
||||
<div class="text-sm font-medium text-red-600 dark:text-red-400">
|
||||
{{ t('admin.accounts.syncErrors') }}
|
||||
</div>
|
||||
<div class="mt-2 max-h-48 overflow-auto rounded-lg bg-gray-50 dark:bg-dark-800 p-3 text-xs font-mono">
|
||||
<div
|
||||
class="mt-2 max-h-48 overflow-auto rounded-lg bg-gray-50 p-3 font-mono text-xs dark:bg-dark-800"
|
||||
>
|
||||
<div v-for="(item, idx) in errorItems" :key="idx" class="whitespace-pre-wrap">
|
||||
{{ item.kind }} {{ item.crs_account_id }} — {{ item.action }}{{ item.error ? `: ${item.error}` : '' }}
|
||||
{{ item.kind }} {{ item.crs_account_id }} — {{ item.action
|
||||
}}{{ item.error ? `: ${item.error}` : '' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- Window stats row (above progress bar, left-right aligned with progress bar) -->
|
||||
<div v-if="windowStats" class="flex items-center justify-between mb-0.5" :title="`5h 窗口用量统计`">
|
||||
<div class="flex items-center gap-1.5 text-[9px] text-gray-500 dark:text-gray-400 cursor-help">
|
||||
<span class="px-1.5 py-0.5 rounded bg-gray-100 dark:bg-gray-800">
|
||||
<div
|
||||
v-if="windowStats"
|
||||
class="mb-0.5 flex items-center justify-between"
|
||||
:title="`5h 窗口用量统计`"
|
||||
>
|
||||
<div
|
||||
class="flex cursor-help items-center gap-1.5 text-[9px] text-gray-500 dark:text-gray-400"
|
||||
>
|
||||
<span class="rounded bg-gray-100 px-1.5 py-0.5 dark:bg-gray-800">
|
||||
{{ formatRequests }} req
|
||||
</span>
|
||||
<span class="px-1.5 py-0.5 rounded bg-gray-100 dark:bg-gray-800">
|
||||
<span class="rounded bg-gray-100 px-1.5 py-0.5 dark:bg-gray-800">
|
||||
{{ formatTokens }}
|
||||
</span>
|
||||
<span class="px-1.5 py-0.5 rounded bg-gray-100 dark:bg-gray-800">
|
||||
${{ formatCost }}
|
||||
</span>
|
||||
<span class="rounded bg-gray-100 px-1.5 py-0.5 dark:bg-gray-800"> ${{ formatCost }} </span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -19,16 +23,13 @@
|
||||
<div class="flex items-center gap-1">
|
||||
<!-- Label badge (fixed width for alignment) -->
|
||||
<span
|
||||
:class="[
|
||||
'text-[10px] font-medium px-1 rounded w-[32px] text-center shrink-0',
|
||||
labelClass
|
||||
]"
|
||||
:class="['w-[32px] shrink-0 rounded px-1 text-center text-[10px] font-medium', labelClass]"
|
||||
>
|
||||
{{ label }}
|
||||
</span>
|
||||
|
||||
<!-- Progress bar container -->
|
||||
<div class="w-8 h-1.5 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden shrink-0">
|
||||
<div class="h-1.5 w-8 shrink-0 overflow-hidden rounded-full bg-gray-200 dark:bg-gray-700">
|
||||
<div
|
||||
:class="['h-full transition-all duration-300', barClass]"
|
||||
:style="{ width: barWidth }"
|
||||
@@ -36,12 +37,12 @@
|
||||
</div>
|
||||
|
||||
<!-- Percentage -->
|
||||
<span :class="['text-[10px] font-medium w-[32px] text-right shrink-0', textClass]">
|
||||
<span :class="['w-[32px] shrink-0 text-right text-[10px] font-medium', textClass]">
|
||||
{{ displayPercent }}
|
||||
</span>
|
||||
|
||||
<!-- Reset time -->
|
||||
<span v-if="resetsAt" class="text-[10px] text-gray-400 shrink-0">
|
||||
<span v-if="resetsAt" class="shrink-0 text-[10px] text-gray-400">
|
||||
{{ formatResetTime }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -54,7 +55,7 @@ import type { WindowStats } from '@/types'
|
||||
|
||||
const props = defineProps<{
|
||||
label: string
|
||||
utilization: number // Percentage (0-100+)
|
||||
utilization: number // Percentage (0-100+)
|
||||
resetsAt?: string | null
|
||||
color: 'indigo' | 'emerald' | 'purple'
|
||||
windowStats?: WindowStats | null
|
||||
|
||||
Reference in New Issue
Block a user