fix(accounts): 账号管理改为单行增量更新并避免全量刷新
- 将编辑与重新授权成功事件改为回传更新后的账号对象 - 在账号列表页按 id 就地补丁更新单行数据并保留运行时容量字段 - 单账号操作(刷新凭证/清错/清限流/临时不可调度重置)改为单行更新 - 后端增强 clear-rate-limit 接口,返回更新后的账号对象 - 同步前端 clearRateLimit API 类型定义 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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'))
|
||||||
|
|||||||
@@ -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 =
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
Reference in New Issue
Block a user