"frontend/git@web.lueluesay.top:chenxi/sub2api.git" did not exist on "b65275235fc1bca21aae40f45c8668fdfa27073b"
Commit 01ef7340 authored by Wang Lvyuan's avatar Wang Lvyuan
Browse files

Merge remote-tracking branch 'origin/main' into openai-model-mapping-fix

parents 4e8615f2 e6d59216
...@@ -236,7 +236,16 @@ ...@@ -236,7 +236,16 @@
<!-- Charts Grid --> <!-- Charts Grid -->
<div class="grid grid-cols-1 gap-6 lg:grid-cols-2"> <div class="grid grid-cols-1 gap-6 lg:grid-cols-2">
<ModelDistributionChart :model-stats="modelStats" :loading="chartsLoading" /> <ModelDistributionChart
:model-stats="modelStats"
:enable-ranking-view="true"
:ranking-items="rankingItems"
:ranking-total-actual-cost="rankingTotalActualCost"
:loading="chartsLoading"
:ranking-loading="rankingLoading"
:ranking-error="rankingError"
@ranking-click="goToUserUsage"
/>
<TokenUsageTrend :trend-data="trendData" :loading="chartsLoading" /> <TokenUsageTrend :trend-data="trendData" :loading="chartsLoading" />
</div> </div>
...@@ -267,11 +276,18 @@ ...@@ -267,11 +276,18 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted } from 'vue' import { ref, computed, onMounted } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import { useAppStore } from '@/stores/app' import { useAppStore } from '@/stores/app'
const { t } = useI18n() const { t } = useI18n()
import { adminAPI } from '@/api/admin' import { adminAPI } from '@/api/admin'
import type { DashboardStats, TrendDataPoint, ModelStat, UserUsageTrendPoint } from '@/types' import type {
DashboardStats,
TrendDataPoint,
ModelStat,
UserUsageTrendPoint,
UserSpendingRankingItem
} from '@/types'
import AppLayout from '@/components/layout/AppLayout.vue' import AppLayout from '@/components/layout/AppLayout.vue'
import LoadingSpinner from '@/components/common/LoadingSpinner.vue' import LoadingSpinner from '@/components/common/LoadingSpinner.vue'
import Icon from '@/components/icons/Icon.vue' import Icon from '@/components/icons/Icon.vue'
...@@ -286,7 +302,6 @@ import { ...@@ -286,7 +302,6 @@ import {
LinearScale, LinearScale,
PointElement, PointElement,
LineElement, LineElement,
Title,
Tooltip, Tooltip,
Legend, Legend,
Filler Filler
...@@ -299,39 +314,42 @@ ChartJS.register( ...@@ -299,39 +314,42 @@ ChartJS.register(
LinearScale, LinearScale,
PointElement, PointElement,
LineElement, LineElement,
Title,
Tooltip, Tooltip,
Legend, Legend,
Filler Filler
) )
const appStore = useAppStore() const appStore = useAppStore()
const router = useRouter()
const stats = ref<DashboardStats | null>(null) const stats = ref<DashboardStats | null>(null)
const loading = ref(false) const loading = ref(false)
const chartsLoading = ref(false) const chartsLoading = ref(false)
const userTrendLoading = ref(false) const userTrendLoading = ref(false)
const rankingLoading = ref(false)
const rankingError = ref(false)
// Chart data // Chart data
const trendData = ref<TrendDataPoint[]>([]) const trendData = ref<TrendDataPoint[]>([])
const modelStats = ref<ModelStat[]>([]) const modelStats = ref<ModelStat[]>([])
const userTrend = ref<UserUsageTrendPoint[]>([]) const userTrend = ref<UserUsageTrendPoint[]>([])
const rankingItems = ref<UserSpendingRankingItem[]>([])
const rankingTotalActualCost = ref(0)
let chartLoadSeq = 0 let chartLoadSeq = 0
let usersTrendLoadSeq = 0 let usersTrendLoadSeq = 0
let rankingLoadSeq = 0
const rankingLimit = 12
// Helper function to format date in local timezone // Helper function to format date in local timezone
const formatLocalDate = (date: Date): string => { const formatLocalDate = (date: Date): string => {
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}` return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
} }
// Initialize date range immediately const getTodayLocalDate = () => formatLocalDate(new Date())
const now = new Date()
const weekAgo = new Date(now)
weekAgo.setDate(weekAgo.getDate() - 6)
// Date range // Date range
const granularity = ref<'day' | 'hour'>('day') const granularity = ref<'day' | 'hour'>('day')
const startDate = ref(formatLocalDate(weekAgo)) const startDate = ref(getTodayLocalDate())
const endDate = ref(formatLocalDate(now)) const endDate = ref(getTodayLocalDate())
// Granularity options for Select component // Granularity options for Select component
const granularityOptions = computed(() => [ const granularityOptions = computed(() => [
...@@ -415,23 +433,29 @@ const lineOptions = computed(() => ({ ...@@ -415,23 +433,29 @@ const lineOptions = computed(() => ({
const userTrendChartData = computed(() => { const userTrendChartData = computed(() => {
if (!userTrend.value?.length) return null if (!userTrend.value?.length) return null
// Extract display name from email (part before @) const getDisplayName = (point: UserUsageTrendPoint): string => {
const getDisplayName = (email: string, userId: number): string => { const username = point.username?.trim()
if (email && email.includes('@')) { if (username) {
return email.split('@')[0] return username
} }
return t('admin.redeem.userPrefix', { id: userId })
const email = point.email?.trim()
if (email) {
return email
}
return t('admin.redeem.userPrefix', { id: point.user_id })
} }
// Group by user // Group by user_id to avoid merging different users with the same display name
const userGroups = new Map<string, { name: string; data: Map<string, number> }>() const userGroups = new Map<number, { name: string; data: Map<string, number> }>()
const allDates = new Set<string>() const allDates = new Set<string>()
userTrend.value.forEach((point) => { userTrend.value.forEach((point) => {
allDates.add(point.date) allDates.add(point.date)
const key = getDisplayName(point.email, point.user_id) const key = point.user_id
if (!userGroups.has(key)) { if (!userGroups.has(key)) {
userGroups.set(key, { name: key, data: new Map() }) userGroups.set(key, { name: getDisplayName(point), data: new Map() })
} }
userGroups.get(key)!.data.set(point.date, point.tokens) userGroups.get(key)!.data.set(point.date, point.tokens)
}) })
...@@ -502,6 +526,17 @@ const formatDuration = (ms: number): string => { ...@@ -502,6 +526,17 @@ const formatDuration = (ms: number): string => {
return `${Math.round(ms)}ms` return `${Math.round(ms)}ms`
} }
const goToUserUsage = (item: UserSpendingRankingItem) => {
void router.push({
path: '/admin/usage',
query: {
user_id: String(item.user_id),
start_date: startDate.value,
end_date: endDate.value
}
})
}
// Date range change handler // Date range change handler
const onDateRangeChange = (range: { const onDateRangeChange = (range: {
startDate: string startDate: string
...@@ -582,14 +617,46 @@ const loadUsersTrend = async () => { ...@@ -582,14 +617,46 @@ const loadUsersTrend = async () => {
} }
} }
const loadUserSpendingRanking = async () => {
const currentSeq = ++rankingLoadSeq
rankingLoading.value = true
rankingError.value = false
try {
const response = await adminAPI.dashboard.getUserSpendingRanking({
start_date: startDate.value,
end_date: endDate.value,
limit: rankingLimit
})
if (currentSeq !== rankingLoadSeq) return
rankingItems.value = response.ranking || []
rankingTotalActualCost.value = response.total_actual_cost || 0
} catch (error) {
if (currentSeq !== rankingLoadSeq) return
console.error('Error loading user spending ranking:', error)
rankingItems.value = []
rankingTotalActualCost.value = 0
rankingError.value = true
} finally {
if (currentSeq === rankingLoadSeq) {
rankingLoading.value = false
}
}
}
const loadDashboardStats = async () => { const loadDashboardStats = async () => {
await loadDashboardSnapshot(true) await Promise.all([
void loadUsersTrend() loadDashboardSnapshot(true),
loadUsersTrend(),
loadUserSpendingRanking()
])
} }
const loadChartData = async () => { const loadChartData = async () => {
await loadDashboardSnapshot(false) await Promise.all([
void loadUsersTrend() loadDashboardSnapshot(false),
loadUsersTrend(),
loadUserSpendingRanking()
])
} }
onMounted(() => { onMounted(() => {
......
...@@ -89,6 +89,7 @@ ...@@ -89,6 +89,7 @@
import { ref, reactive, computed, onMounted, onUnmounted } from 'vue' import { ref, reactive, computed, onMounted, onUnmounted } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { saveAs } from 'file-saver' import { saveAs } from 'file-saver'
import { useRoute } from 'vue-router'
import { useAppStore } from '@/stores/app'; import { adminAPI } from '@/api/admin'; import { adminUsageAPI } from '@/api/admin/usage' import { useAppStore } from '@/stores/app'; import { adminAPI } from '@/api/admin'; import { adminUsageAPI } from '@/api/admin/usage'
import { formatReasoningEffort } from '@/utils/format' import { formatReasoningEffort } from '@/utils/format'
import { resolveUsageRequestType, requestTypeToLegacyStream } from '@/utils/usageRequestType' import { resolveUsageRequestType, requestTypeToLegacyStream } from '@/utils/usageRequestType'
...@@ -104,7 +105,7 @@ import type { AdminUsageLog, TrendDataPoint, ModelStat, GroupStat, AdminUser } f ...@@ -104,7 +105,7 @@ import type { AdminUsageLog, TrendDataPoint, ModelStat, GroupStat, AdminUser } f
const { t } = useI18n() const { t } = useI18n()
const appStore = useAppStore() const appStore = useAppStore()
type DistributionMetric = 'tokens' | 'actual_cost' type DistributionMetric = 'tokens' | 'actual_cost'
const route = useRoute()
const usageStats = ref<AdminUsageStatsResponse | null>(null); const usageLogs = ref<AdminUsageLog[]>([]); const loading = ref(false); const exporting = ref(false) 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 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 modelDistributionMetric = ref<DistributionMetric>('tokens')
...@@ -135,11 +136,43 @@ const formatLD = (d: Date) => { ...@@ -135,11 +136,43 @@ const formatLD = (d: Date) => {
const day = String(d.getDate()).padStart(2, '0') const day = String(d.getDate()).padStart(2, '0')
return `${year}-${month}-${day}` return `${year}-${month}-${day}`
} }
const now = new Date(); const weekAgo = new Date(); weekAgo.setDate(weekAgo.getDate() - 6) const getTodayLocalDate = () => formatLD(new Date())
const startDate = ref(formatLD(weekAgo)); const endDate = ref(formatLD(now)) const startDate = ref(getTodayLocalDate()); const endDate = ref(getTodayLocalDate())
const filters = ref<AdminUsageQueryParams>({ user_id: undefined, model: undefined, group_id: undefined, request_type: undefined, billing_type: null, start_date: startDate.value, end_date: endDate.value }) const filters = ref<AdminUsageQueryParams>({ user_id: undefined, model: undefined, group_id: undefined, request_type: undefined, billing_type: null, start_date: startDate.value, end_date: endDate.value })
const pagination = reactive({ page: 1, page_size: 20, total: 0 }) const pagination = reactive({ page: 1, page_size: 20, total: 0 })
const getSingleQueryValue = (value: string | null | Array<string | null> | undefined): string | undefined => {
if (Array.isArray(value)) return value.find((item): item is string => typeof item === 'string' && item.length > 0)
return typeof value === 'string' && value.length > 0 ? value : undefined
}
const getNumericQueryValue = (value: string | null | Array<string | null> | undefined): number | undefined => {
const raw = getSingleQueryValue(value)
if (!raw) return undefined
const parsed = Number(raw)
return Number.isFinite(parsed) ? parsed : undefined
}
const applyRouteQueryFilters = () => {
const queryStartDate = getSingleQueryValue(route.query.start_date)
const queryEndDate = getSingleQueryValue(route.query.end_date)
const queryUserId = getNumericQueryValue(route.query.user_id)
if (queryStartDate) {
startDate.value = queryStartDate
}
if (queryEndDate) {
endDate.value = queryEndDate
}
filters.value = {
...filters.value,
user_id: queryUserId,
start_date: startDate.value,
end_date: endDate.value
}
}
const loadLogs = async () => { const loadLogs = async () => {
abortController?.abort(); const c = new AbortController(); abortController = c; loading.value = true abortController?.abort(); const c = new AbortController(); abortController = c; loading.value = true
try { try {
...@@ -191,7 +224,7 @@ const loadChartData = async () => { ...@@ -191,7 +224,7 @@ const loadChartData = async () => {
} }
const applyFilters = () => { pagination.page = 1; loadLogs(); loadStats(); loadChartData() } const applyFilters = () => { pagination.page = 1; loadLogs(); loadStats(); loadChartData() }
const refreshData = () => { loadLogs(); loadStats(); loadChartData() } const refreshData = () => { loadLogs(); loadStats(); loadChartData() }
const resetFilters = () => { startDate.value = formatLD(weekAgo); endDate.value = formatLD(now); filters.value = { start_date: startDate.value, end_date: endDate.value, request_type: undefined, billing_type: null }; granularity.value = 'day'; applyFilters() } const resetFilters = () => { startDate.value = getTodayLocalDate(); endDate.value = getTodayLocalDate(); filters.value = { start_date: startDate.value, end_date: endDate.value, request_type: undefined, billing_type: null }; granularity.value = 'day'; applyFilters() }
const handlePageChange = (p: number) => { pagination.page = p; loadLogs() } const handlePageChange = (p: number) => { pagination.page = p; loadLogs() }
const handlePageSizeChange = (s: number) => { pagination.page_size = s; pagination.page = 1; loadLogs() } const handlePageSizeChange = (s: number) => { pagination.page_size = s; pagination.page = 1; loadLogs() }
const cancelExport = () => exportAbortController?.abort() const cancelExport = () => exportAbortController?.abort()
...@@ -329,6 +362,7 @@ const handleColumnClickOutside = (event: MouseEvent) => { ...@@ -329,6 +362,7 @@ const handleColumnClickOutside = (event: MouseEvent) => {
} }
onMounted(() => { onMounted(() => {
applyRouteQueryFilters()
loadLogs() loadLogs()
loadStats() loadStats()
window.setTimeout(() => { window.setTimeout(() => {
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment