fix(accounts): 账号管理改为单行增量更新并避免全量刷新

- 将编辑与重新授权成功事件改为回传更新后的账号对象
- 在账号列表页按 id 就地补丁更新单行数据并保留运行时容量字段
- 单账号操作(刷新凭证/清错/清限流/临时不可调度重置)改为单行更新
- 后端增强 clear-rate-limit 接口,返回更新后的账号对象
- 同步前端 clearRateLimit API 类型定义

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
yangjianbo
2026-02-14 12:06:17 +08:00
parent f6bff97d26
commit 9cafa46dd3
5 changed files with 112 additions and 26 deletions

View File

@@ -1106,7 +1106,13 @@ func (h *AccountHandler) ClearRateLimit(c *gin.Context) {
return return
} }
response.Success(c, gin.H{"message": "Rate limit cleared successfully"}) account, err := h.adminService.GetAccount(c.Request.Context(), accountID)
if err != nil {
response.ErrorFrom(c, err)
return
}
response.Success(c, dto.AccountFromService(account))
} }
// GetTempUnschedulable handles getting temporary unschedulable status // GetTempUnschedulable handles getting temporary unschedulable status

View File

@@ -164,10 +164,10 @@ export async function getUsage(id: number): Promise<AccountUsageInfo> {
/** /**
* Clear account rate limit status * Clear account rate limit status
* @param id - Account ID * @param id - Account ID
* @returns Success confirmation * @returns Updated account
*/ */
export async function clearRateLimit(id: number): Promise<{ message: string }> { export async function clearRateLimit(id: number): Promise<Account> {
const { data } = await apiClient.post<{ message: string }>( const { data } = await apiClient.post<Account>(
`/admin/accounts/${id}/clear-rate-limit` `/admin/accounts/${id}/clear-rate-limit`
) )
return data return data

View File

@@ -1111,7 +1111,7 @@ interface Props {
const props = defineProps<Props>() const props = defineProps<Props>()
const emit = defineEmits<{ const emit = defineEmits<{
close: [] close: []
updated: [] updated: [account: Account]
}>() }>()
const { t } = useI18n() const { t } = useI18n()
@@ -1849,9 +1849,9 @@ const handleSubmit = async () => {
updatePayload.extra = newExtra updatePayload.extra = newExtra
} }
await adminAPI.accounts.update(props.account.id, updatePayload) const updatedAccount = await adminAPI.accounts.update(props.account.id, updatePayload)
appStore.showSuccess(t('admin.accounts.accountUpdated')) appStore.showSuccess(t('admin.accounts.accountUpdated'))
emit('updated') emit('updated', updatedAccount)
handleClose() handleClose()
} catch (error: any) { } catch (error: any) {
// Handle 409 mixed_channel_warning - show confirmation dialog // Handle 409 mixed_channel_warning - show confirmation dialog
@@ -1879,9 +1879,9 @@ const handleMixedChannelConfirm = async () => {
pendingUpdatePayload.value.confirm_mixed_channel_risk = true pendingUpdatePayload.value.confirm_mixed_channel_risk = true
submitting.value = true submitting.value = true
try { try {
await adminAPI.accounts.update(props.account.id, pendingUpdatePayload.value) const updatedAccount = await adminAPI.accounts.update(props.account.id, pendingUpdatePayload.value)
appStore.showSuccess(t('admin.accounts.accountUpdated')) appStore.showSuccess(t('admin.accounts.accountUpdated'))
emit('updated') emit('updated', updatedAccount)
handleClose() handleClose()
} catch (error: any) { } catch (error: any) {
appStore.showError(error.response?.data?.message || error.response?.data?.detail || t('admin.accounts.failedToUpdate')) appStore.showError(error.response?.data?.message || error.response?.data?.detail || t('admin.accounts.failedToUpdate'))

View File

@@ -216,7 +216,7 @@ interface Props {
const props = defineProps<Props>() const props = defineProps<Props>()
const emit = defineEmits<{ const emit = defineEmits<{
close: [] close: []
reauthorized: [] reauthorized: [account: Account]
}>() }>()
const appStore = useAppStore() const appStore = useAppStore()
@@ -370,10 +370,10 @@ const handleExchangeCode = async () => {
}) })
// Clear error status after successful re-authorization // Clear error status after successful re-authorization
await adminAPI.accounts.clearError(props.account.id) const updatedAccount = await adminAPI.accounts.clearError(props.account.id)
appStore.showSuccess(t('admin.accounts.reAuthorizedSuccess')) appStore.showSuccess(t('admin.accounts.reAuthorizedSuccess'))
emit('reauthorized') emit('reauthorized', updatedAccount)
handleClose() handleClose()
} catch (error: any) { } catch (error: any) {
openaiOAuth.error.value = error.response?.data?.detail || t('admin.accounts.oauth.authFailed') openaiOAuth.error.value = error.response?.data?.detail || t('admin.accounts.oauth.authFailed')
@@ -404,9 +404,9 @@ const handleExchangeCode = async () => {
type: 'oauth', type: 'oauth',
credentials credentials
}) })
await adminAPI.accounts.clearError(props.account.id) const updatedAccount = await adminAPI.accounts.clearError(props.account.id)
appStore.showSuccess(t('admin.accounts.reAuthorizedSuccess')) appStore.showSuccess(t('admin.accounts.reAuthorizedSuccess'))
emit('reauthorized') emit('reauthorized', updatedAccount)
handleClose() handleClose()
} catch (error: any) { } catch (error: any) {
geminiOAuth.error.value = error.response?.data?.detail || t('admin.accounts.oauth.authFailed') geminiOAuth.error.value = error.response?.data?.detail || t('admin.accounts.oauth.authFailed')
@@ -436,9 +436,9 @@ const handleExchangeCode = async () => {
type: 'oauth', type: 'oauth',
credentials credentials
}) })
await adminAPI.accounts.clearError(props.account.id) const updatedAccount = await adminAPI.accounts.clearError(props.account.id)
appStore.showSuccess(t('admin.accounts.reAuthorizedSuccess')) appStore.showSuccess(t('admin.accounts.reAuthorizedSuccess'))
emit('reauthorized') emit('reauthorized', updatedAccount)
handleClose() handleClose()
} catch (error: any) { } catch (error: any) {
antigravityOAuth.error.value = error.response?.data?.detail || t('admin.accounts.oauth.authFailed') antigravityOAuth.error.value = error.response?.data?.detail || t('admin.accounts.oauth.authFailed')
@@ -475,10 +475,10 @@ const handleExchangeCode = async () => {
}) })
// Clear error status after successful re-authorization // Clear error status after successful re-authorization
await adminAPI.accounts.clearError(props.account.id) const updatedAccount = await adminAPI.accounts.clearError(props.account.id)
appStore.showSuccess(t('admin.accounts.reAuthorizedSuccess')) appStore.showSuccess(t('admin.accounts.reAuthorizedSuccess'))
emit('reauthorized') emit('reauthorized', updatedAccount)
handleClose() handleClose()
} catch (error: any) { } catch (error: any) {
claudeOAuth.error.value = error.response?.data?.detail || t('admin.accounts.oauth.authFailed') claudeOAuth.error.value = error.response?.data?.detail || t('admin.accounts.oauth.authFailed')
@@ -518,10 +518,10 @@ const handleCookieAuth = async (sessionKey: string) => {
}) })
// Clear error status after successful re-authorization // Clear error status after successful re-authorization
await adminAPI.accounts.clearError(props.account.id) const updatedAccount = await adminAPI.accounts.clearError(props.account.id)
appStore.showSuccess(t('admin.accounts.reAuthorizedSuccess')) appStore.showSuccess(t('admin.accounts.reAuthorizedSuccess'))
emit('reauthorized') emit('reauthorized', updatedAccount)
handleClose() handleClose()
} catch (error: any) { } catch (error: any) {
claudeOAuth.error.value = claudeOAuth.error.value =

View File

@@ -239,8 +239,8 @@
<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> <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> </TablePageLayout>
<CreateAccountModal :show="showCreate" :proxies="proxies" :groups="groups" @close="showCreate = false" @created="reload" /> <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" /> <EditAccountModal :show="showEdit" :account="edAcc" :proxies="proxies" :groups="groups" @close="showEdit = false" @updated="handleAccountUpdated" />
<ReAuthAccountModal :show="showReAuth" :account="reAuthAcc" @close="closeReAuthModal" @reauthorized="load" /> <ReAuthAccountModal :show="showReAuth" :account="reAuthAcc" @close="closeReAuthModal" @reauthorized="handleAccountUpdated" />
<AccountTestModal :show="showTest" :account="testingAcc" @close="closeTestModal" /> <AccountTestModal :show="showTest" :account="testingAcc" @close="closeTestModal" />
<AccountStatsModal :show="showStats" :account="statsAcc" @close="closeStatsModal" /> <AccountStatsModal :show="showStats" :account="statsAcc" @close="closeStatsModal" />
<AccountActionMenu :show="menu.show" :account="menu.acc" :position="menu.pos" @close="menu.show = false" @test="handleTest" @stats="handleViewStats" @reauth="handleReAuth" @refresh-token="handleRefresh" @reset-status="handleResetStatus" @clear-rate-limit="handleClearRateLimit" /> <AccountActionMenu :show="menu.show" :account="menu.acc" :position="menu.pos" @close="menu.show = false" @test="handleTest" @stats="handleViewStats" @reauth="handleReAuth" @refresh-token="handleRefresh" @reset-status="handleResetStatus" @clear-rate-limit="handleClearRateLimit" />
@@ -694,6 +694,53 @@ const handleBulkToggleSchedulable = async (schedulable: boolean) => {
} }
const handleBulkUpdated = () => { showBulkEdit.value = false; selIds.value = []; reload() } const handleBulkUpdated = () => { showBulkEdit.value = false; selIds.value = []; reload() }
const handleDataImported = () => { showImportData.value = false; reload() } const handleDataImported = () => { showImportData.value = false; reload() }
const accountMatchesCurrentFilters = (account: Account) => {
if (params.platform && account.platform !== params.platform) return false
if (params.type && account.type !== params.type) return false
if (params.status) {
if (params.status === 'rate_limited') {
if (!account.rate_limit_reset_at) return false
const resetAt = new Date(account.rate_limit_reset_at).getTime()
if (!Number.isFinite(resetAt) || resetAt <= Date.now()) return false
} else if (account.status !== params.status) {
return false
}
}
const search = String(params.search || '').trim().toLowerCase()
if (search && !account.name.toLowerCase().includes(search)) return false
return true
}
const mergeRuntimeFields = (oldAccount: Account, updatedAccount: Account): Account => ({
...updatedAccount,
current_concurrency: updatedAccount.current_concurrency ?? oldAccount.current_concurrency,
current_window_cost: updatedAccount.current_window_cost ?? oldAccount.current_window_cost,
active_sessions: updatedAccount.active_sessions ?? oldAccount.active_sessions
})
const patchAccountInList = (updatedAccount: Account) => {
const index = accounts.value.findIndex(account => account.id === updatedAccount.id)
if (index === -1) return
const mergedAccount = mergeRuntimeFields(accounts.value[index], updatedAccount)
if (!accountMatchesCurrentFilters(mergedAccount)) {
accounts.value = accounts.value.filter(account => account.id !== mergedAccount.id)
selIds.value = selIds.value.filter(id => id !== mergedAccount.id)
if (menu.acc?.id === mergedAccount.id) {
menu.show = false
menu.acc = null
}
return
}
const nextAccounts = [...accounts.value]
nextAccounts[index] = mergedAccount
accounts.value = nextAccounts
if (edAcc.value?.id === mergedAccount.id) edAcc.value = mergedAccount
if (reAuthAcc.value?.id === mergedAccount.id) reAuthAcc.value = mergedAccount
if (tempUnschedAcc.value?.id === mergedAccount.id) tempUnschedAcc.value = mergedAccount
if (deletingAcc.value?.id === mergedAccount.id) deletingAcc.value = mergedAccount
if (menu.acc?.id === mergedAccount.id) menu.acc = mergedAccount
}
const handleAccountUpdated = (updatedAccount: Account) => {
patchAccountInList(updatedAccount)
}
const formatExportTimestamp = () => { const formatExportTimestamp = () => {
const now = new Date() const now = new Date()
const pad2 = (value: number) => String(value).padStart(2, '0') const pad2 = (value: number) => String(value).padStart(2, '0')
@@ -743,9 +790,32 @@ const closeReAuthModal = () => { showReAuth.value = false; reAuthAcc.value = nul
const handleTest = (a: Account) => { testingAcc.value = a; showTest.value = true } const handleTest = (a: Account) => { testingAcc.value = a; showTest.value = true }
const handleViewStats = (a: Account) => { statsAcc.value = a; showStats.value = true } const handleViewStats = (a: Account) => { statsAcc.value = a; showStats.value = true }
const handleReAuth = (a: Account) => { reAuthAcc.value = a; showReAuth.value = true } const handleReAuth = (a: Account) => { reAuthAcc.value = a; showReAuth.value = true }
const handleRefresh = async (a: Account) => { try { await adminAPI.accounts.refreshCredentials(a.id); load() } catch (error) { console.error('Failed to refresh credentials:', error) } } const handleRefresh = async (a: Account) => {
const handleResetStatus = async (a: Account) => { try { await adminAPI.accounts.clearError(a.id); appStore.showSuccess(t('common.success')); load() } catch (error) { console.error('Failed to reset status:', error) } } try {
const handleClearRateLimit = async (a: Account) => { try { await adminAPI.accounts.clearRateLimit(a.id); appStore.showSuccess(t('common.success')); load() } catch (error) { console.error('Failed to clear rate limit:', error) } } const updated = await adminAPI.accounts.refreshCredentials(a.id)
patchAccountInList(updated)
} catch (error) {
console.error('Failed to refresh credentials:', error)
}
}
const handleResetStatus = async (a: Account) => {
try {
const updated = await adminAPI.accounts.clearError(a.id)
patchAccountInList(updated)
appStore.showSuccess(t('common.success'))
} catch (error) {
console.error('Failed to reset status:', error)
}
}
const handleClearRateLimit = async (a: Account) => {
try {
const updated = await adminAPI.accounts.clearRateLimit(a.id)
patchAccountInList(updated)
appStore.showSuccess(t('common.success'))
} catch (error) {
console.error('Failed to clear rate limit:', error)
}
}
const handleDelete = (a: Account) => { deletingAcc.value = a; showDeleteDialog.value = true } const handleDelete = (a: Account) => { deletingAcc.value = a; showDeleteDialog.value = true }
const confirmDelete = async () => { if(!deletingAcc.value) return; try { await adminAPI.accounts.delete(deletingAcc.value.id); showDeleteDialog.value = false; deletingAcc.value = null; reload() } catch (error) { console.error('Failed to delete account:', error) } } const confirmDelete = async () => { if(!deletingAcc.value) return; try { await adminAPI.accounts.delete(deletingAcc.value.id); showDeleteDialog.value = false; deletingAcc.value = null; reload() } catch (error) { console.error('Failed to delete account:', error) } }
const handleToggleSchedulable = async (a: Account) => { const handleToggleSchedulable = async (a: Account) => {
@@ -762,7 +832,17 @@ const handleToggleSchedulable = async (a: Account) => {
} }
} }
const handleShowTempUnsched = (a: Account) => { tempUnschedAcc.value = a; showTempUnsched.value = true } const handleShowTempUnsched = (a: Account) => { tempUnschedAcc.value = a; showTempUnsched.value = true }
const handleTempUnschedReset = async () => { if(!tempUnschedAcc.value) return; try { await adminAPI.accounts.clearError(tempUnschedAcc.value.id); showTempUnsched.value = false; tempUnschedAcc.value = null; load() } catch (error) { console.error('Failed to reset temp unscheduled:', error) } } const handleTempUnschedReset = async () => {
if(!tempUnschedAcc.value) return
try {
const updated = await adminAPI.accounts.clearError(tempUnschedAcc.value.id)
showTempUnsched.value = false
tempUnschedAcc.value = null
patchAccountInList(updated)
} catch (error) {
console.error('Failed to reset temp unscheduled:', error)
}
}
const formatExpiresAt = (value: number | null) => { const formatExpiresAt = (value: number | null) => {
if (!value) return '-' if (!value) return '-'
return formatDateTime( return formatDateTime(