From 6218eefd61728d5a338778d7f775ceca15c21937 Mon Sep 17 00:00:00 2001 From: liuxiongfeng Date: Fri, 13 Feb 2026 00:22:14 +0800 Subject: [PATCH] refactor(ui): extract mixed channel warning handler --- .../components/account/CreateAccountModal.vue | 87 ++++---------- .../components/account/EditAccountModal.vue | 62 ++++------ .../src/composables/useMixedChannelWarning.ts | 107 ++++++++++++++++++ 3 files changed, 152 insertions(+), 104 deletions(-) create mode 100644 frontend/src/composables/useMixedChannelWarning.ts diff --git a/frontend/src/components/account/CreateAccountModal.vue b/frontend/src/components/account/CreateAccountModal.vue index 6ed606f4..b576a24d 100644 --- a/frontend/src/components/account/CreateAccountModal.vue +++ b/frontend/src/components/account/CreateAccountModal.vue @@ -1964,6 +1964,7 @@ import { import { useOpenAIOAuth } from '@/composables/useOpenAIOAuth' import { useGeminiOAuth } from '@/composables/useGeminiOAuth' import { useAntigravityOAuth } from '@/composables/useAntigravityOAuth' +import { useMixedChannelWarning } from '@/composables/useMixedChannelWarning' import type { Proxy, AdminGroup, AccountPlatform, AccountType } from '@/types' import BaseDialog from '@/components/common/BaseDialog.vue' import ConfirmDialog from '@/components/common/ConfirmDialog.vue' @@ -2102,10 +2103,9 @@ const tempUnschedRules = ref([]) const geminiOAuthType = ref<'code_assist' | 'google_one' | 'ai_studio'>('google_one') const geminiAIStudioOAuthEnabled = ref(false) -// Mixed channel warning dialog state -const showMixedChannelWarning = ref(false) -const mixedChannelWarningDetails = ref<{ groupName: string; currentPlatform: string; otherPlatform: string } | null>(null) -const pendingCreatePayload = ref(null) +const mixedChannelWarning = useMixedChannelWarning() +const showMixedChannelWarning = mixedChannelWarning.show +const mixedChannelWarningDetails = mixedChannelWarning.details const showAdvancedOAuth = ref(false) const showGeminiHelpDialog = ref(false) @@ -2583,6 +2583,7 @@ const resetForm = () => { geminiOAuth.resetState() antigravityOAuth.resetState() oauthFlowRef.value?.reset() + mixedChannelWarning.cancel() } const handleClose = () => { @@ -2593,24 +2594,16 @@ const handleClose = () => { const doCreateAccount = async (payload: any) => { submitting.value = true try { - await adminAPI.accounts.create(payload) - appStore.showSuccess(t('admin.accounts.accountCreated')) - emit('created') - handleClose() - } catch (error: any) { - // Handle 409 mixed_channel_warning - show confirmation dialog - if (error.response?.status === 409 && error.response?.data?.error === 'mixed_channel_warning') { - const details = error.response.data.details || {} - mixedChannelWarningDetails.value = { - groupName: details.group_name || 'Unknown', - currentPlatform: details.current_platform || 'Unknown', - otherPlatform: details.other_platform || 'Unknown' + await mixedChannelWarning.tryRequest(payload, (p) => adminAPI.accounts.create(p), { + onSuccess: () => { + appStore.showSuccess(t('admin.accounts.accountCreated')) + emit('created') + handleClose() + }, + onError: (error: any) => { + appStore.showError(error.response?.data?.detail || t('admin.accounts.failedToCreate')) } - pendingCreatePayload.value = payload - showMixedChannelWarning.value = true - } else { - appStore.showError(error.response?.data?.detail || t('admin.accounts.failedToCreate')) - } + }) } finally { submitting.value = false } @@ -2618,28 +2611,16 @@ const doCreateAccount = async (payload: any) => { // Handle mixed channel warning confirmation const handleMixedChannelConfirm = async () => { - showMixedChannelWarning.value = false - if (pendingCreatePayload.value) { - pendingCreatePayload.value.confirm_mixed_channel_risk = true - submitting.value = true - try { - await adminAPI.accounts.create(pendingCreatePayload.value) - appStore.showSuccess(t('admin.accounts.accountCreated')) - emit('created') - handleClose() - } catch (error: any) { - appStore.showError(error.response?.data?.detail || t('admin.accounts.failedToCreate')) - } finally { - submitting.value = false - pendingCreatePayload.value = null - } + submitting.value = true + try { + await mixedChannelWarning.confirm() + } finally { + submitting.value = false } } const handleMixedChannelCancel = () => { - showMixedChannelWarning.value = false - pendingCreatePayload.value = null - mixedChannelWarningDetails.value = null + mixedChannelWarning.cancel() } const handleSubmit = async () => { @@ -2795,7 +2776,7 @@ const createAccountAndFinish = async ( if (!applyTempUnschedConfig(credentials)) { return } - const payload: any = { + await doCreateAccount({ name: form.name, notes: form.notes, platform, @@ -2809,31 +2790,7 @@ const createAccountAndFinish = async ( group_ids: form.group_ids, expires_at: form.expires_at, auto_pause_on_expired: autoPauseOnExpired.value - } - - try { - await adminAPI.accounts.create(payload) - } catch (error: any) { - // Handle 409 mixed_channel_warning - show confirmation dialog - // Note: upstream Antigravity create path uses createAccountAndFinish directly, so mixed warning - // must be handled here as well (otherwise user only sees a generic "failed" toast). - if (error.response?.status === 409 && error.response?.data?.error === 'mixed_channel_warning') { - const details = error.response.data.details || {} - mixedChannelWarningDetails.value = { - groupName: details.group_name || 'Unknown', - currentPlatform: details.current_platform || 'Unknown', - otherPlatform: details.other_platform || 'Unknown' - } - pendingCreatePayload.value = payload - showMixedChannelWarning.value = true - return - } - throw error - } - - appStore.showSuccess(t('admin.accounts.accountCreated')) - emit('created') - handleClose() + }) } // OpenAI OAuth 授权码兑换 diff --git a/frontend/src/components/account/EditAccountModal.vue b/frontend/src/components/account/EditAccountModal.vue index c5566c5d..8f761039 100644 --- a/frontend/src/components/account/EditAccountModal.vue +++ b/frontend/src/components/account/EditAccountModal.vue @@ -994,6 +994,7 @@ import ProxySelector from '@/components/common/ProxySelector.vue' import GroupSelector from '@/components/common/GroupSelector.vue' import ModelWhitelistSelector from '@/components/account/ModelWhitelistSelector.vue' import { formatDateTimeLocalInput, parseDateTimeLocalInput } from '@/utils/format' +import { useMixedChannelWarning } from '@/composables/useMixedChannelWarning' import { getPresetMappingsByPlatform, commonErrorCodes, @@ -1060,10 +1061,9 @@ const antigravityModelMappings = ref([]) const tempUnschedEnabled = ref(false) const tempUnschedRules = ref([]) -// Mixed channel warning dialog state -const showMixedChannelWarning = ref(false) -const mixedChannelWarningDetails = ref<{ groupName: string; currentPlatform: string; otherPlatform: string } | null>(null) -const pendingUpdatePayload = ref | null>(null) +const mixedChannelWarning = useMixedChannelWarning() +const showMixedChannelWarning = mixedChannelWarning.show +const mixedChannelWarningDetails = mixedChannelWarning.details // Quota control state (Anthropic OAuth/SetupToken only) const windowCostEnabled = ref(false) @@ -1525,11 +1525,13 @@ const parseDateTimeLocal = parseDateTimeLocalInput // Methods const handleClose = () => { + mixedChannelWarning.cancel() emit('close') } const handleSubmit = async () => { if (!props.account) return + const accountID = props.account.id submitting.value = true const updatePayload: Record = { ...form } @@ -1698,24 +1700,18 @@ const handleSubmit = async () => { updatePayload.extra = newExtra } - await adminAPI.accounts.update(props.account.id, updatePayload) - appStore.showSuccess(t('admin.accounts.accountUpdated')) - emit('updated') - handleClose() - } catch (error: any) { - // Handle 409 mixed_channel_warning - show confirmation dialog - if (error.response?.status === 409 && error.response?.data?.error === 'mixed_channel_warning') { - const details = error.response.data.details || {} - mixedChannelWarningDetails.value = { - groupName: details.group_name || 'Unknown', - currentPlatform: details.current_platform || 'Unknown', - otherPlatform: details.other_platform || 'Unknown' + await mixedChannelWarning.tryRequest(updatePayload, (p) => adminAPI.accounts.update(accountID, p), { + onSuccess: () => { + appStore.showSuccess(t('admin.accounts.accountUpdated')) + emit('updated') + handleClose() + }, + onError: (error: any) => { + appStore.showError(error.response?.data?.message || error.response?.data?.detail || t('admin.accounts.failedToUpdate')) } - pendingUpdatePayload.value = updatePayload - showMixedChannelWarning.value = true - } else { - appStore.showError(error.response?.data?.message || error.response?.data?.detail || t('admin.accounts.failedToUpdate')) - } + }) + } catch (error: any) { + appStore.showError(error.response?.data?.message || error.response?.data?.detail || t('admin.accounts.failedToUpdate')) } finally { submitting.value = false } @@ -1723,27 +1719,15 @@ const handleSubmit = async () => { // Handle mixed channel warning confirmation const handleMixedChannelConfirm = async () => { - showMixedChannelWarning.value = false - if (pendingUpdatePayload.value && props.account) { - pendingUpdatePayload.value.confirm_mixed_channel_risk = true - submitting.value = true - try { - await adminAPI.accounts.update(props.account.id, pendingUpdatePayload.value) - appStore.showSuccess(t('admin.accounts.accountUpdated')) - emit('updated') - handleClose() - } catch (error: any) { - appStore.showError(error.response?.data?.message || error.response?.data?.detail || t('admin.accounts.failedToUpdate')) - } finally { - submitting.value = false - pendingUpdatePayload.value = null - } + submitting.value = true + try { + await mixedChannelWarning.confirm() + } finally { + submitting.value = false } } const handleMixedChannelCancel = () => { - showMixedChannelWarning.value = false - pendingUpdatePayload.value = null - mixedChannelWarningDetails.value = null + mixedChannelWarning.cancel() } diff --git a/frontend/src/composables/useMixedChannelWarning.ts b/frontend/src/composables/useMixedChannelWarning.ts new file mode 100644 index 00000000..469b369b --- /dev/null +++ b/frontend/src/composables/useMixedChannelWarning.ts @@ -0,0 +1,107 @@ +import { ref } from 'vue' + +export interface MixedChannelWarningDetails { + groupName: string + currentPlatform: string + otherPlatform: string +} + +function isMixedChannelWarningError(error: any): boolean { + return error?.response?.status === 409 && error?.response?.data?.error === 'mixed_channel_warning' +} + +function extractMixedChannelWarningDetails(error: any): MixedChannelWarningDetails { + const details = error?.response?.data?.details || {} + return { + groupName: details.group_name || 'Unknown', + currentPlatform: details.current_platform || 'Unknown', + otherPlatform: details.other_platform || 'Unknown' + } +} + +export function useMixedChannelWarning() { + const show = ref(false) + const details = ref(null) + + const pendingPayload = ref(null) + const pendingRequest = ref<((payload: any) => Promise) | null>(null) + const pendingOnSuccess = ref<(() => void) | null>(null) + const pendingOnError = ref<((error: any) => void) | null>(null) + + const clearPending = () => { + pendingPayload.value = null + pendingRequest.value = null + pendingOnSuccess.value = null + pendingOnError.value = null + details.value = null + } + + const tryRequest = async ( + payload: any, + request: (payload: any) => Promise, + opts?: { + onSuccess?: () => void + onError?: (error: any) => void + } + ): Promise => { + try { + await request(payload) + opts?.onSuccess?.() + return true + } catch (error: any) { + if (isMixedChannelWarningError(error)) { + details.value = extractMixedChannelWarningDetails(error) + pendingPayload.value = payload + pendingRequest.value = request + pendingOnSuccess.value = opts?.onSuccess || null + pendingOnError.value = opts?.onError || null + show.value = true + return false + } + + if (opts?.onError) { + opts.onError(error) + return false + } + throw error + } + } + + const confirm = async (): Promise => { + show.value = false + if (!pendingPayload.value || !pendingRequest.value) { + clearPending() + return false + } + + pendingPayload.value.confirm_mixed_channel_risk = true + + try { + await pendingRequest.value(pendingPayload.value) + pendingOnSuccess.value?.() + return true + } catch (error: any) { + if (pendingOnError.value) { + pendingOnError.value(error) + return false + } + throw error + } finally { + clearPending() + } + } + + const cancel = () => { + show.value = false + clearPending() + } + + return { + show, + details, + tryRequest, + confirm, + cancel + } +} +