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
2220fd18
Commit
2220fd18
authored
Feb 03, 2026
by
song
Browse files
merge upstream main
parents
11ff73b5
df4c0adf
Changes
67
Hide whitespace changes
Inline
Side-by-side
frontend/src/views/admin/RedeemView.vue
View file @
2220fd18
...
...
@@ -213,7 +213,7 @@
<
Select
v
-
model
=
"
generateForm.type
"
:
options
=
"
typeOptions
"
/>
<
/div
>
<!--
余额
/
并发类型
:
显示数值输入
-->
<
div
v
-
if
=
"
generateForm.type !== 'subscription'
"
>
<
div
v
-
if
=
"
generateForm.type !== 'subscription'
&& generateForm.type !== 'invitation'
"
>
<
label
class
=
"
input-label
"
>
{{
generateForm
.
type
===
'
balance
'
...
...
@@ -230,6 +230,12 @@
class
=
"
input
"
/>
<
/div
>
<!--
邀请码类型
:
显示提示信息
-->
<
div
v
-
if
=
"
generateForm.type === 'invitation'
"
class
=
"
rounded-lg bg-blue-50 p-3 dark:bg-blue-900/20
"
>
<
p
class
=
"
text-sm text-blue-700 dark:text-blue-300
"
>
{{
t
(
'
admin.redeem.invitationHint
'
)
}}
<
/p
>
<
/div
>
<!--
订阅类型
:
显示分组选择和有效天数
-->
<
template
v
-
if
=
"
generateForm.type === 'subscription'
"
>
<
div
>
...
...
@@ -387,7 +393,7 @@
<
/template
>
<
script
setup
lang
=
"
ts
"
>
import
{
ref
,
reactive
,
computed
,
onMounted
,
onUnmounted
}
from
'
vue
'
import
{
ref
,
reactive
,
computed
,
onMounted
,
onUnmounted
,
watch
}
from
'
vue
'
import
{
useI18n
}
from
'
vue-i18n
'
import
{
useAppStore
}
from
'
@/stores/app
'
import
{
useClipboard
}
from
'
@/composables/useClipboard
'
...
...
@@ -499,14 +505,16 @@ const columns = computed<Column[]>(() => [
const
typeOptions
=
computed
(()
=>
[
{
value
:
'
balance
'
,
label
:
t
(
'
admin.redeem.balance
'
)
}
,
{
value
:
'
concurrency
'
,
label
:
t
(
'
admin.redeem.concurrency
'
)
}
,
{
value
:
'
subscription
'
,
label
:
t
(
'
admin.redeem.subscription
'
)
}
{
value
:
'
subscription
'
,
label
:
t
(
'
admin.redeem.subscription
'
)
}
,
{
value
:
'
invitation
'
,
label
:
t
(
'
admin.redeem.invitation
'
)
}
])
const
filterTypeOptions
=
computed
(()
=>
[
{
value
:
''
,
label
:
t
(
'
admin.redeem.allTypes
'
)
}
,
{
value
:
'
balance
'
,
label
:
t
(
'
admin.redeem.balance
'
)
}
,
{
value
:
'
concurrency
'
,
label
:
t
(
'
admin.redeem.concurrency
'
)
}
,
{
value
:
'
subscription
'
,
label
:
t
(
'
admin.redeem.subscription
'
)
}
{
value
:
'
subscription
'
,
label
:
t
(
'
admin.redeem.subscription
'
)
}
,
{
value
:
'
invitation
'
,
label
:
t
(
'
admin.redeem.invitation
'
)
}
])
const
filterStatusOptions
=
computed
(()
=>
[
...
...
@@ -546,6 +554,18 @@ const generateForm = reactive({
validity_days
:
30
}
)
// 监听类型变化,邀请码类型时自动设置 value 为 0
watch
(
()
=>
generateForm
.
type
,
(
newType
)
=>
{
if
(
newType
===
'
invitation
'
)
{
generateForm
.
value
=
0
}
else
if
(
generateForm
.
value
===
0
)
{
generateForm
.
value
=
10
}
}
)
const
loadCodes
=
async
()
=>
{
if
(
abortController
)
{
abortController
.
abort
()
...
...
frontend/src/views/admin/SettingsView.vue
View file @
2220fd18
...
...
@@ -339,6 +339,20 @@
<Toggle
v-model=
"form.promo_code_enabled"
/>
</div>
<!-- Invitation Code -->
<div
class=
"flex items-center justify-between border-t border-gray-100 pt-4 dark:border-dark-700"
>
<div>
<label
class=
"font-medium text-gray-900 dark:text-white"
>
{{
t('admin.settings.registration.invitationCode')
}}
</label>
<p
class=
"text-sm text-gray-500 dark:text-gray-400"
>
{{ t('admin.settings.registration.invitationCodeHint') }}
</p>
</div>
<Toggle
v-model=
"form.invitation_code_enabled"
/>
</div>
<!-- Password Reset - Only show when email verification is enabled -->
<div
v-if=
"form.email_verify_enabled"
...
...
@@ -1115,6 +1129,7 @@ const form = reactive<SettingsForm>({
registration_enabled
:
true
,
email_verify_enabled
:
false
,
promo_code_enabled
:
true
,
invitation_code_enabled
:
false
,
password_reset_enabled
:
false
,
totp_enabled
:
false
,
totp_encryption_key_configured
:
false
,
...
...
@@ -1243,6 +1258,7 @@ async function saveSettings() {
registration_enabled
:
form
.
registration_enabled
,
email_verify_enabled
:
form
.
email_verify_enabled
,
promo_code_enabled
:
form
.
promo_code_enabled
,
invitation_code_enabled
:
form
.
invitation_code_enabled
,
password_reset_enabled
:
form
.
password_reset_enabled
,
totp_enabled
:
form
.
totp_enabled
,
default_balance
:
form
.
default_balance
,
...
...
frontend/src/views/admin/UsageView.vue
View file @
2220fd18
...
...
@@ -37,6 +37,7 @@ import { ref, reactive, computed, onMounted, onUnmounted } from 'vue'
import
{
useI18n
}
from
'
vue-i18n
'
import
{
saveAs
}
from
'
file-saver
'
import
{
useAppStore
}
from
'
@/stores/app
'
;
import
{
adminAPI
}
from
'
@/api/admin
'
;
import
{
adminUsageAPI
}
from
'
@/api/admin/usage
'
import
{
formatReasoningEffort
}
from
'
@/utils/format
'
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
'
...
...
@@ -104,7 +105,7 @@ const exportToExcel = async () => {
const
XLSX
=
await
import
(
'
xlsx
'
)
const
headers
=
[
t
(
'
usage.time
'
),
t
(
'
admin.usage.user
'
),
t
(
'
usage.apiKeyFilter
'
),
t
(
'
admin.usage.account
'
),
t
(
'
usage.model
'
),
t
(
'
admin.usage.group
'
),
t
(
'
admin.usage.account
'
),
t
(
'
usage.model
'
),
t
(
'
usage.reasoningEffort
'
),
t
(
'
admin.usage.group
'
),
t
(
'
usage.type
'
),
t
(
'
admin.usage.inputTokens
'
),
t
(
'
admin.usage.outputTokens
'
),
t
(
'
admin.usage.cacheReadTokens
'
),
t
(
'
admin.usage.cacheCreationTokens
'
),
...
...
@@ -120,6 +121,7 @@ const exportToExcel = async () => {
log
.
api_key
?.
name
||
''
,
log
.
account
?.
name
||
''
,
log
.
model
,
formatReasoningEffort
(
log
.
reasoning_effort
),
log
.
group
?.
name
||
''
,
log
.
stream
?
t
(
'
usage.stream
'
)
:
t
(
'
usage.sync
'
),
log
.
input_tokens
,
...
...
frontend/src/views/auth/EmailVerifyView.vue
View file @
2220fd18
...
...
@@ -201,6 +201,7 @@ const email = ref<string>('')
const
password
=
ref
<
string
>
(
''
)
const
initialTurnstileToken
=
ref
<
string
>
(
''
)
const
promoCode
=
ref
<
string
>
(
''
)
const
invitationCode
=
ref
<
string
>
(
''
)
const
hasRegisterData
=
ref
<
boolean
>
(
false
)
// Public settings
...
...
@@ -230,6 +231,7 @@ onMounted(async () => {
password
.
value
=
registerData
.
password
||
''
initialTurnstileToken
.
value
=
registerData
.
turnstile_token
||
''
promoCode
.
value
=
registerData
.
promo_code
||
''
invitationCode
.
value
=
registerData
.
invitation_code
||
''
hasRegisterData
.
value
=
!!
(
email
.
value
&&
password
.
value
)
}
catch
{
hasRegisterData
.
value
=
false
...
...
@@ -384,7 +386,8 @@ async function handleVerify(): Promise<void> {
password
:
password
.
value
,
verify_code
:
verifyCode
.
value
.
trim
(),
turnstile_token
:
initialTurnstileToken
.
value
||
undefined
,
promo_code
:
promoCode
.
value
||
undefined
promo_code
:
promoCode
.
value
||
undefined
,
invitation_code
:
invitationCode
.
value
||
undefined
})
// Clear session data
...
...
frontend/src/views/auth/RegisterView.vue
View file @
2220fd18
...
...
@@ -95,6 +95,59 @@
<
/p
>
<
/div
>
<!--
Invitation
Code
Input
(
Required
when
enabled
)
-->
<
div
v
-
if
=
"
invitationCodeEnabled
"
>
<
label
for
=
"
invitation_code
"
class
=
"
input-label
"
>
{{
t
(
'
auth.invitationCodeLabel
'
)
}}
<
/label
>
<
div
class
=
"
relative
"
>
<
div
class
=
"
pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3.5
"
>
<
Icon
name
=
"
key
"
size
=
"
md
"
:
class
=
"
invitationValidation.valid ? 'text-green-500' : 'text-gray-400 dark:text-dark-500'
"
/>
<
/div
>
<
input
id
=
"
invitation_code
"
v
-
model
=
"
formData.invitation_code
"
type
=
"
text
"
:
disabled
=
"
isLoading
"
class
=
"
input pl-11 pr-10
"
:
class
=
"
{
'border-green-500 focus:border-green-500 focus:ring-green-500': invitationValidation.valid,
'border-red-500 focus:border-red-500 focus:ring-red-500': invitationValidation.invalid || errors.invitation_code
}
"
:
placeholder
=
"
t('auth.invitationCodePlaceholder')
"
@
input
=
"
handleInvitationCodeInput
"
/>
<!--
Validation
indicator
-->
<
div
v
-
if
=
"
invitationValidating
"
class
=
"
absolute inset-y-0 right-0 flex items-center pr-3.5
"
>
<
svg
class
=
"
h-4 w-4 animate-spin text-gray-400
"
fill
=
"
none
"
viewBox
=
"
0 0 24 24
"
>
<
circle
class
=
"
opacity-25
"
cx
=
"
12
"
cy
=
"
12
"
r
=
"
10
"
stroke
=
"
currentColor
"
stroke
-
width
=
"
4
"
><
/circle
>
<
path
class
=
"
opacity-75
"
fill
=
"
currentColor
"
d
=
"
M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z
"
><
/path
>
<
/svg
>
<
/div
>
<
div
v
-
else
-
if
=
"
invitationValidation.valid
"
class
=
"
absolute inset-y-0 right-0 flex items-center pr-3.5
"
>
<
Icon
name
=
"
checkCircle
"
size
=
"
md
"
class
=
"
text-green-500
"
/>
<
/div
>
<
div
v
-
else
-
if
=
"
invitationValidation.invalid || errors.invitation_code
"
class
=
"
absolute inset-y-0 right-0 flex items-center pr-3.5
"
>
<
Icon
name
=
"
exclamationCircle
"
size
=
"
md
"
class
=
"
text-red-500
"
/>
<
/div
>
<
/div
>
<!--
Invitation
code
validation
result
-->
<
transition
name
=
"
fade
"
>
<
div
v
-
if
=
"
invitationValidation.valid
"
class
=
"
mt-2 flex items-center gap-2 rounded-lg bg-green-50 px-3 py-2 dark:bg-green-900/20
"
>
<
Icon
name
=
"
checkCircle
"
size
=
"
sm
"
class
=
"
text-green-600 dark:text-green-400
"
/>
<
span
class
=
"
text-sm text-green-700 dark:text-green-400
"
>
{{
t
(
'
auth.invitationCodeValid
'
)
}}
<
/span
>
<
/div
>
<
p
v
-
else
-
if
=
"
invitationValidation.invalid
"
class
=
"
input-error-text
"
>
{{
invitationValidation
.
message
}}
<
/p
>
<
p
v
-
else
-
if
=
"
errors.invitation_code
"
class
=
"
input-error-text
"
>
{{
errors
.
invitation_code
}}
<
/p
>
<
/transition
>
<
/div
>
<!--
Promo
Code
Input
(
Optional
)
-->
<
div
v
-
if
=
"
promoCodeEnabled
"
>
<
label
for
=
"
promo_code
"
class
=
"
input-label
"
>
...
...
@@ -239,7 +292,7 @@ import LinuxDoOAuthSection from '@/components/auth/LinuxDoOAuthSection.vue'
import
Icon
from
'
@/components/icons/Icon.vue
'
import
TurnstileWidget
from
'
@/components/TurnstileWidget.vue
'
import
{
useAuthStore
,
useAppStore
}
from
'
@/stores
'
import
{
getPublicSettings
,
validatePromoCode
}
from
'
@/api/auth
'
import
{
getPublicSettings
,
validatePromoCode
,
validateInvitationCode
}
from
'
@/api/auth
'
const
{
t
}
=
useI18n
()
...
...
@@ -261,6 +314,7 @@ const showPassword = ref<boolean>(false)
const
registrationEnabled
=
ref
<
boolean
>
(
true
)
const
emailVerifyEnabled
=
ref
<
boolean
>
(
false
)
const
promoCodeEnabled
=
ref
<
boolean
>
(
true
)
const
invitationCodeEnabled
=
ref
<
boolean
>
(
false
)
const
turnstileEnabled
=
ref
<
boolean
>
(
false
)
const
turnstileSiteKey
=
ref
<
string
>
(
''
)
const
siteName
=
ref
<
string
>
(
'
Sub2API
'
)
...
...
@@ -280,16 +334,27 @@ const promoValidation = reactive({
}
)
let
promoValidateTimeout
:
ReturnType
<
typeof
setTimeout
>
|
null
=
null
// Invitation code validation
const
invitationValidating
=
ref
<
boolean
>
(
false
)
const
invitationValidation
=
reactive
({
valid
:
false
,
invalid
:
false
,
message
:
''
}
)
let
invitationValidateTimeout
:
ReturnType
<
typeof
setTimeout
>
|
null
=
null
const
formData
=
reactive
({
email
:
''
,
password
:
''
,
promo_code
:
''
promo_code
:
''
,
invitation_code
:
''
}
)
const
errors
=
reactive
({
email
:
''
,
password
:
''
,
turnstile
:
''
turnstile
:
''
,
invitation_code
:
''
}
)
// ==================== Lifecycle ====================
...
...
@@ -300,6 +365,7 @@ onMounted(async () => {
registrationEnabled
.
value
=
settings
.
registration_enabled
emailVerifyEnabled
.
value
=
settings
.
email_verify_enabled
promoCodeEnabled
.
value
=
settings
.
promo_code_enabled
invitationCodeEnabled
.
value
=
settings
.
invitation_code_enabled
turnstileEnabled
.
value
=
settings
.
turnstile_enabled
turnstileSiteKey
.
value
=
settings
.
turnstile_site_key
||
''
siteName
.
value
=
settings
.
site_name
||
'
Sub2API
'
...
...
@@ -325,6 +391,9 @@ onUnmounted(() => {
if
(
promoValidateTimeout
)
{
clearTimeout
(
promoValidateTimeout
)
}
if
(
invitationValidateTimeout
)
{
clearTimeout
(
invitationValidateTimeout
)
}
}
)
// ==================== Promo Code Validation ====================
...
...
@@ -400,6 +469,70 @@ function getPromoErrorMessage(errorCode?: string): string {
}
}
// ==================== Invitation Code Validation ====================
function
handleInvitationCodeInput
():
void
{
const
code
=
formData
.
invitation_code
.
trim
()
// Clear previous validation
invitationValidation
.
valid
=
false
invitationValidation
.
invalid
=
false
invitationValidation
.
message
=
''
errors
.
invitation_code
=
''
if
(
!
code
)
{
return
}
// Debounce validation
if
(
invitationValidateTimeout
)
{
clearTimeout
(
invitationValidateTimeout
)
}
invitationValidateTimeout
=
setTimeout
(()
=>
{
validateInvitationCodeDebounced
(
code
)
}
,
500
)
}
async
function
validateInvitationCodeDebounced
(
code
:
string
):
Promise
<
void
>
{
invitationValidating
.
value
=
true
try
{
const
result
=
await
validateInvitationCode
(
code
)
if
(
result
.
valid
)
{
invitationValidation
.
valid
=
true
invitationValidation
.
invalid
=
false
invitationValidation
.
message
=
''
}
else
{
invitationValidation
.
valid
=
false
invitationValidation
.
invalid
=
true
invitationValidation
.
message
=
getInvitationErrorMessage
(
result
.
error_code
)
}
}
catch
{
invitationValidation
.
valid
=
false
invitationValidation
.
invalid
=
true
invitationValidation
.
message
=
t
(
'
auth.invitationCodeInvalid
'
)
}
finally
{
invitationValidating
.
value
=
false
}
}
function
getInvitationErrorMessage
(
errorCode
?:
string
):
string
{
switch
(
errorCode
)
{
case
'
INVITATION_CODE_NOT_FOUND
'
:
return
t
(
'
auth.invitationCodeInvalid
'
)
case
'
INVITATION_CODE_INVALID
'
:
return
t
(
'
auth.invitationCodeInvalid
'
)
case
'
INVITATION_CODE_USED
'
:
return
t
(
'
auth.invitationCodeInvalid
'
)
case
'
INVITATION_CODE_DISABLED
'
:
return
t
(
'
auth.invitationCodeInvalid
'
)
default
:
return
t
(
'
auth.invitationCodeInvalid
'
)
}
}
// ==================== Turnstile Handlers ====================
function
onTurnstileVerify
(
token
:
string
):
void
{
...
...
@@ -429,6 +562,7 @@ function validateForm(): boolean {
errors
.
email
=
''
errors
.
password
=
''
errors
.
turnstile
=
''
errors
.
invitation_code
=
''
let
isValid
=
true
...
...
@@ -450,6 +584,14 @@ function validateForm(): boolean {
isValid
=
false
}
// Invitation code validation (required when enabled)
if
(
invitationCodeEnabled
.
value
)
{
if
(
!
formData
.
invitation_code
.
trim
())
{
errors
.
invitation_code
=
t
(
'
auth.invitationCodeRequired
'
)
isValid
=
false
}
}
// Turnstile validation
if
(
turnstileEnabled
.
value
&&
!
turnstileToken
.
value
)
{
errors
.
turnstile
=
t
(
'
auth.completeVerification
'
)
...
...
@@ -484,6 +626,30 @@ async function handleRegister(): Promise<void> {
}
}
// Check invitation code validation status (if enabled and code provided)
if
(
invitationCodeEnabled
.
value
)
{
// If still validating, wait
if
(
invitationValidating
.
value
)
{
errorMessage
.
value
=
t
(
'
auth.invitationCodeValidating
'
)
return
}
// If invitation code is invalid, block submission
if
(
invitationValidation
.
invalid
)
{
errorMessage
.
value
=
t
(
'
auth.invitationCodeInvalidCannotRegister
'
)
return
}
// If invitation code is required but not validated yet
if
(
formData
.
invitation_code
.
trim
()
&&
!
invitationValidation
.
valid
)
{
errorMessage
.
value
=
t
(
'
auth.invitationCodeValidating
'
)
// Trigger validation
await
validateInvitationCodeDebounced
(
formData
.
invitation_code
.
trim
())
if
(
!
invitationValidation
.
valid
)
{
errorMessage
.
value
=
t
(
'
auth.invitationCodeInvalidCannotRegister
'
)
return
}
}
}
isLoading
.
value
=
true
try
{
...
...
@@ -496,7 +662,8 @@ async function handleRegister(): Promise<void> {
email
:
formData
.
email
,
password
:
formData
.
password
,
turnstile_token
:
turnstileToken
.
value
,
promo_code
:
formData
.
promo_code
||
undefined
promo_code
:
formData
.
promo_code
||
undefined
,
invitation_code
:
formData
.
invitation_code
||
undefined
}
)
)
...
...
@@ -510,7 +677,8 @@ async function handleRegister(): Promise<void> {
email
:
formData
.
email
,
password
:
formData
.
password
,
turnstile_token
:
turnstileEnabled
.
value
?
turnstileToken
.
value
:
undefined
,
promo_code
:
formData
.
promo_code
||
undefined
promo_code
:
formData
.
promo_code
||
undefined
,
invitation_code
:
formData
.
invitation_code
||
undefined
}
)
// Show success toast
...
...
frontend/src/views/setup/SetupWizardView.vue
View file @
2220fd18
...
...
@@ -237,6 +237,18 @@
</div>
</div>
<div
class=
"flex items-center justify-between rounded-xl border border-gray-200 p-3 dark:border-dark-700"
>
<div>
<p
class=
"text-sm font-medium text-gray-900 dark:text-white"
>
{{ t("setup.redis.enableTls") }}
</p>
<p
class=
"text-xs text-gray-500 dark:text-dark-400"
>
{{ t("setup.redis.enableTlsHint") }}
</p>
</div>
<Toggle
v-model=
"formData.redis.enable_tls"
/>
</div>
<button
@
click=
"testRedisConnection"
:disabled=
"testingRedis"
...
...
@@ -482,6 +494,7 @@ import { ref, reactive, computed } from 'vue'
import
{
useI18n
}
from
'
vue-i18n
'
import
{
testDatabase
,
testRedis
,
install
,
type
InstallRequest
}
from
'
@/api/setup
'
import
Select
from
'
@/components/common/Select.vue
'
import
Toggle
from
'
@/components/common/Toggle.vue
'
import
Icon
from
'
@/components/icons/Icon.vue
'
const
{
t
}
=
useI18n
()
...
...
frontend/src/views/user/UsageView.vue
View file @
2220fd18
...
...
@@ -157,6 +157,12 @@
<span
class=
"font-medium text-gray-900 dark:text-white"
>
{{
value
}}
</span>
</
template
>
<
template
#cell-reasoning_effort=
"{ row }"
>
<span
class=
"text-sm text-gray-900 dark:text-white"
>
{{
formatReasoningEffort
(
row
.
reasoning_effort
)
}}
</span>
</
template
>
<
template
#cell-stream=
"{ row }"
>
<span
class=
"inline-flex items-center rounded px-2 py-0.5 text-xs font-medium"
...
...
@@ -438,12 +444,12 @@ import TablePageLayout from '@/components/layout/TablePageLayout.vue'
import
DataTable
from
'
@/components/common/DataTable.vue
'
import
Pagination
from
'
@/components/common/Pagination.vue
'
import
EmptyState
from
'
@/components/common/EmptyState.vue
'
import
Select
from
'
@/components/common/Select.vue
'
import
DateRangePicker
from
'
@/components/common/DateRangePicker.vue
'
import
Icon
from
'
@/components/icons/Icon.vue
'
import
type
{
UsageLog
,
ApiKey
,
UsageQueryParams
,
UsageStatsResponse
}
from
'
@/types
'
import
type
{
Column
}
from
'
@/components/common/types
'
import
{
formatDateTime
}
from
'
@/utils/format
'
import
Select
from
'
@/components/common/Select.vue
'
import
DateRangePicker
from
'
@/components/common/DateRangePicker.vue
'
import
Icon
from
'
@/components/icons/Icon.vue
'
import
type
{
UsageLog
,
ApiKey
,
UsageQueryParams
,
UsageStatsResponse
}
from
'
@/types
'
import
type
{
Column
}
from
'
@/components/common/types
'
import
{
formatDateTime
,
formatReasoningEffort
}
from
'
@/utils/format
'
const
{
t
}
=
useI18n
()
const
appStore
=
useAppStore
()
...
...
@@ -466,6 +472,7 @@ const usageStats = ref<UsageStatsResponse | null>(null)
const
columns
=
computed
<
Column
[]
>
(()
=>
[
{
key
:
'
api_key
'
,
label
:
t
(
'
usage.apiKeyFilter
'
),
sortable
:
false
},
{
key
:
'
model
'
,
label
:
t
(
'
usage.model
'
),
sortable
:
true
},
{
key
:
'
reasoning_effort
'
,
label
:
t
(
'
usage.reasoningEffort
'
),
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
},
...
...
@@ -723,6 +730,7 @@ const exportToCSV = async () => {
'
Time
'
,
'
API Key Name
'
,
'
Model
'
,
'
Reasoning Effort
'
,
'
Type
'
,
'
Input Tokens
'
,
'
Output Tokens
'
,
...
...
@@ -739,6 +747,7 @@ const exportToCSV = async () => {
log
.
created_at
,
log
.
api_key
?.
name
||
''
,
log
.
model
,
formatReasoningEffort
(
log
.
reasoning_effort
),
log
.
stream
?
'
Stream
'
:
'
Sync
'
,
log
.
input_tokens
,
log
.
output_tokens
,
...
...
Prev
1
2
3
4
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