mirror of
https://gitee.com/wanwujie/sub2api
synced 2026-04-23 08:04:45 +08:00
refactor(ui): extract mixed channel warning handler
This commit is contained in:
@@ -1964,6 +1964,7 @@ import {
|
|||||||
import { useOpenAIOAuth } from '@/composables/useOpenAIOAuth'
|
import { useOpenAIOAuth } from '@/composables/useOpenAIOAuth'
|
||||||
import { useGeminiOAuth } from '@/composables/useGeminiOAuth'
|
import { useGeminiOAuth } from '@/composables/useGeminiOAuth'
|
||||||
import { useAntigravityOAuth } from '@/composables/useAntigravityOAuth'
|
import { useAntigravityOAuth } from '@/composables/useAntigravityOAuth'
|
||||||
|
import { useMixedChannelWarning } from '@/composables/useMixedChannelWarning'
|
||||||
import type { Proxy, AdminGroup, AccountPlatform, AccountType } from '@/types'
|
import type { Proxy, AdminGroup, AccountPlatform, AccountType } from '@/types'
|
||||||
import BaseDialog from '@/components/common/BaseDialog.vue'
|
import BaseDialog from '@/components/common/BaseDialog.vue'
|
||||||
import ConfirmDialog from '@/components/common/ConfirmDialog.vue'
|
import ConfirmDialog from '@/components/common/ConfirmDialog.vue'
|
||||||
@@ -2102,10 +2103,9 @@ const tempUnschedRules = ref<TempUnschedRuleForm[]>([])
|
|||||||
const geminiOAuthType = ref<'code_assist' | 'google_one' | 'ai_studio'>('google_one')
|
const geminiOAuthType = ref<'code_assist' | 'google_one' | 'ai_studio'>('google_one')
|
||||||
const geminiAIStudioOAuthEnabled = ref(false)
|
const geminiAIStudioOAuthEnabled = ref(false)
|
||||||
|
|
||||||
// Mixed channel warning dialog state
|
const mixedChannelWarning = useMixedChannelWarning()
|
||||||
const showMixedChannelWarning = ref(false)
|
const showMixedChannelWarning = mixedChannelWarning.show
|
||||||
const mixedChannelWarningDetails = ref<{ groupName: string; currentPlatform: string; otherPlatform: string } | null>(null)
|
const mixedChannelWarningDetails = mixedChannelWarning.details
|
||||||
const pendingCreatePayload = ref<any>(null)
|
|
||||||
const showAdvancedOAuth = ref(false)
|
const showAdvancedOAuth = ref(false)
|
||||||
const showGeminiHelpDialog = ref(false)
|
const showGeminiHelpDialog = ref(false)
|
||||||
|
|
||||||
@@ -2583,6 +2583,7 @@ const resetForm = () => {
|
|||||||
geminiOAuth.resetState()
|
geminiOAuth.resetState()
|
||||||
antigravityOAuth.resetState()
|
antigravityOAuth.resetState()
|
||||||
oauthFlowRef.value?.reset()
|
oauthFlowRef.value?.reset()
|
||||||
|
mixedChannelWarning.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
@@ -2593,24 +2594,16 @@ const handleClose = () => {
|
|||||||
const doCreateAccount = async (payload: any) => {
|
const doCreateAccount = async (payload: any) => {
|
||||||
submitting.value = true
|
submitting.value = true
|
||||||
try {
|
try {
|
||||||
await adminAPI.accounts.create(payload)
|
await mixedChannelWarning.tryRequest(payload, (p) => adminAPI.accounts.create(p), {
|
||||||
appStore.showSuccess(t('admin.accounts.accountCreated'))
|
onSuccess: () => {
|
||||||
emit('created')
|
appStore.showSuccess(t('admin.accounts.accountCreated'))
|
||||||
handleClose()
|
emit('created')
|
||||||
} catch (error: any) {
|
handleClose()
|
||||||
// Handle 409 mixed_channel_warning - show confirmation dialog
|
},
|
||||||
if (error.response?.status === 409 && error.response?.data?.error === 'mixed_channel_warning') {
|
onError: (error: any) => {
|
||||||
const details = error.response.data.details || {}
|
appStore.showError(error.response?.data?.detail || t('admin.accounts.failedToCreate'))
|
||||||
mixedChannelWarningDetails.value = {
|
|
||||||
groupName: details.group_name || 'Unknown',
|
|
||||||
currentPlatform: details.current_platform || 'Unknown',
|
|
||||||
otherPlatform: details.other_platform || 'Unknown'
|
|
||||||
}
|
}
|
||||||
pendingCreatePayload.value = payload
|
})
|
||||||
showMixedChannelWarning.value = true
|
|
||||||
} else {
|
|
||||||
appStore.showError(error.response?.data?.detail || t('admin.accounts.failedToCreate'))
|
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
submitting.value = false
|
submitting.value = false
|
||||||
}
|
}
|
||||||
@@ -2618,28 +2611,16 @@ const doCreateAccount = async (payload: any) => {
|
|||||||
|
|
||||||
// Handle mixed channel warning confirmation
|
// Handle mixed channel warning confirmation
|
||||||
const handleMixedChannelConfirm = async () => {
|
const handleMixedChannelConfirm = async () => {
|
||||||
showMixedChannelWarning.value = false
|
submitting.value = true
|
||||||
if (pendingCreatePayload.value) {
|
try {
|
||||||
pendingCreatePayload.value.confirm_mixed_channel_risk = true
|
await mixedChannelWarning.confirm()
|
||||||
submitting.value = true
|
} finally {
|
||||||
try {
|
submitting.value = false
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleMixedChannelCancel = () => {
|
const handleMixedChannelCancel = () => {
|
||||||
showMixedChannelWarning.value = false
|
mixedChannelWarning.cancel()
|
||||||
pendingCreatePayload.value = null
|
|
||||||
mixedChannelWarningDetails.value = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
@@ -2795,7 +2776,7 @@ const createAccountAndFinish = async (
|
|||||||
if (!applyTempUnschedConfig(credentials)) {
|
if (!applyTempUnschedConfig(credentials)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const payload: any = {
|
await doCreateAccount({
|
||||||
name: form.name,
|
name: form.name,
|
||||||
notes: form.notes,
|
notes: form.notes,
|
||||||
platform,
|
platform,
|
||||||
@@ -2809,31 +2790,7 @@ const createAccountAndFinish = async (
|
|||||||
group_ids: form.group_ids,
|
group_ids: form.group_ids,
|
||||||
expires_at: form.expires_at,
|
expires_at: form.expires_at,
|
||||||
auto_pause_on_expired: autoPauseOnExpired.value
|
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 授权码兑换
|
// OpenAI OAuth 授权码兑换
|
||||||
|
|||||||
@@ -994,6 +994,7 @@ import ProxySelector from '@/components/common/ProxySelector.vue'
|
|||||||
import GroupSelector from '@/components/common/GroupSelector.vue'
|
import GroupSelector from '@/components/common/GroupSelector.vue'
|
||||||
import ModelWhitelistSelector from '@/components/account/ModelWhitelistSelector.vue'
|
import ModelWhitelistSelector from '@/components/account/ModelWhitelistSelector.vue'
|
||||||
import { formatDateTimeLocalInput, parseDateTimeLocalInput } from '@/utils/format'
|
import { formatDateTimeLocalInput, parseDateTimeLocalInput } from '@/utils/format'
|
||||||
|
import { useMixedChannelWarning } from '@/composables/useMixedChannelWarning'
|
||||||
import {
|
import {
|
||||||
getPresetMappingsByPlatform,
|
getPresetMappingsByPlatform,
|
||||||
commonErrorCodes,
|
commonErrorCodes,
|
||||||
@@ -1060,10 +1061,9 @@ const antigravityModelMappings = ref<ModelMapping[]>([])
|
|||||||
const tempUnschedEnabled = ref(false)
|
const tempUnschedEnabled = ref(false)
|
||||||
const tempUnschedRules = ref<TempUnschedRuleForm[]>([])
|
const tempUnschedRules = ref<TempUnschedRuleForm[]>([])
|
||||||
|
|
||||||
// Mixed channel warning dialog state
|
const mixedChannelWarning = useMixedChannelWarning()
|
||||||
const showMixedChannelWarning = ref(false)
|
const showMixedChannelWarning = mixedChannelWarning.show
|
||||||
const mixedChannelWarningDetails = ref<{ groupName: string; currentPlatform: string; otherPlatform: string } | null>(null)
|
const mixedChannelWarningDetails = mixedChannelWarning.details
|
||||||
const pendingUpdatePayload = ref<Record<string, unknown> | null>(null)
|
|
||||||
|
|
||||||
// Quota control state (Anthropic OAuth/SetupToken only)
|
// Quota control state (Anthropic OAuth/SetupToken only)
|
||||||
const windowCostEnabled = ref(false)
|
const windowCostEnabled = ref(false)
|
||||||
@@ -1525,11 +1525,13 @@ const parseDateTimeLocal = parseDateTimeLocalInput
|
|||||||
|
|
||||||
// Methods
|
// Methods
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
|
mixedChannelWarning.cancel()
|
||||||
emit('close')
|
emit('close')
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!props.account) return
|
if (!props.account) return
|
||||||
|
const accountID = props.account.id
|
||||||
|
|
||||||
submitting.value = true
|
submitting.value = true
|
||||||
const updatePayload: Record<string, unknown> = { ...form }
|
const updatePayload: Record<string, unknown> = { ...form }
|
||||||
@@ -1698,24 +1700,18 @@ const handleSubmit = async () => {
|
|||||||
updatePayload.extra = newExtra
|
updatePayload.extra = newExtra
|
||||||
}
|
}
|
||||||
|
|
||||||
await adminAPI.accounts.update(props.account.id, updatePayload)
|
await mixedChannelWarning.tryRequest(updatePayload, (p) => adminAPI.accounts.update(accountID, p), {
|
||||||
appStore.showSuccess(t('admin.accounts.accountUpdated'))
|
onSuccess: () => {
|
||||||
emit('updated')
|
appStore.showSuccess(t('admin.accounts.accountUpdated'))
|
||||||
handleClose()
|
emit('updated')
|
||||||
} catch (error: any) {
|
handleClose()
|
||||||
// Handle 409 mixed_channel_warning - show confirmation dialog
|
},
|
||||||
if (error.response?.status === 409 && error.response?.data?.error === 'mixed_channel_warning') {
|
onError: (error: any) => {
|
||||||
const details = error.response.data.details || {}
|
appStore.showError(error.response?.data?.message || error.response?.data?.detail || t('admin.accounts.failedToUpdate'))
|
||||||
mixedChannelWarningDetails.value = {
|
|
||||||
groupName: details.group_name || 'Unknown',
|
|
||||||
currentPlatform: details.current_platform || 'Unknown',
|
|
||||||
otherPlatform: details.other_platform || 'Unknown'
|
|
||||||
}
|
}
|
||||||
pendingUpdatePayload.value = updatePayload
|
})
|
||||||
showMixedChannelWarning.value = true
|
} catch (error: any) {
|
||||||
} else {
|
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'))
|
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
submitting.value = false
|
submitting.value = false
|
||||||
}
|
}
|
||||||
@@ -1723,27 +1719,15 @@ const handleSubmit = async () => {
|
|||||||
|
|
||||||
// Handle mixed channel warning confirmation
|
// Handle mixed channel warning confirmation
|
||||||
const handleMixedChannelConfirm = async () => {
|
const handleMixedChannelConfirm = async () => {
|
||||||
showMixedChannelWarning.value = false
|
submitting.value = true
|
||||||
if (pendingUpdatePayload.value && props.account) {
|
try {
|
||||||
pendingUpdatePayload.value.confirm_mixed_channel_risk = true
|
await mixedChannelWarning.confirm()
|
||||||
submitting.value = true
|
} finally {
|
||||||
try {
|
submitting.value = false
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleMixedChannelCancel = () => {
|
const handleMixedChannelCancel = () => {
|
||||||
showMixedChannelWarning.value = false
|
mixedChannelWarning.cancel()
|
||||||
pendingUpdatePayload.value = null
|
|
||||||
mixedChannelWarningDetails.value = null
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
107
frontend/src/composables/useMixedChannelWarning.ts
Normal file
107
frontend/src/composables/useMixedChannelWarning.ts
Normal file
@@ -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<MixedChannelWarningDetails | null>(null)
|
||||||
|
|
||||||
|
const pendingPayload = ref<any | null>(null)
|
||||||
|
const pendingRequest = ref<((payload: any) => Promise<any>) | 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<any>,
|
||||||
|
opts?: {
|
||||||
|
onSuccess?: () => void
|
||||||
|
onError?: (error: any) => void
|
||||||
|
}
|
||||||
|
): Promise<boolean> => {
|
||||||
|
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<boolean> => {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user