feat: 支持批量重置状态和批量刷新令牌

- 提取 refreshSingleAccount 私有方法复用单账号刷新逻辑
- 新增 BatchClearError handler (POST /admin/accounts/batch-clear-error)
- 新增 BatchRefresh handler (POST /admin/accounts/batch-refresh)
- 前端 AccountBulkActionsBar 添加批量重置状态/刷新令牌按钮
- AccountsView 添加 handler 支持 partial success 反馈
- i18n 中英文补充批量操作相关翻译
This commit is contained in:
QTom
2026-03-09 17:47:30 +08:00
parent 7a4e65ad4b
commit 252d6c5301
7 changed files with 311 additions and 64 deletions

View File

@@ -581,6 +581,43 @@ export async function validateSoraSessionToken(
return data
}
/**
* Batch operation result type
*/
export interface BatchOperationResult {
total: number
success: number
failed: number
errors?: Array<{ account_id: number; error: string }>
warnings?: Array<{ account_id: number; warning: string }>
}
/**
* Batch clear account errors
* @param accountIds - Array of account IDs
* @returns Batch operation result
*/
export async function batchClearError(accountIds: number[]): Promise<BatchOperationResult> {
const { data } = await apiClient.post<BatchOperationResult>('/admin/accounts/batch-clear-error', {
account_ids: accountIds
})
return data
}
/**
* Batch refresh account credentials
* @param accountIds - Array of account IDs
* @returns Batch operation result
*/
export async function batchRefresh(accountIds: number[]): Promise<BatchOperationResult> {
const { data } = await apiClient.post<BatchOperationResult>('/admin/accounts/batch-refresh', {
account_ids: accountIds,
}, {
timeout: 120000 // 120s timeout for large batch refreshes
})
return data
}
export const accountsAPI = {
list,
listWithEtag,
@@ -615,7 +652,9 @@ export const accountsAPI = {
syncFromCrs,
exportData,
importData,
getAntigravityDefaultModelMapping
getAntigravityDefaultModelMapping,
batchClearError,
batchRefresh
}
export default accountsAPI

View File

@@ -20,6 +20,8 @@
</div>
<div class="flex gap-2">
<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>
@@ -29,5 +31,5 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
defineProps(['selectedIds']); defineEmits(['delete', 'edit', 'clear', 'select-page', 'toggle-schedulable']); const { t } = useI18n()
</script>
defineProps(['selectedIds']); defineEmits(['delete', 'edit', 'clear', 'select-page', 'toggle-schedulable', 'reset-status', 'refresh-token']); const { t } = useI18n()
</script>

View File

@@ -1836,7 +1836,12 @@ export default {
edit: 'Bulk Edit',
delete: 'Bulk Delete',
enableScheduling: 'Enable Scheduling',
disableScheduling: 'Disable Scheduling'
disableScheduling: 'Disable Scheduling',
resetStatus: 'Reset Status',
refreshToken: 'Refresh Token',
resetStatusSuccess: 'Successfully reset {count} account(s) status',
refreshTokenSuccess: 'Successfully refreshed {count} account(s) token',
partialSuccess: 'Partially completed: {success} succeeded, {failed} failed'
},
bulkEdit: {
title: 'Bulk Edit Accounts',

View File

@@ -1983,7 +1983,12 @@ export default {
edit: '批量编辑账号',
delete: '批量删除',
enableScheduling: '批量启用调度',
disableScheduling: '批量停止调度'
disableScheduling: '批量停止调度',
resetStatus: '批量重置状态',
refreshToken: '批量刷新令牌',
resetStatusSuccess: '已成功重置 {count} 个账号状态',
refreshTokenSuccess: '已成功刷新 {count} 个账号令牌',
partialSuccess: '操作部分完成:{success} 成功,{failed} 失败'
},
bulkEdit: {
title: '批量编辑账号',

View File

@@ -131,7 +131,7 @@
</div>
</template>
<template #table>
<AccountBulkActionsBar :selected-ids="selIds" @delete="handleBulkDelete" @edit="showBulkEdit = true" @clear="clearSelection" @select-page="selectPage" @toggle-schedulable="handleBulkToggleSchedulable" />
<AccountBulkActionsBar :selected-ids="selIds" @delete="handleBulkDelete" @reset-status="handleBulkResetStatus" @refresh-token="handleBulkRefreshToken" @edit="showBulkEdit = true" @clear="clearSelection" @select-page="selectPage" @toggle-schedulable="handleBulkToggleSchedulable" />
<div ref="accountTableRef" class="flex min-h-0 flex-1 flex-col overflow-hidden">
<DataTable
:columns="cols"
@@ -889,6 +889,38 @@ const toggleSelectAllVisible = (event: Event) => {
toggleVisible(target.checked)
}
const handleBulkDelete = async () => { if(!confirm(t('common.confirm'))) return; try { await Promise.all(selIds.value.map(id => adminAPI.accounts.delete(id))); clearSelection(); reload() } catch (error) { console.error('Failed to bulk delete accounts:', error) } }
const handleBulkResetStatus = async () => {
if (!confirm(t('common.confirm'))) return
try {
const result = await adminAPI.accounts.batchClearError(selIds.value)
if (result.failed > 0) {
appStore.showError(t('admin.accounts.bulkActions.partialSuccess', { success: result.success, failed: result.failed }))
} else {
appStore.showSuccess(t('admin.accounts.bulkActions.resetStatusSuccess', { count: result.success }))
clearSelection()
}
reload()
} catch (error) {
console.error('Failed to bulk reset status:', error)
appStore.showError(String(error))
}
}
const handleBulkRefreshToken = async () => {
if (!confirm(t('common.confirm'))) return
try {
const result = await adminAPI.accounts.batchRefresh(selIds.value)
if (result.failed > 0) {
appStore.showError(t('admin.accounts.bulkActions.partialSuccess', { success: result.success, failed: result.failed }))
} else {
appStore.showSuccess(t('admin.accounts.bulkActions.refreshTokenSuccess', { count: result.success }))
clearSelection()
}
reload()
} catch (error) {
console.error('Failed to bulk refresh token:', error)
appStore.showError(String(error))
}
}
const updateSchedulableInList = (accountIds: number[], schedulable: boolean) => {
if (accountIds.length === 0) return
const idSet = new Set(accountIds)