Files
order_site/apps/frontend/src/composables/useClaimPage.ts
T
2026-05-21 18:09:39 +08:00

452 lines
13 KiB
TypeScript

import { computed, onBeforeUnmount, ref, watch } from 'vue'
import { showSuccess } from '@/lib/feedback'
import {
confirmClaimRole,
createClaimSession,
fetchClaimDetail,
fetchClaimSessionSummary,
refreshClaimSession,
removeClaimSession,
redeemClaim,
} from '@/services/claim'
import type { ClaimDetailData } from '@/types/claim'
import type { TencentLoginType } from '@/types/tencent/session'
import { notifyTencentActionError } from './tencent/session-errors'
import {
useSessionPolling,
useSessionQrDisplay,
useSessionResultFacts,
buildRedeemBlockedReason,
resolveTaskStatusLabel,
syncLoginTypeFromDetail,
mergeSessionDetail,
shouldRefreshQrImage,
shouldKeepPolling,
DEFAULT_LOGIN_TYPE,
} from './shared'
export function useClaimPage(token: string) {
const detailLoading = ref(true)
const sessionLoading = ref(false)
const redeemLoading = ref(false)
const roleConfirmLoading = ref(false)
const loginType = ref<TencentLoginType>(DEFAULT_LOGIN_TYPE)
const detail = ref<ClaimDetailData | null>(null)
const roleConfirmed = ref(false)
// ── Derived state ──────────────────────────────────────────────────
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'))
// ── Polling ────────────────────────────────────────────────────────
const {
pollWarningMessage,
startPolling,
resetPolling,
resetPollingWarning,
handleSilentPollingError,
} = useSessionPolling({
hasSession,
shouldKeepPolling: computed(() => shouldKeepPolling(detail.value)),
onPollTick: async () => { await refreshSessionSummary({ silent: true }) },
pollWarningPrefix: '领取状态刷新失败',
})
const sessionNotice = computed(
() => pollWarningMessage.value || session.value?.notice || task.value?.lastError || '',
)
// ── QR display ─────────────────────────────────────────────────────
const { qrImage, qrFigureStyle, qrPreviewWidth, handleQrImageLoad } = useSessionQrDisplay({
session,
})
// ── Presentation ───────────────────────────────────────────────────
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, 'claim'),
)
const initButtonLabel = computed(() => `开始初始化 ${loginTypeLabel.value} 登录`)
const scanInstruction = computed(
() => `请使用${loginTypeLabel.value}扫描二维码,并在手机上确认登录`,
)
// Claim pages show 4 role facts, but original only had 3 (no area).
// Override roleFacts to match original exactly:
const claimRoleFacts = 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 } = useSessionResultFacts({
order,
orderItem,
result,
})
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(() =>
buildRedeemBlockedReason({
detail,
hasSession,
loginTypeLabel,
redeemLoading,
roleReady,
roleConfirmed,
sessionNotice,
context: 'claim',
tokenStatus,
requiresSupportReview: computed(() => Boolean(task.value?.requiresSupportReview)),
}),
)
const redeemButtonLabel = computed(() =>
task.value?.requiresSupportReview ? '等待客服兑换' : '开始兑换',
)
const screenshotEmptyTitle = computed(() =>
task.value?.status === 'redeemed' ? '本次没有可展示的截图' : '等待截图',
)
const screenshotEmptyMessage = computed(() =>
task.value?.status === 'redeemed'
? '本次兑换可能未生成截图,或截图产物还未同步完成。'
: '领取完成后,如本次生成了结果截图,这里会展示。',
)
// ── Watchers ───────────────────────────────────────────────────────
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 },
)
// ── Helpers ────────────────────────────────────────────────────────
function applyLoginTypeFromDetail(nextDetail: ClaimDetailData) {
loginType.value = syncLoginTypeFromDetail(nextDetail, loginType.value)
}
function restartPollingIfNeeded(nextDetail = detail.value) {
resetPolling()
if (shouldKeepPolling(nextDetail)) {
startPolling()
}
}
// ── API flows ─────────────────────────────────────────────────────
async function loadDetail() {
detailLoading.value = true
try {
const response = await fetchClaimDetail(token)
detail.value = mergeSessionDetail(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, {
forceRecreate: hasSession.value,
})
detail.value = mergeSessionDetail(detail.value, response.data)
applyLoginTypeFromDetail(response.data)
startPolling()
} catch (error) {
notifyTencentActionError(error, '创建领取会话失败')
} finally {
sessionLoading.value = false
}
}
async function reloadSessionPage() {
if (!hasSession.value) {
return false
}
sessionLoading.value = true
resetPolling()
resetPollingWarning()
try {
const response = await refreshClaimSession(token)
detail.value = mergeSessionDetail(detail.value, response.data)
applyLoginTypeFromDetail(response.data)
restartPollingIfNeeded(response.data)
return true
} catch (error) {
notifyTencentActionError(error, '刷新后端页面失败')
await refreshSessionSummary({ silent: true })
restartPollingIfNeeded()
return false
} finally {
sessionLoading.value = false
}
}
async function closeSessionFlow({ silent = false } = {}) {
if (!hasSession.value) {
return false
}
if (!silent) {
sessionLoading.value = true
}
resetPolling()
resetPollingWarning()
try {
const response = await removeClaimSession(token)
detail.value = mergeSessionDetail(detail.value, response.data)
applyLoginTypeFromDetail(response.data)
if (!silent) {
showSuccess(response.msg || '领取会话已关闭')
}
return true
} catch (error) {
if (!silent) {
notifyTencentActionError(error, '关闭领取会话失败')
}
return false
} finally {
if (!silent) {
sessionLoading.value = false
}
}
}
async function refreshSessionSummary({ silent = false } = {}) {
if (!hasSession.value) {
return false
}
if (!silent) {
sessionLoading.value = true
resetPollingWarning()
}
try {
const currentSession = detail.value?.session || null
const response = await fetchClaimSessionSummary(token)
let nextDetail = mergeSessionDetail(detail.value, response.data)
detail.value = nextDetail
applyLoginTypeFromDetail(nextDetail)
if (shouldRefreshQrImage(currentSession, response.data.session)) {
const fullResponse = await fetchClaimDetail(token)
nextDetail = mergeSessionDetail(nextDetail, fullResponse.data)
detail.value = nextDetail
applyLoginTypeFromDetail(nextDetail)
}
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)
detail.value = mergeSessionDetail(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)
detail.value = mergeSessionDetail(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
}
if (hasSession.value) {
await createSessionFlow(nextLoginType)
return
}
loginType.value = nextLoginType
}
// ── Lifecycle ──────────────────────────────────────────────────────
loadDetail()
onBeforeUnmount(() => {
resetPolling()
})
return {
detailLoading,
sessionLoading,
redeemLoading,
roleConfirmLoading,
loginType,
loginTypeLabel,
loginTabs,
detail,
task,
order,
orderItem,
activityInfo,
hasSession,
qrImage,
qrFigureStyle,
qrPreviewWidth,
statusLabel,
session,
sessionNotice,
roleFacts: claimRoleFacts,
resultFacts,
roleConfirmed,
roleReady,
canConfirmRole,
canRedeem,
redeemBlockedReason,
redeemButtonLabel,
initButtonLabel,
scanInstruction,
screenshotEmptyTitle,
screenshotEmptyMessage,
screenshotUrl,
showScreenshot,
createSessionFlow,
reloadSessionPage,
closeSessionFlow,
refreshSessionSummary,
switchLoginType,
confirmRoleNow,
redeemNow,
handleQrImageLoad,
}
}