diff --git a/frontend/src/components/QRCodeModal.vue b/frontend/src/components/QRCodeModal.vue index 019bfda..04d48cc 100644 --- a/frontend/src/components/QRCodeModal.vue +++ b/frontend/src/components/QRCodeModal.vue @@ -50,6 +50,7 @@ import { ref, computed, watch, onBeforeUnmount } from 'vue' import { useAuthStore } from '@/stores/auth' import { useBreakpoint } from '@/composables/useBreakpoint' +import { usePollStatus } from '@/composables/usePollStatus' import { message } from 'ant-design-vue' import { CheckCircleFilled, @@ -73,6 +74,13 @@ const emit = defineEmits(['update:visible', 'success', 'error']) const authStore = useAuthStore() const { isMobile } = useBreakpoint() +// 使用轮询 composable +const { startPolling: startQRPolling, stopPolling } = usePollStatus({ + interval: 2000, + maxRetries: 90, // 3分钟 = 180秒 / 2秒间隔 = 90次 + backoff: false +}) + const dialogVisible = computed({ get: () => props.visible, set: (val) => emit('update:visible', val), @@ -85,7 +93,6 @@ const errorMessage = ref('') const countdown = ref(180) // 倒计时 3 分钟 const progress = ref(100) -let pollingTimer = null let countdownTimer = null // 获取二维码 @@ -97,8 +104,48 @@ const fetchQRCode = async () => { qrcodeUrl.value = `data:image/png;base64,${result.qrcode_base64}` status.value = 'pending' - // 开始轮询扫码状态 - startPolling() + // 开始轮询扫码状态(使用 composable) + startQRPolling( + async () => { + const result = await authStore.checkQRCodeStatus(sessionId.value) + + // 检查是否完成(成功、过期或失败) + const completed = result.status === 'expired' || result.status === 'failed' || result.success + + return { + completed, + success: result.success === true, + data: result + } + }, + { + onSuccess: (result) => { + status.value = 'success' + stopCountdown() + message.success('登录成功!') + + // 延迟关闭对话框 + setTimeout(() => { + emit('success', result.user) + handleClose() + }, 1500) + }, + onFailure: (result) => { + if (result.status === 'expired') { + status.value = 'expired' + } else { + status.value = 'failed' + errorMessage.value = result.message || '扫码失败' + } + stopCountdown() + }, + onTimeout: () => { + status.value = 'expired' + stopCountdown() + } + } + ) + startCountdown() } catch (error) { status.value = 'failed' @@ -107,57 +154,6 @@ const fetchQRCode = async () => { } } -// 开始轮询扫码状态 -const startPolling = () => { - if (pollingTimer) { - clearInterval(pollingTimer) - } - - pollingTimer = setInterval(async () => { - try { - const result = await authStore.checkQRCodeStatus(sessionId.value) - - if (result.success) { - // 扫码成功 - status.value = 'success' - stopPolling() - stopCountdown() - - message.success('登录成功!') - - // 延迟关闭对话框 - setTimeout(() => { - emit('success', result.user) - handleClose() - }, 1500) - } else if (result.status === 'expired') { - // 二维码过期 - status.value = 'expired' - stopPolling() - stopCountdown() - } else if (result.status === 'failed') { - // 扫码失败 - status.value = 'failed' - errorMessage.value = result.message || '扫码失败' - stopPolling() - stopCountdown() - } - // 否则继续轮询(pending 状态) - } catch (error) { - console.error('轮询扫码状态失败:', error) - // 继续轮询,不中断 - } - }, 2000) // 每 2 秒轮询一次 -} - -// 停止轮询 -const stopPolling = () => { - if (pollingTimer) { - clearInterval(pollingTimer) - pollingTimer = null - } -} - // 开始倒计时 const startCountdown = () => { countdown.value = 180 @@ -172,7 +168,7 @@ const startCountdown = () => { if (countdown.value <= 0) { status.value = 'expired' - stopPolling() + stopPolling() // 停止轮询 stopCountdown() } }, 1000) @@ -193,7 +189,7 @@ const refreshQRCode = () => { // 关闭对话框 const handleClose = () => { - stopPolling() + stopPolling() // 停止轮询 stopCountdown() // 如果有未完成的会话,取消它 @@ -250,30 +246,50 @@ onBeforeUnmount(() => { } .success-icon { - color: #52c41a; + color: #4caf50; +} + +.dark .success-icon { + color: #81c784; } .warning-icon { - color: #faad14; + color: #ff9800; +} + +.dark .warning-icon { + color: #ffb74d; } .error-icon { - color: #ff4d4f; + color: #f44336; +} + +.dark .error-icon { + color: #ef5350; } .status-text { margin-top: 20px; font-size: 16px; - color: #606266; + color: var(--md-sys-color-on-surface-variant); } .status-text.success { - color: #52c41a; + color: #4caf50; font-weight: bold; } +.dark .status-text.success { + color: #81c784; +} + .status-text.error { - color: #ff4d4f; + color: #f44336; +} + +.dark .status-text.error { + color: #ef5350; } .qrcode-wrapper { @@ -286,22 +302,22 @@ onBeforeUnmount(() => { .qrcode-image { width: 240px; height: 240px; - border: 1px solid #d9d9d9; + border: 1px solid var(--md-sys-color-outline-variant); border-radius: 8px; padding: 10px; - background-color: #fff; + background-color: var(--md-sys-color-surface); } .hint-text { margin-top: 20px; font-size: 14px; - color: #8c8c8c; + color: var(--md-sys-color-on-surface-variant); } .countdown-text { margin-top: 10px; font-size: 12px; - color: #8c8c8c; + color: var(--md-sys-color-on-surface-variant); } .mt-4 { diff --git a/frontend/src/views/DashboardView.vue b/frontend/src/views/DashboardView.vue index ef3e62d..d3345b3 100644 --- a/frontend/src/views/DashboardView.vue +++ b/frontend/src/views/DashboardView.vue @@ -175,12 +175,20 @@ import { useUserStore } from '@/stores/user' import { useTaskStore } from '@/stores/task' import { useCheckInStore } from '@/stores/checkIn' import { formatDateTime } from '@/utils/helpers' +import { usePollStatus } from '@/composables/usePollStatus' const authStore = useAuthStore() const userStore = useUserStore() const taskStore = useTaskStore() const checkInStore = useCheckInStore() +// 使用轮询 composable +const { startPolling } = usePollStatus({ + interval: 2000, // 每 2 秒轮询一次 + maxRetries: 15, // 最多 15 次 (30 秒) + backoff: false // 不使用指数退避 +}) + const tokenStatusLoading = ref(false) const checkInLoading = ref(false) const selectedTaskId = ref(null) @@ -260,48 +268,34 @@ const handleCheckIn = async () => { // 显示提示消息 message.info('打卡任务已启动,正在后台处理...') - // 用于存储 interval ID,以便在超时时清理 - let pollIntervalId = null - - // 开始轮询检查打卡状态 - pollIntervalId = setInterval(async () => { - try { + // 使用轮询 composable 检查打卡状态 + startPolling( + async () => { const status = await taskStore.getCheckInRecordStatus(recordId) - - // 只要状态不是 pending,说明打卡请求已经处理完成 - if (status.status !== 'pending') { - clearInterval(pollIntervalId) - checkInLoading.value = false - - if (status.status === 'success') { - // 打卡成功 - message.success('打卡成功!') - checkInStore.fetchMyRecords({ limit: 1 }) - } else { - // 打卡失败或其他状态 (failure, out_of_time, unknown 等) - const errorMsg = status.error_message || status.response_text || '打卡失败' - message.error(errorMsg) - checkInStore.fetchMyRecords({ limit: 1 }) - } + return { + completed: status.status !== 'pending', + success: status.status === 'success', + data: status + } + }, + { + onSuccess: () => { + checkInLoading.value = false + message.success('打卡成功!') + checkInStore.fetchMyRecords({ limit: 1 }) + }, + onFailure: (statusData) => { + checkInLoading.value = false + const errorMsg = statusData.error_message || statusData.response_text || '打卡失败' + message.error(errorMsg) + checkInStore.fetchMyRecords({ limit: 1 }) + }, + onTimeout: () => { + checkInLoading.value = false + message.warning('打卡处理时间较长,请稍后查看打卡记录') } - // status === 'pending' 时继续轮询 - } catch (error) { - // 查询状态失败,停止轮询 - console.error('轮询状态失败:', error) - clearInterval(pollIntervalId) - checkInLoading.value = false - message.error('查询打卡状态失败') } - }, 2000) // 每 2 秒查询一次 - - // 设置超时保护(30 秒后停止轮询) - setTimeout(() => { - if (checkInLoading.value) { - clearInterval(pollIntervalId) - checkInLoading.value = false - message.warning('打卡处理时间较长,请稍后查看打卡记录') - } - }, 30000) + ) } catch (error) { console.error('启动打卡失败:', error) diff --git a/frontend/src/views/LoginView.vue b/frontend/src/views/LoginView.vue index 5c971a2..5062e59 100644 --- a/frontend/src/views/LoginView.vue +++ b/frontend/src/views/LoginView.vue @@ -216,7 +216,7 @@ const handleQRCodeLogin = async () => { // 显示 QR 码弹窗 qrcodeVisible.value = true } catch (error) { - console.error('表单验证失败:', error) + // 表单验证失败,不需要打印错误(由 Ant Design 自动显示错误提示) } } diff --git a/frontend/src/views/TasksView.vue b/frontend/src/views/TasksView.vue index fd11063..27600eb 100644 --- a/frontend/src/views/TasksView.vue +++ b/frontend/src/views/TasksView.vue @@ -370,12 +370,20 @@ import { useBreakpoint } from '@/composables/useBreakpoint' import { useTaskStore } from '@/stores/task' import { useTemplateStore } from '@/stores/template' import { copyToClipboard, formatDateTime } from '@/utils/helpers' +import { usePollStatus } from '@/composables/usePollStatus' const router = useRouter() const taskStore = useTaskStore() const templateStore = useTemplateStore() const { isMobile } = useBreakpoint() +// 使用轮询 composable +const { startPolling } = usePollStatus({ + interval: 2000, + maxRetries: 15, + backoff: false +}) + const loading = ref(false) const showCreateDialog = ref(false) const submitting = ref(false) @@ -675,48 +683,34 @@ const handleCheckIn = async (taskId) => { // 显示提示消息 message.info('打卡任务已启动,正在后台处理...') - // 用于存储 interval ID,以便在超时时清理 - let pollIntervalId = null - - // 开始轮询检查打卡状态 - pollIntervalId = setInterval(async () => { - try { + // 使用轮询 composable 检查打卡状态 + startPolling( + async () => { const status = await taskStore.getCheckInRecordStatus(recordId) - - // 只要状态不是 pending,说明打卡请求已经处理完成 - if (status.status !== 'pending') { - clearInterval(pollIntervalId) - checkInLoading.value[taskId] = false - - if (status.status === 'success') { - // 打卡成功 - message.success('打卡成功!') - await fetchTasks() - } else { - // 打卡失败或其他状态 (failure, out_of_time, unknown 等) - const errorMsg = status.error_message || status.response_text || '打卡失败' - message.error(errorMsg) - await fetchTasks() - } + return { + completed: status.status !== 'pending', + success: status.status === 'success', + data: status + } + }, + { + onSuccess: async () => { + checkInLoading.value[taskId] = false + message.success('打卡成功!') + await fetchTasks() + }, + onFailure: async (statusData) => { + checkInLoading.value[taskId] = false + const errorMsg = statusData.error_message || statusData.response_text || '打卡失败' + message.error(errorMsg) + await fetchTasks() + }, + onTimeout: () => { + checkInLoading.value[taskId] = false + message.warning('打卡处理时间较长,请稍后查看打卡记录') } - // status === 'pending' 时继续轮询 - } catch (error) { - // 查询状态失败,停止轮询 - console.error('轮询状态失败:', error) - clearInterval(pollIntervalId) - checkInLoading.value[taskId] = false - message.error('查询打卡状态失败') } - }, 2000) // 每 2 秒查询一次 - - // 设置超时保护(30 秒后停止轮询) - setTimeout(() => { - if (checkInLoading.value[taskId]) { - clearInterval(pollIntervalId) - checkInLoading.value[taskId] = false - message.warning('打卡处理时间较长,请稍后查看打卡记录') - } - }, 30000) + ) } catch (error) { console.error('启动打卡失败:', error)