From 75f5bb439b27c448baecdad30a42bc49fa7d76e3 Mon Sep 17 00:00:00 2001 From: cnlimiter Date: Sun, 15 Mar 2026 16:25:06 +0800 Subject: [PATCH] =?UTF-8?q?fix(app):=20=E4=BF=AE=E5=A4=8DWebSocket?= =?UTF-8?q?=E8=BF=9E=E6=8E=A5=E5=85=B3=E9=97=AD=E6=97=B6=E7=9A=84=E9=87=8D?= =?UTF-8?q?=E5=A4=8D=E8=BD=AE=E8=AF=A2=E5=92=8Ctoast=E6=98=BE=E7=A4=BA?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/js/app.js | 185 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 136 insertions(+), 49 deletions(-) diff --git a/static/js/app.js b/static/js/app.js index 2686769..f87a386 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -12,6 +12,12 @@ let accountsPollingInterval = null; let isBatchMode = false; let isOutlookBatchMode = false; let outlookAccounts = []; +let taskCompleted = false; // 标记任务是否已完成 +let batchCompleted = false; // 标记批量任务是否已完成 +let taskFinalStatus = null; // 保存任务的最终状态 +let batchFinalStatus = null; // 保存批量任务的最终状态 +let displayedLogs = new Set(); // 用于日志去重 +let toastShown = false; // 标记是否已显示过 toast let availableServices = { tempmail: { available: true, services: [] }, outlook: { available: false, services: [] }, @@ -91,6 +97,7 @@ function initEventListeners() { // 清空日志 elements.clearLogBtn.addEventListener('click', () => { elements.consoleLog.innerHTML = '
[系统] 日志已清空
'; + displayedLogs.clear(); // 清空日志去重集合 }); // 刷新账号列表 @@ -294,6 +301,12 @@ async function handleStartRegistration(e) { // 单次注册 async function handleSingleRegistration(requestData) { + // 重置任务状态 + taskCompleted = false; + taskFinalStatus = null; + displayedLogs.clear(); // 清空日志去重集合 + toastShown = false; // 重置 toast 标志 + addLog('info', '[系统] 正在启动注册任务...'); try { @@ -345,19 +358,30 @@ function connectWebSocket(taskUuid) { // 检查是否完成 if (['completed', 'failed', 'cancelled', 'cancelling'].includes(data.status)) { + // 保存最终状态,用于 onclose 判断 + taskFinalStatus = data.status; + taskCompleted = true; + + // 断开 WebSocket(异步操作) disconnectWebSocket(); + + // 任务完成后再重置按钮 resetButtons(); - if (data.status === 'completed') { - addLog('success', '[成功] 注册成功!'); - toast.success('注册成功!'); - // 刷新账号列表 - loadRecentAccounts(); - } else if (data.status === 'failed') { - addLog('error', '[错误] 注册失败'); - toast.error('注册失败'); - } else if (data.status === 'cancelled' || data.status === 'cancelling') { - addLog('warning', '[警告] 任务已取消'); + // 只显示一次 toast + if (!toastShown) { + toastShown = true; + if (data.status === 'completed') { + addLog('success', '[成功] 注册成功!'); + toast.success('注册成功!'); + // 刷新账号列表 + loadRecentAccounts(); + } else if (data.status === 'failed') { + addLog('error', '[错误] 注册失败'); + toast.error('注册失败'); + } else if (data.status === 'cancelled' || data.status === 'cancelling') { + addLog('warning', '[警告] 任务已取消'); + } } } } else if (data.type === 'pong') { @@ -369,8 +393,12 @@ function connectWebSocket(taskUuid) { console.log('WebSocket 连接关闭:', event.code); stopWebSocketHeartbeat(); - // 如果任务仍在运行,切换到轮询 - if (currentTask && ['pending', 'running'].includes(currentTask.status)) { + // 只有在任务未完成且最终状态不是完成状态时才切换到轮询 + // 使用 taskFinalStatus 而不是 currentTask.status,因为 currentTask 可能已被重置 + const shouldPoll = !taskCompleted && + taskFinalStatus === null; // 如果 taskFinalStatus 有值,说明任务已完成 + + if (shouldPoll && currentTask) { console.log('切换到轮询模式'); useWebSocket = false; startLogPolling(currentTask.task_uuid); @@ -428,6 +456,12 @@ function cancelViaWebSocket() { // 批量注册 async function handleBatchRegistration(requestData) { + // 重置批量任务状态 + batchCompleted = false; + batchFinalStatus = null; + displayedLogs.clear(); // 清空日志去重集合 + toastShown = false; // 重置 toast 标志 + const count = parseInt(elements.batchCount.value) || 5; const intervalMin = parseInt(elements.intervalMin.value) || 5; const intervalMax = parseInt(elements.intervalMax.value) || 30; @@ -546,16 +580,20 @@ function startLogPolling(taskUuid) { stopLogPolling(); resetButtons(); - if (data.status === 'completed') { - addLog('success', '[成功] 注册成功!'); - toast.success('注册成功!'); - // 刷新账号列表 - loadRecentAccounts(); - } else if (data.status === 'failed') { - addLog('error', '[错误] 注册失败'); - toast.error('注册失败'); - } else if (data.status === 'cancelled') { - addLog('warning', '[警告] 任务已取消'); + // 只显示一次 toast + if (!toastShown) { + toastShown = true; + if (data.status === 'completed') { + addLog('success', '[成功] 注册成功!'); + toast.success('注册成功!'); + // 刷新账号列表 + loadRecentAccounts(); + } else if (data.status === 'failed') { + addLog('error', '[错误] 注册失败'); + toast.error('注册失败'); + } else if (data.status === 'cancelled') { + addLog('warning', '[警告] 任务已取消'); + } } } } catch (error) { @@ -584,13 +622,17 @@ function startBatchPolling(batchId) { stopBatchPolling(); resetButtons(); - addLog('info', `[完成] 批量任务完成!成功: ${data.success}, 失败: ${data.failed}`); - if (data.success > 0) { - toast.success(`批量注册完成,成功 ${data.success} 个`); - // 刷新账号列表 - loadRecentAccounts(); - } else { - toast.warning('批量注册完成,但没有成功注册任何账号'); + // 只显示一次 toast + if (!toastShown) { + toastShown = true; + addLog('info', `[完成] 批量任务完成!成功: ${data.success}, 失败: ${data.failed}`); + if (data.success > 0) { + toast.success(`批量注册完成,成功 ${data.success} 个`); + // 刷新账号列表 + loadRecentAccounts(); + } else { + toast.warning('批量注册完成,但没有成功注册任何账号'); + } } } } catch (error) { @@ -736,6 +778,20 @@ function startAccountsPolling() { // 添加日志 function addLog(type, message) { + // 日志去重:使用消息内容的 hash 作为键 + const logKey = `${type}:${message}`; + if (displayedLogs.has(logKey)) { + return; // 已经显示过,跳过 + } + displayedLogs.add(logKey); + + // 限制去重集合大小,避免内存泄漏 + if (displayedLogs.size > 1000) { + // 清空一半的记录 + const keys = Array.from(displayedLogs); + keys.slice(0, 500).forEach(k => displayedLogs.delete(k)); + } + const line = document.createElement('div'); line.className = `log-line ${type}`; @@ -783,6 +839,12 @@ function resetButtons() { currentTask = null; currentBatch = null; isBatchMode = false; + // 重置完成标志 + taskCompleted = false; + batchCompleted = false; + // 重置最终状态标志 + taskFinalStatus = null; + batchFinalStatus = null; // 断开 WebSocket disconnectWebSocket(); disconnectBatchWebSocket(); @@ -869,6 +931,12 @@ function deselectAllOutlookAccounts() { // 处理 Outlook 批量注册 async function handleOutlookBatchRegistration() { + // 重置批量任务状态 + batchCompleted = false; + batchFinalStatus = null; + displayedLogs.clear(); // 清空日志去重集合 + toastShown = false; // 重置 toast 标志 + // 获取选中的账户 const selectedIds = []; document.querySelectorAll('.outlook-account-checkbox:checked').forEach(cb => { @@ -964,22 +1032,33 @@ function connectBatchWebSocket(batchId) { // 检查是否完成 if (['completed', 'failed', 'cancelled', 'cancelling'].includes(data.status)) { + // 保存最终状态,用于 onclose 判断 + batchFinalStatus = data.status; + batchCompleted = true; + + // 断开 WebSocket(异步操作) disconnectBatchWebSocket(); + + // 任务完成后再重置按钮 resetButtons(); - if (data.status === 'completed') { - addLog('success', `[完成] Outlook 批量任务完成!成功: ${data.success}, 失败: ${data.failed}, 跳过: ${data.skipped || 0}`); - if (data.success > 0) { - toast.success(`Outlook 批量注册完成,成功 ${data.success} 个`); - loadRecentAccounts(); - } else { - toast.warning('Outlook 批量注册完成,但没有成功注册任何账号'); + // 只显示一次 toast + if (!toastShown) { + toastShown = true; + if (data.status === 'completed') { + addLog('success', `[完成] Outlook 批量任务完成!成功: ${data.success}, 失败: ${data.failed}, 跳过: ${data.skipped || 0}`); + if (data.success > 0) { + toast.success(`Outlook 批量注册完成,成功 ${data.success} 个`); + loadRecentAccounts(); + } else { + toast.warning('Outlook 批量注册完成,但没有成功注册任何账号'); + } + } else if (data.status === 'failed') { + addLog('error', '[错误] 批量任务执行失败'); + toast.error('批量任务执行失败'); + } else if (data.status === 'cancelled' || data.status === 'cancelling') { + addLog('warning', '[警告] 批量任务已取消'); } - } else if (data.status === 'failed') { - addLog('error', '[错误] 批量任务执行失败'); - toast.error('批量任务执行失败'); - } else if (data.status === 'cancelled' || data.status === 'cancelling') { - addLog('warning', '[警告] 批量任务已取消'); } } } else if (data.type === 'pong') { @@ -991,8 +1070,12 @@ function connectBatchWebSocket(batchId) { console.log('批量任务 WebSocket 连接关闭:', event.code); stopBatchWebSocketHeartbeat(); - // 如果任务仍在运行,切换到轮询 - if (currentBatch && !['completed', 'failed', 'cancelled'].includes(currentBatch.status)) { + // 只有在任务未完成且最终状态不是完成状态时才切换到轮询 + // 使用 batchFinalStatus 而不是 currentBatch.status,因为 currentBatch 可能已被重置 + const shouldPoll = !batchCompleted && + batchFinalStatus === null; // 如果 batchFinalStatus 有值,说明任务已完成 + + if (shouldPoll && currentBatch) { console.log('切换到轮询模式'); startOutlookBatchPolling(currentBatch.batch_id); } @@ -1075,12 +1158,16 @@ function startOutlookBatchPolling(batchId) { stopBatchPolling(); resetButtons(); - addLog('info', `[完成] Outlook 批量任务完成!成功: ${data.success}, 失败: ${data.failed}, 跳过: ${data.skipped || 0}`); - if (data.success > 0) { - toast.success(`Outlook 批量注册完成,成功 ${data.success} 个`); - loadRecentAccounts(); - } else { - toast.warning('Outlook 批量注册完成,但没有成功注册任何账号'); + // 只显示一次 toast + if (!toastShown) { + toastShown = true; + addLog('info', `[完成] Outlook 批量任务完成!成功: ${data.success}, 失败: ${data.failed}, 跳过: ${data.skipped || 0}`); + if (data.success > 0) { + toast.success(`Outlook 批量注册完成,成功 ${data.success} 个`); + loadRecentAccounts(); + } else { + toast.warning('Outlook 批量注册完成,但没有成功注册任何账号'); + } } } } catch (error) {