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
9dcd3cd4
Unverified
Commit
9dcd3cd4
authored
Mar 04, 2026
by
Wesley Liddick
Committed by
GitHub
Mar 04, 2026
Browse files
Merge pull request #754 from xvhuan/perf/admin-core-large-dataset
perf(admin): 优化后台大数据场景加载性能(仪表盘/用户/账号/Ops)
parents
49767ccc
f6fe5b55
Changes
27
Show whitespace changes
Inline
Side-by-side
frontend/src/api/admin/dashboard.ts
View file @
9dcd3cd4
...
...
@@ -120,6 +120,31 @@ export interface GroupStatsResponse {
end_date
:
string
}
export
interface
DashboardSnapshotV2Params
extends
TrendParams
{
include_stats
?:
boolean
include_trend
?:
boolean
include_model_stats
?:
boolean
include_group_stats
?:
boolean
include_users_trend
?:
boolean
users_trend_limit
?:
number
}
export
interface
DashboardSnapshotV2Stats
extends
DashboardStats
{
uptime
:
number
}
export
interface
DashboardSnapshotV2Response
{
generated_at
:
string
start_date
:
string
end_date
:
string
granularity
:
string
stats
?:
DashboardSnapshotV2Stats
trend
?:
TrendDataPoint
[]
models
?:
ModelStat
[]
groups
?:
GroupStat
[]
users_trend
?:
UserUsageTrendPoint
[]
}
/**
* Get group usage statistics
* @param params - Query parameters for filtering
...
...
@@ -130,6 +155,16 @@ export async function getGroupStats(params?: GroupStatsParams): Promise<GroupSta
return
data
}
/**
* Get dashboard snapshot v2 (aggregated response for heavy admin pages).
*/
export
async
function
getSnapshotV2
(
params
?:
DashboardSnapshotV2Params
):
Promise
<
DashboardSnapshotV2Response
>
{
const
{
data
}
=
await
apiClient
.
get
<
DashboardSnapshotV2Response
>
(
'
/admin/dashboard/snapshot-v2
'
,
{
params
})
return
data
}
export
interface
ApiKeyTrendParams
extends
TrendParams
{
limit
?:
number
}
...
...
@@ -233,6 +268,7 @@ export const dashboardAPI = {
getUsageTrend
,
getModelStats
,
getGroupStats
,
getSnapshotV2
,
getApiKeyUsageTrend
,
getUserUsageTrend
,
getBatchUsersUsage
,
...
...
frontend/src/api/admin/ops.ts
View file @
9dcd3cd4
...
...
@@ -259,6 +259,13 @@ export interface OpsErrorDistributionResponse {
items
:
OpsErrorDistributionItem
[]
}
export
interface
OpsDashboardSnapshotV2Response
{
generated_at
:
string
overview
:
OpsDashboardOverview
throughput_trend
:
OpsThroughputTrendResponse
error_trend
:
OpsErrorTrendResponse
}
export
type
OpsOpenAITokenStatsTimeRange
=
'
30m
'
|
'
1h
'
|
'
1d
'
|
'
15d
'
|
'
30d
'
export
interface
OpsOpenAITokenStatsItem
{
...
...
@@ -1004,6 +1011,24 @@ export async function getDashboardOverview(
return
data
}
export
async
function
getDashboardSnapshotV2
(
params
:
{
time_range
?:
'
5m
'
|
'
30m
'
|
'
1h
'
|
'
6h
'
|
'
24h
'
start_time
?:
string
end_time
?:
string
platform
?:
string
group_id
?:
number
|
null
mode
?:
OpsQueryMode
},
options
:
OpsRequestOptions
=
{}
):
Promise
<
OpsDashboardSnapshotV2Response
>
{
const
{
data
}
=
await
apiClient
.
get
<
OpsDashboardSnapshotV2Response
>
(
'
/admin/ops/dashboard/snapshot-v2
'
,
{
params
,
signal
:
options
.
signal
})
return
data
}
export
async
function
getThroughputTrend
(
params
:
{
time_range
?:
'
5m
'
|
'
30m
'
|
'
1h
'
|
'
6h
'
|
'
24h
'
...
...
@@ -1329,6 +1354,7 @@ async function updateMetricThresholds(thresholds: OpsMetricThresholds): Promise<
}
export
const
opsAPI
=
{
getDashboardSnapshotV2
,
getDashboardOverview
,
getThroughputTrend
,
getLatencyHistogram
,
...
...
frontend/src/api/admin/users.ts
View file @
9dcd3cd4
...
...
@@ -22,6 +22,7 @@ export async function list(
role
?:
'
admin
'
|
'
user
'
search
?:
string
attributes
?:
Record
<
number
,
string
>
// attributeId -> value
include_subscriptions
?:
boolean
},
options
?:
{
signal
?:
AbortSignal
...
...
@@ -33,7 +34,8 @@ export async function list(
page_size
:
pageSize
,
status
:
filters
?.
status
,
role
:
filters
?.
role
,
search
:
filters
?.
search
search
:
filters
?.
search
,
include_subscriptions
:
filters
?.
include_subscriptions
}
// Add attribute filters as attr[id]=value
...
...
frontend/src/views/admin/AccountsView.vue
View file @
9dcd3cd4
...
...
@@ -359,7 +359,7 @@ const exportingData = ref(false)
const
showColumnDropdown
=
ref
(
false
)
const
columnDropdownRef
=
ref
<
HTMLElement
|
null
>
(
null
)
const
hiddenColumns
=
reactive
<
Set
<
string
>>
(
new
Set
())
const
DEFAULT_HIDDEN_COLUMNS
=
[
'
proxy
'
,
'
notes
'
,
'
priority
'
,
'
rate_multiplier
'
]
const
DEFAULT_HIDDEN_COLUMNS
=
[
'
today_stats
'
,
'
proxy
'
,
'
notes
'
,
'
priority
'
,
'
rate_multiplier
'
]
const
HIDDEN_COLUMNS_KEY
=
'
account-hidden-columns
'
// Sorting settings
...
...
@@ -546,7 +546,7 @@ const {
handlePageSizeChange
:
baseHandlePageSizeChange
}
=
useTableLoader
<
Account
,
any
>
({
fetchFn
:
adminAPI
.
accounts
.
list
,
initialParams
:
{
platform
:
''
,
type
:
''
,
status
:
''
,
group
:
''
,
search
:
''
}
initialParams
:
{
platform
:
''
,
type
:
''
,
status
:
''
,
group
:
''
,
search
:
''
,
lite
:
'
1
'
}
}
)
const
resetAutoRefreshCache
=
()
=>
{
...
...
@@ -689,6 +689,7 @@ const refreshAccountsIncrementally = async () => {
type
?:
string
status
?:
string
search
?:
string
lite
?:
string
}
,
{
etag
:
autoRefreshETag
.
value
}
)
...
...
frontend/src/views/admin/DashboardView.vue
View file @
9dcd3cd4
...
...
@@ -316,6 +316,7 @@ const trendData = ref<TrendDataPoint[]>([])
const
modelStats
=
ref
<
ModelStat
[]
>
([])
const
userTrend
=
ref
<
UserUsageTrendPoint
[]
>
([])
let
chartLoadSeq
=
0
let
usersTrendLoadSeq
=
0
// Helper function to format date in local timezone
const
formatLocalDate
=
(
date
:
Date
):
string
=>
{
...
...
@@ -523,67 +524,74 @@ const onDateRangeChange = (range: {
}
// Load data
const
loadDashboardStats
=
async
()
=>
{
const
loadDashboardSnapshot
=
async
(
includeStats
:
boolean
)
=>
{
const
currentSeq
=
++
chartLoadSeq
if
(
includeStats
&&
!
stats
.
value
)
{
loading
.
value
=
true
try
{
stats
.
value
=
await
adminAPI
.
dashboard
.
getStats
()
}
catch
(
error
)
{
appStore
.
showError
(
t
(
'
admin.dashboard.failedToLoad
'
))
console
.
error
(
'
Error loading dashboard stats:
'
,
error
)
}
finally
{
loading
.
value
=
false
}
}
const
loadChartData
=
async
()
=>
{
const
currentSeq
=
++
chartLoadSeq
chartsLoading
.
value
=
true
userTrendLoading
.
value
=
true
try
{
const
params
=
{
const
response
=
await
adminAPI
.
dashboard
.
getSnapshotV2
(
{
start_date
:
startDate
.
value
,
end_date
:
endDate
.
value
,
granularity
:
granularity
.
value
}
const
[
trendResponse
,
modelResponse
]
=
await
Promise
.
all
([
adminAPI
.
dashboard
.
getUsageTrend
(
params
),
adminAPI
.
dashboard
.
getModelStats
({
start_date
:
startDate
.
value
,
end_date
:
endDate
.
value
})
])
granularity
:
granularity
.
value
,
include_stats
:
includeStats
,
include_trend
:
true
,
include_model_stats
:
true
,
include_group_stats
:
false
,
include_users_trend
:
false
})
if
(
currentSeq
!==
chartLoadSeq
)
return
trendData
.
value
=
trendResponse
.
trend
||
[]
modelStats
.
value
=
modelResponse
.
models
||
[]
if
(
includeStats
&&
response
.
stats
)
{
stats
.
value
=
response
.
stats
}
trendData
.
value
=
response
.
trend
||
[]
modelStats
.
value
=
response
.
models
||
[]
}
catch
(
error
)
{
if
(
currentSeq
!==
chartLoadSeq
)
return
console
.
error
(
'
Error loading chart data:
'
,
error
)
appStore
.
showError
(
t
(
'
admin.dashboard.failedToLoad
'
))
console
.
error
(
'
Error loading dashboard snapshot:
'
,
error
)
}
finally
{
if
(
currentSeq
!==
chartLoadSeq
)
return
loading
.
value
=
false
chartsLoading
.
value
=
false
}
}
const
loadUsersTrend
=
async
()
=>
{
const
currentSeq
=
++
usersTrendLoadSeq
userTrendLoading
.
value
=
true
try
{
const
params
=
{
const
response
=
await
adminAPI
.
dashboard
.
getUserUsageTrend
(
{
start_date
:
startDate
.
value
,
end_date
:
endDate
.
value
,
granularity
:
granularity
.
value
,
limit
:
12
}
const
userResponse
=
await
adminAPI
.
dashboard
.
getUserUsageTrend
(
params
)
if
(
currentSeq
!==
chartLoadSeq
)
return
userTrend
.
value
=
userResponse
.
trend
||
[]
})
if
(
currentSeq
!==
usersTrendLoadSeq
)
return
userTrend
.
value
=
response
.
trend
||
[]
}
catch
(
error
)
{
if
(
currentSeq
!==
chartLoadSeq
)
return
console
.
error
(
'
Error loading user trend:
'
,
error
)
if
(
currentSeq
!==
usersTrendLoadSeq
)
return
console
.
error
(
'
Error loading users trend:
'
,
error
)
userTrend
.
value
=
[]
}
finally
{
if
(
currentSeq
!==
chart
LoadSeq
)
return
if
(
currentSeq
!==
usersTrend
LoadSeq
)
return
userTrendLoading
.
value
=
false
}
}
const
loadDashboardStats
=
async
()
=>
{
await
loadDashboardSnapshot
(
true
)
void
loadUsersTrend
()
}
const
loadChartData
=
async
()
=>
{
await
loadDashboardSnapshot
(
false
)
void
loadUsersTrend
()
}
onMounted
(()
=>
{
loadDashboardStats
()
loadChartData
()
})
</
script
>
...
...
frontend/src/views/admin/UsersView.vue
View file @
9dcd3cd4
...
...
@@ -655,16 +655,28 @@ const saveColumnsToStorage = () => {
// Toggle column visibility
const
toggleColumn
=
(
key
:
string
)
=>
{
const
wasHidden
=
hiddenColumns
.
has
(
key
)
if
(
hiddenColumns
.
has
(
key
))
{
hiddenColumns
.
delete
(
key
)
}
else
{
hiddenColumns
.
add
(
key
)
}
saveColumnsToStorage
()
if
(
wasHidden
&&
(
key
===
'
usage
'
||
key
.
startsWith
(
'
attr_
'
)))
{
refreshCurrentPageSecondaryData
()
}
if
(
key
===
'
subscriptions
'
)
{
loadUsers
()
}
}
// Check if column is visible (not in hidden set)
const
isColumnVisible
=
(
key
:
string
)
=>
!
hiddenColumns
.
has
(
key
)
const
hasVisibleUsageColumn
=
computed
(()
=>
!
hiddenColumns
.
has
(
'
usage
'
))
const
hasVisibleSubscriptionsColumn
=
computed
(()
=>
!
hiddenColumns
.
has
(
'
subscriptions
'
))
const
hasVisibleAttributeColumns
=
computed
(()
=>
attributeDefinitions
.
value
.
some
((
def
)
=>
def
.
enabled
&&
!
hiddenColumns
.
has
(
`attr_
${
def
.
id
}
`
))
)
// Filtered columns based on visibility
const
columns
=
computed
<
Column
[]
>
(()
=>
...
...
@@ -776,6 +788,60 @@ const editingUser = ref<AdminUser | null>(null)
const
deletingUser
=
ref
<
AdminUser
|
null
>
(
null
)
const
viewingUser
=
ref
<
AdminUser
|
null
>
(
null
)
let
abortController
:
AbortController
|
null
=
null
let
secondaryDataSeq
=
0
const
loadUsersSecondaryData
=
async
(
userIds
:
number
[],
signal
?:
AbortSignal
,
expectedSeq
?:
number
)
=>
{
if
(
userIds
.
length
===
0
)
return
const
tasks
:
Promise
<
void
>
[]
=
[]
if
(
hasVisibleUsageColumn
.
value
)
{
tasks
.
push
(
(
async
()
=>
{
try
{
const
usageResponse
=
await
adminAPI
.
dashboard
.
getBatchUsersUsage
(
userIds
)
if
(
signal
?.
aborted
)
return
if
(
typeof
expectedSeq
===
'
number
'
&&
expectedSeq
!==
secondaryDataSeq
)
return
usageStats
.
value
=
usageResponse
.
stats
}
catch
(
e
)
{
if
(
signal
?.
aborted
)
return
console
.
error
(
'
Failed to load usage stats:
'
,
e
)
}
})()
)
}
if
(
attributeDefinitions
.
value
.
length
>
0
&&
hasVisibleAttributeColumns
.
value
)
{
tasks
.
push
(
(
async
()
=>
{
try
{
const
attrResponse
=
await
adminAPI
.
userAttributes
.
getBatchUserAttributes
(
userIds
)
if
(
signal
?.
aborted
)
return
if
(
typeof
expectedSeq
===
'
number
'
&&
expectedSeq
!==
secondaryDataSeq
)
return
userAttributeValues
.
value
=
attrResponse
.
attributes
}
catch
(
e
)
{
if
(
signal
?.
aborted
)
return
console
.
error
(
'
Failed to load user attribute values:
'
,
e
)
}
})()
)
}
if
(
tasks
.
length
>
0
)
{
await
Promise
.
allSettled
(
tasks
)
}
}
const
refreshCurrentPageSecondaryData
=
()
=>
{
const
userIds
=
users
.
value
.
map
((
u
)
=>
u
.
id
)
if
(
userIds
.
length
===
0
)
return
const
seq
=
++
secondaryDataSeq
void
loadUsersSecondaryData
(
userIds
,
undefined
,
seq
)
}
// Action Menu State
const
activeMenuId
=
ref
<
number
|
null
>
(
null
)
...
...
@@ -913,7 +979,8 @@ const loadUsers = async () => {
role
:
filters
.
role
as
any
,
status
:
filters
.
status
as
any
,
search
:
searchQuery
.
value
||
undefined
,
attributes
:
Object
.
keys
(
attrFilters
).
length
>
0
?
attrFilters
:
undefined
attributes
:
Object
.
keys
(
attrFilters
).
length
>
0
?
attrFilters
:
undefined
,
include_subscriptions
:
hasVisibleSubscriptionsColumn
.
value
},
{
signal
}
)
...
...
@@ -923,38 +990,17 @@ const loadUsers = async () => {
users
.
value
=
response
.
items
pagination
.
total
=
response
.
total
pagination
.
pages
=
response
.
pages
usageStats
.
value
=
{}
userAttributeValues
.
value
=
{}
//
Load usage stats and attribute values for all users in the li
st
//
Defer heavy secondary data so table can render fir
st
.
if
(
response
.
items
.
length
>
0
)
{
const
userIds
=
response
.
items
.
map
((
u
)
=>
u
.
id
)
// Load usage stats
try
{
const
usageResponse
=
await
adminAPI
.
dashboard
.
getBatchUsersUsage
(
userIds
)
if
(
signal
.
aborted
)
{
return
}
usageStats
.
value
=
usageResponse
.
stats
}
catch
(
e
)
{
if
(
signal
.
aborted
)
{
return
}
console
.
error
(
'
Failed to load usage stats:
'
,
e
)
}
// Load attribute values
if
(
attributeDefinitions
.
value
.
length
>
0
)
{
try
{
const
attrResponse
=
await
adminAPI
.
userAttributes
.
getBatchUserAttributes
(
userIds
)
if
(
signal
.
aborted
)
{
return
}
userAttributeValues
.
value
=
attrResponse
.
attributes
}
catch
(
e
)
{
if
(
signal
.
aborted
)
{
return
}
console
.
error
(
'
Failed to load user attribute values:
'
,
e
)
}
}
const
seq
=
++
secondaryDataSeq
window
.
setTimeout
(()
=>
{
if
(
signal
.
aborted
||
seq
!==
secondaryDataSeq
)
return
void
loadUsersSecondaryData
(
userIds
,
signal
,
seq
)
},
50
)
}
}
catch
(
error
:
any
)
{
const
errorInfo
=
error
as
{
name
?:
string
;
code
?:
string
}
...
...
frontend/src/views/admin/ops/OpsDashboard.vue
View file @
9dcd3cd4
...
...
@@ -586,6 +586,32 @@ async function refreshThroughputTrendWithCancel(fetchSeq: number, signal: AbortS
}
}
async
function
refreshCoreSnapshotWithCancel
(
fetchSeq
:
number
,
signal
:
AbortSignal
)
{
if
(
!
opsEnabled
.
value
)
return
loadingTrend
.
value
=
true
loadingErrorTrend
.
value
=
true
try
{
const
data
=
await
opsAPI
.
getDashboardSnapshotV2
(
buildApiParams
(),
{
signal
})
if
(
fetchSeq
!==
dashboardFetchSeq
)
return
overview
.
value
=
data
.
overview
throughputTrend
.
value
=
data
.
throughput_trend
errorTrend
.
value
=
data
.
error_trend
}
catch
(
err
:
any
)
{
if
(
fetchSeq
!==
dashboardFetchSeq
||
isCanceledRequest
(
err
))
return
// Fallback to legacy split endpoints when snapshot endpoint is unavailable.
await
Promise
.
all
([
refreshOverviewWithCancel
(
fetchSeq
,
signal
),
refreshThroughputTrendWithCancel
(
fetchSeq
,
signal
),
refreshErrorTrendWithCancel
(
fetchSeq
,
signal
)
])
}
finally
{
if
(
fetchSeq
===
dashboardFetchSeq
)
{
loadingTrend
.
value
=
false
loadingErrorTrend
.
value
=
false
}
}
}
async
function
refreshLatencyHistogramWithCancel
(
fetchSeq
:
number
,
signal
:
AbortSignal
)
{
if
(
!
opsEnabled
.
value
)
return
loadingLatency
.
value
=
true
...
...
@@ -640,6 +666,14 @@ async function refreshErrorDistributionWithCancel(fetchSeq: number, signal: Abor
}
}
async
function
refreshDeferredPanels
(
fetchSeq
:
number
,
signal
:
AbortSignal
)
{
if
(
!
opsEnabled
.
value
)
return
await
Promise
.
all
([
refreshLatencyHistogramWithCancel
(
fetchSeq
,
signal
),
refreshErrorDistributionWithCancel
(
fetchSeq
,
signal
)
])
}
function
isOpsDisabledError
(
err
:
unknown
):
boolean
{
return
(
!!
err
&&
...
...
@@ -662,12 +696,8 @@ async function fetchData() {
errorMessage
.
value
=
''
try
{
await
Promise
.
all
([
refreshOverviewWithCancel
(
fetchSeq
,
dashboardFetchController
.
signal
),
refreshThroughputTrendWithCancel
(
fetchSeq
,
dashboardFetchController
.
signal
),
refreshCoreSnapshotWithCancel
(
fetchSeq
,
dashboardFetchController
.
signal
),
refreshSwitchTrendWithCancel
(
fetchSeq
,
dashboardFetchController
.
signal
),
refreshLatencyHistogramWithCancel
(
fetchSeq
,
dashboardFetchController
.
signal
),
refreshErrorTrendWithCancel
(
fetchSeq
,
dashboardFetchController
.
signal
),
refreshErrorDistributionWithCancel
(
fetchSeq
,
dashboardFetchController
.
signal
)
])
if
(
fetchSeq
!==
dashboardFetchSeq
)
return
...
...
@@ -680,6 +710,9 @@ async function fetchData() {
if
(
autoRefreshEnabled
.
value
)
{
autoRefreshCountdown
.
value
=
Math
.
floor
(
autoRefreshIntervalMs
.
value
/
1000
)
}
// Defer non-core visual panels to reduce initial blocking.
void
refreshDeferredPanels
(
fetchSeq
,
dashboardFetchController
.
signal
)
}
catch
(
err
)
{
if
(
!
isOpsDisabledError
(
err
))
{
console
.
error
(
'
[ops] failed to fetch dashboard data
'
,
err
)
...
...
Prev
1
2
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