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
62dc0b95
Commit
62dc0b95
authored
Jan 09, 2026
by
shaw
Browse files
Merge branch 'fix/table-pagination-and-features'
parents
f060db0b
7c3d5cad
Changes
26
Show whitespace changes
Inline
Side-by-side
frontend/src/i18n/locales/zh.ts
View file @
62dc0b95
...
...
@@ -1221,12 +1221,16 @@ export default {
accountCreatedSuccess
:
'
账号添加成功
'
,
accountUpdatedSuccess
:
'
账号更新成功
'
,
accountDeletedSuccess
:
'
账号删除成功
'
,
bulkSchedulableEnabled
:
'
成功启用 {count} 个账号的调度
'
,
bulkSchedulableDisabled
:
'
成功停止 {count} 个账号的调度
'
,
bulkActions
:
{
selected
:
'
已选择 {count} 个账号
'
,
selectCurrentPage
:
'
本页全选
'
,
clear
:
'
清除选择
'
,
edit
:
'
批量编辑账号
'
,
delete
:
'
批量删除
'
delete
:
'
批量删除
'
,
enableScheduling
:
'
批量启用调度
'
,
disableScheduling
:
'
批量停止调度
'
},
bulkEdit
:
{
title
:
'
批量编辑账号
'
,
...
...
frontend/src/views/admin/AccountsView.vue
View file @
62dc0b95
...
...
@@ -7,7 +7,7 @@
v-model:searchQuery=
"params.search"
:filters=
"params"
@
update:filters=
"(newFilters) => Object.assign(params, newFilters)"
@
change=
"
r
eload"
@
change=
"
debouncedR
eload"
@
update:searchQuery=
"debouncedReload"
/>
<AccountTableActions
...
...
@@ -19,7 +19,7 @@
</div>
</
template
>
<
template
#table
>
<AccountBulkActionsBar
:selected-ids=
"selIds"
@
delete=
"handleBulkDelete"
@
edit=
"showBulkEdit = true"
@
clear=
"selIds = []"
@
select-page=
"selectPage"
/>
<AccountBulkActionsBar
:selected-ids=
"selIds"
@
delete=
"handleBulkDelete"
@
edit=
"showBulkEdit = true"
@
clear=
"selIds = []"
@
select-page=
"selectPage"
@
toggle-schedulable=
"handleBulkToggleSchedulable"
/>
<DataTable
:columns=
"cols"
:data=
"accounts"
:loading=
"loading"
>
<template
#cell-select
="
{ row }">
<input
type=
"checkbox"
:checked=
"selIds.includes(row.id)"
@
change=
"toggleSel(row.id)"
class=
"rounded border-gray-300 text-primary-600 focus:ring-primary-500"
/>
...
...
@@ -107,7 +107,7 @@
</
template
>
</DataTable>
</template>
<
template
#pagination
><Pagination
v-if=
"pagination.total > 0"
:page=
"pagination.page"
:total=
"pagination.total"
:page-size=
"pagination.page_size"
@
update:page=
"handlePageChange"
/></
template
>
<
template
#pagination
><Pagination
v-if=
"pagination.total > 0"
:page=
"pagination.page"
:total=
"pagination.total"
:page-size=
"pagination.page_size"
@
update:page=
"handlePageChange"
@
update:pageSize=
"handlePageSizeChange"
/></
template
>
</TablePageLayout>
<CreateAccountModal
:show=
"showCreate"
:proxies=
"proxies"
:groups=
"groups"
@
close=
"showCreate = false"
@
created=
"reload"
/>
<EditAccountModal
:show=
"showEdit"
:account=
"edAcc"
:proxies=
"proxies"
:groups=
"groups"
@
close=
"showEdit = false"
@
updated=
"load"
/>
...
...
@@ -175,7 +175,7 @@ const statsAcc = ref<Account | null>(null)
const
togglingSchedulable
=
ref
<
number
|
null
>
(
null
)
const
menu
=
reactive
<
{
show
:
boolean
,
acc
:
Account
|
null
,
pos
:{
top
:
number
,
left
:
number
}
|
null
}
>
({
show
:
false
,
acc
:
null
,
pos
:
null
})
const
{
items
:
accounts
,
loading
,
params
,
pagination
,
load
,
reload
,
debouncedReload
,
handlePageChange
}
=
useTableLoader
<
Account
,
any
>
({
const
{
items
:
accounts
,
loading
,
params
,
pagination
,
load
,
reload
,
debouncedReload
,
handlePageChange
,
handlePageSizeChange
}
=
useTableLoader
<
Account
,
any
>
({
fetchFn
:
adminAPI
.
accounts
.
list
,
initialParams
:
{
platform
:
''
,
type
:
''
,
status
:
''
,
search
:
''
}
})
...
...
@@ -209,6 +209,21 @@ const openMenu = (a: Account, e: MouseEvent) => { menu.acc = a; menu.pos = { top
const
toggleSel
=
(
id
:
number
)
=>
{
const
i
=
selIds
.
value
.
indexOf
(
id
);
if
(
i
===
-
1
)
selIds
.
value
.
push
(
id
);
else
selIds
.
value
.
splice
(
i
,
1
)
}
const
selectPage
=
()
=>
{
selIds
.
value
=
[...
new
Set
([...
selIds
.
value
,
...
accounts
.
value
.
map
(
a
=>
a
.
id
)])]
}
const
handleBulkDelete
=
async
()
=>
{
if
(
!
confirm
(
t
(
'
common.confirm
'
)))
return
;
try
{
await
Promise
.
all
(
selIds
.
value
.
map
(
id
=>
adminAPI
.
accounts
.
delete
(
id
)));
selIds
.
value
=
[];
reload
()
}
catch
(
error
)
{
console
.
error
(
'
Failed to bulk delete accounts:
'
,
error
)
}
}
const
handleBulkToggleSchedulable
=
async
(
schedulable
:
boolean
)
=>
{
const
count
=
selIds
.
value
.
length
try
{
const
result
=
await
adminAPI
.
accounts
.
bulkUpdate
(
selIds
.
value
,
{
schedulable
});
const
message
=
schedulable
?
t
(
'
admin.accounts.bulkSchedulableEnabled
'
,
{
count
:
result
.
success
||
count
})
:
t
(
'
admin.accounts.bulkSchedulableDisabled
'
,
{
count
:
result
.
success
||
count
});
appStore
.
showSuccess
(
message
);
selIds
.
value
=
[];
reload
()
}
catch
(
error
)
{
console
.
error
(
'
Failed to bulk toggle schedulable:
'
,
error
);
appStore
.
showError
(
t
(
'
common.error
'
))
}
}
const
handleBulkUpdated
=
()
=>
{
showBulkEdit
.
value
=
false
;
selIds
.
value
=
[];
reload
()
}
const
closeTestModal
=
()
=>
{
showTest
.
value
=
false
;
testingAcc
.
value
=
null
}
const
closeStatsModal
=
()
=>
{
showStats
.
value
=
false
;
statsAcc
.
value
=
null
}
...
...
frontend/src/views/admin/GroupsView.vue
View file @
62dc0b95
...
...
@@ -16,6 +16,7 @@
type=
"text"
:placeholder=
"t('admin.groups.searchGroups')"
class=
"input pl-10"
@
input=
"handleSearch"
/>
</div>
<Select
...
...
@@ -64,7 +65,7 @@
</
template
>
<
template
#table
>
<DataTable
:columns=
"columns"
:data=
"
displayedG
roups"
:loading=
"loading"
>
<DataTable
:columns=
"columns"
:data=
"
g
roups"
:loading=
"loading"
>
<template
#cell-name
="
{ value }">
<span
class=
"font-medium text-gray-900 dark:text-white"
>
{{
value
}}
</span>
</
template
>
...
...
@@ -932,16 +933,6 @@ const pagination = reactive({
let
abortController
:
AbortController
|
null
=
null
const
displayedGroups
=
computed
(()
=>
{
const
q
=
searchQuery
.
value
.
trim
().
toLowerCase
()
if
(
!
q
)
return
groups
.
value
return
groups
.
value
.
filter
((
group
)
=>
{
const
name
=
group
.
name
?.
toLowerCase
?.()
??
''
const
description
=
group
.
description
?.
toLowerCase
?.()
??
''
return
name
.
includes
(
q
)
||
description
.
includes
(
q
)
}
)
}
)
const
showCreateModal
=
ref
(
false
)
const
showEditModal
=
ref
(
false
)
const
showDeleteDialog
=
ref
(
false
)
...
...
@@ -1011,7 +1002,8 @@ const loadGroups = async () => {
const
response
=
await
adminAPI
.
groups
.
list
(
pagination
.
page
,
pagination
.
page_size
,
{
platform
:
(
filters
.
platform
as
GroupPlatform
)
||
undefined
,
status
:
filters
.
status
as
any
,
is_exclusive
:
filters
.
is_exclusive
?
filters
.
is_exclusive
===
'
true
'
:
undefined
is_exclusive
:
filters
.
is_exclusive
?
filters
.
is_exclusive
===
'
true
'
:
undefined
,
search
:
searchQuery
.
value
.
trim
()
||
undefined
}
,
{
signal
}
)
if
(
signal
.
aborted
)
return
groups
.
value
=
response
.
items
...
...
@@ -1030,6 +1022,15 @@ const loadGroups = async () => {
}
}
let
searchTimeout
:
ReturnType
<
typeof
setTimeout
>
const
handleSearch
=
()
=>
{
clearTimeout
(
searchTimeout
)
searchTimeout
=
setTimeout
(()
=>
{
pagination
.
page
=
1
loadGroups
()
}
,
300
)
}
const
handlePageChange
=
(
page
:
number
)
=>
{
pagination
.
page
=
page
loadGroups
()
...
...
frontend/src/views/admin/ProxiesView.vue
View file @
62dc0b95
...
...
@@ -519,7 +519,7 @@
<
/template
>
<
script
setup
lang
=
"
ts
"
>
import
{
ref
,
reactive
,
computed
,
onMounted
}
from
'
vue
'
import
{
ref
,
reactive
,
computed
,
onMounted
,
onUnmounted
}
from
'
vue
'
import
{
useI18n
}
from
'
vue-i18n
'
import
{
useAppStore
}
from
'
@/stores/app
'
import
{
adminAPI
}
from
'
@/api/admin
'
...
...
@@ -942,4 +942,9 @@ const confirmDelete = async () => {
onMounted
(()
=>
{
loadProxies
()
}
)
onUnmounted
(()
=>
{
clearTimeout
(
searchTimeout
)
abortController
?.
abort
()
}
)
<
/script
>
frontend/src/views/admin/RedeemView.vue
View file @
62dc0b95
...
...
@@ -364,7 +364,7 @@
<
/template
>
<
script
setup
lang
=
"
ts
"
>
import
{
ref
,
reactive
,
computed
,
onMounted
}
from
'
vue
'
import
{
ref
,
reactive
,
computed
,
onMounted
,
onUnmounted
}
from
'
vue
'
import
{
useI18n
}
from
'
vue-i18n
'
import
{
useAppStore
}
from
'
@/stores/app
'
import
{
useClipboard
}
from
'
@/composables/useClipboard
'
...
...
@@ -693,4 +693,9 @@ onMounted(() => {
loadCodes
()
loadSubscriptionGroups
()
}
)
onUnmounted
(()
=>
{
clearTimeout
(
searchTimeout
)
abortController
?.
abort
()
}
)
<
/script
>
frontend/src/views/admin/UsersView.vue
View file @
62dc0b95
...
...
@@ -893,12 +893,13 @@ const loadUsers = async () => {
}
}
}
}
catch
(
error
)
{
}
catch
(
error
:
any
)
{
const
errorInfo
=
error
as
{
name
?:
string
;
code
?:
string
}
if
(
errorInfo
?.
name
===
'
AbortError
'
||
errorInfo
?.
name
===
'
CanceledError
'
||
errorInfo
?.
code
===
'
ERR_CANCELED
'
)
{
return
}
appStore
.
showError
(
t
(
'
admin.users.failedToLoad
'
))
const
message
=
error
.
response
?.
data
?.
detail
||
error
.
message
||
t
(
'
admin.users.failedToLoad
'
)
appStore
.
showError
(
message
)
console
.
error
(
'
Error loading users:
'
,
error
)
}
finally
{
if
(
abortController
===
currentAbortController
)
{
...
...
@@ -917,7 +918,9 @@ const handleSearch = () => {
}
const
handlePageChange
=
(
page
:
number
)
=>
{
pagination
.
page
=
page
// 确保页码在有效范围内
const
validPage
=
Math
.
max
(
1
,
Math
.
min
(
page
,
pagination
.
pages
||
1
))
pagination
.
page
=
validPage
loadUsers
()
}
...
...
@@ -943,6 +946,7 @@ const toggleBuiltInFilter = (key: string) => {
visibleFilters
.
add
(
key
)
}
saveFiltersToStorage
()
pagination
.
page
=
1
loadUsers
()
}
...
...
@@ -957,6 +961,7 @@ const toggleAttributeFilter = (attr: UserAttributeDefinition) => {
activeAttributeFilters
[
attr
.
id
]
=
''
}
saveFiltersToStorage
()
pagination
.
page
=
1
loadUsers
()
}
...
...
@@ -1059,5 +1064,7 @@ onMounted(async () => {
onUnmounted
(()
=>
{
document
.
removeEventListener
(
'
click
'
,
handleClickOutside
)
clearTimeout
(
searchTimeout
)
abortController
?.
abort
()
})
</
script
>
Prev
1
2
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