mirror of
https://gitee.com/wanwujie/sub2api
synced 2026-04-13 11:24:46 +08:00
126 lines
4.1 KiB
Vue
126 lines
4.1 KiB
Vue
<template>
|
|
<div class="card p-4">
|
|
<h3 class="text-sm font-semibold text-gray-900 dark:text-white mb-4">{{ t('admin.dashboard.modelDistribution') }}</h3>
|
|
<div v-if="loading" class="flex items-center justify-center h-48">
|
|
<LoadingSpinner />
|
|
</div>
|
|
<div v-else-if="modelStats.length > 0 && chartData" class="flex items-center gap-6">
|
|
<div class="w-48 h-48">
|
|
<Doughnut :data="chartData" :options="doughnutOptions" />
|
|
</div>
|
|
<div class="flex-1 max-h-48 overflow-y-auto">
|
|
<table class="w-full text-xs">
|
|
<thead>
|
|
<tr class="text-gray-500 dark:text-gray-400">
|
|
<th class="text-left pb-2">{{ t('admin.dashboard.model') }}</th>
|
|
<th class="text-right pb-2">{{ t('admin.dashboard.requests') }}</th>
|
|
<th class="text-right pb-2">{{ t('admin.dashboard.tokens') }}</th>
|
|
<th class="text-right pb-2">{{ t('admin.dashboard.actual') }}</th>
|
|
<th class="text-right pb-2">{{ t('admin.dashboard.standard') }}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="model in modelStats" :key="model.model" class="border-t border-gray-100 dark:border-gray-700">
|
|
<td class="py-1.5 text-gray-900 dark:text-white font-medium truncate max-w-[100px]" :title="model.model">{{ model.model }}</td>
|
|
<td class="py-1.5 text-right text-gray-600 dark:text-gray-400">{{ formatNumber(model.requests) }}</td>
|
|
<td class="py-1.5 text-right text-gray-600 dark:text-gray-400">{{ formatTokens(model.total_tokens) }}</td>
|
|
<td class="py-1.5 text-right text-green-600 dark:text-green-400">${{ formatCost(model.actual_cost) }}</td>
|
|
<td class="py-1.5 text-right text-gray-400 dark:text-gray-500">${{ formatCost(model.cost) }}</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
<div v-else class="flex items-center justify-center h-48 text-gray-500 dark:text-gray-400 text-sm">
|
|
{{ t('admin.dashboard.noDataAvailable') }}
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed } from 'vue'
|
|
import { useI18n } from 'vue-i18n'
|
|
import {
|
|
Chart as ChartJS,
|
|
ArcElement,
|
|
Tooltip,
|
|
Legend
|
|
} from 'chart.js'
|
|
import { Doughnut } from 'vue-chartjs'
|
|
import LoadingSpinner from '@/components/common/LoadingSpinner.vue'
|
|
import type { ModelStat } from '@/types'
|
|
|
|
ChartJS.register(ArcElement, Tooltip, Legend)
|
|
|
|
const { t } = useI18n()
|
|
|
|
const props = defineProps<{
|
|
modelStats: ModelStat[]
|
|
loading?: boolean
|
|
}>()
|
|
|
|
const chartColors = [
|
|
'#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6',
|
|
'#ec4899', '#14b8a6', '#f97316', '#6366f1', '#84cc16'
|
|
]
|
|
|
|
const chartData = computed(() => {
|
|
if (!props.modelStats?.length) return null
|
|
|
|
return {
|
|
labels: props.modelStats.map(m => m.model),
|
|
datasets: [{
|
|
data: props.modelStats.map(m => m.total_tokens),
|
|
backgroundColor: chartColors.slice(0, props.modelStats.length),
|
|
borderWidth: 0,
|
|
}],
|
|
}
|
|
})
|
|
|
|
const doughnutOptions = computed(() => ({
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: {
|
|
display: false,
|
|
},
|
|
tooltip: {
|
|
callbacks: {
|
|
label: (context: any) => {
|
|
const value = context.raw as number
|
|
const total = context.dataset.data.reduce((a: number, b: number) => a + b, 0)
|
|
const percentage = ((value / total) * 100).toFixed(1)
|
|
return `${context.label}: ${formatTokens(value)} (${percentage}%)`
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}))
|
|
|
|
const formatTokens = (value: number): string => {
|
|
if (value >= 1_000_000_000) {
|
|
return `${(value / 1_000_000_000).toFixed(2)}B`
|
|
} else if (value >= 1_000_000) {
|
|
return `${(value / 1_000_000).toFixed(2)}M`
|
|
} else if (value >= 1_000) {
|
|
return `${(value / 1_000).toFixed(2)}K`
|
|
}
|
|
return value.toLocaleString()
|
|
}
|
|
|
|
const formatNumber = (value: number): string => {
|
|
return value.toLocaleString()
|
|
}
|
|
|
|
const formatCost = (value: number): string => {
|
|
if (value >= 1000) {
|
|
return (value / 1000).toFixed(2) + 'K'
|
|
} else if (value >= 1) {
|
|
return value.toFixed(2)
|
|
} else if (value >= 0.01) {
|
|
return value.toFixed(3)
|
|
}
|
|
return value.toFixed(4)
|
|
}
|
|
</script>
|