diff --git a/frontend/src/components/user/profile/TotpDisableDialog.vue b/frontend/src/components/user/profile/TotpDisableDialog.vue index 97bf2a63dd..d46d83d6ce 100644 --- a/frontend/src/components/user/profile/TotpDisableDialog.vue +++ b/frontend/src/components/user/profile/TotpDisableDialog.vue @@ -101,6 +101,7 @@ const methodLoading = ref(true) const verificationMethod = ref<'email' | 'password'>('password') const loading = ref(false) const sendingCode = ref(false) +let disposed = false const codeCooldown = ref(0) const cooldownTimer = ref | null>(null) const form = ref({ @@ -132,6 +133,7 @@ const handleSendCode = async () => { sendingCode.value = true try { await totpAPI.sendVerifyCode() + if (disposed) return appStore.showSuccess(t('profile.totp.codeSent')) // Start cooldown codeCooldown.value = 60 @@ -180,6 +182,7 @@ onMounted(() => { }) onUnmounted(() => { + disposed = true if (cooldownTimer.value) { clearInterval(cooldownTimer.value) cooldownTimer.value = null diff --git a/frontend/src/components/user/profile/TotpSetupModal.vue b/frontend/src/components/user/profile/TotpSetupModal.vue index 0bd4d5e51c..05217cad84 100644 --- a/frontend/src/components/user/profile/TotpSetupModal.vue +++ b/frontend/src/components/user/profile/TotpSetupModal.vue @@ -190,6 +190,7 @@ const methodLoading = ref(true) const verificationMethod = ref<'email' | 'password'>('password') const verifyForm = ref({ emailCode: '', password: '' }) const sendingCode = ref(false) +let disposed = false const codeCooldown = ref(0) const cooldownTimer = ref | null>(null) @@ -328,6 +329,7 @@ const handleSendCode = async () => { sendingCode.value = true try { await totpAPI.sendVerifyCode() + if (disposed) return appStore.showSuccess(t('profile.totp.codeSent')) // Start cooldown codeCooldown.value = 60 @@ -397,6 +399,7 @@ onMounted(() => { }) onUnmounted(() => { + disposed = true if (cooldownTimer.value) { clearInterval(cooldownTimer.value) cooldownTimer.value = null diff --git a/frontend/src/components/user/profile/__tests__/totp-timer-cleanup.spec.ts b/frontend/src/components/user/profile/__tests__/totp-timer-cleanup.spec.ts index 44d9fcf9b9..b4b2170097 100644 --- a/frontend/src/components/user/profile/__tests__/totp-timer-cleanup.spec.ts +++ b/frontend/src/components/user/profile/__tests__/totp-timer-cleanup.spec.ts @@ -79,6 +79,18 @@ describe('TOTP 弹窗定时器清理', () => { clearIntervalSpy.mockRestore() }) + it.each([TotpSetupModal, TotpDisableDialog])('does not start a cooldown after unmounting during send', async (component) => { + let resolve!: (value: unknown) => void + mocks.sendVerifyCode.mockReturnValueOnce(new Promise(res => { resolve = res })) + const wrapper = mount(component) + await flushPromises() + await wrapper.findAll('button').find(b => b.text().includes('profile.totp.sendCode'))!.trigger('click') + wrapper.unmount() + resolve({ success: true }); await flushPromises() + expect(setIntervalSpy).not.toHaveBeenCalled() + expect(mocks.showSuccess).not.toHaveBeenCalled() + }) + it('TotpSetupModal 卸载时清理倒计时定时器', async () => { const wrapper = mount(TotpSetupModal) await flushPromises()