2026-02-04 02:04:54 +08:00
|
|
|
<template>
|
|
|
|
|
<div class="space-y-6 px-4 sm:px-6 lg:px-0">
|
|
|
|
|
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
|
|
|
|
|
<div>
|
2026-02-04 23:59:45 +08:00
|
|
|
<h1 class="text-lg font-semibold">
|
|
|
|
|
成本分析
|
|
|
|
|
</h1>
|
|
|
|
|
<p class="text-xs text-muted-foreground">
|
|
|
|
|
成本趋势、预测与节省统计
|
|
|
|
|
</p>
|
2026-02-04 02:04:54 +08:00
|
|
|
</div>
|
|
|
|
|
<TimeRangePicker v-model="timeRange" />
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4">
|
|
|
|
|
<Card class="p-4 space-y-2">
|
2026-02-04 23:59:45 +08:00
|
|
|
<div class="text-xs text-muted-foreground">
|
|
|
|
|
缓存节省
|
|
|
|
|
</div>
|
|
|
|
|
<div class="text-lg font-semibold">
|
|
|
|
|
{{ formatCurrency(costSavings?.cache_savings ?? 0) }}
|
|
|
|
|
</div>
|
2026-02-04 02:04:54 +08:00
|
|
|
<div class="text-xs text-muted-foreground">
|
|
|
|
|
读取成本 {{ formatCurrency(costSavings?.cache_read_cost ?? 0) }}
|
|
|
|
|
</div>
|
|
|
|
|
</Card>
|
|
|
|
|
<Card class="p-4 space-y-2">
|
2026-02-04 23:59:45 +08:00
|
|
|
<div class="text-xs text-muted-foreground">
|
|
|
|
|
缓存读取 Tokens
|
|
|
|
|
</div>
|
|
|
|
|
<div class="text-lg font-semibold">
|
|
|
|
|
{{ formatTokens(costSavings?.cache_read_tokens ?? 0) }}
|
|
|
|
|
</div>
|
2026-02-04 02:04:54 +08:00
|
|
|
<div class="text-xs text-muted-foreground">
|
|
|
|
|
预计全额成本 {{ formatCurrency(costSavings?.estimated_full_cost ?? 0) }}
|
|
|
|
|
</div>
|
|
|
|
|
</Card>
|
|
|
|
|
<Card class="p-4 space-y-2">
|
2026-02-04 23:59:45 +08:00
|
|
|
<div class="text-xs text-muted-foreground">
|
|
|
|
|
缓存创建成本
|
|
|
|
|
</div>
|
|
|
|
|
<div class="text-lg font-semibold">
|
|
|
|
|
{{ formatCurrency(costSavings?.cache_creation_cost ?? 0) }}
|
|
|
|
|
</div>
|
|
|
|
|
<div class="text-xs text-muted-foreground">
|
|
|
|
|
基于当前时间范围
|
|
|
|
|
</div>
|
2026-02-04 02:04:54 +08:00
|
|
|
</Card>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
|
|
|
|
<Card class="p-4">
|
|
|
|
|
<CostForecastChart
|
|
|
|
|
title="成本趋势预测"
|
|
|
|
|
:history="forecastHistory"
|
|
|
|
|
:forecast="forecastFuture"
|
|
|
|
|
:loading="forecastLoading"
|
|
|
|
|
/>
|
|
|
|
|
</Card>
|
|
|
|
|
<QuotaProgressCard
|
|
|
|
|
title="月卡消耗进度"
|
|
|
|
|
:providers="quotaProviders"
|
|
|
|
|
:loading="quotaLoading"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<UsageProviderTable
|
|
|
|
|
:data="providerStats"
|
|
|
|
|
:is-admin="true"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
2026-03-03 22:04:40 +08:00
|
|
|
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
2026-02-04 02:04:54 +08:00
|
|
|
import Card from '@/components/ui/card.vue'
|
|
|
|
|
import { TimeRangePicker } from '@/components/common'
|
|
|
|
|
import { CostForecastChart, QuotaProgressCard } from '@/components/stats'
|
|
|
|
|
import { UsageProviderTable } from '@/features/usage/components'
|
|
|
|
|
import { adminApi, type CostForecastResponse, type CostSavingsResponse, type QuotaUsageProvider } from '@/api/admin'
|
|
|
|
|
import { usageApi } from '@/api/usage'
|
|
|
|
|
import { formatCurrency, formatTokens } from '@/utils/format'
|
|
|
|
|
import { getDateRangeFromPeriod } from '@/features/usage/composables'
|
|
|
|
|
import type { DateRangeParams } from '@/features/usage/types'
|
|
|
|
|
import type { ProviderStatsItem } from '@/features/usage/types'
|
|
|
|
|
|
|
|
|
|
const timeRange = ref<DateRangeParams>(getDateRangeFromPeriod('last30days'))
|
|
|
|
|
|
|
|
|
|
const forecast = ref<CostForecastResponse | null>(null)
|
|
|
|
|
const costSavings = ref<CostSavingsResponse | null>(null)
|
|
|
|
|
const quotaProviders = ref<QuotaUsageProvider[]>([])
|
|
|
|
|
const providerStats = ref<ProviderStatsItem[]>([])
|
|
|
|
|
|
|
|
|
|
const forecastLoading = ref(false)
|
|
|
|
|
const quotaLoading = ref(false)
|
2026-03-03 22:04:40 +08:00
|
|
|
let forecastRequestId = 0
|
|
|
|
|
let savingsRequestId = 0
|
|
|
|
|
let quotaRequestId = 0
|
|
|
|
|
let providerStatsRequestId = 0
|
|
|
|
|
let loadAllPromise: Promise<void> | null = null
|
|
|
|
|
let hasPendingLoadAll = false
|
|
|
|
|
let loadAllDebounceTimer: ReturnType<typeof setTimeout> | null = null
|
2026-02-04 02:04:54 +08:00
|
|
|
|
|
|
|
|
const forecastHistory = computed(() => forecast.value?.history || [])
|
|
|
|
|
const forecastFuture = computed(() => forecast.value?.forecast || [])
|
|
|
|
|
|
|
|
|
|
function buildTimeRangeParams() {
|
|
|
|
|
return {
|
|
|
|
|
start_date: timeRange.value.start_date,
|
|
|
|
|
end_date: timeRange.value.end_date,
|
|
|
|
|
preset: timeRange.value.preset,
|
|
|
|
|
timezone: timeRange.value.timezone,
|
|
|
|
|
tz_offset_minutes: timeRange.value.tz_offset_minutes
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function loadForecast() {
|
2026-03-03 22:04:40 +08:00
|
|
|
const requestId = ++forecastRequestId
|
2026-02-04 02:04:54 +08:00
|
|
|
forecastLoading.value = true
|
|
|
|
|
try {
|
2026-03-03 22:04:40 +08:00
|
|
|
const data = await adminApi.getCostForecast(buildTimeRangeParams())
|
|
|
|
|
if (requestId !== forecastRequestId) return
|
|
|
|
|
forecast.value = data
|
2026-02-04 02:04:54 +08:00
|
|
|
} finally {
|
2026-03-03 22:04:40 +08:00
|
|
|
if (requestId === forecastRequestId) {
|
|
|
|
|
forecastLoading.value = false
|
|
|
|
|
}
|
2026-02-04 02:04:54 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function loadSavings() {
|
2026-03-03 22:04:40 +08:00
|
|
|
const requestId = ++savingsRequestId
|
|
|
|
|
const data = await adminApi.getCostSavings(buildTimeRangeParams())
|
|
|
|
|
if (requestId !== savingsRequestId) return
|
|
|
|
|
costSavings.value = data
|
2026-02-04 02:04:54 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function loadQuotaUsage() {
|
2026-03-03 22:04:40 +08:00
|
|
|
const requestId = ++quotaRequestId
|
2026-02-04 02:04:54 +08:00
|
|
|
quotaLoading.value = true
|
|
|
|
|
try {
|
|
|
|
|
const response = await adminApi.getQuotaUsage()
|
2026-03-03 22:04:40 +08:00
|
|
|
if (requestId !== quotaRequestId) return
|
2026-02-04 02:04:54 +08:00
|
|
|
quotaProviders.value = response.providers
|
|
|
|
|
} finally {
|
2026-03-03 22:04:40 +08:00
|
|
|
if (requestId === quotaRequestId) {
|
|
|
|
|
quotaLoading.value = false
|
|
|
|
|
}
|
2026-02-04 02:04:54 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function loadProviderStats() {
|
2026-03-03 22:04:40 +08:00
|
|
|
const requestId = ++providerStatsRequestId
|
|
|
|
|
const stats = await usageApi.getUsageByProvider({
|
2026-02-04 02:04:54 +08:00
|
|
|
...buildTimeRangeParams(),
|
|
|
|
|
limit: 8
|
|
|
|
|
})
|
2026-03-03 22:04:40 +08:00
|
|
|
if (requestId !== providerStatsRequestId) return
|
|
|
|
|
providerStats.value = stats
|
2026-02-04 02:04:54 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function loadAll() {
|
2026-03-03 22:04:40 +08:00
|
|
|
if (loadAllPromise) {
|
|
|
|
|
hasPendingLoadAll = true
|
|
|
|
|
return loadAllPromise
|
|
|
|
|
}
|
|
|
|
|
loadAllPromise = Promise.all([loadForecast(), loadSavings(), loadQuotaUsage(), loadProviderStats()])
|
|
|
|
|
.then(() => undefined)
|
|
|
|
|
.finally(() => {
|
|
|
|
|
loadAllPromise = null
|
|
|
|
|
if (hasPendingLoadAll) {
|
|
|
|
|
hasPendingLoadAll = false
|
|
|
|
|
void loadAll()
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
return loadAllPromise
|
2026-02-04 02:04:54 +08:00
|
|
|
}
|
|
|
|
|
|
2026-03-03 22:04:40 +08:00
|
|
|
function scheduleLoadAll() {
|
|
|
|
|
if (loadAllDebounceTimer) {
|
|
|
|
|
clearTimeout(loadAllDebounceTimer)
|
|
|
|
|
}
|
|
|
|
|
loadAllDebounceTimer = setTimeout(() => {
|
|
|
|
|
loadAllDebounceTimer = null
|
|
|
|
|
void loadAll()
|
|
|
|
|
}, 120)
|
|
|
|
|
}
|
2026-02-04 02:04:54 +08:00
|
|
|
|
2026-03-03 22:04:40 +08:00
|
|
|
watch(timeRange, scheduleLoadAll, { deep: true })
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
void loadAll()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
|
|
if (loadAllDebounceTimer) {
|
|
|
|
|
clearTimeout(loadAllDebounceTimer)
|
|
|
|
|
loadAllDebounceTimer = null
|
|
|
|
|
}
|
|
|
|
|
hasPendingLoadAll = false
|
|
|
|
|
loadAllPromise = null
|
|
|
|
|
forecastRequestId += 1
|
|
|
|
|
savingsRequestId += 1
|
|
|
|
|
quotaRequestId += 1
|
|
|
|
|
providerStatsRequestId += 1
|
|
|
|
|
})
|
2026-02-04 02:04:54 +08:00
|
|
|
</script>
|