mirror of
https://gitee.com/wanwujie/sub2api
synced 2026-04-20 06:44:44 +08:00
feat(dashboard): add per-user drill-down for group, model, and endpoint distributions
Click on a group name, model name, or endpoint name in the distribution tables to expand and show per-user usage breakdown (requests, tokens, actual cost, standard cost). Backend: new GET /admin/dashboard/user-breakdown API with group_id, model, endpoint, endpoint_type filters. Frontend: clickable rows with expand/collapse sub-table in all three distribution charts.
This commit is contained in:
@@ -87,27 +87,40 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="item in displayEndpointStats"
|
||||
:key="item.endpoint"
|
||||
class="border-t border-gray-100 dark:border-gray-700"
|
||||
>
|
||||
<td class="max-w-[180px] truncate py-1.5 font-medium text-gray-900 dark:text-white" :title="item.endpoint">
|
||||
{{ item.endpoint }}
|
||||
</td>
|
||||
<td class="py-1.5 text-right text-gray-600 dark:text-gray-400">
|
||||
{{ formatNumber(item.requests) }}
|
||||
</td>
|
||||
<td class="py-1.5 text-right text-gray-600 dark:text-gray-400">
|
||||
{{ formatTokens(item.total_tokens) }}
|
||||
</td>
|
||||
<td class="py-1.5 text-right text-green-600 dark:text-green-400">
|
||||
${{ formatCost(item.actual_cost) }}
|
||||
</td>
|
||||
<td class="py-1.5 text-right text-gray-400 dark:text-gray-500">
|
||||
${{ formatCost(item.cost) }}
|
||||
</td>
|
||||
</tr>
|
||||
<template v-for="item in displayEndpointStats" :key="item.endpoint">
|
||||
<tr
|
||||
class="border-t border-gray-100 cursor-pointer transition-colors hover:bg-gray-50 dark:border-gray-700 dark:hover:bg-dark-700/40"
|
||||
@click="toggleBreakdown(item.endpoint)"
|
||||
>
|
||||
<td class="max-w-[180px] truncate py-1.5 font-medium text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300" :title="item.endpoint">
|
||||
<span class="inline-flex items-center gap-1">
|
||||
<svg v-if="expandedKey === item.endpoint" class="h-3 w-3 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
|
||||
<svg v-else class="h-3 w-3 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/></svg>
|
||||
{{ item.endpoint }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="py-1.5 text-right text-gray-600 dark:text-gray-400">
|
||||
{{ formatNumber(item.requests) }}
|
||||
</td>
|
||||
<td class="py-1.5 text-right text-gray-600 dark:text-gray-400">
|
||||
{{ formatTokens(item.total_tokens) }}
|
||||
</td>
|
||||
<td class="py-1.5 text-right text-green-600 dark:text-green-400">
|
||||
${{ formatCost(item.actual_cost) }}
|
||||
</td>
|
||||
<td class="py-1.5 text-right text-gray-400 dark:text-gray-500">
|
||||
${{ formatCost(item.cost) }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="expandedKey === item.endpoint">
|
||||
<td colspan="5" class="p-0">
|
||||
<UserBreakdownSubTable
|
||||
:items="breakdownItems"
|
||||
:loading="breakdownLoading"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -119,12 +132,14 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { computed, ref } 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 { EndpointStat } from '@/types'
|
||||
import UserBreakdownSubTable from './UserBreakdownSubTable.vue'
|
||||
import type { EndpointStat, UserBreakdownItem } from '@/types'
|
||||
import { getUserBreakdown } from '@/api/admin/dashboard'
|
||||
|
||||
ChartJS.register(ArcElement, Tooltip, Legend)
|
||||
|
||||
@@ -144,6 +159,8 @@ const props = withDefaults(
|
||||
source?: EndpointSource
|
||||
showMetricToggle?: boolean
|
||||
showSourceToggle?: boolean
|
||||
startDate?: string
|
||||
endDate?: string
|
||||
}>(),
|
||||
{
|
||||
upstreamEndpointStats: () => [],
|
||||
@@ -162,6 +179,33 @@ const emit = defineEmits<{
|
||||
'update:source': [value: EndpointSource]
|
||||
}>()
|
||||
|
||||
const expandedKey = ref<string | null>(null)
|
||||
const breakdownItems = ref<UserBreakdownItem[]>([])
|
||||
const breakdownLoading = ref(false)
|
||||
|
||||
const toggleBreakdown = async (endpoint: string) => {
|
||||
if (expandedKey.value === endpoint) {
|
||||
expandedKey.value = null
|
||||
return
|
||||
}
|
||||
expandedKey.value = endpoint
|
||||
breakdownLoading.value = true
|
||||
breakdownItems.value = []
|
||||
try {
|
||||
const res = await getUserBreakdown({
|
||||
start_date: props.startDate,
|
||||
end_date: props.endDate,
|
||||
endpoint,
|
||||
endpoint_type: props.source,
|
||||
})
|
||||
breakdownItems.value = res.users || []
|
||||
} catch {
|
||||
breakdownItems.value = []
|
||||
} finally {
|
||||
breakdownLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const chartColors = [
|
||||
'#3b82f6',
|
||||
'#10b981',
|
||||
|
||||
Reference in New Issue
Block a user