diff --git a/frontend/src/api/admin/accounts.ts b/frontend/src/api/admin/accounts.ts
index a146f1f7..8a127793 100644
--- a/frontend/src/api/admin/accounts.ts
+++ b/frontend/src/api/admin/accounts.ts
@@ -370,8 +370,8 @@ export async function batchUpdateCredentials(request: {
* @returns Success confirmation
*/
export async function bulkUpdate(
- accountIds: number[],
- updates: Record
+ accountIdsOrPayload: number[] | Record,
+ updates?: Record
): 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
}
diff --git a/frontend/src/components/account/BulkEditAccountModal.vue b/frontend/src/components/account/BulkEditAccountModal.vue
index 13c30cf9..b55456ff 100644
--- a/frontend/src/components/account/BulkEditAccountModal.vue
+++ b/frontend/src/components/account/BulkEditAccountModal.vue
@@ -17,7 +17,7 @@
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
- {{ t('admin.accounts.bulkEdit.selectionInfo', { count: accountIds.length }) }}
+ {{ t('admin.accounts.bulkEdit.selectionInfo', { count: targetMode === 'filtered' ? targetPreviewCount : accountIds.length }) }}
@@ -27,7 +27,7 @@
- {{ t('admin.accounts.bulkEdit.mixedPlatformWarning', { platforms: selectedPlatforms.join(', ') }) }}
+ {{ t('admin.accounts.bulkEdit.mixedPlatformWarning', { platforms: targetSelectedPlatforms.join(', ') }) }}
@@ -227,7 +227,7 @@
@@ -933,6 +933,13 @@ interface Props {
accountIds: number[]
selectedPlatforms: AccountPlatform[]
selectedTypes: AccountType[]
+ target?: {
+ mode: 'selected' | 'filtered'
+ filters?: Record
+ 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.selectedPlatforms.length === 1 &&
- props.selectedPlatforms[0] === 'openai' &&
- props.selectedTypes.length > 0 &&
- props.selectedTypes.every(t => t === 'oauth' || t === 'apikey')
+ targetSelectedPlatforms.value.length === 1 &&
+ targetSelectedPlatforms.value[0] === 'openai' &&
+ targetSelectedTypes.value.length > 0 &&
+ targetSelectedTypes.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.selectedPlatforms.length === 1 &&
- props.selectedPlatforms[0] === 'anthropic' &&
- props.selectedTypes.every(t => t === 'oauth' || t === 'setup-token')
+ targetSelectedPlatforms.value.length === 1 &&
+ targetSelectedPlatforms.value[0] === 'anthropic' &&
+ targetSelectedTypes.value.every(t => t === 'oauth' || t === 'setup-token')
)
})
const filteredPresets = computed(() => {
- if (props.selectedPlatforms.length === 0) return []
+ if (targetSelectedPlatforms.value.length === 0) return []
const dedupedPresets = new Map[number]>()
- for (const platform of props.selectedPlatforms) {
+ for (const platform of targetSelectedPlatforms.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.selectedPlatforms.length === 1 &&
- (props.selectedPlatforms[0] === 'antigravity' || props.selectedPlatforms[0] === 'anthropic')
+ targetSelectedPlatforms.value.length === 1 &&
+ (targetSelectedPlatforms.value[0] === 'antigravity' || targetSelectedPlatforms.value[0] === 'anthropic')
const handleClose = () => {
showMixedChannelWarning.value = false
@@ -1309,7 +1329,7 @@ const preCheckMixedChannelRisk = async (built: Record): Promise
try {
const result = await adminAPI.accounts.checkMixedChannelRisk({
- platform: props.selectedPlatforms[0],
+ platform: targetSelectedPlatforms.value[0],
group_ids: groupIds.value
})
if (!result.has_risk) return true
@@ -1325,7 +1345,7 @@ const preCheckMixedChannelRisk = async (built: Record): 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) => {
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
diff --git a/frontend/src/components/admin/account/AccountBulkActionsBar.vue b/frontend/src/components/admin/account/AccountBulkActionsBar.vue
index 3b987bd0..a632bdd4 100644
--- a/frontend/src/components/admin/account/AccountBulkActionsBar.vue
+++ b/frontend/src/components/admin/account/AccountBulkActionsBar.vue
@@ -1,9 +1,13 @@
-
+
-
+
{{ t('admin.accounts.bulkActions.selected', { count: selectedIds.length }) }}
+
+ {{ t('admin.accounts.bulkEdit.title') }}
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/views/admin/AccountsView.vue b/frontend/src/views/admin/AccountsView.vue
index bc4c6215..2f061118 100644
--- a/frontend/src/views/admin/AccountsView.vue
+++ b/frontend/src/views/admin/AccountsView.vue
@@ -141,7 +141,17 @@
-
+
-
+
@@ -364,6 +384,29 @@ const proxies = ref([])
const groups = ref([])
const accountTableRef = ref(null)
const dataTableRef = ref | 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(() => {
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(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
+ 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__'