style(frontend): 统一 Views 模块代码风格

- 移除语句末尾分号,规范代码格式
- 优化组件结构和类型定义
- 改进视图文档和示例
- 提升代码一致性
This commit is contained in:
ianshaw
2025-12-25 08:41:36 -08:00
parent f79b0f0fad
commit 5763f5ced3
25 changed files with 5374 additions and 2439 deletions

View File

@@ -6,15 +6,31 @@
<!-- Total Requests -->
<div class="card p-4">
<div class="flex items-center gap-3">
<div class="p-2 rounded-lg bg-blue-100 dark:bg-blue-900/30">
<svg class="w-5 h-5 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
<div class="rounded-lg bg-blue-100 p-2 dark:bg-blue-900/30">
<svg
class="h-5 w-5 text-blue-600 dark:text-blue-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
/>
</svg>
</div>
<div>
<p class="text-xs font-medium text-gray-500 dark:text-gray-400">{{ t('usage.totalRequests') }}</p>
<p class="text-xl font-bold text-gray-900 dark:text-white">{{ usageStats?.total_requests?.toLocaleString() || '0' }}</p>
<p class="text-xs text-gray-500 dark:text-gray-400">{{ t('usage.inSelectedRange') }}</p>
<p class="text-xs font-medium text-gray-500 dark:text-gray-400">
{{ t('usage.totalRequests') }}
</p>
<p class="text-xl font-bold text-gray-900 dark:text-white">
{{ usageStats?.total_requests?.toLocaleString() || '0' }}
</p>
<p class="text-xs text-gray-500 dark:text-gray-400">
{{ t('usage.inSelectedRange') }}
</p>
</div>
</div>
</div>
@@ -22,15 +38,32 @@
<!-- Total Tokens -->
<div class="card p-4">
<div class="flex items-center gap-3">
<div class="p-2 rounded-lg bg-amber-100 dark:bg-amber-900/30">
<svg class="w-5 h-5 text-amber-600 dark:text-amber-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m21 7.5-9-5.25L3 7.5m18 0-9 5.25m9-5.25v9l-9 5.25M3 7.5l9 5.25M3 7.5v9l9 5.25m0-9v9" />
<div class="rounded-lg bg-amber-100 p-2 dark:bg-amber-900/30">
<svg
class="h-5 w-5 text-amber-600 dark:text-amber-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="m21 7.5-9-5.25L3 7.5m18 0-9 5.25m9-5.25v9l-9 5.25M3 7.5l9 5.25M3 7.5v9l9 5.25m0-9v9"
/>
</svg>
</div>
<div>
<p class="text-xs font-medium text-gray-500 dark:text-gray-400">{{ t('usage.totalTokens') }}</p>
<p class="text-xl font-bold text-gray-900 dark:text-white">{{ formatTokens(usageStats?.total_tokens || 0) }}</p>
<p class="text-xs text-gray-500 dark:text-gray-400">{{ t('usage.in') }}: {{ formatTokens(usageStats?.total_input_tokens || 0) }} / {{ t('usage.out') }}: {{ formatTokens(usageStats?.total_output_tokens || 0) }}</p>
<p class="text-xs font-medium text-gray-500 dark:text-gray-400">
{{ t('usage.totalTokens') }}
</p>
<p class="text-xl font-bold text-gray-900 dark:text-white">
{{ formatTokens(usageStats?.total_tokens || 0) }}
</p>
<p class="text-xs text-gray-500 dark:text-gray-400">
{{ t('usage.in') }}: {{ formatTokens(usageStats?.total_input_tokens || 0) }} /
{{ t('usage.out') }}: {{ formatTokens(usageStats?.total_output_tokens || 0) }}
</p>
</div>
</div>
</div>
@@ -38,16 +71,31 @@
<!-- Total Cost -->
<div class="card p-4">
<div class="flex items-center gap-3">
<div class="p-2 rounded-lg bg-green-100 dark:bg-green-900/30">
<svg class="w-5 h-5 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<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="rounded-lg bg-green-100 p-2 dark:bg-green-900/30">
<svg
class="h-5 w-5 text-green-600 dark:text-green-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<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 class="min-w-0 flex-1">
<p class="text-xs font-medium text-gray-500 dark:text-gray-400">{{ t('usage.totalCost') }}</p>
<p class="text-xl font-bold text-green-600 dark:text-green-400">${{ (usageStats?.total_actual_cost || 0).toFixed(4) }}</p>
<p class="text-xs font-medium text-gray-500 dark:text-gray-400">
{{ t('usage.totalCost') }}
</p>
<p class="text-xl font-bold text-green-600 dark:text-green-400">
${{ (usageStats?.total_actual_cost || 0).toFixed(4) }}
</p>
<p class="text-xs text-gray-500 dark:text-gray-400">
<span class="line-through">${{ (usageStats?.total_cost || 0).toFixed(4) }}</span> {{ t('usage.standardCost') }}
<span class="line-through">${{ (usageStats?.total_cost || 0).toFixed(4) }}</span>
{{ t('usage.standardCost') }}
</p>
</div>
</div>
@@ -56,14 +104,28 @@
<!-- Average Duration -->
<div class="card p-4">
<div class="flex items-center gap-3">
<div class="p-2 rounded-lg bg-purple-100 dark:bg-purple-900/30">
<svg class="w-5 h-5 text-purple-600 dark:text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z" />
<div class="rounded-lg bg-purple-100 p-2 dark:bg-purple-900/30">
<svg
class="h-5 w-5 text-purple-600 dark:text-purple-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</div>
<div>
<p class="text-xs font-medium text-gray-500 dark:text-gray-400">{{ t('usage.avgDuration') }}</p>
<p class="text-xl font-bold text-gray-900 dark:text-white">{{ formatDuration(usageStats?.average_duration_ms || 0) }}</p>
<p class="text-xs font-medium text-gray-500 dark:text-gray-400">
{{ t('usage.avgDuration') }}
</p>
<p class="text-xl font-bold text-gray-900 dark:text-white">
{{ formatDuration(usageStats?.average_duration_ms || 0) }}
</p>
<p class="text-xs text-gray-500 dark:text-gray-400">{{ t('usage.perRequest') }}</p>
</div>
</div>
@@ -75,7 +137,9 @@
<!-- Chart Controls -->
<div class="card p-4">
<div class="flex items-center gap-4">
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">{{ t('admin.dashboard.granularity') }}:</span>
<span class="text-sm font-medium text-gray-700 dark:text-gray-300"
>{{ t('admin.dashboard.granularity') }}:</span
>
<div class="w-28">
<Select
v-model="granularity"
@@ -88,14 +152,8 @@
<!-- Charts Grid -->
<div class="grid grid-cols-1 gap-6 lg:grid-cols-2">
<ModelDistributionChart
:model-stats="modelStats"
:loading="chartsLoading"
/>
<TokenUsageTrend
:trend-data="trendData"
:loading="chartsLoading"
/>
<ModelDistributionChart :model-stats="modelStats" :loading="chartsLoading" />
<TokenUsageTrend :trend-data="trendData" :loading="chartsLoading" />
</div>
</div>
@@ -120,19 +178,30 @@
@click="clearUserFilter"
class="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
<!-- User Dropdown -->
<div
v-if="showUserDropdown && (userSearchResults.length > 0 || userSearchKeyword)"
class="absolute z-50 w-full mt-1 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg max-h-60 overflow-auto"
class="absolute z-50 mt-1 max-h-60 w-full overflow-auto rounded-lg border border-gray-200 bg-white shadow-lg dark:border-gray-700 dark:bg-gray-800"
>
<div v-if="userSearchLoading" class="px-4 py-3 text-sm text-gray-500 dark:text-gray-400">
<div
v-if="userSearchLoading"
class="px-4 py-3 text-sm text-gray-500 dark:text-gray-400"
>
{{ t('common.loading') }}
</div>
<div v-else-if="userSearchResults.length === 0 && userSearchKeyword" class="px-4 py-3 text-sm text-gray-500 dark:text-gray-400">
<div
v-else-if="userSearchResults.length === 0 && userSearchKeyword"
class="px-4 py-3 text-sm text-gray-500 dark:text-gray-400"
>
{{ t('common.noOptionsFound') }}
</div>
<button
@@ -142,7 +211,7 @@
class="w-full px-4 py-2 text-left text-sm hover:bg-gray-100 dark:hover:bg-gray-700"
>
<span class="font-medium text-gray-900 dark:text-white">{{ user.email }}</span>
<span class="text-gray-500 dark:text-gray-400 ml-2">#{{ user.id }}</span>
<span class="ml-2 text-gray-500 dark:text-gray-400">#{{ user.id }}</span>
</button>
</div>
</div>
@@ -171,17 +240,11 @@
</div>
<!-- Actions -->
<div class="flex items-center gap-3 ml-auto">
<button
@click="resetFilters"
class="btn btn-secondary"
>
<div class="ml-auto flex items-center gap-3">
<button @click="resetFilters" class="btn btn-secondary">
{{ t('common.reset') }}
</button>
<button
@click="exportToCSV"
class="btn btn-primary"
>
<button @click="exportToCSV" class="btn btn-primary">
{{ t('usage.exportCsv') }}
</button>
</div>
@@ -191,20 +254,20 @@
<!-- Usage Table -->
<div class="card overflow-hidden">
<DataTable
:columns="columns"
:data="usageLogs"
:loading="loading"
>
<DataTable :columns="columns" :data="usageLogs" :loading="loading">
<template #cell-user="{ row }">
<div class="text-sm">
<span class="font-medium text-gray-900 dark:text-white">{{ row.user?.email || '-' }}</span>
<span class="text-gray-500 dark:text-gray-400 ml-1">#{{ row.user_id }}</span>
<span class="font-medium text-gray-900 dark:text-white">{{
row.user?.email || '-'
}}</span>
<span class="ml-1 text-gray-500 dark:text-gray-400">#{{ row.user_id }}</span>
</div>
</template>
<template #cell-api_key="{ row }">
<span class="text-sm text-gray-900 dark:text-white">{{ row.api_key?.name || '-' }}</span>
<span class="text-sm text-gray-900 dark:text-white">{{
row.api_key?.name || '-'
}}</span>
</template>
<template #cell-model="{ value }">
@@ -213,85 +276,160 @@
<template #cell-stream="{ row }">
<span
class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium"
:class="row.stream
? 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200'
: 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200'"
class="inline-flex items-center rounded px-2 py-0.5 text-xs font-medium"
:class="
row.stream
? 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200'
: 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200'
"
>
{{ row.stream ? t('usage.stream') : t('usage.sync') }}
</span>
</template>
<template #cell-tokens="{ row }">
<div class="text-sm space-y-1.5">
<div class="space-y-1.5 text-sm">
<!-- Input / Output Tokens -->
<div class="flex items-center gap-2">
<!-- Input -->
<div class="inline-flex items-center gap-1">
<svg class="w-3.5 h-3.5 text-emerald-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 14l-7 7m0 0l-7-7m7 7V3" />
<svg
class="h-3.5 w-3.5 text-emerald-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 14l-7 7m0 0l-7-7m7 7V3"
/>
</svg>
<span class="font-medium text-gray-900 dark:text-white">{{ row.input_tokens.toLocaleString() }}</span>
<span class="font-medium text-gray-900 dark:text-white">{{
row.input_tokens.toLocaleString()
}}</span>
</div>
<!-- Output -->
<div class="inline-flex items-center gap-1">
<svg class="w-3.5 h-3.5 text-violet-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 10l7-7m0 0l7 7m-7-7v18" />
<svg
class="h-3.5 w-3.5 text-violet-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 10l7-7m0 0l7 7m-7-7v18"
/>
</svg>
<span class="font-medium text-gray-900 dark:text-white">{{ row.output_tokens.toLocaleString() }}</span>
<span class="font-medium text-gray-900 dark:text-white">{{
row.output_tokens.toLocaleString()
}}</span>
</div>
</div>
<!-- Cache Tokens (Read + Write) -->
<div v-if="row.cache_read_tokens > 0 || row.cache_creation_tokens > 0" class="flex items-center gap-2">
<div
v-if="row.cache_read_tokens > 0 || row.cache_creation_tokens > 0"
class="flex items-center gap-2"
>
<!-- Cache Read -->
<div v-if="row.cache_read_tokens > 0" class="inline-flex items-center gap-1">
<svg class="w-3.5 h-3.5 text-sky-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4" />
<svg
class="h-3.5 w-3.5 text-sky-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4"
/>
</svg>
<span class="text-sky-600 dark:text-sky-400 font-medium">{{ formatCacheTokens(row.cache_read_tokens) }}</span>
<span class="font-medium text-sky-600 dark:text-sky-400">{{
formatCacheTokens(row.cache_read_tokens)
}}</span>
</div>
<!-- Cache Write -->
<div v-if="row.cache_creation_tokens > 0" class="inline-flex items-center gap-1">
<svg class="w-3.5 h-3.5 text-amber-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
<svg
class="h-3.5 w-3.5 text-amber-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
/>
</svg>
<span class="text-amber-600 dark:text-amber-400 font-medium">{{ formatCacheTokens(row.cache_creation_tokens) }}</span>
<span class="font-medium text-amber-600 dark:text-amber-400">{{
formatCacheTokens(row.cache_creation_tokens)
}}</span>
</div>
</div>
</div>
</template>
<template #cell-cost="{ row }">
<div class="text-sm flex items-center gap-1.5">
<div class="flex items-center gap-1.5 text-sm">
<span class="font-medium text-green-600 dark:text-green-400">
${{ row.actual_cost.toFixed(6) }}
</span>
<!-- Cost Detail Tooltip -->
<div class="relative group">
<div class="flex items-center justify-center w-4 h-4 rounded-full bg-gray-100 dark:bg-gray-700 cursor-help transition-colors group-hover:bg-blue-100 dark:group-hover:bg-blue-900/50">
<svg class="w-3 h-3 text-gray-400 dark:text-gray-500 group-hover:text-blue-500 dark:group-hover:text-blue-400" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" />
<div class="group relative">
<div
class="flex h-4 w-4 cursor-help items-center justify-center rounded-full bg-gray-100 transition-colors group-hover:bg-blue-100 dark:bg-gray-700 dark:group-hover:bg-blue-900/50"
>
<svg
class="h-3 w-3 text-gray-400 group-hover:text-blue-500 dark:text-gray-500 dark:group-hover:text-blue-400"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fill-rule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
clip-rule="evenodd"
/>
</svg>
</div>
<!-- Tooltip Content (right side) -->
<div class="absolute z-[100] invisible group-hover:visible opacity-0 group-hover:opacity-100 transition-all duration-200 left-full top-1/2 -translate-y-1/2 ml-2">
<div class="bg-gray-900 dark:bg-gray-800 text-white text-xs rounded-lg py-2.5 px-3 shadow-xl whitespace-nowrap border border-gray-700 dark:border-gray-600">
<div
class="invisible absolute left-full top-1/2 z-[100] ml-2 -translate-y-1/2 opacity-0 transition-all duration-200 group-hover:visible group-hover:opacity-100"
>
<div
class="whitespace-nowrap rounded-lg border border-gray-700 bg-gray-900 px-3 py-2.5 text-xs text-white shadow-xl dark:border-gray-600 dark:bg-gray-800"
>
<div class="space-y-1.5">
<div class="flex items-center justify-between gap-6">
<span class="text-gray-400">{{ t('usage.rate') }}</span>
<span class="font-semibold text-blue-400">{{ (row.rate_multiplier || 1).toFixed(2) }}x</span>
<span class="font-semibold text-blue-400"
>{{ (row.rate_multiplier || 1).toFixed(2) }}x</span
>
</div>
<div class="flex items-center justify-between gap-6">
<span class="text-gray-400">{{ t('usage.original') }}</span>
<span class="font-medium text-white">${{ row.total_cost.toFixed(6) }}</span>
</div>
<div class="flex items-center justify-between gap-6 pt-1.5 border-t border-gray-700">
<div
class="flex items-center justify-between gap-6 border-t border-gray-700 pt-1.5"
>
<span class="text-gray-400">{{ t('usage.billed') }}</span>
<span class="font-semibold text-green-400">${{ row.actual_cost.toFixed(6) }}</span>
<span class="font-semibold text-green-400"
>${{ row.actual_cost.toFixed(6) }}</span
>
</div>
</div>
<!-- Tooltip Arrow (left side) -->
<div class="absolute right-full top-1/2 -translate-y-1/2 w-0 h-0 border-t-[6px] border-t-transparent border-b-[6px] border-b-transparent border-r-[6px] border-r-gray-900 dark:border-r-gray-800"></div>
<div
class="absolute right-full top-1/2 h-0 w-0 -translate-y-1/2 border-b-[6px] border-r-[6px] border-t-[6px] border-b-transparent border-r-gray-900 border-t-transparent dark:border-r-gray-800"
></div>
</div>
</div>
</div>
@@ -300,28 +438,37 @@
<template #cell-billing_type="{ row }">
<span
class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium"
:class="row.billing_type === 1
? 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200'
: 'bg-emerald-100 text-emerald-800 dark:bg-emerald-900 dark:text-emerald-200'"
class="inline-flex items-center rounded px-2 py-0.5 text-xs font-medium"
:class="
row.billing_type === 1
? 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200'
: 'bg-emerald-100 text-emerald-800 dark:bg-emerald-900 dark:text-emerald-200'
"
>
{{ row.billing_type === 1 ? t('usage.subscription') : t('usage.balance') }}
</span>
</template>
<template #cell-first_token="{ row }">
<span v-if="row.first_token_ms != null" class="text-sm text-gray-600 dark:text-gray-400">
<span
v-if="row.first_token_ms != null"
class="text-sm text-gray-600 dark:text-gray-400"
>
{{ formatDuration(row.first_token_ms) }}
</span>
<span v-else class="text-sm text-gray-400 dark:text-gray-500">-</span>
</template>
<template #cell-duration="{ row }">
<span class="text-sm text-gray-600 dark:text-gray-400">{{ formatDuration(row.duration_ms) }}</span>
<span class="text-sm text-gray-600 dark:text-gray-400">{{
formatDuration(row.duration_ms)
}}</span>
</template>
<template #cell-created_at="{ value }">
<span class="text-sm text-gray-600 dark:text-gray-400">{{ formatDateTime(value) }}</span>
<span class="text-sm text-gray-600 dark:text-gray-400">{{
formatDateTime(value)
}}</span>
</template>
<template #empty>
@@ -357,7 +504,12 @@ import ModelDistributionChart from '@/components/charts/ModelDistributionChart.v
import TokenUsageTrend from '@/components/charts/TokenUsageTrend.vue'
import type { UsageLog, TrendDataPoint, ModelStat } from '@/types'
import type { Column } from '@/components/common/types'
import type { SimpleUser, SimpleApiKey, AdminUsageStatsResponse, AdminUsageQueryParams } from '@/api/admin/usage'
import type {
SimpleUser,
SimpleApiKey,
AdminUsageStatsResponse,
AdminUsageQueryParams
} from '@/api/admin/usage'
const { t } = useI18n()
const appStore = useAppStore()
@@ -374,7 +526,7 @@ const granularity = ref<'day' | 'hour'>('day')
// Granularity options for Select component
const granularityOptions = computed(() => [
{ value: 'day', label: t('admin.dashboard.day') },
{ value: 'hour', label: t('admin.dashboard.hour') },
{ value: 'hour', label: t('admin.dashboard.hour') }
])
const columns = computed<Column[]>(() => [
@@ -405,7 +557,7 @@ let searchTimeout: ReturnType<typeof setTimeout> | null = null
const apiKeyOptions = computed(() => {
return [
{ value: null, label: t('usage.allApiKeys') },
...apiKeys.value.map(key => ({
...apiKeys.value.map((key) => ({
value: key.id,
label: key.name
}))
@@ -494,7 +646,11 @@ const loadApiKeysForUser = async (userId: number) => {
}
// Handle date range change from DateRangePicker
const onDateRangeChange = (range: { startDate: string; endDate: string; preset: string | null }) => {
const onDateRangeChange = (range: {
startDate: string
endDate: string
preset: string | null
}) => {
filters.value.start_date = range.startDate
filters.value.end_date = range.endDate
applyFilters()
@@ -586,7 +742,7 @@ const loadChartData = async () => {
end_date: filters.value.end_date || endDate.value,
granularity: granularity.value,
user_id: filters.value.user_id,
api_key_id: filters.value.api_key_id ? Number(filters.value.api_key_id) : undefined,
api_key_id: filters.value.api_key_id ? Number(filters.value.api_key_id) : undefined
}
const [trendResponse, modelResponse] = await Promise.all([
@@ -595,8 +751,8 @@ const loadChartData = async () => {
start_date: params.start_date,
end_date: params.end_date,
user_id: params.user_id,
api_key_id: params.api_key_id,
}),
api_key_id: params.api_key_id
})
])
trendData.value = trendResponse.trend || []
@@ -650,8 +806,21 @@ const exportToCSV = () => {
return
}
const headers = ['User', 'API Key', 'Model', 'Type', 'Input Tokens', 'Output Tokens', 'Cache Read Tokens', 'Cache Write Tokens', 'Total Cost', 'Billing Type', 'Duration (ms)', 'Time']
const rows = usageLogs.value.map(log => [
const headers = [
'User',
'API Key',
'Model',
'Type',
'Input Tokens',
'Output Tokens',
'Cache Read Tokens',
'Cache Write Tokens',
'Total Cost',
'Billing Type',
'Duration (ms)',
'Time'
]
const rows = usageLogs.value.map((log) => [
log.user?.email || '',
log.api_key?.name || '',
log.model,
@@ -666,10 +835,7 @@ const exportToCSV = () => {
log.created_at
])
const csvContent = [
headers.join(','),
...rows.map(row => row.join(','))
].join('\n')
const csvContent = [headers.join(','), ...rows.map((row) => row.join(','))].join('\n')
const blob = new Blob([csvContent], { type: 'text/csv' })
const url = window.URL.createObjectURL(blob)