Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
陈曦
sub2api
Commits
e4a4dfd0
Commit
e4a4dfd0
authored
Mar 14, 2026
by
InCerry
Browse files
Merge remote-tracking branch 'origin/main' into fix/enc_coot
# Conflicts: # backend/internal/service/openai_gateway_service.go
parents
2666422b
e6d59216
Changes
82
Hide whitespace changes
Inline
Side-by-side
frontend/src/views/admin/DashboardView.vue
View file @
e4a4dfd0
...
...
@@ -236,7 +236,16 @@
<!-- Charts Grid -->
<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"
/>
</div>
...
...
@@ -267,11 +276,18 @@
<
script
setup
lang=
"ts"
>
import
{
ref
,
computed
,
onMounted
}
from
'
vue
'
import
{
useI18n
}
from
'
vue-i18n
'
import
{
useRouter
}
from
'
vue-router
'
import
{
useAppStore
}
from
'
@/stores/app
'
const
{
t
}
=
useI18n
()
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
LoadingSpinner
from
'
@/components/common/LoadingSpinner.vue
'
import
Icon
from
'
@/components/icons/Icon.vue
'
...
...
@@ -286,7 +302,6 @@ import {
LinearScale
,
PointElement
,
LineElement
,
Title
,
Tooltip
,
Legend
,
Filler
...
...
@@ -299,39 +314,42 @@ ChartJS.register(
LinearScale
,
PointElement
,
LineElement
,
Title
,
Tooltip
,
Legend
,
Filler
)
const
appStore
=
useAppStore
()
const
router
=
useRouter
()
const
stats
=
ref
<
DashboardStats
|
null
>
(
null
)
const
loading
=
ref
(
false
)
const
chartsLoading
=
ref
(
false
)
const
userTrendLoading
=
ref
(
false
)
const
rankingLoading
=
ref
(
false
)
const
rankingError
=
ref
(
false
)
// Chart data
const
trendData
=
ref
<
TrendDataPoint
[]
>
([])
const
modelStats
=
ref
<
ModelStat
[]
>
([])
const
userTrend
=
ref
<
UserUsageTrendPoint
[]
>
([])
const
rankingItems
=
ref
<
UserSpendingRankingItem
[]
>
([])
const
rankingTotalActualCost
=
ref
(
0
)
let
chartLoadSeq
=
0
let
usersTrendLoadSeq
=
0
let
rankingLoadSeq
=
0
const
rankingLimit
=
12
// Helper function to format date in local timezone
const
formatLocalDate
=
(
date
:
Date
):
string
=>
{
return
`
${
date
.
getFullYear
()}
-
${
String
(
date
.
getMonth
()
+
1
).
padStart
(
2
,
'
0
'
)}
-
${
String
(
date
.
getDate
()).
padStart
(
2
,
'
0
'
)}
`
}
// Initialize date range immediately
const
now
=
new
Date
()
const
weekAgo
=
new
Date
(
now
)
weekAgo
.
setDate
(
weekAgo
.
getDate
()
-
6
)
const
getTodayLocalDate
=
()
=>
formatLocalDate
(
new
Date
())
// Date range
const
granularity
=
ref
<
'
day
'
|
'
hour
'
>
(
'
day
'
)
const
startDate
=
ref
(
format
LocalDate
(
weekAgo
))
const
endDate
=
ref
(
format
LocalDate
(
now
))
const
startDate
=
ref
(
getToday
LocalDate
())
const
endDate
=
ref
(
getToday
LocalDate
())
// Granularity options for Select component
const
granularityOptions
=
computed
(()
=>
[
...
...
@@ -415,23 +433,29 @@ const lineOptions = computed(() => ({
const
userTrendChartData
=
computed
(()
=>
{
if
(
!
userTrend
.
value
?.
length
)
return
null
// Extract d
isplay
n
ame
from email (part before @)
const
getDisplayName
=
(
email
:
string
,
userId
:
number
):
string
=>
{
if
(
email
&&
email
.
includes
(
'
@
'
)
)
{
return
email
.
split
(
'
@
'
)[
0
]
const
getD
isplay
N
ame
=
(
point
:
UserUsageTrendPoint
):
string
=>
{
const
username
=
point
.
username
?.
trim
()
if
(
username
)
{
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
const
userGroups
=
new
Map
<
string
,
{
name
:
string
;
data
:
Map
<
string
,
number
>
}
>
()
// Group by user
_id to avoid merging different users with the same display name
const
userGroups
=
new
Map
<
number
,
{
name
:
string
;
data
:
Map
<
string
,
number
>
}
>
()
const
allDates
=
new
Set
<
string
>
()
userTrend
.
value
.
forEach
((
point
)
=>
{
allDates
.
add
(
point
.
date
)
const
key
=
getDisplayName
(
point
.
email
,
point
.
user_id
)
const
key
=
point
.
user_id
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
)
})
...
...
@@ -502,6 +526,17 @@ const formatDuration = (ms: number): string => {
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
const
onDateRangeChange
=
(
range
:
{
startDate
:
string
...
...
@@ -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
()
=>
{
await
loadDashboardSnapshot
(
true
)
void
loadUsersTrend
()
await
Promise
.
all
([
loadDashboardSnapshot
(
true
),
loadUsersTrend
(),
loadUserSpendingRanking
()
])
}
const
loadChartData
=
async
()
=>
{
await
loadDashboardSnapshot
(
false
)
void
loadUsersTrend
()
await
Promise
.
all
([
loadDashboardSnapshot
(
false
),
loadUsersTrend
(),
loadUserSpendingRanking
()
])
}
onMounted
(()
=>
{
...
...
frontend/src/views/admin/UsageView.vue
View file @
e4a4dfd0
...
...
@@ -89,6 +89,7 @@
import
{
ref
,
reactive
,
computed
,
onMounted
,
onUnmounted
}
from
'
vue
'
import
{
useI18n
}
from
'
vue-i18n
'
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
{
formatReasoningEffort
}
from
'
@/utils/format
'
import
{
resolveUsageRequestType
,
requestTypeToLegacyStream
}
from
'
@/utils/usageRequestType
'
...
...
@@ -104,7 +105,7 @@ import type { AdminUsageLog, TrendDataPoint, ModelStat, GroupStat, AdminUser } f
const
{
t
}
=
useI18n
()
const
appStore
=
useAppStore
()
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
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
'
)
...
...
@@ -135,11 +136,43 @@ const formatLD = (d: Date) => {
const
day
=
String
(
d
.
getDate
()).
padStart
(
2
,
'
0
'
)
return
`
${
year
}
-
${
month
}
-
${
day
}
`
}
const
now
=
new
Date
();
const
weekAgo
=
new
Date
();
weekAgo
.
setDate
(
weekAgo
.
get
Date
()
-
6
)
const
startDate
=
ref
(
formatLD
(
weekAgo
));
const
endDate
=
ref
(
formatLD
(
now
))
const
getTodayLocalDate
=
()
=>
formatLD
(
new
Date
())
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
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
()
=>
{
abortController
?.
abort
();
const
c
=
new
AbortController
();
abortController
=
c
;
loading
.
value
=
true
try
{
...
...
@@ -191,7 +224,7 @@ const loadChartData = async () => {
}
const
applyFilters
=
()
=>
{
pagination
.
page
=
1
;
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
handlePageSizeChange
=
(
s
:
number
)
=>
{
pagination
.
page_size
=
s
;
pagination
.
page
=
1
;
loadLogs
()
}
const
cancelExport
=
()
=>
exportAbortController
?.
abort
()
...
...
@@ -329,6 +362,7 @@ const handleColumnClickOutside = (event: MouseEvent) => {
}
onMounted
(()
=>
{
applyRouteQueryFilters
()
loadLogs
()
loadStats
()
window
.
setTimeout
(()
=>
{
...
...
Prev
1
2
3
4
5
Next
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment