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
0484c59e
Commit
0484c59e
authored
Dec 20, 2025
by
shaw
Browse files
feat: /admin/usage页面增加模型分布情况显示
parent
ef81aeb4
Changes
8
Hide whitespace changes
Inline
Side-by-side
backend/internal/handler/admin/dashboard_handler.go
View file @
0484c59e
...
@@ -127,12 +127,25 @@ func (h *DashboardHandler) GetRealtimeMetrics(c *gin.Context) {
...
@@ -127,12 +127,25 @@ func (h *DashboardHandler) GetRealtimeMetrics(c *gin.Context) {
// GetUsageTrend handles getting usage trend data
// GetUsageTrend handles getting usage trend data
// GET /api/v1/admin/dashboard/trend
// GET /api/v1/admin/dashboard/trend
// Query params: start_date, end_date (YYYY-MM-DD), granularity (day/hour)
// Query params: start_date, end_date (YYYY-MM-DD), granularity (day/hour)
, user_id, api_key_id
func
(
h
*
DashboardHandler
)
GetUsageTrend
(
c
*
gin
.
Context
)
{
func
(
h
*
DashboardHandler
)
GetUsageTrend
(
c
*
gin
.
Context
)
{
startTime
,
endTime
:=
parseTimeRange
(
c
)
startTime
,
endTime
:=
parseTimeRange
(
c
)
granularity
:=
c
.
DefaultQuery
(
"granularity"
,
"day"
)
granularity
:=
c
.
DefaultQuery
(
"granularity"
,
"day"
)
trend
,
err
:=
h
.
usageRepo
.
GetUsageTrend
(
c
.
Request
.
Context
(),
startTime
,
endTime
,
granularity
)
// Parse optional filter params
var
userID
,
apiKeyID
int64
if
userIDStr
:=
c
.
Query
(
"user_id"
);
userIDStr
!=
""
{
if
id
,
err
:=
strconv
.
ParseInt
(
userIDStr
,
10
,
64
);
err
==
nil
{
userID
=
id
}
}
if
apiKeyIDStr
:=
c
.
Query
(
"api_key_id"
);
apiKeyIDStr
!=
""
{
if
id
,
err
:=
strconv
.
ParseInt
(
apiKeyIDStr
,
10
,
64
);
err
==
nil
{
apiKeyID
=
id
}
}
trend
,
err
:=
h
.
usageRepo
.
GetUsageTrendWithFilters
(
c
.
Request
.
Context
(),
startTime
,
endTime
,
granularity
,
userID
,
apiKeyID
)
if
err
!=
nil
{
if
err
!=
nil
{
response
.
Error
(
c
,
500
,
"Failed to get usage trend"
)
response
.
Error
(
c
,
500
,
"Failed to get usage trend"
)
return
return
...
@@ -148,11 +161,24 @@ func (h *DashboardHandler) GetUsageTrend(c *gin.Context) {
...
@@ -148,11 +161,24 @@ func (h *DashboardHandler) GetUsageTrend(c *gin.Context) {
// GetModelStats handles getting model usage statistics
// GetModelStats handles getting model usage statistics
// GET /api/v1/admin/dashboard/models
// GET /api/v1/admin/dashboard/models
// Query params: start_date, end_date (YYYY-MM-DD)
// Query params: start_date, end_date (YYYY-MM-DD)
, user_id, api_key_id
func
(
h
*
DashboardHandler
)
GetModelStats
(
c
*
gin
.
Context
)
{
func
(
h
*
DashboardHandler
)
GetModelStats
(
c
*
gin
.
Context
)
{
startTime
,
endTime
:=
parseTimeRange
(
c
)
startTime
,
endTime
:=
parseTimeRange
(
c
)
stats
,
err
:=
h
.
usageRepo
.
GetModelStats
(
c
.
Request
.
Context
(),
startTime
,
endTime
)
// Parse optional filter params
var
userID
,
apiKeyID
int64
if
userIDStr
:=
c
.
Query
(
"user_id"
);
userIDStr
!=
""
{
if
id
,
err
:=
strconv
.
ParseInt
(
userIDStr
,
10
,
64
);
err
==
nil
{
userID
=
id
}
}
if
apiKeyIDStr
:=
c
.
Query
(
"api_key_id"
);
apiKeyIDStr
!=
""
{
if
id
,
err
:=
strconv
.
ParseInt
(
apiKeyIDStr
,
10
,
64
);
err
==
nil
{
apiKeyID
=
id
}
}
stats
,
err
:=
h
.
usageRepo
.
GetModelStatsWithFilters
(
c
.
Request
.
Context
(),
startTime
,
endTime
,
userID
,
apiKeyID
)
if
err
!=
nil
{
if
err
!=
nil
{
response
.
Error
(
c
,
500
,
"Failed to get model statistics"
)
response
.
Error
(
c
,
500
,
"Failed to get model statistics"
)
return
return
...
...
backend/internal/repository/usage_log_repo.go
View file @
0484c59e
...
@@ -431,68 +431,6 @@ type UserUsageTrendPoint struct {
...
@@ -431,68 +431,6 @@ type UserUsageTrendPoint struct {
ActualCost
float64
`json:"actual_cost"`
// 实际扣除
ActualCost
float64
`json:"actual_cost"`
// 实际扣除
}
}
// GetUsageTrend returns usage trend data grouped by date
// granularity: "day" or "hour"
func
(
r
*
UsageLogRepository
)
GetUsageTrend
(
ctx
context
.
Context
,
startTime
,
endTime
time
.
Time
,
granularity
string
)
([]
TrendDataPoint
,
error
)
{
var
results
[]
TrendDataPoint
// Choose date format based on granularity
var
dateFormat
string
if
granularity
==
"hour"
{
dateFormat
=
"YYYY-MM-DD HH24:00"
}
else
{
dateFormat
=
"YYYY-MM-DD"
}
err
:=
r
.
db
.
WithContext
(
ctx
)
.
Model
(
&
model
.
UsageLog
{})
.
Select
(
`
TO_CHAR(created_at, ?) as date,
COUNT(*) as requests,
COALESCE(SUM(input_tokens), 0) as input_tokens,
COALESCE(SUM(output_tokens), 0) as output_tokens,
COALESCE(SUM(cache_creation_tokens + cache_read_tokens), 0) as cache_tokens,
COALESCE(SUM(input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens), 0) as total_tokens,
COALESCE(SUM(total_cost), 0) as cost,
COALESCE(SUM(actual_cost), 0) as actual_cost
`
,
dateFormat
)
.
Where
(
"created_at >= ? AND created_at < ?"
,
startTime
,
endTime
)
.
Group
(
"date"
)
.
Order
(
"date ASC"
)
.
Scan
(
&
results
)
.
Error
if
err
!=
nil
{
return
nil
,
err
}
return
results
,
nil
}
// GetModelStats returns usage statistics grouped by model
func
(
r
*
UsageLogRepository
)
GetModelStats
(
ctx
context
.
Context
,
startTime
,
endTime
time
.
Time
)
([]
ModelStat
,
error
)
{
var
results
[]
ModelStat
err
:=
r
.
db
.
WithContext
(
ctx
)
.
Model
(
&
model
.
UsageLog
{})
.
Select
(
`
model,
COUNT(*) as requests,
COALESCE(SUM(input_tokens), 0) as input_tokens,
COALESCE(SUM(output_tokens), 0) as output_tokens,
COALESCE(SUM(input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens), 0) as total_tokens,
COALESCE(SUM(total_cost), 0) as cost,
COALESCE(SUM(actual_cost), 0) as actual_cost
`
)
.
Where
(
"created_at >= ? AND created_at < ?"
,
startTime
,
endTime
)
.
Group
(
"model"
)
.
Order
(
"total_tokens DESC"
)
.
Scan
(
&
results
)
.
Error
if
err
!=
nil
{
return
nil
,
err
}
return
results
,
nil
}
// ApiKeyUsageTrendPoint represents API key usage trend data point
// ApiKeyUsageTrendPoint represents API key usage trend data point
type
ApiKeyUsageTrendPoint
struct
{
type
ApiKeyUsageTrendPoint
struct
{
Date
string
`json:"date"`
Date
string
`json:"date"`
...
@@ -959,6 +897,76 @@ func (r *UsageLogRepository) GetBatchApiKeyUsageStats(ctx context.Context, apiKe
...
@@ -959,6 +897,76 @@ func (r *UsageLogRepository) GetBatchApiKeyUsageStats(ctx context.Context, apiKe
return
result
,
nil
return
result
,
nil
}
}
// GetUsageTrendWithFilters returns usage trend data with optional user/api_key filters
func
(
r
*
UsageLogRepository
)
GetUsageTrendWithFilters
(
ctx
context
.
Context
,
startTime
,
endTime
time
.
Time
,
granularity
string
,
userID
,
apiKeyID
int64
)
([]
TrendDataPoint
,
error
)
{
var
results
[]
TrendDataPoint
var
dateFormat
string
if
granularity
==
"hour"
{
dateFormat
=
"YYYY-MM-DD HH24:00"
}
else
{
dateFormat
=
"YYYY-MM-DD"
}
db
:=
r
.
db
.
WithContext
(
ctx
)
.
Model
(
&
model
.
UsageLog
{})
.
Select
(
`
TO_CHAR(created_at, ?) as date,
COUNT(*) as requests,
COALESCE(SUM(input_tokens), 0) as input_tokens,
COALESCE(SUM(output_tokens), 0) as output_tokens,
COALESCE(SUM(cache_creation_tokens + cache_read_tokens), 0) as cache_tokens,
COALESCE(SUM(input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens), 0) as total_tokens,
COALESCE(SUM(total_cost), 0) as cost,
COALESCE(SUM(actual_cost), 0) as actual_cost
`
,
dateFormat
)
.
Where
(
"created_at >= ? AND created_at < ?"
,
startTime
,
endTime
)
if
userID
>
0
{
db
=
db
.
Where
(
"user_id = ?"
,
userID
)
}
if
apiKeyID
>
0
{
db
=
db
.
Where
(
"api_key_id = ?"
,
apiKeyID
)
}
err
:=
db
.
Group
(
"date"
)
.
Order
(
"date ASC"
)
.
Scan
(
&
results
)
.
Error
if
err
!=
nil
{
return
nil
,
err
}
return
results
,
nil
}
// GetModelStatsWithFilters returns model statistics with optional user/api_key filters
func
(
r
*
UsageLogRepository
)
GetModelStatsWithFilters
(
ctx
context
.
Context
,
startTime
,
endTime
time
.
Time
,
userID
,
apiKeyID
int64
)
([]
ModelStat
,
error
)
{
var
results
[]
ModelStat
db
:=
r
.
db
.
WithContext
(
ctx
)
.
Model
(
&
model
.
UsageLog
{})
.
Select
(
`
model,
COUNT(*) as requests,
COALESCE(SUM(input_tokens), 0) as input_tokens,
COALESCE(SUM(output_tokens), 0) as output_tokens,
COALESCE(SUM(input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens), 0) as total_tokens,
COALESCE(SUM(total_cost), 0) as cost,
COALESCE(SUM(actual_cost), 0) as actual_cost
`
)
.
Where
(
"created_at >= ? AND created_at < ?"
,
startTime
,
endTime
)
if
userID
>
0
{
db
=
db
.
Where
(
"user_id = ?"
,
userID
)
}
if
apiKeyID
>
0
{
db
=
db
.
Where
(
"api_key_id = ?"
,
apiKeyID
)
}
err
:=
db
.
Group
(
"model"
)
.
Order
(
"total_tokens DESC"
)
.
Scan
(
&
results
)
.
Error
if
err
!=
nil
{
return
nil
,
err
}
return
results
,
nil
}
// GetGlobalStats gets usage statistics for all users within a time range
// GetGlobalStats gets usage statistics for all users within a time range
func
(
r
*
UsageLogRepository
)
GetGlobalStats
(
ctx
context
.
Context
,
startTime
,
endTime
time
.
Time
)
(
*
UsageStats
,
error
)
{
func
(
r
*
UsageLogRepository
)
GetGlobalStats
(
ctx
context
.
Context
,
startTime
,
endTime
time
.
Time
)
(
*
UsageStats
,
error
)
{
var
stats
struct
{
var
stats
struct
{
...
...
frontend/src/api/admin/dashboard.ts
View file @
0484c59e
...
@@ -38,6 +38,8 @@ export interface TrendParams {
...
@@ -38,6 +38,8 @@ export interface TrendParams {
start_date
?:
string
;
start_date
?:
string
;
end_date
?:
string
;
end_date
?:
string
;
granularity
?:
'
day
'
|
'
hour
'
;
granularity
?:
'
day
'
|
'
hour
'
;
user_id
?:
number
;
api_key_id
?:
number
;
}
}
export
interface
TrendResponse
{
export
interface
TrendResponse
{
...
@@ -57,6 +59,13 @@ export async function getUsageTrend(params?: TrendParams): Promise<TrendResponse
...
@@ -57,6 +59,13 @@ export async function getUsageTrend(params?: TrendParams): Promise<TrendResponse
return
data
;
return
data
;
}
}
export
interface
ModelStatsParams
{
start_date
?:
string
;
end_date
?:
string
;
user_id
?:
number
;
api_key_id
?:
number
;
}
export
interface
ModelStatsResponse
{
export
interface
ModelStatsResponse
{
models
:
ModelStat
[];
models
:
ModelStat
[];
start_date
:
string
;
start_date
:
string
;
...
@@ -68,7 +77,7 @@ export interface ModelStatsResponse {
...
@@ -68,7 +77,7 @@ export interface ModelStatsResponse {
* @param params - Query parameters for filtering
* @param params - Query parameters for filtering
* @returns Model usage statistics
* @returns Model usage statistics
*/
*/
export
async
function
getModelStats
(
params
?:
{
start_date
?:
string
;
end_date
?:
string
}
):
Promise
<
ModelStatsResponse
>
{
export
async
function
getModelStats
(
params
?:
ModelStatsParams
):
Promise
<
ModelStatsResponse
>
{
const
{
data
}
=
await
apiClient
.
get
<
ModelStatsResponse
>
(
'
/admin/dashboard/models
'
,
{
params
});
const
{
data
}
=
await
apiClient
.
get
<
ModelStatsResponse
>
(
'
/admin/dashboard/models
'
,
{
params
});
return
data
;
return
data
;
}
}
...
...
frontend/src/components/charts/ModelDistributionChart.vue
0 → 100644
View file @
0484c59e
<
template
>
<div
class=
"card p-4"
>
<h3
class=
"text-sm font-semibold text-gray-900 dark:text-white mb-4"
>
{{
t
(
'
admin.dashboard.modelDistribution
'
)
}}
</h3>
<div
v-if=
"loading"
class=
"flex items-center justify-center h-48"
>
<LoadingSpinner
/>
</div>
<div
v-else-if=
"modelStats.length > 0 && chartData"
class=
"flex items-center gap-6"
>
<div
class=
"w-48 h-48"
>
<Doughnut
:data=
"chartData"
:options=
"doughnutOptions"
/>
</div>
<div
class=
"flex-1 max-h-48 overflow-y-auto"
>
<table
class=
"w-full text-xs"
>
<thead>
<tr
class=
"text-gray-500 dark:text-gray-400"
>
<th
class=
"text-left pb-2"
>
{{
t
(
'
admin.dashboard.model
'
)
}}
</th>
<th
class=
"text-right pb-2"
>
{{
t
(
'
admin.dashboard.requests
'
)
}}
</th>
<th
class=
"text-right pb-2"
>
{{
t
(
'
admin.dashboard.tokens
'
)
}}
</th>
<th
class=
"text-right pb-2"
>
{{
t
(
'
admin.dashboard.actual
'
)
}}
</th>
<th
class=
"text-right pb-2"
>
{{
t
(
'
admin.dashboard.standard
'
)
}}
</th>
</tr>
</thead>
<tbody>
<tr
v-for=
"model in modelStats"
:key=
"model.model"
class=
"border-t border-gray-100 dark:border-gray-700"
>
<td
class=
"py-1.5 text-gray-900 dark:text-white font-medium truncate max-w-[100px]"
:title=
"model.model"
>
{{
model
.
model
}}
</td>
<td
class=
"py-1.5 text-right text-gray-600 dark:text-gray-400"
>
{{
formatNumber
(
model
.
requests
)
}}
</td>
<td
class=
"py-1.5 text-right text-gray-600 dark:text-gray-400"
>
{{
formatTokens
(
model
.
total_tokens
)
}}
</td>
<td
class=
"py-1.5 text-right text-green-600 dark:text-green-400"
>
$
{{
formatCost
(
model
.
actual_cost
)
}}
</td>
<td
class=
"py-1.5 text-right text-gray-400 dark:text-gray-500"
>
$
{{
formatCost
(
model
.
cost
)
}}
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div
v-else
class=
"flex items-center justify-center h-48 text-gray-500 dark:text-gray-400 text-sm"
>
{{
t
(
'
admin.dashboard.noDataAvailable
'
)
}}
</div>
</div>
</
template
>
<
script
setup
lang=
"ts"
>
import
{
computed
}
from
'
vue
'
import
{
useI18n
}
from
'
vue-i18n
'
import
{
Chart
as
ChartJS
,
ArcElement
,
Tooltip
,
Legend
}
from
'
chart.js
'
import
{
Doughnut
}
from
'
vue-chartjs
'
import
LoadingSpinner
from
'
@/components/common/LoadingSpinner.vue
'
import
type
{
ModelStat
}
from
'
@/types
'
ChartJS
.
register
(
ArcElement
,
Tooltip
,
Legend
)
const
{
t
}
=
useI18n
()
const
props
=
defineProps
<
{
modelStats
:
ModelStat
[]
loading
?:
boolean
}
>
()
const
chartColors
=
[
'
#3b82f6
'
,
'
#10b981
'
,
'
#f59e0b
'
,
'
#ef4444
'
,
'
#8b5cf6
'
,
'
#ec4899
'
,
'
#14b8a6
'
,
'
#f97316
'
,
'
#6366f1
'
,
'
#84cc16
'
]
const
chartData
=
computed
(()
=>
{
if
(
!
props
.
modelStats
?.
length
)
return
null
return
{
labels
:
props
.
modelStats
.
map
(
m
=>
m
.
model
),
datasets
:
[{
data
:
props
.
modelStats
.
map
(
m
=>
m
.
total_tokens
),
backgroundColor
:
chartColors
.
slice
(
0
,
props
.
modelStats
.
length
),
borderWidth
:
0
,
}],
}
})
const
doughnutOptions
=
computed
(()
=>
({
responsive
:
true
,
maintainAspectRatio
:
false
,
plugins
:
{
legend
:
{
display
:
false
,
},
tooltip
:
{
callbacks
:
{
label
:
(
context
:
any
)
=>
{
const
value
=
context
.
raw
as
number
const
total
=
context
.
dataset
.
data
.
reduce
((
a
:
number
,
b
:
number
)
=>
a
+
b
,
0
)
const
percentage
=
((
value
/
total
)
*
100
).
toFixed
(
1
)
return
`
${
context
.
label
}
:
${
formatTokens
(
value
)}
(
${
percentage
}
%)`
},
},
},
},
}))
const
formatTokens
=
(
value
:
number
):
string
=>
{
if
(
value
>=
1
_000_000_000
)
{
return
`
${(
value
/
1
_000_000_000
).
toFixed
(
2
)}
B`
}
else
if
(
value
>=
1
_000_000
)
{
return
`
${(
value
/
1
_000_000
).
toFixed
(
2
)}
M`
}
else
if
(
value
>=
1
_000
)
{
return
`
${(
value
/
1
_000
).
toFixed
(
2
)}
K`
}
return
value
.
toLocaleString
()
}
const
formatNumber
=
(
value
:
number
):
string
=>
{
return
value
.
toLocaleString
()
}
const
formatCost
=
(
value
:
number
):
string
=>
{
if
(
value
>=
1000
)
{
return
(
value
/
1000
).
toFixed
(
2
)
+
'
K
'
}
else
if
(
value
>=
1
)
{
return
value
.
toFixed
(
2
)
}
else
if
(
value
>=
0.01
)
{
return
value
.
toFixed
(
3
)
}
return
value
.
toFixed
(
4
)
}
</
script
>
frontend/src/components/charts/TokenUsageTrend.vue
0 → 100644
View file @
0484c59e
<
template
>
<div
class=
"card p-4"
>
<h3
class=
"text-sm font-semibold text-gray-900 dark:text-white mb-4"
>
{{
t
(
'
admin.dashboard.tokenUsageTrend
'
)
}}
</h3>
<div
v-if=
"loading"
class=
"flex items-center justify-center h-48"
>
<LoadingSpinner
/>
</div>
<div
v-else-if=
"trendData.length > 0 && chartData"
class=
"h-48"
>
<Line
:data=
"chartData"
:options=
"lineOptions"
/>
</div>
<div
v-else
class=
"flex items-center justify-center h-48 text-gray-500 dark:text-gray-400 text-sm"
>
{{
t
(
'
admin.dashboard.noDataAvailable
'
)
}}
</div>
</div>
</
template
>
<
script
setup
lang=
"ts"
>
import
{
computed
}
from
'
vue
'
import
{
useI18n
}
from
'
vue-i18n
'
import
{
Chart
as
ChartJS
,
CategoryScale
,
LinearScale
,
PointElement
,
LineElement
,
Title
,
Tooltip
,
Legend
,
Filler
}
from
'
chart.js
'
import
{
Line
}
from
'
vue-chartjs
'
import
LoadingSpinner
from
'
@/components/common/LoadingSpinner.vue
'
import
type
{
TrendDataPoint
}
from
'
@/types
'
ChartJS
.
register
(
CategoryScale
,
LinearScale
,
PointElement
,
LineElement
,
Title
,
Tooltip
,
Legend
,
Filler
)
const
{
t
}
=
useI18n
()
const
props
=
defineProps
<
{
trendData
:
TrendDataPoint
[]
loading
?:
boolean
}
>
()
const
isDarkMode
=
computed
(()
=>
{
return
document
.
documentElement
.
classList
.
contains
(
'
dark
'
)
})
const
chartColors
=
computed
(()
=>
({
text
:
isDarkMode
.
value
?
'
#e5e7eb
'
:
'
#374151
'
,
grid
:
isDarkMode
.
value
?
'
#374151
'
:
'
#e5e7eb
'
,
input
:
'
#3b82f6
'
,
output
:
'
#10b981
'
,
cache
:
'
#f59e0b
'
,
}))
const
chartData
=
computed
(()
=>
{
if
(
!
props
.
trendData
?.
length
)
return
null
return
{
labels
:
props
.
trendData
.
map
(
d
=>
d
.
date
),
datasets
:
[
{
label
:
'
Input
'
,
data
:
props
.
trendData
.
map
(
d
=>
d
.
input_tokens
),
borderColor
:
chartColors
.
value
.
input
,
backgroundColor
:
`
${
chartColors
.
value
.
input
}
20`
,
fill
:
true
,
tension
:
0.3
,
},
{
label
:
'
Output
'
,
data
:
props
.
trendData
.
map
(
d
=>
d
.
output_tokens
),
borderColor
:
chartColors
.
value
.
output
,
backgroundColor
:
`
${
chartColors
.
value
.
output
}
20`
,
fill
:
true
,
tension
:
0.3
,
},
{
label
:
'
Cache
'
,
data
:
props
.
trendData
.
map
(
d
=>
d
.
cache_tokens
),
borderColor
:
chartColors
.
value
.
cache
,
backgroundColor
:
`
${
chartColors
.
value
.
cache
}
20`
,
fill
:
true
,
tension
:
0.3
,
},
],
}
})
const
lineOptions
=
computed
(()
=>
({
responsive
:
true
,
maintainAspectRatio
:
false
,
interaction
:
{
intersect
:
false
,
mode
:
'
index
'
as
const
,
},
plugins
:
{
legend
:
{
position
:
'
top
'
as
const
,
labels
:
{
color
:
chartColors
.
value
.
text
,
usePointStyle
:
true
,
pointStyle
:
'
circle
'
,
padding
:
15
,
font
:
{
size
:
11
,
},
},
},
tooltip
:
{
callbacks
:
{
label
:
(
context
:
any
)
=>
{
return
`
${
context
.
dataset
.
label
}
:
${
formatTokens
(
context
.
raw
)}
`
},
footer
:
(
tooltipItems
:
any
)
=>
{
const
dataIndex
=
tooltipItems
[
0
]?.
dataIndex
if
(
dataIndex
!==
undefined
&&
props
.
trendData
[
dataIndex
])
{
const
data
=
props
.
trendData
[
dataIndex
]
return
`Actual: $
${
formatCost
(
data
.
actual_cost
)}
| Standard: $
${
formatCost
(
data
.
cost
)}
`
}
return
''
},
},
},
},
scales
:
{
x
:
{
grid
:
{
color
:
chartColors
.
value
.
grid
,
},
ticks
:
{
color
:
chartColors
.
value
.
text
,
font
:
{
size
:
10
,
},
},
},
y
:
{
grid
:
{
color
:
chartColors
.
value
.
grid
,
},
ticks
:
{
color
:
chartColors
.
value
.
text
,
font
:
{
size
:
10
,
},
callback
:
(
value
:
string
|
number
)
=>
formatTokens
(
Number
(
value
)),
},
},
},
}))
const
formatTokens
=
(
value
:
number
):
string
=>
{
if
(
value
>=
1
_000_000_000
)
{
return
`
${(
value
/
1
_000_000_000
).
toFixed
(
2
)}
B`
}
else
if
(
value
>=
1
_000_000
)
{
return
`
${(
value
/
1
_000_000
).
toFixed
(
2
)}
M`
}
else
if
(
value
>=
1
_000
)
{
return
`
${(
value
/
1
_000
).
toFixed
(
2
)}
K`
}
return
value
.
toLocaleString
()
}
const
formatCost
=
(
value
:
number
):
string
=>
{
if
(
value
>=
1000
)
{
return
(
value
/
1000
).
toFixed
(
2
)
+
'
K
'
}
else
if
(
value
>=
1
)
{
return
value
.
toFixed
(
2
)
}
else
if
(
value
>=
0.01
)
{
return
value
.
toFixed
(
3
)
}
return
value
.
toFixed
(
4
)
}
</
script
>
frontend/src/views/admin/DashboardView.vue
View file @
0484c59e
...
@@ -180,51 +180,14 @@
...
@@ -180,51 +180,14 @@
<!-- 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"
>
<!-- Model Distribution Chart -->
<ModelDistributionChart
<div
class=
"card p-4"
>
:model-stats=
"modelStats"
<h3
class=
"text-sm font-semibold text-gray-900 dark:text-white mb-4"
>
{{
t
(
'
admin.dashboard.modelDistribution
'
)
}}
</h3>
:loading=
"chartsLoading"
<div
class=
"flex items-center gap-6"
>
/>
<div
class=
"w-48 h-48"
>
<TokenUsageTrend
<Doughnut
v-if=
"modelChartData"
:data=
"modelChartData"
:options=
"doughnutOptions"
/>
:trend-data=
"trendData"
<div
v-else
class=
"flex items-center justify-center h-full text-gray-500 dark:text-gray-400 text-sm"
>
:loading=
"chartsLoading"
{{
t
(
'
admin.dashboard.noDataAvailable
'
)
}}
/>
</div>
</div>
<div
class=
"flex-1 max-h-48 overflow-y-auto"
>
<table
class=
"w-full text-xs"
>
<thead>
<tr
class=
"text-gray-500 dark:text-gray-400"
>
<th
class=
"text-left pb-2"
>
{{
t
(
'
admin.dashboard.model
'
)
}}
</th>
<th
class=
"text-right pb-2"
>
{{
t
(
'
admin.dashboard.requests
'
)
}}
</th>
<th
class=
"text-right pb-2"
>
{{
t
(
'
admin.dashboard.tokens
'
)
}}
</th>
<th
class=
"text-right pb-2"
>
{{
t
(
'
admin.dashboard.actual
'
)
}}
</th>
<th
class=
"text-right pb-2"
>
{{
t
(
'
admin.dashboard.standard
'
)
}}
</th>
</tr>
</thead>
<tbody>
<tr
v-for=
"model in modelStats"
:key=
"model.model"
class=
"border-t border-gray-100 dark:border-gray-700"
>
<td
class=
"py-1.5 text-gray-900 dark:text-white font-medium truncate max-w-[100px]"
:title=
"model.model"
>
{{
model
.
model
}}
</td>
<td
class=
"py-1.5 text-right text-gray-600 dark:text-gray-400"
>
{{
formatNumber
(
model
.
requests
)
}}
</td>
<td
class=
"py-1.5 text-right text-gray-600 dark:text-gray-400"
>
{{
formatTokens
(
model
.
total_tokens
)
}}
</td>
<td
class=
"py-1.5 text-right text-green-600 dark:text-green-400"
>
$
{{
formatCost
(
model
.
actual_cost
)
}}
</td>
<td
class=
"py-1.5 text-right text-gray-400 dark:text-gray-500"
>
$
{{
formatCost
(
model
.
cost
)
}}
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Token Usage Trend Chart -->
<div
class=
"card p-4"
>
<h3
class=
"text-sm font-semibold text-gray-900 dark:text-white mb-4"
>
{{
t
(
'
admin.dashboard.tokenUsageTrend
'
)
}}
</h3>
<div
class=
"h-48"
>
<Line
v-if=
"trendChartData"
:data=
"trendChartData"
:options=
"lineOptions"
/>
<div
v-else
class=
"flex items-center justify-center h-full text-gray-500 dark:text-gray-400 text-sm"
>
{{
t
(
'
admin.dashboard.noDataAvailable
'
)
}}
</div>
</div>
</div>
</div>
</div>
<!-- User Usage Trend (Full Width) -->
<!-- User Usage Trend (Full Width) -->
...
@@ -244,7 +207,7 @@
...
@@ -244,7 +207,7 @@
</template>
</template>
<
script
setup
lang=
"ts"
>
<
script
setup
lang=
"ts"
>
import
{
ref
,
computed
,
onMounted
,
watch
}
from
'
vue
'
import
{
ref
,
computed
,
onMounted
}
from
'
vue
'
import
{
useI18n
}
from
'
vue-i18n
'
import
{
useI18n
}
from
'
vue-i18n
'
import
{
useAppStore
}
from
'
@/stores/app
'
import
{
useAppStore
}
from
'
@/stores/app
'
...
@@ -255,6 +218,8 @@ import AppLayout from '@/components/layout/AppLayout.vue'
...
@@ -255,6 +218,8 @@ import AppLayout from '@/components/layout/AppLayout.vue'
import
LoadingSpinner
from
'
@/components/common/LoadingSpinner.vue
'
import
LoadingSpinner
from
'
@/components/common/LoadingSpinner.vue
'
import
DateRangePicker
from
'
@/components/common/DateRangePicker.vue
'
import
DateRangePicker
from
'
@/components/common/DateRangePicker.vue
'
import
Select
from
'
@/components/common/Select.vue
'
import
Select
from
'
@/components/common/Select.vue
'
import
ModelDistributionChart
from
'
@/components/charts/ModelDistributionChart.vue
'
import
TokenUsageTrend
from
'
@/components/charts/TokenUsageTrend.vue
'
import
{
import
{
Chart
as
ChartJS
,
Chart
as
ChartJS
,
...
@@ -262,13 +227,12 @@ import {
...
@@ -262,13 +227,12 @@ import {
LinearScale
,
LinearScale
,
PointElement
,
PointElement
,
LineElement
,
LineElement
,
ArcElement
,
Title
,
Title
,
Tooltip
,
Tooltip
,
Legend
,
Legend
,
Filler
Filler
}
from
'
chart.js
'
}
from
'
chart.js
'
import
{
Line
,
Doughnut
}
from
'
vue-chartjs
'
import
{
Line
}
from
'
vue-chartjs
'
// Register Chart.js components
// Register Chart.js components
ChartJS
.
register
(
ChartJS
.
register
(
...
@@ -276,7 +240,6 @@ ChartJS.register(
...
@@ -276,7 +240,6 @@ ChartJS.register(
LinearScale
,
LinearScale
,
PointElement
,
PointElement
,
LineElement
,
LineElement
,
ArcElement
,
Title
,
Title
,
Tooltip
,
Tooltip
,
Legend
,
Legend
,
...
@@ -286,6 +249,7 @@ ChartJS.register(
...
@@ -286,6 +249,7 @@ ChartJS.register(
const
appStore
=
useAppStore
()
const
appStore
=
useAppStore
()
const
stats
=
ref
<
DashboardStats
|
null
>
(
null
)
const
stats
=
ref
<
DashboardStats
|
null
>
(
null
)
const
loading
=
ref
(
false
)
const
loading
=
ref
(
false
)
const
chartsLoading
=
ref
(
false
)
// Chart data
// Chart data
const
trendData
=
ref
<
TrendDataPoint
[]
>
([])
const
trendData
=
ref
<
TrendDataPoint
[]
>
([])
...
@@ -312,34 +276,9 @@ const isDarkMode = computed(() => {
...
@@ -312,34 +276,9 @@ const isDarkMode = computed(() => {
const
chartColors
=
computed
(()
=>
({
const
chartColors
=
computed
(()
=>
({
text
:
isDarkMode
.
value
?
'
#e5e7eb
'
:
'
#374151
'
,
text
:
isDarkMode
.
value
?
'
#e5e7eb
'
:
'
#374151
'
,
grid
:
isDarkMode
.
value
?
'
#374151
'
:
'
#e5e7eb
'
,
grid
:
isDarkMode
.
value
?
'
#374151
'
:
'
#e5e7eb
'
,
input
:
'
#3b82f6
'
,
output
:
'
#10b981
'
,
cache
:
'
#f59e0b
'
,
total
:
'
#8b5cf6
'
,
}))
// Doughnut chart options
const
doughnutOptions
=
computed
(()
=>
({
responsive
:
true
,
maintainAspectRatio
:
false
,
plugins
:
{
legend
:
{
display
:
false
,
},
tooltip
:
{
callbacks
:
{
label
:
(
context
:
any
)
=>
{
const
value
=
context
.
raw
as
number
const
total
=
context
.
dataset
.
data
.
reduce
((
a
:
number
,
b
:
number
)
=>
a
+
b
,
0
)
const
percentage
=
((
value
/
total
)
*
100
).
toFixed
(
1
)
return
`
${
context
.
label
}
:
${
formatTokens
(
value
)}
(
${
percentage
}
%)`
},
},
},
},
}))
}))
// Line chart options
// Line chart options
(for user trend chart)
const
lineOptions
=
computed
(()
=>
({
const
lineOptions
=
computed
(()
=>
({
responsive
:
true
,
responsive
:
true
,
maintainAspectRatio
:
false
,
maintainAspectRatio
:
false
,
...
@@ -365,15 +304,6 @@ const lineOptions = computed(() => ({
...
@@ -365,15 +304,6 @@ const lineOptions = computed(() => ({
label
:
(
context
:
any
)
=>
{
label
:
(
context
:
any
)
=>
{
return
`
${
context
.
dataset
.
label
}
:
${
formatTokens
(
context
.
raw
)}
`
return
`
${
context
.
dataset
.
label
}
:
${
formatTokens
(
context
.
raw
)}
`
},
},
footer
:
(
tooltipItems
:
any
)
=>
{
// Show both costs for the day if we have trend data
const
dataIndex
=
tooltipItems
[
0
]?.
dataIndex
if
(
dataIndex
!==
undefined
&&
trendData
.
value
[
dataIndex
])
{
const
data
=
trendData
.
value
[
dataIndex
]
return
`Actual: $
${
formatCost
(
data
.
actual_cost
)}
| Standard: $
${
formatCost
(
data
.
cost
)}
`
}
return
''
},
},
},
},
},
},
},
...
@@ -404,60 +334,6 @@ const lineOptions = computed(() => ({
...
@@ -404,60 +334,6 @@ const lineOptions = computed(() => ({
},
},
}))
}))
// Model chart data
const
modelChartData
=
computed
(()
=>
{
if
(
!
modelStats
.
value
?.
length
)
return
null
const
colors
=
[
'
#3b82f6
'
,
'
#10b981
'
,
'
#f59e0b
'
,
'
#ef4444
'
,
'
#8b5cf6
'
,
'
#ec4899
'
,
'
#14b8a6
'
,
'
#f97316
'
,
'
#6366f1
'
,
'
#84cc16
'
]
return
{
labels
:
modelStats
.
value
.
map
(
m
=>
m
.
model
),
datasets
:
[{
data
:
modelStats
.
value
.
map
(
m
=>
m
.
total_tokens
),
backgroundColor
:
colors
.
slice
(
0
,
modelStats
.
value
.
length
),
borderWidth
:
0
,
}],
}
})
// Trend chart data
const
trendChartData
=
computed
(()
=>
{
if
(
!
trendData
.
value
?.
length
)
return
null
return
{
labels
:
trendData
.
value
.
map
(
d
=>
d
.
date
),
datasets
:
[
{
label
:
'
Input
'
,
data
:
trendData
.
value
.
map
(
d
=>
d
.
input_tokens
),
borderColor
:
chartColors
.
value
.
input
,
backgroundColor
:
`
${
chartColors
.
value
.
input
}
20`
,
fill
:
true
,
tension
:
0.3
,
},
{
label
:
'
Output
'
,
data
:
trendData
.
value
.
map
(
d
=>
d
.
output_tokens
),
borderColor
:
chartColors
.
value
.
output
,
backgroundColor
:
`
${
chartColors
.
value
.
output
}
20`
,
fill
:
true
,
tension
:
0.3
,
},
{
label
:
'
Cache
'
,
data
:
trendData
.
value
.
map
(
d
=>
d
.
cache_tokens
),
borderColor
:
chartColors
.
value
.
cache
,
backgroundColor
:
`
${
chartColors
.
value
.
cache
}
20`
,
fill
:
true
,
tension
:
0.3
,
},
],
}
})
// User trend chart data
// User trend chart data
const
userTrendChartData
=
computed
(()
=>
{
const
userTrendChartData
=
computed
(()
=>
{
if
(
!
userTrend
.
value
?.
length
)
return
null
if
(
!
userTrend
.
value
?.
length
)
return
null
...
@@ -578,6 +454,7 @@ const loadDashboardStats = async () => {
...
@@ -578,6 +454,7 @@ const loadDashboardStats = async () => {
}
}
const
loadChartData
=
async
()
=>
{
const
loadChartData
=
async
()
=>
{
chartsLoading
.
value
=
true
try
{
try
{
const
params
=
{
const
params
=
{
start_date
:
startDate
.
value
,
start_date
:
startDate
.
value
,
...
@@ -596,6 +473,8 @@ const loadChartData = async () => {
...
@@ -596,6 +473,8 @@ const loadChartData = async () => {
userTrend
.
value
=
userResponse
.
trend
||
[]
userTrend
.
value
=
userResponse
.
trend
||
[]
}
catch
(
error
)
{
}
catch
(
error
)
{
console
.
error
(
'
Error loading chart data:
'
,
error
)
console
.
error
(
'
Error loading chart data:
'
,
error
)
}
finally
{
chartsLoading
.
value
=
false
}
}
}
}
...
@@ -604,11 +483,6 @@ onMounted(() => {
...
@@ -604,11 +483,6 @@ onMounted(() => {
initializeDateRange
()
initializeDateRange
()
loadChartData
()
loadChartData
()
})
})
// Watch for dark mode changes
watch
(
isDarkMode
,
()
=>
{
// Force chart re-render on theme change
})
</
script
>
</
script
>
<
style
scoped
>
<
style
scoped
>
...
...
frontend/src/views/admin/UsageView.vue
View file @
0484c59e
...
@@ -47,7 +47,7 @@
...
@@ -47,7 +47,7 @@
<p
class=
"text-xs font-medium text-gray-500 dark:text-gray-400"
>
{{
t
(
'
usage.totalCost
'
)
}}
</p>
<p
class=
"text-xs font-medium text-gray-500 dark:text-gray-400"
>
{{
t
(
'
usage.totalCost
'
)
}}
</p>
<p
class=
"text-xl font-bold text-green-600 dark:text-green-400"
>
$
{{
(
usageStats
?.
total_actual_cost
||
0
).
toFixed
(
4
)
}}
</p>
<p
class=
"text-xl font-bold text-green-600 dark:text-green-400"
>
$
{{
(
usageStats
?.
total_actual_cost
||
0
).
toFixed
(
4
)
}}
</p>
<p
class=
"text-xs text-gray-500 dark:text-gray-400"
>
<p
class=
"text-xs text-gray-500 dark:text-gray-400"
>
{{
t
(
'
usage.actualCost
'
)
}}
/
<span
class=
"line-through"
>
$
{{
(
usageStats
?.
total_cost
||
0
).
toFixed
(
4
)
}}
</span>
{{
t
(
'
usage.standardCost
'
)
}}
<span
class=
"line-through"
>
$
{{
(
usageStats
?.
total_cost
||
0
).
toFixed
(
4
)
}}
</span>
{{
t
(
'
usage.standardCost
'
)
}}
</p>
</p>
</div>
</div>
</div>
</div>
...
@@ -70,6 +70,35 @@
...
@@ -70,6 +70,35 @@
</div>
</div>
</div>
</div>
<!-- Charts Section -->
<div
class=
"space-y-4"
>
<!-- Chart Controls -->
<div
class=
"card p-4"
>
<div
class=
"flex items-center gap-4"
>
<span
class=
"text-sm font-medium text-gray-700 dark:text-gray-300"
>
{{
t
(
'
admin.dashboard.granularity
'
)
}}
:
</span>
<div
class=
"w-28"
>
<Select
v-model=
"granularity"
:options=
"granularityOptions"
@
change=
"onGranularityChange"
/>
</div>
</div>
</div>
<!-- Charts Grid -->
<div
class=
"grid grid-cols-1 gap-6 lg:grid-cols-2"
>
<ModelDistributionChart
:model-stats=
"modelStats"
:loading=
"chartsLoading"
/>
<TokenUsageTrend
:trend-data=
"trendData"
:loading=
"chartsLoading"
/>
</div>
</div>
<!-- Filters -->
<!-- Filters -->
<div
class=
"card"
>
<div
class=
"card"
>
<div
class=
"px-6 py-4"
>
<div
class=
"px-6 py-4"
>
...
@@ -324,7 +353,9 @@ import Pagination from '@/components/common/Pagination.vue'
...
@@ -324,7 +353,9 @@ import Pagination from '@/components/common/Pagination.vue'
import
EmptyState
from
'
@/components/common/EmptyState.vue
'
import
EmptyState
from
'
@/components/common/EmptyState.vue
'
import
Select
from
'
@/components/common/Select.vue
'
import
Select
from
'
@/components/common/Select.vue
'
import
DateRangePicker
from
'
@/components/common/DateRangePicker.vue
'
import
DateRangePicker
from
'
@/components/common/DateRangePicker.vue
'
import
type
{
UsageLog
}
from
'
@/types
'
import
ModelDistributionChart
from
'
@/components/charts/ModelDistributionChart.vue
'
import
TokenUsageTrend
from
'
@/components/charts/TokenUsageTrend.vue
'
import
type
{
UsageLog
,
TrendDataPoint
,
ModelStat
}
from
'
@/types
'
import
type
{
Column
}
from
'
@/components/common/types
'
import
type
{
Column
}
from
'
@/components/common/types
'
import
type
{
SimpleUser
,
SimpleApiKey
,
AdminUsageStatsResponse
,
AdminUsageQueryParams
}
from
'
@/api/admin/usage
'
import
type
{
SimpleUser
,
SimpleApiKey
,
AdminUsageStatsResponse
,
AdminUsageQueryParams
}
from
'
@/api/admin/usage
'
...
@@ -334,6 +365,18 @@ const appStore = useAppStore()
...
@@ -334,6 +365,18 @@ const appStore = useAppStore()
// Usage stats from API
// Usage stats from API
const
usageStats
=
ref
<
AdminUsageStatsResponse
|
null
>
(
null
)
const
usageStats
=
ref
<
AdminUsageStatsResponse
|
null
>
(
null
)
// Chart data
const
trendData
=
ref
<
TrendDataPoint
[]
>
([])
const
modelStats
=
ref
<
ModelStat
[]
>
([])
const
chartsLoading
=
ref
(
false
)
const
granularity
=
ref
<
'
day
'
|
'
hour
'
>
(
'
day
'
)
// Granularity options for Select component
const
granularityOptions
=
computed
(()
=>
[
{
value
:
'
day
'
,
label
:
t
(
'
admin.dashboard.day
'
)
},
{
value
:
'
hour
'
,
label
:
t
(
'
admin.dashboard.hour
'
)
},
])
const
columns
=
computed
<
Column
[]
>
(()
=>
[
const
columns
=
computed
<
Column
[]
>
(()
=>
[
{
key
:
'
user
'
,
label
:
t
(
'
admin.usage.user
'
),
sortable
:
false
},
{
key
:
'
user
'
,
label
:
t
(
'
admin.usage.user
'
),
sortable
:
false
},
{
key
:
'
api_key
'
,
label
:
t
(
'
usage.apiKeyFilter
'
),
sortable
:
false
},
{
key
:
'
api_key
'
,
label
:
t
(
'
usage.apiKeyFilter
'
),
sortable
:
false
},
...
@@ -535,10 +578,45 @@ const loadUsageStats = async () => {
...
@@ -535,10 +578,45 @@ const loadUsageStats = async () => {
}
}
}
}
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
,
api_key_id
:
filters
.
value
.
api_key_id
?
Number
(
filters
.
value
.
api_key_id
)
:
undefined
,
}
const
[
trendResponse
,
modelResponse
]
=
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
,
api_key_id
:
params
.
api_key_id
,
}),
])
trendData
.
value
=
trendResponse
.
trend
||
[]
modelStats
.
value
=
modelResponse
.
models
||
[]
}
catch
(
error
)
{
console
.
error
(
'
Failed to load chart data:
'
,
error
)
}
finally
{
chartsLoading
.
value
=
false
}
}
const
onGranularityChange
=
()
=>
{
loadChartData
()
}
const
applyFilters
=
()
=>
{
const
applyFilters
=
()
=>
{
pagination
.
value
.
page
=
1
pagination
.
value
.
page
=
1
loadUsageLogs
()
loadUsageLogs
()
loadUsageStats
()
loadUsageStats
()
loadChartData
()
}
}
const
resetFilters
=
()
=>
{
const
resetFilters
=
()
=>
{
...
@@ -552,11 +630,13 @@ const resetFilters = () => {
...
@@ -552,11 +630,13 @@ const resetFilters = () => {
start_date
:
undefined
,
start_date
:
undefined
,
end_date
:
undefined
end_date
:
undefined
}
}
granularity
.
value
=
'
day
'
// Reset date range to default (last 7 days)
// Reset date range to default (last 7 days)
initializeDateRange
()
initializeDateRange
()
pagination
.
value
.
page
=
1
pagination
.
value
.
page
=
1
loadUsageLogs
()
loadUsageLogs
()
loadUsageStats
()
loadUsageStats
()
loadChartData
()
}
}
const
handlePageChange
=
(
page
:
number
)
=>
{
const
handlePageChange
=
(
page
:
number
)
=>
{
...
@@ -614,6 +694,7 @@ onMounted(() => {
...
@@ -614,6 +694,7 @@ onMounted(() => {
initializeDateRange
()
initializeDateRange
()
loadUsageLogs
()
loadUsageLogs
()
loadUsageStats
()
loadUsageStats
()
loadChartData
()
document
.
addEventListener
(
'
click
'
,
handleClickOutside
)
document
.
addEventListener
(
'
click
'
,
handleClickOutside
)
})
})
...
...
frontend/tsconfig.tsbuildinfo
View file @
0484c59e
{"root":["./src/main.ts","./src/vite-env.d.ts","./src/api/auth.ts","./src/api/client.ts","./src/api/groups.ts","./src/api/index.ts","./src/api/keys.ts","./src/api/redeem.ts","./src/api/setup.ts","./src/api/subscriptions.ts","./src/api/usage.ts","./src/api/user.ts","./src/api/admin/accounts.ts","./src/api/admin/dashboard.ts","./src/api/admin/groups.ts","./src/api/admin/index.ts","./src/api/admin/proxies.ts","./src/api/admin/redeem.ts","./src/api/admin/settings.ts","./src/api/admin/subscriptions.ts","./src/api/admin/system.ts","./src/api/admin/usage.ts","./src/api/admin/users.ts","./src/components/account/index.ts","./src/components/common/index.ts","./src/components/common/types.ts","./src/components/layout/index.ts","./src/composables/useAccountOAuth.ts","./src/composables/useClipboard.ts","./src/i18n/index.ts","./src/i18n/locales/en.ts","./src/i18n/locales/zh.ts","./src/router/index.ts","./src/router/meta.d.ts","./src/stores/app.ts","./src/stores/auth.ts","./src/stores/index.ts","./src/types/index.ts","./src/utils/format.ts","./src/views/auth/index.ts","./src/App.vue","./src/components/TurnstileWidget.vue","./src/components/account/AccountStatusIndicator.vue","./src/components/account/AccountTestModal.vue","./src/components/account/AccountTodayStatsCell.vue","./src/components/account/AccountUsageCell.vue","./src/components/account/CreateAccountModal.vue","./src/components/account/EditAccountModal.vue","./src/components/account/OAuthAuthorizationFlow.vue","./src/components/account/ReAuthAccountModal.vue","./src/components/account/SetupTokenTimeWindow.vue","./src/components/account/UsageProgressBar.vue","./src/components/common/ConfirmDialog.vue","./src/components/common/DataTable.vue","./src/components/common/DateRangePicker.vue","./src/components/common/EmptyState.vue","./src/components/common/GroupBadge.vue","./src/components/common/GroupSelector.vue","./src/components/common/LoadingSpinner.vue","./src/components/common/LocaleSwitcher.vue","./src/components/common/Modal.vue","./src/components/common/Pagination.vue","./src/components/common/ProxySelector.vue","./src/components/common/Select.vue","./src/components/common/StatCard.vue","./src/components/common/SubscriptionProgressMini.vue","./src/components/common/Toast.vue","./src/components/common/Toggle.vue","./src/components/common/VersionBadge.vue","./src/components/keys/UseKeyModal.vue","./src/components/layout/AppHeader.vue","./src/components/layout/AppLayout.vue","./src/components/layout/AppSidebar.vue","./src/components/layout/AuthLayout.vue","./src/views/HomeView.vue","./src/views/NotFoundView.vue","./src/views/admin/AccountsView.vue","./src/views/admin/DashboardView.vue","./src/views/admin/GroupsView.vue","./src/views/admin/ProxiesView.vue","./src/views/admin/RedeemView.vue","./src/views/admin/SettingsView.vue","./src/views/admin/SubscriptionsView.vue","./src/views/admin/UsageView.vue","./src/views/admin/UsersView.vue","./src/views/auth/EmailVerifyView.vue","./src/views/auth/LoginView.vue","./src/views/auth/RegisterView.vue","./src/views/setup/SetupWizardView.vue","./src/views/user/DashboardView.vue","./src/views/user/KeysView.vue","./src/views/user/ProfileView.vue","./src/views/user/RedeemView.vue","./src/views/user/SubscriptionsView.vue","./src/views/user/UsageView.vue"],"version":"5.6.3"}
{"root":["./src/main.ts","./src/vite-env.d.ts","./src/api/auth.ts","./src/api/client.ts","./src/api/groups.ts","./src/api/index.ts","./src/api/keys.ts","./src/api/redeem.ts","./src/api/setup.ts","./src/api/subscriptions.ts","./src/api/usage.ts","./src/api/user.ts","./src/api/admin/accounts.ts","./src/api/admin/dashboard.ts","./src/api/admin/groups.ts","./src/api/admin/index.ts","./src/api/admin/proxies.ts","./src/api/admin/redeem.ts","./src/api/admin/settings.ts","./src/api/admin/subscriptions.ts","./src/api/admin/system.ts","./src/api/admin/usage.ts","./src/api/admin/users.ts","./src/components/account/index.ts","./src/components/common/index.ts","./src/components/common/types.ts","./src/components/layout/index.ts","./src/composables/useAccountOAuth.ts","./src/composables/useClipboard.ts","./src/i18n/index.ts","./src/i18n/locales/en.ts","./src/i18n/locales/zh.ts","./src/router/index.ts","./src/router/meta.d.ts","./src/stores/app.ts","./src/stores/auth.ts","./src/stores/index.ts","./src/types/index.ts","./src/utils/format.ts","./src/views/auth/index.ts","./src/App.vue","./src/components/TurnstileWidget.vue","./src/components/account/AccountStatusIndicator.vue","./src/components/account/AccountTestModal.vue","./src/components/account/AccountTodayStatsCell.vue","./src/components/account/AccountUsageCell.vue","./src/components/account/CreateAccountModal.vue","./src/components/account/EditAccountModal.vue","./src/components/account/OAuthAuthorizationFlow.vue","./src/components/account/ReAuthAccountModal.vue","./src/components/account/SetupTokenTimeWindow.vue","./src/components/account/UsageProgressBar.vue","./src/components/charts/ModelDistributionChart.vue","./src/components/charts/TokenUsageTrend.vue","./src/components/common/ConfirmDialog.vue","./src/components/common/DataTable.vue","./src/components/common/DateRangePicker.vue","./src/components/common/EmptyState.vue","./src/components/common/GroupBadge.vue","./src/components/common/GroupSelector.vue","./src/components/common/LoadingSpinner.vue","./src/components/common/LocaleSwitcher.vue","./src/components/common/Modal.vue","./src/components/common/Pagination.vue","./src/components/common/ProxySelector.vue","./src/components/common/Select.vue","./src/components/common/StatCard.vue","./src/components/common/SubscriptionProgressMini.vue","./src/components/common/Toast.vue","./src/components/common/Toggle.vue","./src/components/common/VersionBadge.vue","./src/components/keys/UseKeyModal.vue","./src/components/layout/AppHeader.vue","./src/components/layout/AppLayout.vue","./src/components/layout/AppSidebar.vue","./src/components/layout/AuthLayout.vue","./src/views/HomeView.vue","./src/views/NotFoundView.vue","./src/views/admin/AccountsView.vue","./src/views/admin/DashboardView.vue","./src/views/admin/GroupsView.vue","./src/views/admin/ProxiesView.vue","./src/views/admin/RedeemView.vue","./src/views/admin/SettingsView.vue","./src/views/admin/SubscriptionsView.vue","./src/views/admin/UsageView.vue","./src/views/admin/UsersView.vue","./src/views/auth/EmailVerifyView.vue","./src/views/auth/LoginView.vue","./src/views/auth/RegisterView.vue","./src/views/setup/SetupWizardView.vue","./src/views/user/DashboardView.vue","./src/views/user/KeysView.vue","./src/views/user/ProfileView.vue","./src/views/user/RedeemView.vue","./src/views/user/SubscriptionsView.vue","./src/views/user/UsageView.vue"],"version":"5.6.3"}
\ No newline at end of file
\ No newline at end of file
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