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
df00805a
"frontend/src/git@web.lueluesay.top:chenxi/sub2api.git" did not exist on "4fee20ecd57126b3b0dac66faa6086f70584bf77"
Commit
df00805a
authored
Feb 27, 2026
by
shaw
Browse files
feat(frontend): 为管理端用量页面添加列显示设置
parent
a88ee965
Changes
3
Hide whitespace changes
Inline
Side-by-side
frontend/src/components/admin/usage/UsageFilters.vue
View file @
df00805a
...
...
@@ -160,6 +160,7 @@
<
button
type
=
"
button
"
@
click
=
"
$emit('reset')
"
class
=
"
btn btn-secondary
"
>
{{
t
(
'
common.reset
'
)
}}
<
/button
>
<
slot
name
=
"
after-reset
"
/>
<
button
type
=
"
button
"
@
click
=
"
$emit('cleanup')
"
class
=
"
btn btn-danger
"
>
{{
t
(
'
admin.usage.cleanup.button
'
)
}}
<
/button
>
...
...
frontend/src/components/admin/usage/UsageTable.vue
View file @
df00805a
<
template
>
<div
class=
"card overflow-hidden"
>
<div
class=
"overflow-auto"
>
<DataTable
:columns=
"cols"
:data=
"data"
:loading=
"loading"
>
<DataTable
:columns=
"col
umn
s"
:data=
"data"
:loading=
"loading"
>
<template
#cell-user
="
{ row }">
<div
class=
"text-sm"
>
<span
class=
"font-medium text-gray-900 dark:text-white"
>
{{
row
.
user
?.
email
||
'
-
'
}}
</span>
...
...
@@ -123,7 +123,7 @@
</
template
>
<
template
#cell-user_agent=
"{ row }"
>
<span
v-if=
"row.user_agent"
class=
"text-sm text-gray-600 dark:text-gray-400 block max-w-[320px]
whitespace-normal break-all
"
:title=
"row.user_agent"
>
{{
formatUserAgent
(
row
.
user_agent
)
}}
</span>
<span
v-if=
"row.user_agent"
class=
"text-sm text-gray-600 dark:text-gray-400 block max-w-[320px]
truncate
"
:title=
"row.user_agent"
>
{{
formatUserAgent
(
row
.
user_agent
)
}}
</span>
<span
v-else
class=
"text-sm text-gray-400 dark:text-gray-500"
>
-
</span>
</
template
>
...
...
@@ -268,7 +268,7 @@
</template>
<
script
setup
lang=
"ts"
>
import
{
ref
,
computed
}
from
'
vue
'
import
{
ref
}
from
'
vue
'
import
{
useI18n
}
from
'
vue-i18n
'
import
{
formatDateTime
,
formatReasoningEffort
}
from
'
@/utils/format
'
import
DataTable
from
'
@/components/common/DataTable.vue
'
...
...
@@ -276,7 +276,7 @@ import EmptyState from '@/components/common/EmptyState.vue'
import
Icon
from
'
@/components/icons/Icon.vue
'
import
type
{
AdminUsageLog
}
from
'
@/types
'
defineProps
([
'
data
'
,
'
loading
'
])
defineProps
([
'
data
'
,
'
loading
'
,
'
columns
'
])
const
{
t
}
=
useI18n
()
// Tooltip state - cost
...
...
@@ -289,23 +289,6 @@ const tokenTooltipVisible = ref(false)
const
tokenTooltipPosition
=
ref
({
x
:
0
,
y
:
0
})
const
tokenTooltipData
=
ref
<
AdminUsageLog
|
null
>
(
null
)
const
cols
=
computed
(()
=>
[
{
key
:
'
user
'
,
label
:
t
(
'
admin.usage.user
'
),
sortable
:
false
},
{
key
:
'
api_key
'
,
label
:
t
(
'
usage.apiKeyFilter
'
),
sortable
:
false
},
{
key
:
'
account
'
,
label
:
t
(
'
admin.usage.account
'
),
sortable
:
false
},
{
key
:
'
model
'
,
label
:
t
(
'
usage.model
'
),
sortable
:
true
},
{
key
:
'
reasoning_effort
'
,
label
:
t
(
'
usage.reasoningEffort
'
),
sortable
:
false
},
{
key
:
'
group
'
,
label
:
t
(
'
admin.usage.group
'
),
sortable
:
false
},
{
key
:
'
stream
'
,
label
:
t
(
'
usage.type
'
),
sortable
:
false
},
{
key
:
'
tokens
'
,
label
:
t
(
'
usage.tokens
'
),
sortable
:
false
},
{
key
:
'
cost
'
,
label
:
t
(
'
usage.cost
'
),
sortable
:
false
},
{
key
:
'
first_token
'
,
label
:
t
(
'
usage.firstToken
'
),
sortable
:
false
},
{
key
:
'
duration
'
,
label
:
t
(
'
usage.duration
'
),
sortable
:
false
},
{
key
:
'
created_at
'
,
label
:
t
(
'
usage.time
'
),
sortable
:
true
},
{
key
:
'
user_agent
'
,
label
:
t
(
'
usage.userAgent
'
),
sortable
:
false
},
{
key
:
'
ip_address
'
,
label
:
t
(
'
admin.usage.ipAddress
'
),
sortable
:
false
}
])
const
formatCacheTokens
=
(
tokens
:
number
):
string
=>
{
if
(
tokens
>=
1000000
)
return
`
${(
tokens
/
1000000
).
toFixed
(
1
)}
M`
if
(
tokens
>=
1000
)
return
`
${(
tokens
/
1000
).
toFixed
(
1
)}
K`
...
...
frontend/src/views/admin/UsageView.vue
View file @
df00805a
...
...
@@ -17,8 +17,43 @@
<TokenUsageTrend
:trend-data=
"trendData"
:loading=
"chartsLoading"
/>
</div>
</div>
<UsageFilters
v-model=
"filters"
v-model:startDate=
"startDate"
v-model:endDate=
"endDate"
:exporting=
"exporting"
@
change=
"applyFilters"
@
refresh=
"refreshData"
@
reset=
"resetFilters"
@
cleanup=
"openCleanupDialog"
@
export=
"exportToExcel"
/>
<UsageTable
:data=
"usageLogs"
:loading=
"loading"
/>
<UsageFilters
v-model=
"filters"
v-model:startDate=
"startDate"
v-model:endDate=
"endDate"
:exporting=
"exporting"
@
change=
"applyFilters"
@
refresh=
"refreshData"
@
reset=
"resetFilters"
@
cleanup=
"openCleanupDialog"
@
export=
"exportToExcel"
>
<template
#after-reset
>
<div
class=
"relative"
ref=
"columnDropdownRef"
>
<button
@
click=
"showColumnDropdown = !showColumnDropdown"
class=
"btn btn-secondary px-2 md:px-3"
:title=
"t('admin.users.columnSettings')"
>
<svg
class=
"h-4 w-4 md:mr-1.5"
fill=
"none"
stroke=
"currentColor"
viewBox=
"0 0 24 24"
stroke-width=
"1.5"
>
<path
stroke-linecap=
"round"
stroke-linejoin=
"round"
d=
"M9 4.5v15m6-15v15m-10.875 0h15.75c.621 0 1.125-.504 1.125-1.125V5.625c0-.621-.504-1.125-1.125-1.125H4.125C3.504 4.5 3 5.004 3 5.625v12.75c0 .621.504 1.125 1.125 1.125z"
/>
</svg>
<span
class=
"hidden md:inline"
>
{{
t
(
'
admin.users.columnSettings
'
)
}}
</span>
</button>
<div
v-if=
"showColumnDropdown"
class=
"absolute right-0 top-full z-50 mt-1 max-h-80 w-48 overflow-y-auto rounded-lg border border-gray-200 bg-white py-1 shadow-lg dark:border-dark-600 dark:bg-dark-800"
>
<button
v-for=
"col in toggleableColumns"
:key=
"col.key"
@
click=
"toggleColumn(col.key)"
class=
"flex w-full items-center justify-between px-4 py-2 text-left text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-dark-700"
>
<span>
{{
col
.
label
}}
</span>
<Icon
v-if=
"isColumnVisible(col.key)"
name=
"check"
size=
"sm"
class=
"text-primary-500"
:stroke-width=
"2"
/>
</button>
</div>
</div>
</
template
>
</UsageFilters>
<UsageTable
:data=
"usageLogs"
:loading=
"loading"
:columns=
"visibleColumns"
/>
<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>
...
...
@@ -43,6 +78,7 @@ import UsageStatsCards from '@/components/admin/usage/UsageStatsCards.vue'; impo
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
Icon
from
'
@/components/icons/Icon.vue
'
import
type
{
AdminUsageLog
,
TrendDataPoint
,
ModelStat
}
from
'
@/types
'
;
import
type
{
AdminUsageStatsResponse
,
AdminUsageQueryParams
}
from
'
@/api/admin/usage
'
const
{
t
}
=
useI18n
()
...
...
@@ -141,6 +177,77 @@ const exportToExcel = async () => {
finally
{
if
(
exportAbortController
===
c
)
{
exportAbortController
=
null
;
exporting
.
value
=
false
;
exportProgress
.
show
=
false
}
}
}
onMounted
(()
=>
{
loadLogs
();
loadStats
();
loadChartData
()
})
onUnmounted
(()
=>
{
abortController
?.
abort
();
exportAbortController
?.
abort
()
})
// Column visibility
const
ALWAYS_VISIBLE
=
[
'
user
'
,
'
created_at
'
]
const
DEFAULT_HIDDEN_COLUMNS
=
[
'
reasoning_effort
'
,
'
user_agent
'
]
const
HIDDEN_COLUMNS_KEY
=
'
usage-hidden-columns
'
const
allColumns
=
computed
(()
=>
[
{
key
:
'
user
'
,
label
:
t
(
'
admin.usage.user
'
),
sortable
:
false
},
{
key
:
'
api_key
'
,
label
:
t
(
'
usage.apiKeyFilter
'
),
sortable
:
false
},
{
key
:
'
account
'
,
label
:
t
(
'
admin.usage.account
'
),
sortable
:
false
},
{
key
:
'
model
'
,
label
:
t
(
'
usage.model
'
),
sortable
:
true
},
{
key
:
'
reasoning_effort
'
,
label
:
t
(
'
usage.reasoningEffort
'
),
sortable
:
false
},
{
key
:
'
group
'
,
label
:
t
(
'
admin.usage.group
'
),
sortable
:
false
},
{
key
:
'
stream
'
,
label
:
t
(
'
usage.type
'
),
sortable
:
false
},
{
key
:
'
tokens
'
,
label
:
t
(
'
usage.tokens
'
),
sortable
:
false
},
{
key
:
'
cost
'
,
label
:
t
(
'
usage.cost
'
),
sortable
:
false
},
{
key
:
'
first_token
'
,
label
:
t
(
'
usage.firstToken
'
),
sortable
:
false
},
{
key
:
'
duration
'
,
label
:
t
(
'
usage.duration
'
),
sortable
:
false
},
{
key
:
'
created_at
'
,
label
:
t
(
'
usage.time
'
),
sortable
:
true
},
{
key
:
'
user_agent
'
,
label
:
t
(
'
usage.userAgent
'
),
sortable
:
false
},
{
key
:
'
ip_address
'
,
label
:
t
(
'
admin.usage.ipAddress
'
),
sortable
:
false
}
])
const
hiddenColumns
=
reactive
<
Set
<
string
>>
(
new
Set
())
const
toggleableColumns
=
computed
(()
=>
allColumns
.
value
.
filter
(
col
=>
!
ALWAYS_VISIBLE
.
includes
(
col
.
key
))
)
const
visibleColumns
=
computed
(()
=>
allColumns
.
value
.
filter
(
col
=>
ALWAYS_VISIBLE
.
includes
(
col
.
key
)
||
!
hiddenColumns
.
has
(
col
.
key
)
)
)
const
isColumnVisible
=
(
key
:
string
)
=>
!
hiddenColumns
.
has
(
key
)
const
toggleColumn
=
(
key
:
string
)
=>
{
if
(
hiddenColumns
.
has
(
key
))
{
hiddenColumns
.
delete
(
key
)
}
else
{
hiddenColumns
.
add
(
key
)
}
try
{
localStorage
.
setItem
(
HIDDEN_COLUMNS_KEY
,
JSON
.
stringify
([...
hiddenColumns
]))
}
catch
(
e
)
{
console
.
error
(
'
Failed to save columns:
'
,
e
)
}
}
const
loadSavedColumns
=
()
=>
{
try
{
const
saved
=
localStorage
.
getItem
(
HIDDEN_COLUMNS_KEY
)
if
(
saved
)
{
(
JSON
.
parse
(
saved
)
as
string
[]).
forEach
(
key
=>
hiddenColumns
.
add
(
key
))
}
else
{
DEFAULT_HIDDEN_COLUMNS
.
forEach
(
key
=>
hiddenColumns
.
add
(
key
))
}
}
catch
{
DEFAULT_HIDDEN_COLUMNS
.
forEach
(
key
=>
hiddenColumns
.
add
(
key
))
}
}
const
showColumnDropdown
=
ref
(
false
)
const
columnDropdownRef
=
ref
<
HTMLElement
|
null
>
(
null
)
const
handleColumnClickOutside
=
(
event
:
MouseEvent
)
=>
{
if
(
columnDropdownRef
.
value
&&
!
columnDropdownRef
.
value
.
contains
(
event
.
target
as
HTMLElement
))
{
showColumnDropdown
.
value
=
false
}
}
onMounted
(()
=>
{
loadLogs
();
loadStats
();
loadChartData
();
loadSavedColumns
();
document
.
addEventListener
(
'
click
'
,
handleColumnClickOutside
)
})
onUnmounted
(()
=>
{
abortController
?.
abort
();
exportAbortController
?.
abort
();
document
.
removeEventListener
(
'
click
'
,
handleColumnClickOutside
)
})
</
script
>
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