fix(admin/usage): 恢复成本 Tooltip 明细并优化账号筛选
问题修复: - 恢复 Cost Tooltip 的成本分项明细 (input_cost, output_cost, cache 成本) - 修复 Token Tooltip 双分隔线显示问题 - 修复 Tooltip 翻译键缺失问题,新增 costDetails/tokenDetails - 恢复 Excel 导出格式化 (aoa_to_sheet + 翻译列头) 功能优化: - 账号筛选从前端搜索改为后端搜索,避免一次加载 1000 条数据 - 行为与用户/API Key 筛选保持一致 (debounce + 后端分页)
This commit is contained in:
@@ -85,9 +85,40 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Account Filter -->
|
<!-- Account Filter -->
|
||||||
<div class="w-full sm:w-auto sm:min-w-[220px]">
|
<div ref="accountSearchRef" class="usage-filter-dropdown relative w-full sm:w-auto sm:min-w-[220px]">
|
||||||
<label class="input-label">{{ t('admin.usage.account') }}</label>
|
<label class="input-label">{{ t('admin.usage.account') }}</label>
|
||||||
<Select v-model="filters.account_id" :options="accountOptions" searchable @change="emitChange" />
|
<input
|
||||||
|
v-model="accountKeyword"
|
||||||
|
type="text"
|
||||||
|
class="input pr-8"
|
||||||
|
:placeholder="t('admin.usage.searchAccountPlaceholder')"
|
||||||
|
@input="debounceAccountSearch"
|
||||||
|
@focus="showAccountDropdown = true"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
v-if="filters.account_id"
|
||||||
|
type="button"
|
||||||
|
@click="clearAccount"
|
||||||
|
class="absolute right-2 top-9 text-gray-400"
|
||||||
|
aria-label="Clear account filter"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
v-if="showAccountDropdown && (accountResults.length > 0 || accountKeyword)"
|
||||||
|
class="absolute z-50 mt-1 max-h-60 w-full overflow-auto rounded-lg border bg-white shadow-lg dark:bg-gray-800"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-for="a in accountResults"
|
||||||
|
:key="a.id"
|
||||||
|
type="button"
|
||||||
|
@click="selectAccount(a)"
|
||||||
|
class="w-full px-4 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||||
|
>
|
||||||
|
<span class="truncate">{{ a.name }}</span>
|
||||||
|
<span class="ml-2 text-xs text-gray-400">#{{ a.id }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Stream Type Filter -->
|
<!-- Stream Type Filter -->
|
||||||
@@ -166,6 +197,7 @@ const filters = toRef(props, 'modelValue')
|
|||||||
|
|
||||||
const userSearchRef = ref<HTMLElement | null>(null)
|
const userSearchRef = ref<HTMLElement | null>(null)
|
||||||
const apiKeySearchRef = ref<HTMLElement | null>(null)
|
const apiKeySearchRef = ref<HTMLElement | null>(null)
|
||||||
|
const accountSearchRef = ref<HTMLElement | null>(null)
|
||||||
|
|
||||||
const userKeyword = ref('')
|
const userKeyword = ref('')
|
||||||
const userResults = ref<SimpleUser[]>([])
|
const userResults = ref<SimpleUser[]>([])
|
||||||
@@ -177,9 +209,17 @@ const apiKeyResults = ref<SimpleApiKey[]>([])
|
|||||||
const showApiKeyDropdown = ref(false)
|
const showApiKeyDropdown = ref(false)
|
||||||
let apiKeySearchTimeout: ReturnType<typeof setTimeout> | null = null
|
let apiKeySearchTimeout: ReturnType<typeof setTimeout> | null = null
|
||||||
|
|
||||||
|
interface SimpleAccount {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
const accountKeyword = ref('')
|
||||||
|
const accountResults = ref<SimpleAccount[]>([])
|
||||||
|
const showAccountDropdown = ref(false)
|
||||||
|
let accountSearchTimeout: ReturnType<typeof setTimeout> | null = null
|
||||||
|
|
||||||
const modelOptions = ref<SelectOption[]>([{ value: null, label: t('admin.usage.allModels') }])
|
const modelOptions = ref<SelectOption[]>([{ value: null, label: t('admin.usage.allModels') }])
|
||||||
const groupOptions = ref<SelectOption[]>([{ value: null, label: t('admin.usage.allGroups') }])
|
const groupOptions = ref<SelectOption[]>([{ value: null, label: t('admin.usage.allGroups') }])
|
||||||
const accountOptions = ref<SelectOption[]>([{ value: null, label: t('admin.usage.allAccounts') }])
|
|
||||||
|
|
||||||
const streamTypeOptions = ref<SelectOption[]>([
|
const streamTypeOptions = ref<SelectOption[]>([
|
||||||
{ value: null, label: t('admin.usage.allTypes') },
|
{ value: null, label: t('admin.usage.allTypes') },
|
||||||
@@ -278,6 +318,37 @@ const onClearApiKey = () => {
|
|||||||
emitChange()
|
emitChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const debounceAccountSearch = () => {
|
||||||
|
if (accountSearchTimeout) clearTimeout(accountSearchTimeout)
|
||||||
|
accountSearchTimeout = setTimeout(async () => {
|
||||||
|
if (!accountKeyword.value) {
|
||||||
|
accountResults.value = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await adminAPI.accounts.list(1, 20, { search: accountKeyword.value })
|
||||||
|
accountResults.value = res.items.map((a) => ({ id: a.id, name: a.name }))
|
||||||
|
} catch {
|
||||||
|
accountResults.value = []
|
||||||
|
}
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectAccount = (a: SimpleAccount) => {
|
||||||
|
accountKeyword.value = a.name
|
||||||
|
showAccountDropdown.value = false
|
||||||
|
filters.value.account_id = a.id
|
||||||
|
emitChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearAccount = () => {
|
||||||
|
accountKeyword.value = ''
|
||||||
|
accountResults.value = []
|
||||||
|
showAccountDropdown.value = false
|
||||||
|
filters.value.account_id = undefined
|
||||||
|
emitChange()
|
||||||
|
}
|
||||||
|
|
||||||
const onApiKeyFocus = () => {
|
const onApiKeyFocus = () => {
|
||||||
showApiKeyDropdown.value = true
|
showApiKeyDropdown.value = true
|
||||||
// Trigger search if no results yet
|
// Trigger search if no results yet
|
||||||
@@ -292,9 +363,11 @@ const onDocumentClick = (e: MouseEvent) => {
|
|||||||
|
|
||||||
const clickedInsideUser = userSearchRef.value?.contains(target) ?? false
|
const clickedInsideUser = userSearchRef.value?.contains(target) ?? false
|
||||||
const clickedInsideApiKey = apiKeySearchRef.value?.contains(target) ?? false
|
const clickedInsideApiKey = apiKeySearchRef.value?.contains(target) ?? false
|
||||||
|
const clickedInsideAccount = accountSearchRef.value?.contains(target) ?? false
|
||||||
|
|
||||||
if (!clickedInsideUser) showUserDropdown.value = false
|
if (!clickedInsideUser) showUserDropdown.value = false
|
||||||
if (!clickedInsideApiKey) showApiKeyDropdown.value = false
|
if (!clickedInsideApiKey) showApiKeyDropdown.value = false
|
||||||
|
if (!clickedInsideAccount) showAccountDropdown.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
@@ -333,20 +406,27 @@ watch(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => filters.value.account_id,
|
||||||
|
(accountId) => {
|
||||||
|
if (!accountId) {
|
||||||
|
accountKeyword.value = ''
|
||||||
|
accountResults.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
document.addEventListener('click', onDocumentClick)
|
document.addEventListener('click', onDocumentClick)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [gs, ms, as] = await Promise.all([
|
const [gs, ms] = await Promise.all([
|
||||||
adminAPI.groups.list(1, 1000),
|
adminAPI.groups.list(1, 1000),
|
||||||
adminAPI.dashboard.getModelStats({ start_date: props.startDate, end_date: props.endDate }),
|
adminAPI.dashboard.getModelStats({ start_date: props.startDate, end_date: props.endDate })
|
||||||
adminAPI.accounts.list(1, 1000)
|
|
||||||
])
|
])
|
||||||
|
|
||||||
groupOptions.value.push(...gs.items.map((g: any) => ({ value: g.id, label: g.name })))
|
groupOptions.value.push(...gs.items.map((g: any) => ({ value: g.id, label: g.name })))
|
||||||
|
|
||||||
accountOptions.value.push(...as.items.map((a: any) => ({ value: a.id, label: a.name })))
|
|
||||||
|
|
||||||
const uniqueModels = new Set<string>()
|
const uniqueModels = new Set<string>()
|
||||||
ms.models?.forEach((s: any) => s.model && uniqueModels.add(s.model))
|
ms.models?.forEach((s: any) => s.model && uniqueModels.add(s.model))
|
||||||
modelOptions.value.push(
|
modelOptions.value.push(
|
||||||
|
|||||||
@@ -143,8 +143,8 @@
|
|||||||
>
|
>
|
||||||
<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="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="space-y-1.5">
|
||||||
<div class="mb-2 border-b border-gray-700 pb-1.5">
|
<div>
|
||||||
<div class="text-xs font-semibold text-gray-300 mb-1">Token {{ t('usage.details') }}</div>
|
<div class="text-xs font-semibold text-gray-300 mb-1">{{ t('usage.tokenDetails') }}</div>
|
||||||
<div v-if="tokenTooltipData && tokenTooltipData.input_tokens > 0" class="flex items-center justify-between gap-4">
|
<div v-if="tokenTooltipData && tokenTooltipData.input_tokens > 0" class="flex items-center justify-between gap-4">
|
||||||
<span class="text-gray-400">{{ t('admin.usage.inputTokens') }}</span>
|
<span class="text-gray-400">{{ t('admin.usage.inputTokens') }}</span>
|
||||||
<span class="font-medium text-white">{{ tokenTooltipData.input_tokens.toLocaleString() }}</span>
|
<span class="font-medium text-white">{{ tokenTooltipData.input_tokens.toLocaleString() }}</span>
|
||||||
@@ -184,6 +184,27 @@
|
|||||||
>
|
>
|
||||||
<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="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="space-y-1.5">
|
||||||
|
<!-- Cost Breakdown -->
|
||||||
|
<div class="mb-2 border-b border-gray-700 pb-1.5">
|
||||||
|
<div class="text-xs font-semibold text-gray-300 mb-1">{{ t('usage.costDetails') }}</div>
|
||||||
|
<div v-if="tooltipData && tooltipData.input_cost > 0" class="flex items-center justify-between gap-4">
|
||||||
|
<span class="text-gray-400">{{ t('admin.usage.inputCost') }}</span>
|
||||||
|
<span class="font-medium text-white">${{ tooltipData.input_cost.toFixed(6) }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="tooltipData && tooltipData.output_cost > 0" class="flex items-center justify-between gap-4">
|
||||||
|
<span class="text-gray-400">{{ t('admin.usage.outputCost') }}</span>
|
||||||
|
<span class="font-medium text-white">${{ tooltipData.output_cost.toFixed(6) }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="tooltipData && tooltipData.cache_creation_cost > 0" class="flex items-center justify-between gap-4">
|
||||||
|
<span class="text-gray-400">{{ t('admin.usage.cacheCreationCost') }}</span>
|
||||||
|
<span class="font-medium text-white">${{ tooltipData.cache_creation_cost.toFixed(6) }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="tooltipData && tooltipData.cache_read_cost > 0" class="flex items-center justify-between gap-4">
|
||||||
|
<span class="text-gray-400">{{ t('admin.usage.cacheReadCost') }}</span>
|
||||||
|
<span class="font-medium text-white">${{ tooltipData.cache_read_cost.toFixed(6) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Rate and Summary -->
|
||||||
<div class="flex items-center justify-between gap-6">
|
<div class="flex items-center justify-between gap-6">
|
||||||
<span class="text-gray-400">{{ t('usage.rate') }}</span>
|
<span class="text-gray-400">{{ t('usage.rate') }}</span>
|
||||||
<span class="font-semibold text-blue-400">{{ (tooltipData?.rate_multiplier || 1).toFixed(2) }}x</span>
|
<span class="font-semibold text-blue-400">{{ (tooltipData?.rate_multiplier || 1).toFixed(2) }}x</span>
|
||||||
|
|||||||
@@ -376,6 +376,8 @@ export default {
|
|||||||
usage: {
|
usage: {
|
||||||
title: 'Usage Records',
|
title: 'Usage Records',
|
||||||
description: 'View and analyze your API usage history',
|
description: 'View and analyze your API usage history',
|
||||||
|
costDetails: 'Cost Breakdown',
|
||||||
|
tokenDetails: 'Token Breakdown',
|
||||||
totalRequests: 'Total Requests',
|
totalRequests: 'Total Requests',
|
||||||
totalTokens: 'Total Tokens',
|
totalTokens: 'Total Tokens',
|
||||||
totalCost: 'Total Cost',
|
totalCost: 'Total Cost',
|
||||||
@@ -1691,6 +1693,7 @@ export default {
|
|||||||
userFilter: 'User',
|
userFilter: 'User',
|
||||||
searchUserPlaceholder: 'Search user by email...',
|
searchUserPlaceholder: 'Search user by email...',
|
||||||
searchApiKeyPlaceholder: 'Search API key by name...',
|
searchApiKeyPlaceholder: 'Search API key by name...',
|
||||||
|
searchAccountPlaceholder: 'Search account by name...',
|
||||||
selectedUser: 'Selected',
|
selectedUser: 'Selected',
|
||||||
user: 'User',
|
user: 'User',
|
||||||
account: 'Account',
|
account: 'Account',
|
||||||
|
|||||||
@@ -373,6 +373,8 @@ export default {
|
|||||||
usage: {
|
usage: {
|
||||||
title: '使用记录',
|
title: '使用记录',
|
||||||
description: '查看和分析您的 API 使用历史',
|
description: '查看和分析您的 API 使用历史',
|
||||||
|
costDetails: '成本明细',
|
||||||
|
tokenDetails: 'Token 明细',
|
||||||
totalRequests: '总请求数',
|
totalRequests: '总请求数',
|
||||||
totalTokens: '总 Token',
|
totalTokens: '总 Token',
|
||||||
totalCost: '总消费',
|
totalCost: '总消费',
|
||||||
@@ -1836,6 +1838,7 @@ export default {
|
|||||||
userFilter: '用户',
|
userFilter: '用户',
|
||||||
searchUserPlaceholder: '按邮箱搜索用户...',
|
searchUserPlaceholder: '按邮箱搜索用户...',
|
||||||
searchApiKeyPlaceholder: '按名称搜索 API 密钥...',
|
searchApiKeyPlaceholder: '按名称搜索 API 密钥...',
|
||||||
|
searchAccountPlaceholder: '按名称搜索账号...',
|
||||||
selectedUser: '已选择',
|
selectedUser: '已选择',
|
||||||
user: '用户',
|
user: '用户',
|
||||||
account: '账户',
|
account: '账户',
|
||||||
|
|||||||
@@ -85,11 +85,48 @@ const exportToExcel = async () => {
|
|||||||
if (all.length >= total || res.items.length < 100) break; p++
|
if (all.length >= total || res.items.length < 100) break; p++
|
||||||
}
|
}
|
||||||
if(!c.signal.aborted) {
|
if(!c.signal.aborted) {
|
||||||
// 动态加载 xlsx,降低首屏包体并减少高危依赖的常驻暴露面。
|
|
||||||
const XLSX = await import('xlsx')
|
const XLSX = await import('xlsx')
|
||||||
const ws = XLSX.utils.json_to_sheet(all); const wb = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(wb, ws, 'Usage')
|
const headers = [
|
||||||
saveAs(new Blob([XLSX.write(wb, { bookType: 'xlsx', type: 'array' })], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }), `usage_${Date.now()}.xlsx`)
|
t('usage.time'), t('admin.usage.user'), t('usage.apiKeyFilter'),
|
||||||
appStore.showSuccess('Export Success')
|
t('admin.usage.account'), t('usage.model'), t('admin.usage.group'),
|
||||||
|
t('usage.type'),
|
||||||
|
t('admin.usage.inputTokens'), t('admin.usage.outputTokens'),
|
||||||
|
t('admin.usage.cacheReadTokens'), t('admin.usage.cacheCreationTokens'),
|
||||||
|
t('admin.usage.inputCost'), t('admin.usage.outputCost'),
|
||||||
|
t('admin.usage.cacheReadCost'), t('admin.usage.cacheCreationCost'),
|
||||||
|
t('usage.rate'), t('usage.original'), t('usage.billed'),
|
||||||
|
t('usage.billingType'), t('usage.firstToken'), t('usage.duration'),
|
||||||
|
t('admin.usage.requestId')
|
||||||
|
]
|
||||||
|
const rows = all.map(log => [
|
||||||
|
log.created_at,
|
||||||
|
log.user?.email || '',
|
||||||
|
log.api_key?.name || '',
|
||||||
|
log.account?.name || '',
|
||||||
|
log.model,
|
||||||
|
log.group?.name || '',
|
||||||
|
log.stream ? t('usage.stream') : t('usage.sync'),
|
||||||
|
log.input_tokens,
|
||||||
|
log.output_tokens,
|
||||||
|
log.cache_read_tokens,
|
||||||
|
log.cache_creation_tokens,
|
||||||
|
log.input_cost?.toFixed(6) || '0.000000',
|
||||||
|
log.output_cost?.toFixed(6) || '0.000000',
|
||||||
|
log.cache_read_cost?.toFixed(6) || '0.000000',
|
||||||
|
log.cache_creation_cost?.toFixed(6) || '0.000000',
|
||||||
|
log.rate_multiplier?.toFixed(2) || '1.00',
|
||||||
|
log.total_cost?.toFixed(6) || '0.000000',
|
||||||
|
log.actual_cost?.toFixed(6) || '0.000000',
|
||||||
|
log.billing_type === 1 ? t('usage.subscription') : t('usage.balance'),
|
||||||
|
log.first_token_ms ?? '',
|
||||||
|
log.duration_ms,
|
||||||
|
log.request_id || ''
|
||||||
|
])
|
||||||
|
const ws = XLSX.utils.aoa_to_sheet([headers, ...rows])
|
||||||
|
const wb = XLSX.utils.book_new()
|
||||||
|
XLSX.utils.book_append_sheet(wb, ws, 'Usage')
|
||||||
|
saveAs(new Blob([XLSX.write(wb, { bookType: 'xlsx', type: 'array' })], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }), `usage_${filters.value.start_date}_to_${filters.value.end_date}.xlsx`)
|
||||||
|
appStore.showSuccess(t('usage.exportSuccess'))
|
||||||
}
|
}
|
||||||
} catch (error) { console.error('Failed to export:', error); appStore.showError('Export Failed') }
|
} catch (error) { console.error('Failed to export:', error); appStore.showError('Export Failed') }
|
||||||
finally { if(exportAbortController === c) { exportAbortController = null; exporting.value = false; exportProgress.show = false } }
|
finally { if(exportAbortController === c) { exportAbortController = null; exporting.value = false; exportProgress.show = false } }
|
||||||
|
|||||||
@@ -342,8 +342,8 @@
|
|||||||
>
|
>
|
||||||
<div class="space-y-1.5">
|
<div class="space-y-1.5">
|
||||||
<!-- Token Breakdown -->
|
<!-- Token Breakdown -->
|
||||||
<div class="mb-2 border-b border-gray-700 pb-1.5">
|
<div>
|
||||||
<div class="text-xs font-semibold text-gray-300 mb-1">Token 明细</div>
|
<div class="text-xs font-semibold text-gray-300 mb-1">{{ t('usage.tokenDetails') }}</div>
|
||||||
<div v-if="tokenTooltipData && tokenTooltipData.input_tokens > 0" class="flex items-center justify-between gap-4">
|
<div v-if="tokenTooltipData && tokenTooltipData.input_tokens > 0" class="flex items-center justify-between gap-4">
|
||||||
<span class="text-gray-400">{{ t('admin.usage.inputTokens') }}</span>
|
<span class="text-gray-400">{{ t('admin.usage.inputTokens') }}</span>
|
||||||
<span class="font-medium text-white">{{ tokenTooltipData.input_tokens.toLocaleString() }}</span>
|
<span class="font-medium text-white">{{ tokenTooltipData.input_tokens.toLocaleString() }}</span>
|
||||||
@@ -389,6 +389,27 @@
|
|||||||
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"
|
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="space-y-1.5">
|
||||||
|
<!-- Cost Breakdown -->
|
||||||
|
<div class="mb-2 border-b border-gray-700 pb-1.5">
|
||||||
|
<div class="text-xs font-semibold text-gray-300 mb-1">{{ t('usage.costDetails') }}</div>
|
||||||
|
<div v-if="tooltipData && tooltipData.input_cost > 0" class="flex items-center justify-between gap-4">
|
||||||
|
<span class="text-gray-400">{{ t('admin.usage.inputCost') }}</span>
|
||||||
|
<span class="font-medium text-white">${{ tooltipData.input_cost.toFixed(6) }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="tooltipData && tooltipData.output_cost > 0" class="flex items-center justify-between gap-4">
|
||||||
|
<span class="text-gray-400">{{ t('admin.usage.outputCost') }}</span>
|
||||||
|
<span class="font-medium text-white">${{ tooltipData.output_cost.toFixed(6) }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="tooltipData && tooltipData.cache_creation_cost > 0" class="flex items-center justify-between gap-4">
|
||||||
|
<span class="text-gray-400">{{ t('admin.usage.cacheCreationCost') }}</span>
|
||||||
|
<span class="font-medium text-white">${{ tooltipData.cache_creation_cost.toFixed(6) }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="tooltipData && tooltipData.cache_read_cost > 0" class="flex items-center justify-between gap-4">
|
||||||
|
<span class="text-gray-400">{{ t('admin.usage.cacheReadCost') }}</span>
|
||||||
|
<span class="font-medium text-white">${{ tooltipData.cache_read_cost.toFixed(6) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Rate and Summary -->
|
||||||
<div class="flex items-center justify-between gap-6">
|
<div class="flex items-center justify-between gap-6">
|
||||||
<span class="text-gray-400">{{ t('usage.rate') }}</span>
|
<span class="text-gray-400">{{ t('usage.rate') }}</span>
|
||||||
<span class="font-semibold text-blue-400"
|
<span class="font-semibold text-blue-400"
|
||||||
|
|||||||
Reference in New Issue
Block a user