frontend: use composables

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