452 lines
13 KiB
TypeScript
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,
|
|
}
|
|
}
|