feat(account): 优化批量更新实现,使用统一 SQL 合并 JSONB 字段
- 新增 BulkUpdate 仓储方法,使用单条 SQL 更新所有账户 - credentials/extra 使用 COALESCE(...) || ? 合并,只更新传入的 key - name/proxy_id/concurrency/priority/status 只在提供时更新 - 分组绑定仍逐账号处理(需要独立操作) - 前端优化:Base URL 留空则不修改,按勾选字段更新 - 完善 i18n 文案:说明留空不修改、批量更新行为
This commit is contained in:
@@ -87,6 +87,19 @@ type UpdateAccountRequest struct {
|
|||||||
GroupIDs *[]int64 `json:"group_ids"`
|
GroupIDs *[]int64 `json:"group_ids"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BulkUpdateAccountsRequest represents the payload for bulk editing accounts
|
||||||
|
type BulkUpdateAccountsRequest struct {
|
||||||
|
AccountIDs []int64 `json:"account_ids" binding:"required,min=1"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
ProxyID *int64 `json:"proxy_id"`
|
||||||
|
Concurrency *int `json:"concurrency"`
|
||||||
|
Priority *int `json:"priority"`
|
||||||
|
Status string `json:"status" binding:"omitempty,oneof=active inactive error"`
|
||||||
|
GroupIDs *[]int64 `json:"group_ids"`
|
||||||
|
Credentials map[string]any `json:"credentials"`
|
||||||
|
Extra map[string]any `json:"extra"`
|
||||||
|
}
|
||||||
|
|
||||||
// AccountWithConcurrency extends Account with real-time concurrency info
|
// AccountWithConcurrency extends Account with real-time concurrency info
|
||||||
type AccountWithConcurrency struct {
|
type AccountWithConcurrency struct {
|
||||||
*model.Account
|
*model.Account
|
||||||
@@ -522,6 +535,48 @@ func (h *AccountHandler) BatchUpdateCredentials(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BulkUpdate handles bulk updating accounts with selected fields/credentials.
|
||||||
|
// POST /api/v1/admin/accounts/bulk-update
|
||||||
|
func (h *AccountHandler) BulkUpdate(c *gin.Context) {
|
||||||
|
var req BulkUpdateAccountsRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
response.BadRequest(c, "Invalid request: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
hasUpdates := req.Name != "" ||
|
||||||
|
req.ProxyID != nil ||
|
||||||
|
req.Concurrency != nil ||
|
||||||
|
req.Priority != nil ||
|
||||||
|
req.Status != "" ||
|
||||||
|
req.GroupIDs != nil ||
|
||||||
|
len(req.Credentials) > 0 ||
|
||||||
|
len(req.Extra) > 0
|
||||||
|
|
||||||
|
if !hasUpdates {
|
||||||
|
response.BadRequest(c, "No updates provided")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := h.adminService.BulkUpdateAccounts(c.Request.Context(), &service.BulkUpdateAccountsInput{
|
||||||
|
AccountIDs: req.AccountIDs,
|
||||||
|
Name: req.Name,
|
||||||
|
ProxyID: req.ProxyID,
|
||||||
|
Concurrency: req.Concurrency,
|
||||||
|
Priority: req.Priority,
|
||||||
|
Status: req.Status,
|
||||||
|
GroupIDs: req.GroupIDs,
|
||||||
|
Credentials: req.Credentials,
|
||||||
|
Extra: req.Extra,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
response.InternalError(c, "Failed to bulk update accounts: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Success(c, result)
|
||||||
|
}
|
||||||
|
|
||||||
// ========== OAuth Handlers ==========
|
// ========== OAuth Handlers ==========
|
||||||
|
|
||||||
// GenerateAuthURLRequest represents the request for generating auth URL
|
// GenerateAuthURLRequest represents the request for generating auth URL
|
||||||
|
|||||||
@@ -5,9 +5,11 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"github.com/Wei-Shaw/sub2api/internal/model"
|
"github.com/Wei-Shaw/sub2api/internal/model"
|
||||||
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/service/ports"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/clause"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AccountRepository struct {
|
type AccountRepository struct {
|
||||||
@@ -352,3 +354,47 @@ func (r *AccountRepository) UpdateExtra(ctx context.Context, id int64, updates m
|
|||||||
return r.db.WithContext(ctx).Model(&model.Account{}).Where("id = ?", id).
|
return r.db.WithContext(ctx).Model(&model.Account{}).Where("id = ?", id).
|
||||||
Update("extra", account.Extra).Error
|
Update("extra", account.Extra).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BulkUpdate updates multiple accounts with the provided fields.
|
||||||
|
// It merges credentials/extra JSONB fields instead of overwriting them.
|
||||||
|
func (r *AccountRepository) BulkUpdate(ctx context.Context, ids []int64, updates ports.AccountBulkUpdate) (int64, error) {
|
||||||
|
if len(ids) == 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
updateMap := map[string]any{}
|
||||||
|
|
||||||
|
if updates.Name != nil {
|
||||||
|
updateMap["name"] = *updates.Name
|
||||||
|
}
|
||||||
|
if updates.ProxyID != nil {
|
||||||
|
updateMap["proxy_id"] = updates.ProxyID
|
||||||
|
}
|
||||||
|
if updates.Concurrency != nil {
|
||||||
|
updateMap["concurrency"] = *updates.Concurrency
|
||||||
|
}
|
||||||
|
if updates.Priority != nil {
|
||||||
|
updateMap["priority"] = *updates.Priority
|
||||||
|
}
|
||||||
|
if updates.Status != nil {
|
||||||
|
updateMap["status"] = *updates.Status
|
||||||
|
}
|
||||||
|
if len(updates.Credentials) > 0 {
|
||||||
|
updateMap["credentials"] = gorm.Expr("COALESCE(credentials,'{}') || ?", updates.Credentials)
|
||||||
|
}
|
||||||
|
if len(updates.Extra) > 0 {
|
||||||
|
updateMap["extra"] = gorm.Expr("COALESCE(extra,'{}') || ?", updates.Extra)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(updateMap) == 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
result := r.db.WithContext(ctx).
|
||||||
|
Model(&model.Account{}).
|
||||||
|
Where("id IN ?", ids).
|
||||||
|
Clauses(clause.Returning{}).
|
||||||
|
Updates(updateMap)
|
||||||
|
|
||||||
|
return result.RowsAffected, result.Error
|
||||||
|
}
|
||||||
|
|||||||
@@ -194,6 +194,7 @@ func registerRoutes(r *gin.Engine, h *handler.Handlers, s *service.Services, rep
|
|||||||
accounts.GET("/:id/models", h.Admin.Account.GetAvailableModels)
|
accounts.GET("/:id/models", h.Admin.Account.GetAvailableModels)
|
||||||
accounts.POST("/batch", h.Admin.Account.BatchCreate)
|
accounts.POST("/batch", h.Admin.Account.BatchCreate)
|
||||||
accounts.POST("/batch-update-credentials", h.Admin.Account.BatchUpdateCredentials)
|
accounts.POST("/batch-update-credentials", h.Admin.Account.BatchUpdateCredentials)
|
||||||
|
accounts.POST("/bulk-update", h.Admin.Account.BulkUpdate)
|
||||||
|
|
||||||
// Claude OAuth routes
|
// Claude OAuth routes
|
||||||
accounts.POST("/generate-auth-url", h.Admin.OAuth.GenerateAuthURL)
|
accounts.POST("/generate-auth-url", h.Admin.OAuth.GenerateAuthURL)
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ type AdminService interface {
|
|||||||
RefreshAccountCredentials(ctx context.Context, id int64) (*model.Account, error)
|
RefreshAccountCredentials(ctx context.Context, id int64) (*model.Account, error)
|
||||||
ClearAccountError(ctx context.Context, id int64) (*model.Account, error)
|
ClearAccountError(ctx context.Context, id int64) (*model.Account, error)
|
||||||
SetAccountSchedulable(ctx context.Context, id int64, schedulable bool) (*model.Account, error)
|
SetAccountSchedulable(ctx context.Context, id int64, schedulable bool) (*model.Account, error)
|
||||||
|
BulkUpdateAccounts(ctx context.Context, input *BulkUpdateAccountsInput) (*BulkUpdateAccountsResult, error)
|
||||||
|
|
||||||
// Proxy management
|
// Proxy management
|
||||||
ListProxies(ctx context.Context, page, pageSize int, protocol, status, search string) ([]model.Proxy, int64, error)
|
ListProxies(ctx context.Context, page, pageSize int, protocol, status, search string) ([]model.Proxy, int64, error)
|
||||||
@@ -140,6 +141,33 @@ type UpdateAccountInput struct {
|
|||||||
GroupIDs *[]int64
|
GroupIDs *[]int64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BulkUpdateAccountsInput describes the payload for bulk updating accounts.
|
||||||
|
type BulkUpdateAccountsInput struct {
|
||||||
|
AccountIDs []int64
|
||||||
|
Name string
|
||||||
|
ProxyID *int64
|
||||||
|
Concurrency *int
|
||||||
|
Priority *int
|
||||||
|
Status string
|
||||||
|
GroupIDs *[]int64
|
||||||
|
Credentials map[string]any
|
||||||
|
Extra map[string]any
|
||||||
|
}
|
||||||
|
|
||||||
|
// BulkUpdateAccountResult captures the result for a single account update.
|
||||||
|
type BulkUpdateAccountResult struct {
|
||||||
|
AccountID int64 `json:"account_id"`
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BulkUpdateAccountsResult is the aggregated response for bulk updates.
|
||||||
|
type BulkUpdateAccountsResult struct {
|
||||||
|
Success int `json:"success"`
|
||||||
|
Failed int `json:"failed"`
|
||||||
|
Results []BulkUpdateAccountResult `json:"results"`
|
||||||
|
}
|
||||||
|
|
||||||
type CreateProxyInput struct {
|
type CreateProxyInput struct {
|
||||||
Name string
|
Name string
|
||||||
Protocol string
|
Protocol string
|
||||||
@@ -694,6 +722,65 @@ func (s *adminServiceImpl) UpdateAccount(ctx context.Context, id int64, input *U
|
|||||||
return account, nil
|
return account, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BulkUpdateAccounts updates multiple accounts in one request.
|
||||||
|
// It merges credentials/extra keys instead of overwriting the whole object.
|
||||||
|
func (s *adminServiceImpl) BulkUpdateAccounts(ctx context.Context, input *BulkUpdateAccountsInput) (*BulkUpdateAccountsResult, error) {
|
||||||
|
result := &BulkUpdateAccountsResult{
|
||||||
|
Results: make([]BulkUpdateAccountResult, 0, len(input.AccountIDs)),
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(input.AccountIDs) == 0 {
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare bulk updates for columns and JSONB fields.
|
||||||
|
repoUpdates := ports.AccountBulkUpdate{
|
||||||
|
Credentials: input.Credentials,
|
||||||
|
Extra: input.Extra,
|
||||||
|
}
|
||||||
|
if input.Name != "" {
|
||||||
|
repoUpdates.Name = &input.Name
|
||||||
|
}
|
||||||
|
if input.ProxyID != nil {
|
||||||
|
repoUpdates.ProxyID = input.ProxyID
|
||||||
|
}
|
||||||
|
if input.Concurrency != nil {
|
||||||
|
repoUpdates.Concurrency = input.Concurrency
|
||||||
|
}
|
||||||
|
if input.Priority != nil {
|
||||||
|
repoUpdates.Priority = input.Priority
|
||||||
|
}
|
||||||
|
if input.Status != "" {
|
||||||
|
repoUpdates.Status = &input.Status
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run bulk update for column/jsonb fields first.
|
||||||
|
if _, err := s.accountRepo.BulkUpdate(ctx, input.AccountIDs, repoUpdates); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle group bindings per account (requires individual operations).
|
||||||
|
for _, accountID := range input.AccountIDs {
|
||||||
|
entry := BulkUpdateAccountResult{AccountID: accountID}
|
||||||
|
|
||||||
|
if input.GroupIDs != nil {
|
||||||
|
if err := s.accountRepo.BindGroups(ctx, accountID, *input.GroupIDs); err != nil {
|
||||||
|
entry.Success = false
|
||||||
|
entry.Error = err.Error()
|
||||||
|
result.Failed++
|
||||||
|
result.Results = append(result.Results, entry)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.Success = true
|
||||||
|
result.Success++
|
||||||
|
result.Results = append(result.Results, entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *adminServiceImpl) DeleteAccount(ctx context.Context, id int64) error {
|
func (s *adminServiceImpl) DeleteAccount(ctx context.Context, id int64) error {
|
||||||
return s.accountRepo.Delete(ctx, id)
|
return s.accountRepo.Delete(ctx, id)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,4 +38,17 @@ type AccountRepository interface {
|
|||||||
ClearRateLimit(ctx context.Context, id int64) error
|
ClearRateLimit(ctx context.Context, id int64) error
|
||||||
UpdateSessionWindow(ctx context.Context, id int64, start, end *time.Time, status string) error
|
UpdateSessionWindow(ctx context.Context, id int64, start, end *time.Time, status string) error
|
||||||
UpdateExtra(ctx context.Context, id int64, updates map[string]any) error
|
UpdateExtra(ctx context.Context, id int64, updates map[string]any) error
|
||||||
|
BulkUpdate(ctx context.Context, ids []int64, updates AccountBulkUpdate) (int64, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AccountBulkUpdate describes the fields that can be updated in a bulk operation.
|
||||||
|
// Nil pointers mean "do not change".
|
||||||
|
type AccountBulkUpdate struct {
|
||||||
|
Name *string
|
||||||
|
ProxyID *int64
|
||||||
|
Concurrency *int
|
||||||
|
Priority *int
|
||||||
|
Status *string
|
||||||
|
Credentials map[string]any
|
||||||
|
Extra map[string]any
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -255,7 +255,7 @@ export async function bulkUpdate(
|
|||||||
results: Array<{ account_id: number; success: boolean; error?: string }>;
|
results: Array<{ account_id: number; success: boolean; error?: string }>;
|
||||||
}>('/admin/accounts/bulk-update', {
|
}>('/admin/accounts/bulk-update', {
|
||||||
account_ids: accountIds,
|
account_ids: accountIds,
|
||||||
updates
|
...updates
|
||||||
});
|
});
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -443,7 +443,7 @@ import { ref, watch, computed } from 'vue'
|
|||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useAppStore } from '@/stores/app'
|
import { useAppStore } from '@/stores/app'
|
||||||
import { adminAPI } from '@/api/admin'
|
import { adminAPI } from '@/api/admin'
|
||||||
import type { Proxy, Group, Account } from '@/types'
|
import type { Proxy, Group } from '@/types'
|
||||||
import Modal from '@/components/common/Modal.vue'
|
import Modal from '@/components/common/Modal.vue'
|
||||||
import Select from '@/components/common/Select.vue'
|
import Select from '@/components/common/Select.vue'
|
||||||
import ProxySelector from '@/components/common/ProxySelector.vue'
|
import ProxySelector from '@/components/common/ProxySelector.vue'
|
||||||
@@ -496,7 +496,6 @@ const concurrency = ref(1)
|
|||||||
const priority = ref(1)
|
const priority = ref(1)
|
||||||
const status = ref<'active' | 'inactive'>('active')
|
const status = ref<'active' | 'inactive'>('active')
|
||||||
const groupIds = ref<number[]>([])
|
const groupIds = ref<number[]>([])
|
||||||
const accountCache = ref<Record<number, Account>>({})
|
|
||||||
|
|
||||||
// All models list (combined Anthropic + OpenAI)
|
// All models list (combined Anthropic + OpenAI)
|
||||||
const allModels = [
|
const allModels = [
|
||||||
@@ -613,22 +612,10 @@ const buildModelMappingObject = (): Record<string, string> | null => {
|
|||||||
return Object.keys(mapping).length > 0 ? mapping : null
|
return Object.keys(mapping).length > 0 ? mapping : null
|
||||||
}
|
}
|
||||||
|
|
||||||
const getDefaultBaseUrl = (platform: string) => {
|
const buildUpdatePayload = (): Record<string, unknown> | null => {
|
||||||
return platform === 'openai' ? 'https://api.openai.com' : 'https://api.anthropic.com'
|
|
||||||
}
|
|
||||||
|
|
||||||
const getAccountDetails = async (accountId: number): Promise<Account> => {
|
|
||||||
if (accountCache.value[accountId]) return accountCache.value[accountId]
|
|
||||||
const account = await adminAPI.accounts.getById(accountId)
|
|
||||||
accountCache.value[accountId] = account
|
|
||||||
return account
|
|
||||||
}
|
|
||||||
|
|
||||||
const buildUpdatePayload = (account: Account): Record<string, unknown> | null => {
|
|
||||||
const updates: Record<string, unknown> = {}
|
const updates: Record<string, unknown> = {}
|
||||||
let credentials: Record<string, unknown> | null = null
|
const credentials: Record<string, unknown> = {}
|
||||||
let credentialsChanged = false
|
let credentialsChanged = false
|
||||||
const isAnthropic = account.platform === 'anthropic'
|
|
||||||
|
|
||||||
if (enableProxy.value) {
|
if (enableProxy.value) {
|
||||||
updates.proxy_id = proxyId.value
|
updates.proxy_id = proxyId.value
|
||||||
@@ -650,47 +637,34 @@ const buildUpdatePayload = (account: Account): Record<string, unknown> | null =>
|
|||||||
updates.group_ids = groupIds.value
|
updates.group_ids = groupIds.value
|
||||||
}
|
}
|
||||||
|
|
||||||
if (account.type === 'apikey') {
|
if (enableBaseUrl.value) {
|
||||||
const baseCredentials = (account.credentials || {}) as Record<string, unknown>
|
const baseUrlValue = baseUrl.value.trim()
|
||||||
credentials = { ...baseCredentials }
|
if (baseUrlValue) {
|
||||||
|
credentials.base_url = baseUrlValue
|
||||||
if (enableBaseUrl.value) {
|
|
||||||
credentials.base_url = baseUrl.value.trim() || getDefaultBaseUrl(account.platform)
|
|
||||||
credentialsChanged = true
|
credentialsChanged = true
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (enableModelRestriction.value) {
|
if (enableModelRestriction.value) {
|
||||||
const modelMapping = buildModelMappingObject()
|
const modelMapping = buildModelMappingObject()
|
||||||
if (modelMapping) {
|
if (modelMapping) {
|
||||||
credentials.model_mapping = modelMapping
|
credentials.model_mapping = modelMapping
|
||||||
} else {
|
|
||||||
delete credentials.model_mapping
|
|
||||||
}
|
|
||||||
credentialsChanged = true
|
credentialsChanged = true
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (enableCustomErrorCodes.value) {
|
if (enableCustomErrorCodes.value) {
|
||||||
credentials.custom_error_codes_enabled = true
|
credentials.custom_error_codes_enabled = true
|
||||||
credentials.custom_error_codes = [...selectedErrorCodes.value]
|
credentials.custom_error_codes = [...selectedErrorCodes.value]
|
||||||
credentialsChanged = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (enableInterceptWarmup.value && isAnthropic) {
|
|
||||||
credentials.intercept_warmup_requests = interceptWarmupRequests.value
|
|
||||||
credentialsChanged = true
|
|
||||||
}
|
|
||||||
} else if (enableInterceptWarmup.value && isAnthropic) {
|
|
||||||
const baseCredentials = (account.credentials || {}) as Record<string, unknown>
|
|
||||||
credentials = { ...baseCredentials }
|
|
||||||
if (interceptWarmupRequests.value) {
|
|
||||||
credentials.intercept_warmup_requests = true
|
|
||||||
} else {
|
|
||||||
delete credentials.intercept_warmup_requests
|
|
||||||
}
|
|
||||||
credentialsChanged = true
|
credentialsChanged = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (credentials && credentialsChanged) {
|
if (enableInterceptWarmup.value) {
|
||||||
|
credentials.intercept_warmup_requests = interceptWarmupRequests.value
|
||||||
|
credentialsChanged = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (credentialsChanged) {
|
||||||
updates.credentials = credentials
|
updates.credentials = credentials
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -722,39 +696,37 @@ const handleSubmit = async () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updates = buildUpdatePayload()
|
||||||
|
if (!updates) {
|
||||||
|
appStore.showError(t('admin.accounts.bulkEdit.noFieldsSelected'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
submitting.value = true
|
submitting.value = true
|
||||||
let success = 0
|
|
||||||
let failed = 0
|
|
||||||
|
|
||||||
for (const accountId of props.accountIds) {
|
try {
|
||||||
try {
|
const res = await adminAPI.accounts.bulkUpdate(props.accountIds, updates)
|
||||||
const account = await getAccountDetails(accountId)
|
const success = res.success || 0
|
||||||
const updates = buildUpdatePayload(account)
|
const failed = res.failed || 0
|
||||||
if (!updates) {
|
|
||||||
continue
|
if (success > 0 && failed === 0) {
|
||||||
}
|
appStore.showSuccess(t('admin.accounts.bulkEdit.success', { count: success }))
|
||||||
await adminAPI.accounts.update(accountId, updates)
|
} else if (success > 0) {
|
||||||
success++
|
appStore.showError(t('admin.accounts.bulkEdit.partialSuccess', { success, failed }))
|
||||||
} catch (error: any) {
|
} else {
|
||||||
failed++
|
appStore.showError(t('admin.accounts.bulkEdit.failed'))
|
||||||
console.error(`Error bulk updating account ${accountId}:`, error)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (success > 0 && failed === 0) {
|
if (success > 0) {
|
||||||
appStore.showSuccess(t('admin.accounts.bulkEdit.success', { count: success }))
|
emit('updated')
|
||||||
} else if (success > 0) {
|
handleClose()
|
||||||
appStore.showError(t('admin.accounts.bulkEdit.partialSuccess', { success, failed }))
|
}
|
||||||
} else {
|
} catch (error: any) {
|
||||||
appStore.showError(t('admin.accounts.bulkEdit.failed'))
|
appStore.showError(error.response?.data?.detail || t('admin.accounts.bulkEdit.failed'))
|
||||||
|
console.error('Error bulk updating accounts:', error)
|
||||||
|
} finally {
|
||||||
|
submitting.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (success > 0) {
|
|
||||||
emit('updated')
|
|
||||||
handleClose()
|
|
||||||
}
|
|
||||||
|
|
||||||
submitting.value = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset form when modal closes
|
// Reset form when modal closes
|
||||||
@@ -784,7 +756,6 @@ watch(() => props.show, (newShow) => {
|
|||||||
priority.value = 1
|
priority.value = 1
|
||||||
status.value = 'active'
|
status.value = 'active'
|
||||||
groupIds.value = []
|
groupIds.value = []
|
||||||
accountCache.value = {}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -756,7 +756,7 @@ export default {
|
|||||||
title: 'Bulk Edit Accounts',
|
title: 'Bulk Edit Accounts',
|
||||||
selectionInfo: '{count} account(s) selected. Only checked or filled fields will be updated; others stay unchanged.',
|
selectionInfo: '{count} account(s) selected. Only checked or filled fields will be updated; others stay unchanged.',
|
||||||
baseUrlPlaceholder: 'https://api.anthropic.com or https://api.openai.com',
|
baseUrlPlaceholder: 'https://api.anthropic.com or https://api.openai.com',
|
||||||
baseUrlNotice: 'Applies to API Key accounts only; leave empty to use the platform default',
|
baseUrlNotice: 'Applies to API Key accounts only; leave empty to keep existing value',
|
||||||
submit: 'Update Accounts',
|
submit: 'Update Accounts',
|
||||||
updating: 'Updating...',
|
updating: 'Updating...',
|
||||||
success: 'Updated {count} account(s)',
|
success: 'Updated {count} account(s)',
|
||||||
|
|||||||
@@ -887,7 +887,7 @@ export default {
|
|||||||
title: '批量编辑账号',
|
title: '批量编辑账号',
|
||||||
selectionInfo: '已选择 {count} 个账号。只更新您勾选或填写的字段,未勾选的字段保持不变。',
|
selectionInfo: '已选择 {count} 个账号。只更新您勾选或填写的字段,未勾选的字段保持不变。',
|
||||||
baseUrlPlaceholder: 'https://api.anthropic.com 或 https://api.openai.com',
|
baseUrlPlaceholder: 'https://api.anthropic.com 或 https://api.openai.com',
|
||||||
baseUrlNotice: '仅适用于 API Key 账号,留空使用对应平台默认地址',
|
baseUrlNotice: '仅适用于 API Key 账号,留空则不修改',
|
||||||
submit: '批量更新',
|
submit: '批量更新',
|
||||||
updating: '更新中...',
|
updating: '更新中...',
|
||||||
success: '成功更新 {count} 个账号',
|
success: '成功更新 {count} 个账号',
|
||||||
|
|||||||
Reference in New Issue
Block a user