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
c8e2f614
Commit
c8e2f614
authored
Jan 20, 2026
by
cyhhao
Browse files
Merge branch 'main' of github.com:Wei-Shaw/sub2api
parents
c0347cde
c95a8649
Changes
167
Hide whitespace changes
Inline
Side-by-side
frontend/src/views/admin/GroupsView.vue
View file @
c8e2f614
...
...
@@ -243,7 +243,7 @@
/>
<
p
class
=
"
input-hint
"
>
{{
t
(
'
admin.groups.platformHint
'
)
}}
<
/p
>
<
/div
>
<
div
v
-
if
=
"
createForm.subscription_type !== 'subscription'
"
>
<
div
>
<
label
class
=
"
input-label
"
>
{{
t
(
'
admin.groups.form.rateMultiplier
'
)
}}
<
/label
>
<
input
v
-
model
.
number
=
"
createForm.rate_multiplier
"
...
...
@@ -680,7 +680,7 @@
/>
<
p
class
=
"
input-hint
"
>
{{
t
(
'
admin.groups.platformNotEditable
'
)
}}
<
/p
>
<
/div
>
<
div
v
-
if
=
"
editForm.subscription_type !== 'subscription'
"
>
<
div
>
<
label
class
=
"
input-label
"
>
{{
t
(
'
admin.groups.form.rateMultiplier
'
)
}}
<
/label
>
<
input
v
-
model
.
number
=
"
editForm.rate_multiplier
"
...
...
@@ -1107,7 +1107,7 @@ import { useI18n } from 'vue-i18n'
import
{
useAppStore
}
from
'
@/stores/app
'
import
{
useOnboardingStore
}
from
'
@/stores/onboarding
'
import
{
adminAPI
}
from
'
@/api/admin
'
import
type
{
Group
,
GroupPlatform
,
SubscriptionType
}
from
'
@/types
'
import
type
{
Admin
Group
,
GroupPlatform
,
SubscriptionType
}
from
'
@/types
'
import
type
{
Column
}
from
'
@/components/common/types
'
import
AppLayout
from
'
@/components/layout/AppLayout.vue
'
import
TablePageLayout
from
'
@/components/layout/TablePageLayout.vue
'
...
...
@@ -1202,7 +1202,7 @@ const fallbackGroupOptionsForEdit = computed(() => {
return
options
}
)
const
groups
=
ref
<
Group
[]
>
([])
const
groups
=
ref
<
Admin
Group
[]
>
([])
const
loading
=
ref
(
false
)
const
searchQuery
=
ref
(
''
)
const
filters
=
reactive
({
...
...
@@ -1223,8 +1223,8 @@ const showCreateModal = ref(false)
const
showEditModal
=
ref
(
false
)
const
showDeleteDialog
=
ref
(
false
)
const
submitting
=
ref
(
false
)
const
editingGroup
=
ref
<
Group
|
null
>
(
null
)
const
deletingGroup
=
ref
<
Group
|
null
>
(
null
)
const
editingGroup
=
ref
<
Admin
Group
|
null
>
(
null
)
const
deletingGroup
=
ref
<
Admin
Group
|
null
>
(
null
)
const
createForm
=
reactive
({
name
:
''
,
...
...
@@ -1529,7 +1529,7 @@ const handleCreateGroup = async () => {
}
}
const
handleEdit
=
async
(
group
:
Group
)
=>
{
const
handleEdit
=
async
(
group
:
Admin
Group
)
=>
{
editingGroup
.
value
=
group
editForm
.
name
=
group
.
name
editForm
.
description
=
group
.
description
||
''
...
...
@@ -1585,7 +1585,7 @@ const handleUpdateGroup = async () => {
}
}
const
handleDelete
=
(
group
:
Group
)
=>
{
const
handleDelete
=
(
group
:
Admin
Group
)
=>
{
deletingGroup
.
value
=
group
showDeleteDialog
.
value
=
true
}
...
...
@@ -1605,12 +1605,11 @@ const confirmDelete = async () => {
}
}
// 监听 subscription_type 变化,订阅模式时
重置 rate_multiplier 为 1,
is_exclusive 为 true
// 监听 subscription_type 变化,订阅模式时
is_exclusive
默认
为 true
watch
(
()
=>
createForm
.
subscription_type
,
(
newVal
)
=>
{
if
(
newVal
===
'
subscription
'
)
{
createForm
.
rate_multiplier
=
1.0
createForm
.
is_exclusive
=
true
}
}
...
...
frontend/src/views/admin/SettingsView.vue
View file @
c8e2f614
...
...
@@ -720,6 +720,21 @@
{{ t('admin.settings.site.homeContentIframeWarning') }}
</p>
</div>
<!-- Hide CCS Import Button -->
<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.site.hideCcsImportButton')
}}
</label>
<p
class=
"text-sm text-gray-500 dark:text-gray-400"
>
{{ t('admin.settings.site.hideCcsImportButtonHint') }}
</p>
</div>
<Toggle
v-model=
"form.hide_ccs_import_button"
/>
</div>
</div>
</div>
...
...
@@ -1007,6 +1022,7 @@ const form = reactive<SettingsForm>({
contact_info
:
''
,
doc_url
:
''
,
home_content
:
''
,
hide_ccs_import_button
:
false
,
smtp_host
:
''
,
smtp_port
:
587
,
smtp_username
:
''
,
...
...
@@ -1128,6 +1144,7 @@ async function saveSettings() {
contact_info
:
form
.
contact_info
,
doc_url
:
form
.
doc_url
,
home_content
:
form
.
home_content
,
hide_ccs_import_button
:
form
.
hide_ccs_import_button
,
smtp_host
:
form
.
smtp_host
,
smtp_port
:
form
.
smtp_port
,
smtp_username
:
form
.
smtp_username
,
...
...
frontend/src/views/admin/SubscriptionsView.vue
View file @
c8e2f614
...
...
@@ -93,6 +93,57 @@
>
<Icon
name=
"refresh"
size=
"md"
:class=
"loading ? 'animate-spin' : ''"
/>
</button>
<!-- Column Settings Dropdown -->
<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>
<!-- Dropdown menu -->
<div
v-if=
"showColumnDropdown"
class=
"absolute right-0 z-50 mt-2 w-48 origin-top-right rounded-lg border border-gray-200 bg-white shadow-lg dark:border-gray-700 dark:bg-gray-800"
>
<div
class=
"p-2"
>
<!-- User column mode selection -->
<div
class=
"mb-2 border-b border-gray-200 pb-2 dark:border-gray-700"
>
<div
class=
"px-3 py-1 text-xs font-medium text-gray-500 dark:text-gray-400"
>
{{
t
(
'
admin.subscriptions.columns.user
'
)
}}
</div>
<button
@
click=
"setUserColumnMode('email')"
class=
"flex w-full items-center justify-between rounded-md px-3 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-gray-700"
>
<span>
{{
t
(
'
admin.users.columns.email
'
)
}}
</span>
<Icon
v-if=
"userColumnMode === 'email'"
name=
"check"
size=
"sm"
class=
"text-primary-500"
/>
</button>
<button
@
click=
"setUserColumnMode('username')"
class=
"flex w-full items-center justify-between rounded-md px-3 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-gray-700"
>
<span>
{{
t
(
'
admin.users.columns.username
'
)
}}
</span>
<Icon
v-if=
"userColumnMode === 'username'"
name=
"check"
size=
"sm"
class=
"text-primary-500"
/>
</button>
</div>
<!-- Other columns toggle -->
<button
v-for=
"col in toggleableColumns"
:key=
"col.key"
@
click=
"toggleColumn(col.key)"
class=
"flex w-full items-center justify-between rounded-md px-3 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-gray-700"
>
<span>
{{
col
.
label
}}
</span>
<Icon
v-if=
"isColumnVisible(col.key)"
name=
"check"
size=
"sm"
class=
"text-primary-500"
/>
</button>
</div>
</div>
</div>
<button
@
click=
"showAssignModal = true"
class=
"btn btn-primary"
>
<Icon
name=
"plus"
size=
"md"
class=
"mr-2"
/>
{{
t
(
'
admin.subscriptions.assignSubscription
'
)
}}
...
...
@@ -110,12 +161,18 @@
class=
"flex h-8 w-8 items-center justify-center rounded-full bg-primary-100 dark:bg-primary-900/30"
>
<span
class=
"text-sm font-medium text-primary-700 dark:text-primary-300"
>
{{
row
.
user
?.
email
?.
charAt
(
0
).
toUpperCase
()
||
'
?
'
}}
{{
userColumnMode
===
'
email
'
?
(
row
.
user
?.
email
?.
charAt
(
0
).
toUpperCase
()
||
'
?
'
)
:
(
row
.
user
?.
username
?.
charAt
(
0
).
toUpperCase
()
||
'
?
'
)
}}
</span>
</div>
<span
class=
"font-medium text-gray-900 dark:text-white"
>
{{
row
.
user
?.
email
||
t
(
'
admin.redeem.userPrefix
'
,
{
id
:
row
.
user_id
}
)
}}
<
/span
>
<span
class=
"font-medium text-gray-900 dark:text-white"
>
{{
userColumnMode
===
'
email
'
?
(
row
.
user
?.
email
||
t
(
'
admin.redeem.userPrefix
'
,
{
id
:
row
.
user_id
}
))
:
(
row
.
user
?.
username
||
'
-
'
)
}}
<
/span
>
<
/div
>
<
/template
>
...
...
@@ -302,10 +359,10 @@
<
button
v
-
if
=
"
row.status === 'active'
"
@
click
=
"
handleExtend(row)
"
class
=
"
flex flex-col items-center gap-0.5 rounded-lg p-1.5 text-gray-500 transition-colors hover:bg-
green
-50 hover:text-
green
-600 dark:hover:bg-
green
-900/20 dark:hover:text-
green
-400
"
class
=
"
flex flex-col items-center gap-0.5 rounded-lg p-1.5 text-gray-500 transition-colors hover:bg-
blue
-50 hover:text-
blue
-600 dark:hover:bg-
blue
-900/20 dark:hover:text-
blue
-400
"
>
<
Icon
name
=
"
c
lock
"
size
=
"
sm
"
/>
<
span
class
=
"
text-xs
"
>
{{
t
(
'
admin.subscriptions.
extend
'
)
}}
<
/span
>
<
Icon
name
=
"
c
alendar
"
size
=
"
sm
"
/>
<
span
class
=
"
text-xs
"
>
{{
t
(
'
admin.subscriptions.
adjust
'
)
}}
<
/span
>
<
/button
>
<
button
v
-
if
=
"
row.status === 'active'
"
...
...
@@ -455,10 +512,10 @@
<
/template
>
<
/BaseDialog
>
<!--
Extend
Subscription
Modal
-->
<!--
Adjust
Subscription
Modal
-->
<
BaseDialog
:
show
=
"
showExtendModal
"
:
title
=
"
t('admin.subscriptions.
extend
Subscription')
"
:
title
=
"
t('admin.subscriptions.
adjust
Subscription')
"
width
=
"
narrow
"
@
close
=
"
closeExtendModal
"
>
...
...
@@ -470,7 +527,7 @@
>
<
div
class
=
"
rounded-lg bg-gray-50 p-4 dark:bg-dark-700
"
>
<
p
class
=
"
text-sm text-gray-600 dark:text-gray-400
"
>
{{
t
(
'
admin.subscriptions.
extend
ingFor
'
)
}}
{{
t
(
'
admin.subscriptions.
adjust
ingFor
'
)
}}
<
span
class
=
"
font-medium text-gray-900 dark:text-white
"
>
{{
extendingSubscription
.
user
?.
email
}}
<
/span
>
...
...
@@ -485,10 +542,25 @@
}}
<
/span
>
<
/p
>
<
p
v
-
if
=
"
extendingSubscription.expires_at
"
class
=
"
mt-1 text-sm text-gray-600 dark:text-gray-400
"
>
{{
t
(
'
admin.subscriptions.remainingDays
'
)
}}
:
<
span
class
=
"
font-medium text-gray-900 dark:text-white
"
>
{{
getDaysRemaining
(
extendingSubscription
.
expires_at
)
??
0
}}
<
/span
>
<
/p
>
<
/div
>
<
div
>
<
label
class
=
"
input-label
"
>
{{
t
(
'
admin.subscriptions.form.extendDays
'
)
}}
<
/label
>
<
input
v
-
model
.
number
=
"
extendForm.days
"
type
=
"
number
"
min
=
"
1
"
required
class
=
"
input
"
/>
<
label
class
=
"
input-label
"
>
{{
t
(
'
admin.subscriptions.form.adjustDays
'
)
}}
<
/label
>
<
div
class
=
"
flex items-center gap-2
"
>
<
input
v
-
model
.
number
=
"
extendForm.days
"
type
=
"
number
"
required
class
=
"
input text-center
"
:
placeholder
=
"
t('admin.subscriptions.adjustDaysPlaceholder')
"
/>
<
/div
>
<
p
class
=
"
input-hint
"
>
{{
t
(
'
admin.subscriptions.adjustHint
'
)
}}
<
/p
>
<
/div
>
<
/form
>
<
template
#
footer
>
...
...
@@ -502,7 +574,7 @@
:
disabled
=
"
submitting
"
class
=
"
btn btn-primary
"
>
{{
submitting
?
t
(
'
admin.subscriptions.
extend
ing
'
)
:
t
(
'
admin.subscriptions.
extend
'
)
}}
{{
submitting
?
t
(
'
admin.subscriptions.
adjust
ing
'
)
:
t
(
'
admin.subscriptions.
adjust
'
)
}}
<
/button
>
<
/div
>
<
/template
>
...
...
@@ -545,8 +617,43 @@ import Icon from '@/components/icons/Icon.vue'
const
{
t
}
=
useI18n
()
const
appStore
=
useAppStore
()
const
columns
=
computed
<
Column
[]
>
(()
=>
[
{
key
:
'
user
'
,
label
:
t
(
'
admin.subscriptions.columns.user
'
),
sortable
:
true
}
,
// User column display mode: 'email' or 'username'
const
userColumnMode
=
ref
<
'
email
'
|
'
username
'
>
(
'
email
'
)
const
USER_COLUMN_MODE_KEY
=
'
subscription-user-column-mode
'
const
loadUserColumnMode
=
()
=>
{
try
{
const
saved
=
localStorage
.
getItem
(
USER_COLUMN_MODE_KEY
)
if
(
saved
===
'
email
'
||
saved
===
'
username
'
)
{
userColumnMode
.
value
=
saved
}
}
catch
(
e
)
{
console
.
error
(
'
Failed to load user column mode:
'
,
e
)
}
}
const
saveUserColumnMode
=
()
=>
{
try
{
localStorage
.
setItem
(
USER_COLUMN_MODE_KEY
,
userColumnMode
.
value
)
}
catch
(
e
)
{
console
.
error
(
'
Failed to save user column mode:
'
,
e
)
}
}
const
setUserColumnMode
=
(
mode
:
'
email
'
|
'
username
'
)
=>
{
userColumnMode
.
value
=
mode
saveUserColumnMode
()
}
// All available columns
const
allColumns
=
computed
<
Column
[]
>
(()
=>
[
{
key
:
'
user
'
,
label
:
userColumnMode
.
value
===
'
email
'
?
t
(
'
admin.subscriptions.columns.user
'
)
:
t
(
'
admin.users.columns.username
'
),
sortable
:
true
}
,
{
key
:
'
group
'
,
label
:
t
(
'
admin.subscriptions.columns.group
'
),
sortable
:
true
}
,
{
key
:
'
usage
'
,
label
:
t
(
'
admin.subscriptions.columns.usage
'
),
sortable
:
false
}
,
{
key
:
'
expires_at
'
,
label
:
t
(
'
admin.subscriptions.columns.expires
'
),
sortable
:
true
}
,
...
...
@@ -554,6 +661,69 @@ const columns = computed<Column[]>(() => [
{
key
:
'
actions
'
,
label
:
t
(
'
admin.subscriptions.columns.actions
'
),
sortable
:
false
}
])
// Columns that can be toggled (exclude user and actions which are always visible)
const
toggleableColumns
=
computed
(()
=>
allColumns
.
value
.
filter
(
col
=>
col
.
key
!==
'
user
'
&&
col
.
key
!==
'
actions
'
)
)
// Hidden columns set
const
hiddenColumns
=
reactive
<
Set
<
string
>>
(
new
Set
())
// Default hidden columns
const
DEFAULT_HIDDEN_COLUMNS
:
string
[]
=
[]
// localStorage key
const
HIDDEN_COLUMNS_KEY
=
'
subscription-hidden-columns
'
// Load saved column settings
const
loadSavedColumns
=
()
=>
{
try
{
const
saved
=
localStorage
.
getItem
(
HIDDEN_COLUMNS_KEY
)
if
(
saved
)
{
const
parsed
=
JSON
.
parse
(
saved
)
as
string
[]
parsed
.
forEach
(
key
=>
hiddenColumns
.
add
(
key
))
}
else
{
DEFAULT_HIDDEN_COLUMNS
.
forEach
(
key
=>
hiddenColumns
.
add
(
key
))
}
}
catch
(
e
)
{
console
.
error
(
'
Failed to load saved columns:
'
,
e
)
DEFAULT_HIDDEN_COLUMNS
.
forEach
(
key
=>
hiddenColumns
.
add
(
key
))
}
}
// Save column settings to localStorage
const
saveColumnsToStorage
=
()
=>
{
try
{
localStorage
.
setItem
(
HIDDEN_COLUMNS_KEY
,
JSON
.
stringify
([...
hiddenColumns
]))
}
catch
(
e
)
{
console
.
error
(
'
Failed to save columns:
'
,
e
)
}
}
// Toggle column visibility
const
toggleColumn
=
(
key
:
string
)
=>
{
if
(
hiddenColumns
.
has
(
key
))
{
hiddenColumns
.
delete
(
key
)
}
else
{
hiddenColumns
.
add
(
key
)
}
saveColumnsToStorage
()
}
// Check if column is visible
const
isColumnVisible
=
(
key
:
string
)
=>
!
hiddenColumns
.
has
(
key
)
// Filtered columns for display
const
columns
=
computed
<
Column
[]
>
(()
=>
allColumns
.
value
.
filter
(
col
=>
col
.
key
===
'
user
'
||
col
.
key
===
'
actions
'
||
!
hiddenColumns
.
has
(
col
.
key
)
)
)
// Column dropdown state
const
showColumnDropdown
=
ref
(
false
)
const
columnDropdownRef
=
ref
<
HTMLElement
|
null
>
(
null
)
// Filter options
const
statusOptions
=
computed
(()
=>
[
{
value
:
''
,
label
:
t
(
'
admin.subscriptions.allStatus
'
)
}
,
...
...
@@ -845,17 +1015,27 @@ const closeExtendModal = () => {
const
handleExtendSubscription
=
async
()
=>
{
if
(
!
extendingSubscription
.
value
)
return
// 前端验证:调整后剩余天数必须 > 0
if
(
extendingSubscription
.
value
.
expires_at
)
{
const
currentDaysRemaining
=
getDaysRemaining
(
extendingSubscription
.
value
.
expires_at
)
??
0
const
newDaysRemaining
=
currentDaysRemaining
+
extendForm
.
days
if
(
newDaysRemaining
<=
0
)
{
appStore
.
showError
(
t
(
'
admin.subscriptions.adjustWouldExpire
'
))
return
}
}
submitting
.
value
=
true
try
{
await
adminAPI
.
subscriptions
.
extend
(
extendingSubscription
.
value
.
id
,
{
days
:
extendForm
.
days
}
)
appStore
.
showSuccess
(
t
(
'
admin.subscriptions.subscription
Extend
ed
'
))
appStore
.
showSuccess
(
t
(
'
admin.subscriptions.subscription
Adjust
ed
'
))
closeExtendModal
()
loadSubscriptions
()
}
catch
(
error
:
any
)
{
appStore
.
showError
(
error
.
response
?.
data
?.
detail
||
t
(
'
admin.subscriptions.failedTo
Extend
'
))
console
.
error
(
'
Error
extend
ing subscription:
'
,
error
)
appStore
.
showError
(
error
.
response
?.
data
?.
detail
||
t
(
'
admin.subscriptions.failedTo
Adjust
'
))
console
.
error
(
'
Error
adjust
ing subscription:
'
,
error
)
}
finally
{
submitting
.
value
=
false
}
...
...
@@ -949,14 +1129,19 @@ const formatResetTime = (windowStart: string, period: 'daily' | 'weekly' | 'mont
}
}
// Handle click outside to close
user
dropdown
// Handle click outside to close dropdown
s
const
handleClickOutside
=
(
event
:
MouseEvent
)
=>
{
const
target
=
event
.
target
as
HTMLElement
if
(
!
target
.
closest
(
'
[data-assign-user-search]
'
))
showUserDropdown
.
value
=
false
if
(
!
target
.
closest
(
'
[data-filter-user-search]
'
))
showFilterUserDropdown
.
value
=
false
if
(
columnDropdownRef
.
value
&&
!
columnDropdownRef
.
value
.
contains
(
target
))
{
showColumnDropdown
.
value
=
false
}
}
onMounted
(()
=>
{
loadUserColumnMode
()
loadSavedColumns
()
loadSubscriptions
()
loadGroups
()
document
.
addEventListener
(
'
click
'
,
handleClickOutside
)
...
...
frontend/src/views/admin/UsageView.vue
View file @
c8e2f614
...
...
@@ -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,15 +40,17 @@ 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
'
import
type
{
Admin
UsageLog
,
TrendDataPoint
,
ModelStat
}
from
'
@/types
'
;
import
type
{
AdminUsageStatsResponse
,
AdminUsageQueryParams
}
from
'
@/api/admin/usage
'
const
{
t
}
=
useI18n
()
const
appStore
=
useAppStore
()
const
usageStats
=
ref
<
AdminUsageStatsResponse
|
null
>
(
null
);
const
usageLogs
=
ref
<
UsageLog
[]
>
([]);
const
loading
=
ref
(
false
);
const
exporting
=
ref
(
false
)
const
usageStats
=
ref
<
AdminUsageStatsResponse
|
null
>
(
null
);
const
usageLogs
=
ref
<
Admin
UsageLog
[]
>
([]);
const
loading
=
ref
(
false
);
const
exporting
=
ref
(
false
)
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,22 +76,23 @@ 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
const
c
=
new
AbortController
();
exportAbortController
=
c
try
{
const
all
:
UsageLog
[]
=
[];
let
p
=
1
;
let
total
=
pagination
.
total
const
all
:
Admin
UsageLog
[]
=
[];
let
p
=
1
;
let
total
=
pagination
.
total
while
(
true
)
{
const
res
=
await
adminUsageAPI
.
list
({
page
:
p
,
page_size
:
100
,
...
filters
.
value
},
{
signal
:
c
.
signal
})
if
(
c
.
signal
.
aborted
)
break
;
if
(
p
===
1
)
{
total
=
res
.
total
;
exportProgress
.
total
=
total
}
...
...
frontend/src/views/admin/UsersView.vue
View file @
c8e2f614
...
...
@@ -492,7 +492,7 @@ import Icon from '@/components/icons/Icon.vue'
const
{
t
}
=
useI18n
()
import
{
adminAPI
}
from
'
@/api/admin
'
import
type
{
User
,
UserAttributeDefinition
}
from
'
@/types
'
import
type
{
Admin
User
,
UserAttributeDefinition
}
from
'
@/types
'
import
type
{
BatchUserUsageStats
}
from
'
@/api/admin/dashboard
'
import
type
{
Column
}
from
'
@/components/common/types
'
import
AppLayout
from
'
@/components/layout/AppLayout.vue
'
...
...
@@ -637,7 +637,7 @@ const columns = computed<Column[]>(() =>
)
)
const
users
=
ref
<
User
[]
>
([])
const
users
=
ref
<
Admin
User
[]
>
([])
const
loading
=
ref
(
false
)
const
searchQuery
=
ref
(
''
)
...
...
@@ -736,16 +736,16 @@ const showEditModal = ref(false)
const
showDeleteDialog
=
ref
(
false
)
const
showApiKeysModal
=
ref
(
false
)
const
showAttributesModal
=
ref
(
false
)
const
editingUser
=
ref
<
User
|
null
>
(
null
)
const
deletingUser
=
ref
<
User
|
null
>
(
null
)
const
viewingUser
=
ref
<
User
|
null
>
(
null
)
const
editingUser
=
ref
<
Admin
User
|
null
>
(
null
)
const
deletingUser
=
ref
<
Admin
User
|
null
>
(
null
)
const
viewingUser
=
ref
<
Admin
User
|
null
>
(
null
)
let
abortController
:
AbortController
|
null
=
null
// Action Menu State
const
activeMenuId
=
ref
<
number
|
null
>
(
null
)
const
menuPosition
=
ref
<
{
top
:
number
;
left
:
number
}
|
null
>
(
null
)
const
openActionMenu
=
(
user
:
User
,
e
:
MouseEvent
)
=>
{
const
openActionMenu
=
(
user
:
Admin
User
,
e
:
MouseEvent
)
=>
{
if
(
activeMenuId
.
value
===
user
.
id
)
{
closeActionMenu
()
}
else
{
...
...
@@ -821,11 +821,11 @@ const handleClickOutside = (event: MouseEvent) => {
// Allowed groups modal state
const
showAllowedGroupsModal
=
ref
(
false
)
const
allowedGroupsUser
=
ref
<
User
|
null
>
(
null
)
const
allowedGroupsUser
=
ref
<
Admin
User
|
null
>
(
null
)
// Balance (Deposit/Withdraw) modal state
const
showBalanceModal
=
ref
(
false
)
const
balanceUser
=
ref
<
User
|
null
>
(
null
)
const
balanceUser
=
ref
<
Admin
User
|
null
>
(
null
)
const
balanceOperation
=
ref
<
'
add
'
|
'
subtract
'
>
(
'
add
'
)
// 计算剩余天数
...
...
@@ -998,7 +998,7 @@ const applyFilter = () => {
loadUsers
()
}
const
handleEdit
=
(
user
:
User
)
=>
{
const
handleEdit
=
(
user
:
Admin
User
)
=>
{
editingUser
.
value
=
user
showEditModal
.
value
=
true
}
...
...
@@ -1008,7 +1008,7 @@ const closeEditModal = () => {
editingUser
.
value
=
null
}
const
handleToggleStatus
=
async
(
user
:
User
)
=>
{
const
handleToggleStatus
=
async
(
user
:
Admin
User
)
=>
{
const
newStatus
=
user
.
status
===
'
active
'
?
'
disabled
'
:
'
active
'
try
{
await
adminAPI
.
users
.
toggleStatus
(
user
.
id
,
newStatus
)
...
...
@@ -1022,7 +1022,7 @@ const handleToggleStatus = async (user: User) => {
}
}
const
handleViewApiKeys
=
(
user
:
User
)
=>
{
const
handleViewApiKeys
=
(
user
:
Admin
User
)
=>
{
viewingUser
.
value
=
user
showApiKeysModal
.
value
=
true
}
...
...
@@ -1032,7 +1032,7 @@ const closeApiKeysModal = () => {
viewingUser
.
value
=
null
}
const
handleAllowedGroups
=
(
user
:
User
)
=>
{
const
handleAllowedGroups
=
(
user
:
Admin
User
)
=>
{
allowedGroupsUser
.
value
=
user
showAllowedGroupsModal
.
value
=
true
}
...
...
@@ -1042,7 +1042,7 @@ const closeAllowedGroupsModal = () => {
allowedGroupsUser
.
value
=
null
}
const
handleDelete
=
(
user
:
User
)
=>
{
const
handleDelete
=
(
user
:
Admin
User
)
=>
{
deletingUser
.
value
=
user
showDeleteDialog
.
value
=
true
}
...
...
@@ -1061,13 +1061,13 @@ const confirmDelete = async () => {
}
}
const
handleDeposit
=
(
user
:
User
)
=>
{
const
handleDeposit
=
(
user
:
Admin
User
)
=>
{
balanceUser
.
value
=
user
balanceOperation
.
value
=
'
add
'
showBalanceModal
.
value
=
true
}
const
handleWithdraw
=
(
user
:
User
)
=>
{
const
handleWithdraw
=
(
user
:
Admin
User
)
=>
{
balanceUser
.
value
=
user
balanceOperation
.
value
=
'
subtract
'
showBalanceModal
.
value
=
true
...
...
frontend/src/views/user/KeysView.vue
View file @
c8e2f614
...
...
@@ -133,6 +133,7 @@
</button>
<!-- Import to CC Switch Button -->
<button
v-if=
"!publicSettings?.hide_ccs_import_button"
@
click=
"importToCcswitch(row)"
class=
"flex flex-col items-center gap-0.5 rounded-lg p-1.5 text-gray-500 transition-colors hover:bg-blue-50 hover:text-blue-600 dark:hover:bg-blue-900/20 dark:hover:text-blue-400"
>
...
...
frontend/tsconfig.json
View file @
c8e2f614
...
...
@@ -21,5 +21,6 @@
"types"
:
[
"vite/client"
]
},
"include"
:
[
"src/**/*.ts"
,
"src/**/*.tsx"
,
"src/**/*.vue"
],
"exclude"
:
[
"src/**/__tests__/**"
,
"src/**/*.spec.ts"
,
"src/**/*.test.ts"
],
"references"
:
[{
"path"
:
"./tsconfig.node.json"
}]
}
Prev
1
…
5
6
7
8
9
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