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
2ab6b34f
Commit
2ab6b34f
authored
Apr 27, 2026
by
KnowSky404
Browse files
feat: add filtered-result account bulk edit
parent
764afbe3
Changes
4
Show whitespace changes
Inline
Side-by-side
frontend/src/api/admin/accounts.ts
View file @
2ab6b34f
...
...
@@ -370,8 +370,8 @@ export async function batchUpdateCredentials(request: {
* @returns Success confirmation
*/
export
async
function
bulkUpdate
(
accountIds
:
number
[],
updates
:
Record
<
string
,
unknown
>
accountIds
OrPayload
:
number
[]
|
Record
<
string
,
unknown
>
,
updates
?
:
Record
<
string
,
unknown
>
):
Promise
<
{
success
:
number
failed
:
number
...
...
@@ -379,16 +379,19 @@ export async function bulkUpdate(
failed_ids
?:
number
[]
results
:
Array
<
{
account_id
:
number
;
success
:
boolean
;
error
?:
string
}
>
}
>
{
const
payload
=
Array
.
isArray
(
accountIdsOrPayload
)
?
{
account_ids
:
accountIdsOrPayload
,
...(
updates
??
{})
}
:
accountIdsOrPayload
const
{
data
}
=
await
apiClient
.
post
<
{
success
:
number
failed
:
number
success_ids
?:
number
[]
failed_ids
?:
number
[]
results
:
Array
<
{
account_id
:
number
;
success
:
boolean
;
error
?:
string
}
>
}
>
(
'
/admin/accounts/bulk-update
'
,
{
account_ids
:
accountIds
,
...
updates
})
}
>
(
'
/admin/accounts/bulk-update
'
,
payload
)
return
data
}
...
...
frontend/src/components/account/BulkEditAccountModal.vue
View file @
2ab6b34f
...
...
@@ -17,7 +17,7 @@
d=
"M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
{{
t
(
'
admin.accounts.bulkEdit.selectionInfo
'
,
{
count
:
accountIds
.
length
}
)
}}
{{
t
(
'
admin.accounts.bulkEdit.selectionInfo
'
,
{
count
:
targetMode
===
'
filtered
'
?
targetPreviewCount
:
accountIds
.
length
}
)
}}
<
/p
>
<
/div
>
...
...
@@ -27,7 +27,7 @@
<
svg
class
=
"
mr-1.5 inline h-5 w-5
"
fill
=
"
none
"
viewBox
=
"
0 0 24 24
"
stroke
=
"
currentColor
"
>
<
path
stroke
-
linecap
=
"
round
"
stroke
-
linejoin
=
"
round
"
stroke
-
width
=
"
2
"
d
=
"
M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z
"
/>
<
/svg
>
{{
t
(
'
admin.accounts.bulkEdit.mixedPlatformWarning
'
,
{
platforms
:
s
electedPlatforms
.
join
(
'
,
'
)
}
)
}}
{{
t
(
'
admin.accounts.bulkEdit.mixedPlatformWarning
'
,
{
platforms
:
targetS
electedPlatforms
.
join
(
'
,
'
)
}
)
}}
<
/p
>
<
/div
>
...
...
@@ -227,7 +227,7 @@
<
ModelWhitelistSelector
v
-
model
=
"
allowedModels
"
:
platforms
=
"
s
electedPlatforms
"
:
platforms
=
"
targetS
electedPlatforms
"
/>
<
p
class
=
"
text-xs text-gray-500 dark:text-gray-400
"
>
...
...
@@ -933,6 +933,13 @@ interface Props {
accountIds
:
number
[]
selectedPlatforms
:
AccountPlatform
[]
selectedTypes
:
AccountType
[]
target
?:
{
mode
:
'
selected
'
|
'
filtered
'
filters
?:
Record
<
string
,
unknown
>
previewCount
?:
number
selectedPlatforms
?:
AccountPlatform
[]
selectedTypes
?:
AccountType
[]
}
proxies
:
ProxyConfig
[]
groups
:
AdminGroup
[]
}
...
...
@@ -947,40 +954,53 @@ const { t } = useI18n()
const
appStore
=
useAppStore
()
// Platform awareness
const
isMixedPlatform
=
computed
(()
=>
props
.
selectedPlatforms
.
length
>
1
)
const
targetMode
=
computed
(()
=>
props
.
target
?.
mode
??
'
selected
'
)
const
targetPreviewCount
=
computed
(()
=>
props
.
target
?.
previewCount
??
props
.
accountIds
.
length
)
const
targetSelectedPlatforms
=
computed
(()
=>
props
.
target
?.
selectedPlatforms
??
props
.
selectedPlatforms
)
const
targetSelectedTypes
=
computed
(()
=>
props
.
target
?.
selectedTypes
??
props
.
selectedTypes
)
const
isMixedPlatform
=
computed
(()
=>
targetSelectedPlatforms
.
value
.
length
>
1
)
const
allOpenAIPassthroughCapable
=
computed
(()
=>
{
return
(
props
.
s
electedPlatforms
.
length
===
1
&&
props
.
s
electedPlatforms
[
0
]
===
'
openai
'
&&
props
.
s
electedTypes
.
length
>
0
&&
props
.
s
electedTypes
.
every
(
t
=>
t
===
'
oauth
'
||
t
===
'
apikey
'
)
targetS
electedPlatforms
.
value
.
length
===
1
&&
targetS
electedPlatforms
.
value
[
0
]
===
'
openai
'
&&
targetS
electedTypes
.
value
.
length
>
0
&&
targetS
electedTypes
.
value
.
every
(
t
=>
t
===
'
oauth
'
||
t
===
'
apikey
'
)
)
}
)
const
allOpenAIOAuth
=
computed
(()
=>
{
return
(
props
.
selectedPlatforms
.
length
===
1
&&
props
.
selectedPlatforms
[
0
]
===
'
openai
'
&&
props
.
selectedTypes
.
length
>
0
&&
props
.
selectedTypes
.
every
(
t
=>
t
===
'
oauth
'
)
targetSelectedPlatforms
.
value
.
length
===
1
&&
targetSelectedPlatforms
.
value
[
0
]
===
'
openai
'
&&
targetSelectedTypes
.
value
.
length
>
0
&&
targetSelectedTypes
.
value
.
every
(
t
=>
t
===
'
oauth
'
)
)
}
)
const
allOpenAIAPIKey
=
computed
(()
=>
{
return
(
targetSelectedPlatforms
.
value
.
length
===
1
&&
targetSelectedPlatforms
.
value
[
0
]
===
'
openai
'
&&
targetSelectedTypes
.
value
.
length
>
0
&&
targetSelectedTypes
.
value
.
every
(
t
=>
t
===
'
apikey
'
)
)
}
)
// 是否全部为 Anthropic OAuth/SetupToken(RPM 配置仅在此条件下显示)
const
allAnthropicOAuthOrSetupToken
=
computed
(()
=>
{
return
(
props
.
s
electedPlatforms
.
length
===
1
&&
props
.
s
electedPlatforms
[
0
]
===
'
anthropic
'
&&
props
.
s
electedTypes
.
every
(
t
=>
t
===
'
oauth
'
||
t
===
'
setup-token
'
)
targetS
electedPlatforms
.
value
.
length
===
1
&&
targetS
electedPlatforms
.
value
[
0
]
===
'
anthropic
'
&&
targetS
electedTypes
.
value
.
every
(
t
=>
t
===
'
oauth
'
||
t
===
'
setup-token
'
)
)
}
)
const
filteredPresets
=
computed
(()
=>
{
if
(
props
.
s
electedPlatforms
.
length
===
0
)
return
[]
if
(
targetS
electedPlatforms
.
value
.
length
===
0
)
return
[]
const
dedupedPresets
=
new
Map
<
string
,
ReturnType
<
typeof
getPresetMappingsByPlatform
>
[
number
]
>
()
for
(
const
platform
of
props
.
s
electedPlatforms
)
{
for
(
const
platform
of
targetS
electedPlatforms
.
value
)
{
for
(
const
preset
of
getPresetMappingsByPlatform
(
platform
))
{
const
key
=
`${preset.from
}
=>${preset.to
}
`
if
(
!
dedupedPresets
.
has
(
key
))
{
...
...
@@ -1291,8 +1311,8 @@ const mixedChannelConfirmed = ref(false)
const
canPreCheck
=
()
=>
enableGroups
.
value
&&
groupIds
.
value
.
length
>
0
&&
props
.
s
electedPlatforms
.
length
===
1
&&
(
props
.
s
electedPlatforms
[
0
]
===
'
antigravity
'
||
props
.
s
electedPlatforms
[
0
]
===
'
anthropic
'
)
targetS
electedPlatforms
.
value
.
length
===
1
&&
(
targetS
electedPlatforms
.
value
[
0
]
===
'
antigravity
'
||
targetS
electedPlatforms
.
value
[
0
]
===
'
anthropic
'
)
const
handleClose
=
()
=>
{
showMixedChannelWarning
.
value
=
false
...
...
@@ -1309,7 +1329,7 @@ const preCheckMixedChannelRisk = async (built: Record<string, unknown>): Promise
try
{
const
result
=
await
adminAPI
.
accounts
.
checkMixedChannelRisk
({
platform
:
props
.
s
electedPlatforms
[
0
],
platform
:
targetS
electedPlatforms
.
value
[
0
],
group_ids
:
groupIds
.
value
}
)
if
(
!
result
.
has_risk
)
return
true
...
...
@@ -1325,7 +1345,7 @@ const preCheckMixedChannelRisk = async (built: Record<string, unknown>): Promise
}
const
handleSubmit
=
async
()
=>
{
if
(
props
.
accountIds
.
length
===
0
)
{
if
(
targetMode
.
value
===
'
selected
'
&&
props
.
accountIds
.
length
===
0
)
{
appStore
.
showError
(
t
(
'
admin.accounts.bulkEdit.noSelection
'
))
return
}
...
...
@@ -1373,7 +1393,12 @@ const submitBulkUpdate = async (baseUpdates: Record<string, unknown>) => {
submitting
.
value
=
true
try
{
const
res
=
await
adminAPI
.
accounts
.
bulkUpdate
(
props
.
accountIds
,
updates
)
const
res
=
targetMode
.
value
===
'
filtered
'
&&
props
.
target
?.
filters
?
await
adminAPI
.
accounts
.
bulkUpdate
({
filters
:
props
.
target
.
filters
,
...
updates
}
)
:
await
adminAPI
.
accounts
.
bulkUpdate
(
props
.
accountIds
,
updates
)
const
success
=
res
.
success
||
0
const
failed
=
res
.
failed
||
0
...
...
frontend/src/components/admin/account/AccountBulkActionsBar.vue
View file @
2ab6b34f
<
template
>
<div
v-if=
"selectedIds.length > 0"
class=
"mb-4 flex items-center justify-between
p-3
bg-primary-50
rounded-lg
dark:bg-primary-900/20"
>
<div
class=
"mb-4 flex items-center justify-between
rounded-lg
bg-primary-50
p-3
dark:bg-primary-900/20"
>
<div
class=
"flex flex-wrap items-center gap-2"
>
<span
class=
"text-sm font-medium text-primary-900 dark:text-primary-100"
>
<span
v-if=
"selectedIds.length > 0"
class=
"text-sm font-medium text-primary-900 dark:text-primary-100"
>
{{
t
(
'
admin.accounts.bulkActions.selected
'
,
{
count
:
selectedIds
.
length
}
)
}}
<
/span
>
<
span
v
-
else
class
=
"
text-sm font-medium text-primary-900 dark:text-primary-100
"
>
{{
t
(
'
admin.accounts.bulkEdit.title
'
)
}}
<
/span
>
<
template
v
-
if
=
"
selectedIds.length > 0
"
>
<
button
@
click
=
"
$emit('select-page')
"
class
=
"
text-xs font-medium text-primary-700 hover:text-primary-800 dark:text-primary-300 dark:hover:text-primary-200
"
...
...
@@ -17,19 +21,25 @@
>
{{
t
(
'
admin.accounts.bulkActions.clear
'
)
}}
<
/button
>
<
/template
>
<
/div
>
<
div
class
=
"
flex gap-2
"
>
<
template
v
-
if
=
"
selectedIds.length > 0
"
>
<
button
@
click
=
"
$emit('delete')
"
class
=
"
btn btn-danger btn-sm
"
>
{{
t
(
'
admin.accounts.bulkActions.delete
'
)
}}
<
/button
>
<
button
@
click
=
"
$emit('reset-status')
"
class
=
"
btn btn-secondary btn-sm
"
>
{{
t
(
'
admin.accounts.bulkActions.resetStatus
'
)
}}
<
/button
>
<
button
@
click
=
"
$emit('refresh-token')
"
class
=
"
btn btn-secondary btn-sm
"
>
{{
t
(
'
admin.accounts.bulkActions.refreshToken
'
)
}}
<
/button
>
<
button
@
click
=
"
$emit('toggle-schedulable', true)
"
class
=
"
btn btn-success btn-sm
"
>
{{
t
(
'
admin.accounts.bulkActions.enableScheduling
'
)
}}
<
/button
>
<
button
@
click
=
"
$emit('toggle-schedulable', false)
"
class
=
"
btn btn-warning btn-sm
"
>
{{
t
(
'
admin.accounts.bulkActions.disableScheduling
'
)
}}
<
/button
>
<
button
@
click
=
"
$emit('edit')
"
class
=
"
btn btn-primary btn-sm
"
>
{{
t
(
'
admin.accounts.bulkActions.edit
'
)
}}
<
/button
>
<
button
@
click
=
"
$emit('edit-selected')
"
class
=
"
btn btn-primary btn-sm
"
>
{{
t
(
'
admin.accounts.bulkActions.edit
'
)
}}
<
/button
>
<
/template
>
<
button
@
click
=
"
$emit('edit-filtered')
"
class
=
"
btn btn-primary btn-sm
"
>
{{
t
(
'
admin.accounts.bulkEdit.submit
'
)
}}
<
/button
>
<
/div
>
<
/div
>
<
/template
>
<
script
setup
lang
=
"
ts
"
>
import
{
useI18n
}
from
'
vue-i18n
'
defineProps
([
'
selectedIds
'
]);
defineEmits
([
'
delete
'
,
'
edit
'
,
'
clear
'
,
'
select-page
'
,
'
toggle-schedulable
'
,
'
reset-status
'
,
'
refresh-token
'
]);
const
{
t
}
=
useI18n
()
defineProps
([
'
selectedIds
'
]);
defineEmits
([
'
delete
'
,
'
edit
-selected
'
,
'
edit-filtered
'
,
'
clear
'
,
'
select-page
'
,
'
toggle-schedulable
'
,
'
reset-status
'
,
'
refresh-token
'
]);
const
{
t
}
=
useI18n
()
<
/script
>
frontend/src/views/admin/AccountsView.vue
View file @
2ab6b34f
...
...
@@ -141,7 +141,17 @@
<
/div
>
<
/template
>
<
template
#
table
>
<
AccountBulkActionsBar
:
selected
-
ids
=
"
selIds
"
@
delete
=
"
handleBulkDelete
"
@
reset
-
status
=
"
handleBulkResetStatus
"
@
refresh
-
token
=
"
handleBulkRefreshToken
"
@
edit
=
"
showBulkEdit = true
"
@
clear
=
"
clearSelection
"
@
select
-
page
=
"
selectPage
"
@
toggle
-
schedulable
=
"
handleBulkToggleSchedulable
"
/>
<
AccountBulkActionsBar
:
selected
-
ids
=
"
selIds
"
@
delete
=
"
handleBulkDelete
"
@
reset
-
status
=
"
handleBulkResetStatus
"
@
refresh
-
token
=
"
handleBulkRefreshToken
"
@
edit
-
selected
=
"
openBulkEditSelected
"
@
edit
-
filtered
=
"
openBulkEditFiltered
"
@
clear
=
"
clearSelection
"
@
select
-
page
=
"
selectPage
"
@
toggle
-
schedulable
=
"
handleBulkToggleSchedulable
"
/>
<
div
ref
=
"
accountTableRef
"
class
=
"
flex min-h-0 flex-1 flex-col overflow-hidden
"
>
<
DataTable
ref
=
"
dataTableRef
"
...
...
@@ -303,7 +313,17 @@
<
AccountActionMenu
:
show
=
"
menu.show
"
:
account
=
"
menu.acc
"
:
position
=
"
menu.pos
"
@
close
=
"
menu.show = false
"
@
test
=
"
handleTest
"
@
stats
=
"
handleViewStats
"
@
schedule
=
"
handleSchedule
"
@
reauth
=
"
handleReAuth
"
@
refresh
-
token
=
"
handleRefresh
"
@
recover
-
state
=
"
handleRecoverState
"
@
reset
-
quota
=
"
handleResetQuota
"
@
set
-
privacy
=
"
handleSetPrivacy
"
/>
<
SyncFromCrsModal
:
show
=
"
showSync
"
@
close
=
"
showSync = false
"
@
synced
=
"
reload
"
/>
<
ImportDataModal
:
show
=
"
showImportData
"
@
close
=
"
showImportData = false
"
@
imported
=
"
handleDataImported
"
/>
<
BulkEditAccountModal
:
show
=
"
showBulkEdit
"
:
account
-
ids
=
"
selIds
"
:
selected
-
platforms
=
"
selPlatforms
"
:
selected
-
types
=
"
selTypes
"
:
proxies
=
"
proxies
"
:
groups
=
"
groups
"
@
close
=
"
showBulkEdit = false
"
@
updated
=
"
handleBulkUpdated
"
/>
<
BulkEditAccountModal
:
show
=
"
showBulkEdit
"
:
account
-
ids
=
"
selIds
"
:
selected
-
platforms
=
"
selPlatforms
"
:
selected
-
types
=
"
selTypes
"
:
target
=
"
bulkEditTarget
"
:
proxies
=
"
proxies
"
:
groups
=
"
groups
"
@
close
=
"
showBulkEdit = false
"
@
updated
=
"
handleBulkUpdated
"
/>
<
TempUnschedStatusModal
:
show
=
"
showTempUnsched
"
:
account
=
"
tempUnschedAcc
"
@
close
=
"
showTempUnsched = false
"
@
reset
=
"
handleTempUnschedReset
"
/>
<
ConfirmDialog
:
show
=
"
showDeleteDialog
"
:
title
=
"
t('admin.accounts.deleteAccount')
"
:
message
=
"
t('admin.accounts.deleteConfirm', { name: deletingAcc?.name
}
)
"
:
confirm
-
text
=
"
t('common.delete')
"
:
cancel
-
text
=
"
t('common.cancel')
"
:
danger
=
"
true
"
@
confirm
=
"
confirmDelete
"
@
cancel
=
"
showDeleteDialog = false
"
/>
<
ConfirmDialog
:
show
=
"
showExportDataDialog
"
:
title
=
"
t('admin.accounts.dataExport')
"
:
message
=
"
t('admin.accounts.dataExportConfirmMessage')
"
:
confirm
-
text
=
"
t('admin.accounts.dataExportConfirm')
"
:
cancel
-
text
=
"
t('common.cancel')
"
@
confirm
=
"
handleExportData
"
@
cancel
=
"
showExportDataDialog = false
"
>
...
...
@@ -364,6 +384,29 @@ const proxies = ref<AccountProxy[]>([])
const
groups
=
ref
<
AdminGroup
[]
>
([])
const
accountTableRef
=
ref
<
HTMLElement
|
null
>
(
null
)
const
dataTableRef
=
ref
<
InstanceType
<
typeof
DataTable
>
|
null
>
(
null
)
type
AccountBulkEditTarget
=
|
{
mode
:
'
selected
'
accountIds
:
number
[]
selectedPlatforms
:
AccountPlatform
[]
selectedTypes
:
AccountType
[]
}
|
{
mode
:
'
filtered
'
filters
:
{
platform
?:
string
type
?:
string
status
?:
string
group
?:
string
search
?:
string
privacy_mode
?:
string
sort_by
?:
string
sort_order
?:
AccountSortOrder
}
previewCount
:
number
selectedPlatforms
:
AccountPlatform
[]
selectedTypes
:
AccountType
[]
}
const
selPlatforms
=
computed
<
AccountPlatform
[]
>
(()
=>
{
const
platforms
=
new
Set
(
accounts
.
value
...
...
@@ -387,6 +430,7 @@ const showImportData = ref(false)
const
showExportDataDialog
=
ref
(
false
)
const
includeProxyOnExport
=
ref
(
true
)
const
showBulkEdit
=
ref
(
false
)
const
bulkEditTarget
=
ref
<
AccountBulkEditTarget
|
null
>
(
null
)
const
showTempUnsched
=
ref
(
false
)
const
showDeleteDialog
=
ref
(
false
)
const
showReAuth
=
ref
(
false
)
...
...
@@ -1216,7 +1260,56 @@ const handleBulkToggleSchedulable = async (schedulable: boolean) => {
appStore
.
showError
(
t
(
'
common.error
'
))
}
}
const
handleBulkUpdated
=
()
=>
{
showBulkEdit
.
value
=
false
;
clearSelection
();
reload
()
}
const
buildBulkEditFilterSnapshot
=
()
=>
{
const
rawParams
=
toRaw
(
params
)
as
Record
<
string
,
unknown
>
return
{
platform
:
typeof
rawParams
.
platform
===
'
string
'
?
rawParams
.
platform
:
''
,
type
:
typeof
rawParams
.
type
===
'
string
'
?
rawParams
.
type
:
''
,
status
:
typeof
rawParams
.
status
===
'
string
'
?
rawParams
.
status
:
''
,
group
:
typeof
rawParams
.
group
===
'
string
'
?
rawParams
.
group
:
''
,
search
:
typeof
rawParams
.
search
===
'
string
'
?
rawParams
.
search
:
''
,
privacy_mode
:
typeof
rawParams
.
privacy_mode
===
'
string
'
?
rawParams
.
privacy_mode
:
''
,
sort_by
:
typeof
rawParams
.
sort_by
===
'
string
'
?
rawParams
.
sort_by
:
''
,
sort_order
:
rawParams
.
sort_order
===
'
desc
'
?
'
desc
'
:
'
asc
'
}
}
const
collectSelectionMetadata
=
(
rows
:
Account
[])
=>
{
const
selectedPlatforms
=
Array
.
from
(
new
Set
(
rows
.
map
(
account
=>
account
.
platform
)))
const
selectedTypes
=
Array
.
from
(
new
Set
(
rows
.
map
(
account
=>
account
.
type
)))
return
{
selectedPlatforms
,
selectedTypes
}
}
const
openBulkEditSelected
=
()
=>
{
bulkEditTarget
.
value
=
{
mode
:
'
selected
'
,
accountIds
:
[...
selIds
.
value
],
selectedPlatforms
:
[...
selPlatforms
.
value
],
selectedTypes
:
[...
selTypes
.
value
]
}
showBulkEdit
.
value
=
true
}
const
openBulkEditFiltered
=
async
()
=>
{
const
filters
=
buildBulkEditFilterSnapshot
()
const
preview
=
await
adminAPI
.
accounts
.
list
(
1
,
100
,
filters
)
const
{
selectedPlatforms
,
selectedTypes
}
=
collectSelectionMetadata
(
preview
.
items
)
bulkEditTarget
.
value
=
{
mode
:
'
filtered
'
,
filters
,
previewCount
:
preview
.
total
,
selectedPlatforms
,
selectedTypes
}
showBulkEdit
.
value
=
true
}
const
handleBulkUpdated
=
()
=>
{
showBulkEdit
.
value
=
false
bulkEditTarget
.
value
=
null
clearSelection
()
reload
()
}
const
handleDataImported
=
()
=>
{
showImportData
.
value
=
false
;
reload
()
}
const
ACCOUNT_UNGROUPED_GROUP_QUERY_VALUE
=
'
ungrouped
'
const
ACCOUNT_PRIVACY_MODE_UNSET_QUERY_VALUE
=
'
__unset__
'
...
...
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