mirror of
https://gitee.com/wanwujie/sub2api
synced 2026-04-09 17:44:46 +08:00
Merge branch 'Wei-Shaw:main' into fix/open-issues-cleanup
This commit is contained in:
@@ -2402,6 +2402,11 @@ const handleCreateGroup = async () => {
|
||||
sora_storage_quota_bytes: createQuotaGb ? Math.round(createQuotaGb * 1024 * 1024 * 1024) : 0,
|
||||
model_routing: convertRoutingRulesToApiFormat(createModelRoutingRules.value)
|
||||
}
|
||||
// v-model.number 清空输入框时产生 "",转为 null 让后端设为无限制
|
||||
const emptyToNull = (v: any) => v === '' ? null : v
|
||||
requestData.daily_limit_usd = emptyToNull(requestData.daily_limit_usd)
|
||||
requestData.weekly_limit_usd = emptyToNull(requestData.weekly_limit_usd)
|
||||
requestData.monthly_limit_usd = emptyToNull(requestData.monthly_limit_usd)
|
||||
await adminAPI.groups.create(requestData)
|
||||
appStore.showSuccess(t('admin.groups.groupCreated'))
|
||||
closeCreateModal()
|
||||
@@ -2488,6 +2493,11 @@ const handleUpdateGroup = async () => {
|
||||
: editForm.fallback_group_id_on_invalid_request,
|
||||
model_routing: convertRoutingRulesToApiFormat(editModelRoutingRules.value)
|
||||
}
|
||||
// v-model.number 清空输入框时产生 "",转为 null 让后端设为无限制
|
||||
const emptyToNull = (v: any) => v === '' ? null : v
|
||||
payload.daily_limit_usd = emptyToNull(payload.daily_limit_usd)
|
||||
payload.weekly_limit_usd = emptyToNull(payload.weekly_limit_usd)
|
||||
payload.monthly_limit_usd = emptyToNull(payload.monthly_limit_usd)
|
||||
await adminAPI.groups.update(editingGroup.value.id, payload)
|
||||
appStore.showSuccess(t('admin.groups.groupUpdated'))
|
||||
closeEditModal()
|
||||
|
||||
@@ -26,7 +26,20 @@
|
||||
:show-metric-toggle="true"
|
||||
/>
|
||||
</div>
|
||||
<TokenUsageTrend :trend-data="trendData" :loading="chartsLoading" />
|
||||
<div class="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
||||
<EndpointDistributionChart
|
||||
v-model:source="endpointDistributionSource"
|
||||
v-model:metric="endpointDistributionMetric"
|
||||
:endpoint-stats="inboundEndpointStats"
|
||||
:upstream-endpoint-stats="upstreamEndpointStats"
|
||||
:endpoint-path-stats="endpointPathStats"
|
||||
:loading="endpointStatsLoading"
|
||||
:show-source-toggle="true"
|
||||
:show-metric-toggle="true"
|
||||
:title="t('usage.endpointDistribution')"
|
||||
/>
|
||||
<TokenUsageTrend :trend-data="trendData" :loading="chartsLoading" />
|
||||
</div>
|
||||
</div>
|
||||
<UsageFilters v-model="filters" v-model:startDate="startDate" v-model:endDate="endDate" :exporting="exporting" @change="applyFilters" @refresh="refreshData" @reset="resetFilters" @cleanup="openCleanupDialog" @export="exportToExcel">
|
||||
<template #after-reset>
|
||||
@@ -99,19 +112,28 @@ import UsageTable from '@/components/admin/usage/UsageTable.vue'; import UsageEx
|
||||
import UsageCleanupDialog from '@/components/admin/usage/UsageCleanupDialog.vue'
|
||||
import UserBalanceHistoryModal from '@/components/admin/user/UserBalanceHistoryModal.vue'
|
||||
import ModelDistributionChart from '@/components/charts/ModelDistributionChart.vue'; import GroupDistributionChart from '@/components/charts/GroupDistributionChart.vue'; import TokenUsageTrend from '@/components/charts/TokenUsageTrend.vue'
|
||||
import EndpointDistributionChart from '@/components/charts/EndpointDistributionChart.vue'
|
||||
import Icon from '@/components/icons/Icon.vue'
|
||||
import type { AdminUsageLog, TrendDataPoint, ModelStat, GroupStat, AdminUser } from '@/types'; import type { AdminUsageStatsResponse, AdminUsageQueryParams } from '@/api/admin/usage'
|
||||
import type { AdminUsageLog, TrendDataPoint, ModelStat, GroupStat, EndpointStat, AdminUser } from '@/types'; import type { AdminUsageStatsResponse, AdminUsageQueryParams } from '@/api/admin/usage'
|
||||
|
||||
const { t } = useI18n()
|
||||
const appStore = useAppStore()
|
||||
type DistributionMetric = 'tokens' | 'actual_cost'
|
||||
type EndpointSource = 'inbound' | 'upstream' | 'path'
|
||||
const route = useRoute()
|
||||
const usageStats = ref<AdminUsageStatsResponse | null>(null); const usageLogs = ref<AdminUsageLog[]>([]); const loading = ref(false); const exporting = ref(false)
|
||||
const trendData = ref<TrendDataPoint[]>([]); const modelStats = ref<ModelStat[]>([]); const groupStats = ref<GroupStat[]>([]); const chartsLoading = ref(false); const granularity = ref<'day' | 'hour'>('day')
|
||||
const modelDistributionMetric = ref<DistributionMetric>('tokens')
|
||||
const groupDistributionMetric = ref<DistributionMetric>('tokens')
|
||||
const endpointDistributionMetric = ref<DistributionMetric>('tokens')
|
||||
const endpointDistributionSource = ref<EndpointSource>('inbound')
|
||||
const inboundEndpointStats = ref<EndpointStat[]>([])
|
||||
const upstreamEndpointStats = ref<EndpointStat[]>([])
|
||||
const endpointPathStats = ref<EndpointStat[]>([])
|
||||
const endpointStatsLoading = ref(false)
|
||||
let abortController: AbortController | null = null; let exportAbortController: AbortController | null = null
|
||||
let chartReqSeq = 0
|
||||
let statsReqSeq = 0
|
||||
const exportProgress = reactive({ show: false, progress: 0, current: 0, total: 0, estimatedTime: '' })
|
||||
const cleanupDialogVisible = ref(false)
|
||||
// Balance history modal state
|
||||
@@ -183,13 +205,25 @@ const loadLogs = async () => {
|
||||
} catch (error: any) { if(error?.name !== 'AbortError') console.error('Failed to load usage logs:', error) } finally { if(abortController === c) loading.value = false }
|
||||
}
|
||||
const loadStats = async () => {
|
||||
const seq = ++statsReqSeq
|
||||
endpointStatsLoading.value = true
|
||||
try {
|
||||
const requestType = filters.value.request_type
|
||||
const legacyStream = requestType ? requestTypeToLegacyStream(requestType) : filters.value.stream
|
||||
const s = await adminAPI.usage.getStats({ ...filters.value, stream: legacyStream === null ? undefined : legacyStream })
|
||||
if (seq !== statsReqSeq) return
|
||||
usageStats.value = s
|
||||
inboundEndpointStats.value = s.endpoints || []
|
||||
upstreamEndpointStats.value = s.upstream_endpoints || []
|
||||
endpointPathStats.value = s.endpoint_paths || []
|
||||
} catch (error) {
|
||||
if (seq !== statsReqSeq) return
|
||||
console.error('Failed to load usage stats:', error)
|
||||
inboundEndpointStats.value = []
|
||||
upstreamEndpointStats.value = []
|
||||
endpointPathStats.value = []
|
||||
} finally {
|
||||
if (seq === statsReqSeq) endpointStatsLoading.value = false
|
||||
}
|
||||
}
|
||||
const loadChartData = async () => {
|
||||
@@ -246,6 +280,7 @@ const exportToExcel = async () => {
|
||||
const headers = [
|
||||
t('usage.time'), t('admin.usage.user'), t('usage.apiKeyFilter'),
|
||||
t('admin.usage.account'), t('usage.model'), t('usage.reasoningEffort'), t('admin.usage.group'),
|
||||
t('usage.inboundEndpoint'), t('usage.upstreamEndpoint'),
|
||||
t('usage.type'),
|
||||
t('admin.usage.inputTokens'), t('admin.usage.outputTokens'),
|
||||
t('admin.usage.cacheReadTokens'), t('admin.usage.cacheCreationTokens'),
|
||||
@@ -263,7 +298,8 @@ const exportToExcel = async () => {
|
||||
if (c.signal.aborted) break; if (p === 1) { total = res.total; exportProgress.total = total }
|
||||
const rows = (res.items || []).map((log: AdminUsageLog) => [
|
||||
log.created_at, log.user?.email || '', log.api_key?.name || '', log.account?.name || '', log.model,
|
||||
formatReasoningEffort(log.reasoning_effort), log.group?.name || '', getRequestTypeLabel(log),
|
||||
formatReasoningEffort(log.reasoning_effort), log.group?.name || '',
|
||||
log.inbound_endpoint || '', log.upstream_endpoint || '', getRequestTypeLabel(log),
|
||||
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',
|
||||
@@ -301,6 +337,7 @@ const allColumns = computed(() => [
|
||||
{ key: 'account', label: t('admin.usage.account'), sortable: false },
|
||||
{ key: 'model', label: t('usage.model'), sortable: true },
|
||||
{ key: 'reasoning_effort', label: t('usage.reasoningEffort'), sortable: false },
|
||||
{ key: 'endpoint', label: t('usage.endpoint'), sortable: false },
|
||||
{ key: 'group', label: t('admin.usage.group'), sortable: false },
|
||||
{ key: 'stream', label: t('usage.type'), sortable: false },
|
||||
{ key: 'tokens', label: t('usage.tokens'), sortable: false },
|
||||
@@ -343,12 +380,18 @@ const loadSavedColumns = () => {
|
||||
try {
|
||||
const saved = localStorage.getItem(HIDDEN_COLUMNS_KEY)
|
||||
if (saved) {
|
||||
(JSON.parse(saved) as string[]).forEach(key => hiddenColumns.add(key))
|
||||
(JSON.parse(saved) as string[]).forEach((key) => {
|
||||
hiddenColumns.add(key)
|
||||
})
|
||||
} else {
|
||||
DEFAULT_HIDDEN_COLUMNS.forEach(key => hiddenColumns.add(key))
|
||||
DEFAULT_HIDDEN_COLUMNS.forEach((key) => {
|
||||
hiddenColumns.add(key)
|
||||
})
|
||||
}
|
||||
} catch {
|
||||
DEFAULT_HIDDEN_COLUMNS.forEach(key => hiddenColumns.add(key))
|
||||
DEFAULT_HIDDEN_COLUMNS.forEach((key) => {
|
||||
hiddenColumns.add(key)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user