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
ef5a4105
Commit
ef5a4105
authored
Jan 18, 2026
by
yangjianbo
Browse files
feat(usage): 添加清理任务与统计过滤
parent
74a3c745
Changes
44
Show whitespace changes
Inline
Side-by-side
frontend/src/i18n/locales/en.ts
View file @
ef5a4105
...
...
@@ -1893,7 +1893,43 @@ export default {
cacheCreationTokens
:
'
Cache Creation Tokens
'
,
cacheReadTokens
:
'
Cache Read Tokens
'
,
failedToLoad
:
'
Failed to load usage records
'
,
ipAddress
:
'
IP
'
billingType
:
'
Billing Type
'
,
allBillingTypes
:
'
All Billing Types
'
,
billingTypeBalance
:
'
Balance
'
,
billingTypeSubscription
:
'
Subscription
'
,
ipAddress
:
'
IP
'
,
cleanup
:
{
button
:
'
Cleanup
'
,
title
:
'
Cleanup Usage Records
'
,
warning
:
'
Cleanup is irreversible and will affect historical stats.
'
,
submit
:
'
Submit Cleanup
'
,
submitting
:
'
Submitting...
'
,
confirmTitle
:
'
Confirm Cleanup
'
,
confirmMessage
:
'
Are you sure you want to submit this cleanup task? This action cannot be undone.
'
,
confirmSubmit
:
'
Confirm Cleanup
'
,
cancel
:
'
Cancel
'
,
cancelConfirmTitle
:
'
Confirm Cancel
'
,
cancelConfirmMessage
:
'
Are you sure you want to cancel this cleanup task?
'
,
cancelConfirm
:
'
Confirm Cancel
'
,
cancelSuccess
:
'
Cleanup task canceled
'
,
cancelFailed
:
'
Failed to cancel cleanup task
'
,
recentTasks
:
'
Recent Cleanup Tasks
'
,
loadingTasks
:
'
Loading tasks...
'
,
noTasks
:
'
No cleanup tasks yet
'
,
range
:
'
Range
'
,
deletedRows
:
'
Deleted
'
,
missingRange
:
'
Please select a date range
'
,
submitSuccess
:
'
Cleanup task created
'
,
submitFailed
:
'
Failed to create cleanup task
'
,
loadFailed
:
'
Failed to load cleanup tasks
'
,
status
:
{
pending
:
'
Pending
'
,
running
:
'
Running
'
,
succeeded
:
'
Succeeded
'
,
failed
:
'
Failed
'
,
canceled
:
'
Canceled
'
}
}
},
// Ops Monitoring
...
...
frontend/src/i18n/locales/zh.ts
View file @
ef5a4105
...
...
@@ -2041,7 +2041,43 @@ export default {
cacheCreationTokens
:
'
缓存创建 Token
'
,
cacheReadTokens
:
'
缓存读取 Token
'
,
failedToLoad
:
'
加载使用记录失败
'
,
ipAddress
:
'
IP
'
billingType
:
'
计费类型
'
,
allBillingTypes
:
'
全部计费类型
'
,
billingTypeBalance
:
'
钱包余额
'
,
billingTypeSubscription
:
'
订阅套餐
'
,
ipAddress
:
'
IP
'
,
cleanup
:
{
button
:
'
清理
'
,
title
:
'
清理使用记录
'
,
warning
:
'
清理不可恢复,且会影响历史统计回看。
'
,
submit
:
'
提交清理
'
,
submitting
:
'
提交中...
'
,
confirmTitle
:
'
确认清理
'
,
confirmMessage
:
'
确定要提交清理任务吗?清理不可恢复。
'
,
confirmSubmit
:
'
确认清理
'
,
cancel
:
'
取消任务
'
,
cancelConfirmTitle
:
'
确认取消
'
,
cancelConfirmMessage
:
'
确定要取消该清理任务吗?
'
,
cancelConfirm
:
'
确认取消
'
,
cancelSuccess
:
'
清理任务已取消
'
,
cancelFailed
:
'
取消清理任务失败
'
,
recentTasks
:
'
最近清理任务
'
,
loadingTasks
:
'
正在加载任务...
'
,
noTasks
:
'
暂无清理任务
'
,
range
:
'
时间范围
'
,
deletedRows
:
'
删除数量
'
,
missingRange
:
'
请选择时间范围
'
,
submitSuccess
:
'
清理任务已创建
'
,
submitFailed
:
'
创建清理任务失败
'
,
loadFailed
:
'
加载清理任务失败
'
,
status
:
{
pending
:
'
待执行
'
,
running
:
'
执行中
'
,
succeeded
:
'
已完成
'
,
failed
:
'
失败
'
,
canceled
:
'
已取消
'
}
}
},
// Ops Monitoring
...
...
frontend/src/types/index.ts
View file @
ef5a4105
...
...
@@ -618,6 +618,7 @@ export interface UsageLog {
actual_cost
:
number
rate_multiplier
:
number
account_rate_multiplier
?:
number
|
null
billing_type
:
number
stream
:
boolean
duration_ms
:
number
...
...
@@ -642,6 +643,33 @@ export interface UsageLog {
subscription
?:
UserSubscription
}
export
interface
UsageCleanupFilters
{
start_time
:
string
end_time
:
string
user_id
?:
number
api_key_id
?:
number
account_id
?:
number
group_id
?:
number
model
?:
string
|
null
stream
?:
boolean
|
null
billing_type
?:
number
|
null
}
export
interface
UsageCleanupTask
{
id
:
number
status
:
string
filters
:
UsageCleanupFilters
created_by
:
number
deleted_rows
:
number
error_message
?:
string
|
null
canceled_by
?:
number
|
null
canceled_at
?:
string
|
null
started_at
?:
string
|
null
finished_at
?:
string
|
null
created_at
:
string
updated_at
:
string
}
export
interface
RedeemCode
{
id
:
number
code
:
string
...
...
@@ -865,6 +893,7 @@ export interface UsageQueryParams {
group_id
?:
number
model
?:
string
stream
?:
boolean
billing_type
?:
number
|
null
start_date
?:
string
end_date
?:
string
}
...
...
frontend/src/views/admin/UsageView.vue
View file @
ef5a4105
...
...
@@ -17,12 +17,19 @@
<TokenUsageTrend
:trend-data=
"trendData"
:loading=
"chartsLoading"
/>
</div>
</div>
<UsageFilters
v-model=
"filters"
v-model:startDate=
"startDate"
v-model:endDate=
"endDate"
:exporting=
"exporting"
@
change=
"applyFilters"
@
reset=
"resetFilters"
@
export=
"exportToExcel"
/>
<UsageFilters
v-model=
"filters"
v-model:startDate=
"startDate"
v-model:endDate=
"endDate"
:exporting=
"exporting"
@
change=
"applyFilters"
@
reset=
"resetFilters"
@
cleanup=
"openCleanupDialog"
@
export=
"exportToExcel"
/>
<UsageTable
:data=
"usageLogs"
:loading=
"loading"
/>
<Pagination
v-if=
"pagination.total > 0"
:page=
"pagination.page"
:total=
"pagination.total"
:page-size=
"pagination.page_size"
@
update:page=
"handlePageChange"
@
update:pageSize=
"handlePageSizeChange"
/>
</div>
</AppLayout>
<UsageExportProgress
:show=
"exportProgress.show"
:progress=
"exportProgress.progress"
:current=
"exportProgress.current"
:total=
"exportProgress.total"
:estimated-time=
"exportProgress.estimatedTime"
@
cancel=
"cancelExport"
/>
<UsageCleanupDialog
:show=
"cleanupDialogVisible"
:filters=
"filters"
:start-date=
"startDate"
:end-date=
"endDate"
@
close=
"cleanupDialogVisible = false"
/>
</
template
>
<
script
setup
lang=
"ts"
>
...
...
@@ -33,6 +40,7 @@ import { useAppStore } from '@/stores/app'; import { adminAPI } from '@/api/admi
import
AppLayout
from
'
@/components/layout/AppLayout.vue
'
;
import
Pagination
from
'
@/components/common/Pagination.vue
'
;
import
Select
from
'
@/components/common/Select.vue
'
import
UsageStatsCards
from
'
@/components/admin/usage/UsageStatsCards.vue
'
;
import
UsageFilters
from
'
@/components/admin/usage/UsageFilters.vue
'
import
UsageTable
from
'
@/components/admin/usage/UsageTable.vue
'
;
import
UsageExportProgress
from
'
@/components/admin/usage/UsageExportProgress.vue
'
import
UsageCleanupDialog
from
'
@/components/admin/usage/UsageCleanupDialog.vue
'
import
ModelDistributionChart
from
'
@/components/charts/ModelDistributionChart.vue
'
;
import
TokenUsageTrend
from
'
@/components/charts/TokenUsageTrend.vue
'
import
type
{
UsageLog
,
TrendDataPoint
,
ModelStat
}
from
'
@/types
'
;
import
type
{
AdminUsageStatsResponse
,
AdminUsageQueryParams
}
from
'
@/api/admin/usage
'
...
...
@@ -42,6 +50,7 @@ const usageStats = ref<AdminUsageStatsResponse | null>(null); const usageLogs =
const
trendData
=
ref
<
TrendDataPoint
[]
>
([]);
const
modelStats
=
ref
<
ModelStat
[]
>
([]);
const
chartsLoading
=
ref
(
false
);
const
granularity
=
ref
<
'
day
'
|
'
hour
'
>
(
'
day
'
)
let
abortController
:
AbortController
|
null
=
null
;
let
exportAbortController
:
AbortController
|
null
=
null
const
exportProgress
=
reactive
({
show
:
false
,
progress
:
0
,
current
:
0
,
total
:
0
,
estimatedTime
:
''
})
const
cleanupDialogVisible
=
ref
(
false
)
const
granularityOptions
=
computed
(()
=>
[{
value
:
'
day
'
,
label
:
t
(
'
admin.dashboard.day
'
)
},
{
value
:
'
hour
'
,
label
:
t
(
'
admin.dashboard.hour
'
)
}])
// Use local timezone to avoid UTC timezone issues
...
...
@@ -53,7 +62,7 @@ const formatLD = (d: Date) => {
}
const
now
=
new
Date
();
const
weekAgo
=
new
Date
();
weekAgo
.
setDate
(
weekAgo
.
getDate
()
-
6
)
const
startDate
=
ref
(
formatLD
(
weekAgo
));
const
endDate
=
ref
(
formatLD
(
now
))
const
filters
=
ref
<
AdminUsageQueryParams
>
({
user_id
:
undefined
,
model
:
undefined
,
group_id
:
undefined
,
start_date
:
startDate
.
value
,
end_date
:
endDate
.
value
})
const
filters
=
ref
<
AdminUsageQueryParams
>
({
user_id
:
undefined
,
model
:
undefined
,
group_id
:
undefined
,
billing_type
:
null
,
start_date
:
startDate
.
value
,
end_date
:
endDate
.
value
})
const
pagination
=
reactive
({
page
:
1
,
page_size
:
20
,
total
:
0
})
const
loadLogs
=
async
()
=>
{
...
...
@@ -67,16 +76,17 @@ const loadStats = async () => { try { const s = await adminAPI.usage.getStats(fi
const
loadChartData
=
async
()
=>
{
chartsLoading
.
value
=
true
try
{
const
params
=
{
start_date
:
filters
.
value
.
start_date
||
startDate
.
value
,
end_date
:
filters
.
value
.
end_date
||
endDate
.
value
,
granularity
:
granularity
.
value
,
user_id
:
filters
.
value
.
user_id
,
model
:
filters
.
value
.
model
,
api_key_id
:
filters
.
value
.
api_key_id
,
account_id
:
filters
.
value
.
account_id
,
group_id
:
filters
.
value
.
group_id
,
stream
:
filters
.
value
.
stream
}
const
[
trendRes
,
modelRes
]
=
await
Promise
.
all
([
adminAPI
.
dashboard
.
getUsageTrend
(
params
),
adminAPI
.
dashboard
.
getModelStats
({
start_date
:
params
.
start_date
,
end_date
:
params
.
end_date
,
user_id
:
params
.
user_id
,
model
:
params
.
model
,
api_key_id
:
params
.
api_key_id
,
account_id
:
params
.
account_id
,
group_id
:
params
.
group_id
,
stream
:
params
.
stream
})])
const
params
=
{
start_date
:
filters
.
value
.
start_date
||
startDate
.
value
,
end_date
:
filters
.
value
.
end_date
||
endDate
.
value
,
granularity
:
granularity
.
value
,
user_id
:
filters
.
value
.
user_id
,
model
:
filters
.
value
.
model
,
api_key_id
:
filters
.
value
.
api_key_id
,
account_id
:
filters
.
value
.
account_id
,
group_id
:
filters
.
value
.
group_id
,
stream
:
filters
.
value
.
stream
,
billing_type
:
filters
.
value
.
billing_type
}
const
[
trendRes
,
modelRes
]
=
await
Promise
.
all
([
adminAPI
.
dashboard
.
getUsageTrend
(
params
),
adminAPI
.
dashboard
.
getModelStats
({
start_date
:
params
.
start_date
,
end_date
:
params
.
end_date
,
user_id
:
params
.
user_id
,
model
:
params
.
model
,
api_key_id
:
params
.
api_key_id
,
account_id
:
params
.
account_id
,
group_id
:
params
.
group_id
,
stream
:
params
.
stream
,
billing_type
:
params
.
billing_type
})])
trendData
.
value
=
trendRes
.
trend
||
[];
modelStats
.
value
=
modelRes
.
models
||
[]
}
catch
(
error
)
{
console
.
error
(
'
Failed to load chart data:
'
,
error
)
}
finally
{
chartsLoading
.
value
=
false
}
}
const
applyFilters
=
()
=>
{
pagination
.
page
=
1
;
loadLogs
();
loadStats
();
loadChartData
()
}
const
resetFilters
=
()
=>
{
startDate
.
value
=
formatLD
(
weekAgo
);
endDate
.
value
=
formatLD
(
now
);
filters
.
value
=
{
start_date
:
startDate
.
value
,
end_date
:
endDate
.
value
};
granularity
.
value
=
'
day
'
;
applyFilters
()
}
const
resetFilters
=
()
=>
{
startDate
.
value
=
formatLD
(
weekAgo
);
endDate
.
value
=
formatLD
(
now
);
filters
.
value
=
{
start_date
:
startDate
.
value
,
end_date
:
endDate
.
value
,
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
()
const
openCleanupDialog
=
()
=>
{
cleanupDialogVisible
.
value
=
true
}
const
exportToExcel
=
async
()
=>
{
if
(
exporting
.
value
)
return
;
exporting
.
value
=
true
;
exportProgress
.
show
=
true
...
...
Prev
1
2
3
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