import { computed, onBeforeUnmount, ref, watch } from 'vue' import { showSuccess } from '@/lib/feedback' import { confirmClaimRole, createClaimSession, fetchClaimDetail, fetchClaimSessionSummary, redeemClaim, } from '@/services/claim' import type { ClaimDetailData, ClaimTaskStatus } from '@/types/claim' import type { TencentLoginType } from '@/types/tencent/session' import { notifyTencentActionError } from './tencent/session-errors' const DEFAULT_LOGIN_TYPE: TencentLoginType = 'qq' const POLL_INTERVAL_MS = 2500 const POLL_FAILURE_LIMIT = 3 const ACTIVE_TASK_STATUSES = new Set([ 'claimed', 'role_confirmed', 'redeeming', ]) export function useClaimPage(token: string) { const detailLoading = ref(true) const sessionLoading = ref(false) const redeemLoading = ref(false) const roleConfirmLoading = ref(false) const loginType = ref(DEFAULT_LOGIN_TYPE) const detail = ref(null) const roleConfirmed = ref(false) const pollWarningMessage = ref('') const qrImageNaturalWidth = ref(0) let pollTimer: ReturnType | null = null let pollToken = 0 let pollFailureCount = 0 const session = computed(() => detail.value?.session || null) const task = computed(() => detail.value?.task || null) const order = computed(() => detail.value?.order || null) const orderItem = computed(() => detail.value?.orderItem || null) const result = computed(() => detail.value?.result || null) const tokenStatus = computed(() => detail.value?.tokenStatus || 'active') const activityInfo = computed(() => session.value?.activityInfo || null) const roleReady = computed(() => Boolean(activityInfo.value?.role?.ready)) const hasSession = computed(() => Boolean(session.value?.sessionId)) const loginTypeLabel = computed(() => (loginType.value === 'wx' ? '微信' : 'QQ')) const sessionNotice = computed(() => pollWarningMessage.value || session.value?.notice || task.value?.lastError || '') const qrImage = computed(() => session.value?.qrImageBase64 ? `data:image/png;base64,${session.value.qrImageBase64}` : '', ) const screenshotUrl = computed(() => result.value?.screenshotUrl || '') const showScreenshot = computed(() => Boolean(screenshotUrl.value)) const loginTabs = [ { value: 'qq' as const, label: 'QQ账号登录' }, { value: 'wx' as const, label: '微信账号登录' }, ] const statusLabel = computed(() => resolveTaskStatusLabel(task.value?.status, session.value?.status)) const initButtonLabel = computed(() => `开始初始化 ${loginTypeLabel.value} 登录`) const scanInstruction = computed(() => `请使用${loginTypeLabel.value}扫描二维码,并在手机上确认登录`) const roleFacts = computed(() => [ { label: '登录昵称', value: activityInfo.value?.nickname || '等待扫码登录', }, { label: '当前角色', value: activityInfo.value?.role?.roleName || '后端浏览器同步中', accent: true, }, { label: '角色 ID', value: activityInfo.value?.role?.roleId || '未识别', }, { label: '任务状态', value: statusLabel.value, }, ]) const resultFacts = computed(() => [ { label: '订单号', value: order.value?.platformOrderId || '-', }, { label: '商品', value: orderItem.value?.skuName || '-', }, { label: '业务返回码', value: result.value?.resultCode || '-', }, { label: '业务消息', value: result.value?.resultMessage || '尚未兑换', }, ]) const canConfirmRole = computed(() => Boolean( task.value && !task.value.requiresSupportReview && hasSession.value && roleReady.value && task.value.status === 'claimed', ), ) const canRedeem = computed(() => Boolean( task.value && !task.value.requiresSupportReview && hasSession.value && roleConfirmed.value && !redeemLoading.value && (task.value.status === 'role_confirmed' || task.value.status === 'redeeming'), ), ) const redeemBlockedReason = computed(() => { if (!detail.value) { return '正在加载领取信息' } if (tokenStatus.value !== 'active') { return '当前领取链接不可用' } if (!hasSession.value) { return `请先选择${loginTypeLabel.value}并初始化登录会话` } if (task.value?.requiresSupportReview) { if (!roleReady.value) { return '扫码成功后,系统会自动同步登录信息,准备发给客服复核' } return '当前商品需要客服复核角色并代你发起兑换,请联系人工继续' } if (redeemLoading.value) { return '兑换任务正在执行中' } if (!roleReady.value) { return sessionNotice.value || '扫码成功后,后端正在同步角色和大区信息' } if (!roleConfirmed.value) { return '请先确认当前角色与大区无误,再开始兑换' } return '' }) const redeemButtonLabel = computed(() => (task.value?.requiresSupportReview ? '等待客服兑换' : '开始兑换')) const screenshotEmptyTitle = computed(() => task.value?.status === 'redeemed' ? '本次没有可展示的截图' : '等待截图', ) const screenshotEmptyMessage = computed(() => task.value?.status === 'redeemed' ? '本次兑换可能未生成截图,或截图产物还未同步完成。' : '领取完成后,如本次生成了结果截图,这里会展示。' ) function applyLoginTypeFromDetail(nextDetail: ClaimDetailData) { loginType.value = syncLoginTypeFromDetail(nextDetail) } watch( () => [ task.value?.taskId || '', activityInfo.value?.nickname || '', activityInfo.value?.role?.roleId || '', activityInfo.value?.role?.roleName || '', activityInfo.value?.role?.area || '', activityInfo.value?.role?.partition || '', ].join('|'), () => { roleConfirmed.value = Boolean(task.value?.status === 'role_confirmed' || task.value?.status === 'redeeming' || task.value?.status === 'redeemed') }, { immediate: true }, ) async function loadDetail() { detailLoading.value = true try { const response = await fetchClaimDetail(token) if (response.code !== 0) { throw new Error(response.msg || '领取详情加载失败') } detail.value = mergeClaimDetailData(detail.value, response.data) applyLoginTypeFromDetail(response.data) restartPollingIfNeeded() } catch (error) { notifyTencentActionError(error, '领取详情加载失败') } finally { detailLoading.value = false } } async function createSessionFlow(nextLoginType = loginType.value) { sessionLoading.value = true resetPolling() resetPollingWarning() try { loginType.value = nextLoginType const response = await createClaimSession(token, nextLoginType) if (response.code !== 0) { throw new Error(response.msg || '创建领取会话失败') } detail.value = mergeClaimDetailData(detail.value, response.data) applyLoginTypeFromDetail(response.data) startPolling() } catch (error) { notifyTencentActionError(error, '创建领取会话失败') } finally { sessionLoading.value = false } } async function refreshSessionSummary({ silent = false } = {}) { if (!hasSession.value) { return false } if (!silent) { sessionLoading.value = true resetPollingWarning() } try { const response = await fetchClaimSessionSummary(token) if (response.code !== 0) { throw new Error(response.msg || '领取会话状态刷新失败') } detail.value = mergeClaimDetailData(detail.value, response.data) applyLoginTypeFromDetail(response.data) if (silent) { resetPollingWarning() } return true } catch (error) { if (silent) { handleSilentPollingError(error) } else { notifyTencentActionError(error, '领取会话状态刷新失败') } return false } finally { if (!silent) { sessionLoading.value = false } } } async function confirmRoleNow() { if (!canConfirmRole.value) { return } roleConfirmLoading.value = true try { const response = await confirmClaimRole(token) if (response.code !== 0) { throw new Error(response.msg || '角色确认失败') } detail.value = mergeClaimDetailData(detail.value, response.data) roleConfirmed.value = true showSuccess(response.msg || '角色已确认') restartPollingIfNeeded(response.data) } catch (error) { notifyTencentActionError(error, '角色确认失败') await refreshSessionSummary({ silent: true }) } finally { roleConfirmLoading.value = false } } async function redeemNow() { if (!canRedeem.value) { return } redeemLoading.value = true resetPolling() resetPollingWarning() try { const response = await redeemClaim(token) if (response.code !== 0) { throw new Error(response.msg || '领取兑换失败') } detail.value = mergeClaimDetailData(detail.value, response.data) showSuccess(response.msg || '兑换完成') restartPollingIfNeeded(response.data) } catch (error) { notifyTencentActionError(error, '领取兑换失败') await refreshSessionSummary({ silent: true }) restartPollingIfNeeded() } finally { redeemLoading.value = false } } async function switchLoginType(nextLoginType: TencentLoginType) { if (nextLoginType === loginType.value) { return } loginType.value = nextLoginType if (hasSession.value) { await createSessionFlow(nextLoginType) } } function startPolling() { const tokenId = ++pollToken const loop = async () => { if (tokenId !== pollToken || !hasSession.value) { return } await refreshSessionSummary({ silent: true }) if (tokenId !== pollToken || !shouldKeepPolling(detail.value)) { return } pollTimer = setTimeout(() => { void loop() }, POLL_INTERVAL_MS) } pollTimer = setTimeout(() => { void loop() }, POLL_INTERVAL_MS) } function restartPollingIfNeeded(nextDetail = detail.value) { resetPolling() if (shouldKeepPolling(nextDetail)) { startPolling() } } function resetPolling() { pollToken += 1 if (pollTimer) { clearTimeout(pollTimer) pollTimer = null } } function resetPollingWarning() { pollFailureCount = 0 pollWarningMessage.value = '' } function handleSilentPollingError(error: unknown) { console.error(error) pollFailureCount += 1 if (pollFailureCount < POLL_FAILURE_LIMIT) { return } pollWarningMessage.value = '领取状态刷新失败,已暂停自动轮询,请手动刷新页面。' resetPolling() } function handleQrImageLoad(event: Event) { const target = event.target if (!(target instanceof HTMLImageElement)) { return } qrImageNaturalWidth.value = target.naturalWidth || 0 } const qrDisplayWidth = computed(() => { const naturalWidth = qrImageNaturalWidth.value if (!naturalWidth) { return 220 } if (naturalWidth < 160) { return Math.min(naturalWidth * 2, 220) } return Math.min(naturalWidth, 240) }) const qrFigureStyle = computed(() => ({ width: `${qrDisplayWidth.value}px`, maxWidth: '100%', })) const qrPreviewWidth = computed(() => `${Math.min(Math.max(qrDisplayWidth.value + 120, 360), 480)}px`) loadDetail() onBeforeUnmount(() => { resetPolling() }) return { detailLoading, sessionLoading, redeemLoading, roleConfirmLoading, loginType, loginTypeLabel, loginTabs, detail, task, order, orderItem, activityInfo, hasSession, qrImage, qrFigureStyle, qrPreviewWidth, statusLabel, session, sessionNotice, roleFacts, resultFacts, roleConfirmed, roleReady, canConfirmRole, canRedeem, redeemBlockedReason, redeemButtonLabel, initButtonLabel, scanInstruction, screenshotEmptyTitle, screenshotEmptyMessage, screenshotUrl, showScreenshot, createSessionFlow, refreshSessionSummary, switchLoginType, confirmRoleNow, redeemNow, handleQrImageLoad, } } function syncLoginTypeFromDetail(detail: ClaimDetailData) { const nextLoginType = String(detail.session?.loginType || detail.task.loginType || '').trim() return nextLoginType === 'wx' ? 'wx' : 'qq' } function shouldKeepPolling(detail: ClaimDetailData | null) { if (!detail?.session?.sessionId) { return false } return ACTIVE_TASK_STATUSES.has(detail.task.status) } function resolveTaskStatusLabel(taskStatus?: string, sessionStatus?: string) { switch (taskStatus) { case 'link_generated': return '等待开始领取' case 'claimed': if (sessionStatus === 'scanned') { return '确认中' } if (sessionStatus === 'logged_in' || sessionStatus === 'ready_to_redeem') { return '已登录' } return '领取中' case 'role_confirmed': return '角色已确认' case 'redeeming': return '正在兑换' case 'redeemed': return '兑换成功' case 'waiting_inventory': return '等待库存' case 'retry_pending': return '等待重试' case 'manual_review': return '等待人工处理' case 'expired': return '链接已过期' case 'closed': return '任务已关闭' default: return sessionStatus || taskStatus || '等待中' } } function mergeClaimDetailData(current: ClaimDetailData | null, next: ClaimDetailData) { if (!current?.session) { return next } if (!next.session) { return { ...next, session: current.session, } } return { ...next, session: { ...current.session, ...next.session, qrImageBase64: typeof next.session.qrImageBase64 === 'string' ? next.session.qrImageBase64 : current.session.qrImageBase64, activityInfo: next.session.activityInfo ?? current.session.activityInfo ?? null, redeem: next.session.redeem ?? current.session.redeem ?? null, artifacts: next.session.artifacts || current.session.artifacts, }, } }