增加人工渠道
This commit is contained in:
@@ -0,0 +1,836 @@
|
||||
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import { showConfirm, showSuccess } from '@/lib/feedback'
|
||||
import {
|
||||
closeAdminManualRedeemTask,
|
||||
confirmAdminManualRedeemRole,
|
||||
createAdminManualRedeemSession,
|
||||
createAdminManualRedeemTask,
|
||||
fetchAdminManualRedeemDetail,
|
||||
fetchAdminManualRedeemSessionSummary,
|
||||
fetchAdminManualRedeemSkuSuggestions,
|
||||
fetchAdminTaskScreenshot,
|
||||
refreshAdminManualRedeemSession,
|
||||
redeemAdminManualRedeem,
|
||||
removeAdminManualRedeemSession,
|
||||
} from '@/services/admin'
|
||||
import type { AdminInventorySkuSuggestion, AdminManualRedeemDetail } from '@/types/admin'
|
||||
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 useAdminManualRedeemPage() {
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const form = reactive({
|
||||
proofValue: '',
|
||||
skuCode: '',
|
||||
remark: '',
|
||||
})
|
||||
const suggestions = ref<AdminInventorySkuSuggestion[]>([])
|
||||
const detail = ref<AdminManualRedeemDetail | null>(null)
|
||||
const detailLoading = ref(false)
|
||||
const createLoading = ref(false)
|
||||
const sessionLoading = ref(false)
|
||||
const roleConfirmLoading = ref(false)
|
||||
const redeemLoading = ref(false)
|
||||
const closeLoading = ref(false)
|
||||
const pollWarningMessage = ref('')
|
||||
const qrImageNaturalWidth = ref(0)
|
||||
const screenshotBlob = ref<Blob | null>(null)
|
||||
const screenshotUrl = ref('')
|
||||
const screenshotLoading = ref(false)
|
||||
const loginType = ref<TencentLoginType>(DEFAULT_LOGIN_TYPE)
|
||||
|
||||
let pollTimer: ReturnType<typeof setTimeout> | 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 manualRequest = computed(() => detail.value?.manualRequest || null)
|
||||
const activityInfo = computed(() => session.value?.activityInfo || null)
|
||||
const roleReady = computed(() => Boolean(activityInfo.value?.role?.ready))
|
||||
const roleConfirmed = computed(() =>
|
||||
Boolean(task.value && ['role_confirmed', 'redeeming', 'redeemed'].includes(task.value.status)),
|
||||
)
|
||||
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 currentTaskId = computed(() => Number(task.value?.taskId || route.query.taskId || 0) || 0)
|
||||
const reviewReady = computed(() => Boolean(session.value?.review?.capturedAt) && task.value?.status !== 'redeemed')
|
||||
const resultReady = computed(() => Boolean(result.value?.screenshotReady) && task.value?.status === 'redeemed')
|
||||
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: activityInfo.value?.role?.area || '未识别',
|
||||
},
|
||||
])
|
||||
const resultFacts = computed(() => [
|
||||
{
|
||||
label: '唯一凭据',
|
||||
value: manualRequest.value?.proofValue || 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.status === 'claimed'
|
||||
&& hasSession.value
|
||||
&& roleReady.value
|
||||
&& !roleConfirmLoading.value,
|
||||
),
|
||||
)
|
||||
const canRedeem = computed(() =>
|
||||
Boolean(
|
||||
task.value
|
||||
&& ['role_confirmed', 'redeeming'].includes(task.value.status)
|
||||
&& !redeemLoading.value,
|
||||
),
|
||||
)
|
||||
const redeemBlockedReason = computed(() => {
|
||||
if (!task.value) {
|
||||
return '请先创建人工兑换任务并预占库存'
|
||||
}
|
||||
|
||||
if (!hasSession.value) {
|
||||
return `请先生成${loginTypeLabel.value}二维码并等待客户登录`
|
||||
}
|
||||
|
||||
if (redeemLoading.value) {
|
||||
return '兑换任务正在执行中'
|
||||
}
|
||||
|
||||
if (!roleReady.value) {
|
||||
return sessionNotice.value || '客户登录后,系统会自动同步角色和大区信息'
|
||||
}
|
||||
|
||||
if (!roleConfirmed.value) {
|
||||
return '请先让客户确认角色截图,再点击确认当前角色'
|
||||
}
|
||||
|
||||
return ''
|
||||
})
|
||||
const redeemButtonLabel = computed(() => (redeemLoading.value ? '兑换中...' : '开始兑换'))
|
||||
const screenshotEmptyTitle = computed(() => (task.value?.status === 'redeemed' ? '未获取到结果图' : '等待角色截图'))
|
||||
const screenshotEmptyMessage = computed(() =>
|
||||
task.value?.status === 'redeemed'
|
||||
? '本次兑换可能未生成最终截图,或截图仍在同步中。'
|
||||
: '客户登录并识别到角色后,这里会展示给客服复核的角色截图。'
|
||||
)
|
||||
const canCreateTask = computed(() => Boolean(form.proofValue.trim() && form.skuCode.trim()) && !createLoading.value)
|
||||
const hasActiveTask = computed(() => Boolean(task.value?.taskId))
|
||||
|
||||
watch(qrImage, () => {
|
||||
qrImageNaturalWidth.value = 0
|
||||
})
|
||||
|
||||
watch(
|
||||
() => currentTaskId.value,
|
||||
(taskId, previousTaskId) => {
|
||||
if (!taskId || taskId === previousTaskId) {
|
||||
return
|
||||
}
|
||||
|
||||
void loadDetail(taskId)
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
void loadSkuSuggestions()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
resetPolling()
|
||||
clearScreenshotPreview()
|
||||
})
|
||||
|
||||
async function loadSkuSuggestions(keyword = '') {
|
||||
try {
|
||||
const response = await fetchAdminManualRedeemSkuSuggestions({
|
||||
credentialType: 'tencent_code',
|
||||
keyword: keyword.trim(),
|
||||
})
|
||||
suggestions.value = response.data.items
|
||||
} catch (error) {
|
||||
notifyTencentActionError(error, '读取人工兑换 SKU 建议失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDetail(taskId = currentTaskId.value) {
|
||||
if (!taskId) {
|
||||
detail.value = null
|
||||
clearScreenshotPreview()
|
||||
return false
|
||||
}
|
||||
|
||||
detailLoading.value = true
|
||||
|
||||
try {
|
||||
const response = await fetchAdminManualRedeemDetail(taskId)
|
||||
detail.value = response.data
|
||||
loginType.value = syncLoginTypeFromDetail(response.data, loginType.value)
|
||||
await loadScreenshotPreview(response.data)
|
||||
restartPollingIfNeeded()
|
||||
return true
|
||||
} catch (error) {
|
||||
notifyTencentActionError(error, '读取人工兑换详情失败')
|
||||
return false
|
||||
} finally {
|
||||
detailLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function createTaskNow() {
|
||||
if (!canCreateTask.value) {
|
||||
return
|
||||
}
|
||||
|
||||
createLoading.value = true
|
||||
|
||||
try {
|
||||
const response = await createAdminManualRedeemTask({
|
||||
proofValue: form.proofValue.trim(),
|
||||
skuCode: form.skuCode.trim(),
|
||||
skuName: resolveSkuName(form.skuCode),
|
||||
remark: form.remark.trim(),
|
||||
})
|
||||
|
||||
detail.value = response.data
|
||||
loginType.value = syncLoginTypeFromDetail(response.data, loginType.value)
|
||||
await loadScreenshotPreview(response.data)
|
||||
await router.replace({
|
||||
path: route.path,
|
||||
query: {
|
||||
taskId: String(response.data.task.taskId),
|
||||
},
|
||||
})
|
||||
showSuccess(response.msg || '人工兑换任务已创建')
|
||||
} catch (error) {
|
||||
notifyTencentActionError(error, '创建人工兑换任务失败')
|
||||
} finally {
|
||||
createLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function createSessionFlow(nextLoginType = loginType.value) {
|
||||
if (!task.value?.taskId) {
|
||||
return
|
||||
}
|
||||
|
||||
sessionLoading.value = true
|
||||
resetPolling()
|
||||
resetPollingWarning()
|
||||
|
||||
try {
|
||||
loginType.value = nextLoginType
|
||||
const response = await createAdminManualRedeemSession(task.value.taskId, {
|
||||
loginType: nextLoginType,
|
||||
forceRecreate: hasSession.value,
|
||||
})
|
||||
|
||||
detail.value = response.data
|
||||
loginType.value = syncLoginTypeFromDetail(response.data, nextLoginType)
|
||||
await loadScreenshotPreview(response.data)
|
||||
startPolling()
|
||||
} catch (error) {
|
||||
notifyTencentActionError(error, '创建人工兑换登录会话失败')
|
||||
} finally {
|
||||
sessionLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshSessionFlow() {
|
||||
if (!task.value?.taskId) {
|
||||
return false
|
||||
}
|
||||
|
||||
sessionLoading.value = true
|
||||
resetPolling()
|
||||
resetPollingWarning()
|
||||
|
||||
try {
|
||||
const response = await refreshAdminManualRedeemSession(task.value.taskId)
|
||||
detail.value = response.data
|
||||
loginType.value = syncLoginTypeFromDetail(response.data, loginType.value)
|
||||
await loadScreenshotPreview(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 (!task.value?.taskId) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (!silent) {
|
||||
sessionLoading.value = true
|
||||
}
|
||||
|
||||
resetPolling()
|
||||
resetPollingWarning()
|
||||
|
||||
try {
|
||||
const response = await removeAdminManualRedeemSession(task.value.taskId)
|
||||
detail.value = response.data
|
||||
loginType.value = syncLoginTypeFromDetail(response.data, loginType.value)
|
||||
await loadScreenshotPreview(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 (!task.value?.taskId) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (!silent) {
|
||||
sessionLoading.value = true
|
||||
resetPollingWarning()
|
||||
}
|
||||
|
||||
try {
|
||||
const currentSession = detail.value?.session || null
|
||||
const response = await fetchAdminManualRedeemSessionSummary(task.value.taskId)
|
||||
detail.value = mergeDetail(detail.value, response.data)
|
||||
loginType.value = syncLoginTypeFromDetail(response.data, loginType.value)
|
||||
|
||||
if (shouldRefreshQrImage(currentSession, response.data.session)) {
|
||||
const fullResponse = await fetchAdminManualRedeemDetail(task.value.taskId)
|
||||
detail.value = mergeDetail(detail.value, fullResponse.data)
|
||||
loginType.value = syncLoginTypeFromDetail(fullResponse.data, loginType.value)
|
||||
}
|
||||
|
||||
await loadScreenshotPreview(detail.value)
|
||||
|
||||
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 (!task.value?.taskId || !canConfirmRole.value) {
|
||||
return
|
||||
}
|
||||
|
||||
roleConfirmLoading.value = true
|
||||
|
||||
try {
|
||||
const response = await confirmAdminManualRedeemRole(task.value.taskId)
|
||||
detail.value = response.data
|
||||
await loadScreenshotPreview(response.data)
|
||||
showSuccess(response.msg || '角色已确认')
|
||||
restartPollingIfNeeded(response.data)
|
||||
} catch (error) {
|
||||
notifyTencentActionError(error, '确认角色失败')
|
||||
await refreshSessionSummary({ silent: true })
|
||||
} finally {
|
||||
roleConfirmLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function redeemNow() {
|
||||
if (!task.value?.taskId || !canRedeem.value) {
|
||||
return
|
||||
}
|
||||
|
||||
redeemLoading.value = true
|
||||
resetPolling()
|
||||
resetPollingWarning()
|
||||
|
||||
try {
|
||||
const response = await redeemAdminManualRedeem(task.value.taskId)
|
||||
detail.value = response.data
|
||||
await loadScreenshotPreview(response.data)
|
||||
showSuccess(response.msg || '兑换完成')
|
||||
restartPollingIfNeeded(response.data)
|
||||
} catch (error) {
|
||||
notifyTencentActionError(error, '人工兑换失败')
|
||||
await refreshSessionSummary({ silent: true })
|
||||
restartPollingIfNeeded()
|
||||
} finally {
|
||||
redeemLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function closeTaskNow() {
|
||||
if (!task.value?.taskId) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await showConfirm(
|
||||
`确认关闭任务 ${task.value.taskNo} 吗?关闭后会释放未消耗的资源,并结束当前人工兑换流程。`,
|
||||
'关闭人工兑换',
|
||||
)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
closeLoading.value = true
|
||||
|
||||
try {
|
||||
const response = await closeAdminManualRedeemTask(task.value.taskId)
|
||||
detail.value = response.data
|
||||
await loadScreenshotPreview(response.data)
|
||||
resetPolling()
|
||||
showSuccess(response.msg || '人工兑换任务已关闭')
|
||||
} catch (error) {
|
||||
notifyTencentActionError(error, '关闭人工兑换任务失败')
|
||||
} finally {
|
||||
closeLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function switchLoginType(nextLoginType: TencentLoginType) {
|
||||
if (nextLoginType === loginType.value) {
|
||||
return
|
||||
}
|
||||
|
||||
if (hasSession.value) {
|
||||
await createSessionFlow(nextLoginType)
|
||||
return
|
||||
}
|
||||
|
||||
loginType.value = nextLoginType
|
||||
}
|
||||
|
||||
function resetCurrentTask() {
|
||||
resetPolling()
|
||||
detail.value = null
|
||||
clearScreenshotPreview()
|
||||
void router.replace({
|
||||
path: route.path,
|
||||
query: {},
|
||||
})
|
||||
}
|
||||
|
||||
function handleQrImageLoad(event: Event) {
|
||||
const target = event.target
|
||||
|
||||
if (!(target instanceof HTMLImageElement)) {
|
||||
return
|
||||
}
|
||||
|
||||
qrImageNaturalWidth.value = target.naturalWidth || 0
|
||||
}
|
||||
|
||||
async function copyQrImage() {
|
||||
if (!qrImage.value) {
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(qrImage.value)
|
||||
const blob = await response.blob()
|
||||
await writeImageBlobToClipboard(blob, '复制二维码失败')
|
||||
showSuccess('二维码已复制到剪贴板')
|
||||
return true
|
||||
} catch (error) {
|
||||
notifyTencentActionError(error, '复制二维码失败')
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function copyScreenshot() {
|
||||
if (!screenshotBlob.value) {
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
await writeImageBlobToClipboard(screenshotBlob.value, '复制截图失败')
|
||||
showSuccess('截图已复制到剪贴板')
|
||||
return true
|
||||
} catch (error) {
|
||||
notifyTencentActionError(error, '复制截图失败')
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function downloadScreenshot() {
|
||||
if (!screenshotBlob.value || !screenshotUrl.value) {
|
||||
return false
|
||||
}
|
||||
|
||||
const anchor = document.createElement('a')
|
||||
anchor.href = screenshotUrl.value
|
||||
anchor.download = task.value?.status === 'redeemed'
|
||||
? `manual-redeem-result-${task.value?.taskNo || 'task'}.png`
|
||||
: `manual-redeem-review-${task.value?.taskNo || 'task'}.png`
|
||||
anchor.click()
|
||||
return true
|
||||
}
|
||||
|
||||
async function writeImageBlobToClipboard(blob: Blob, fallbackMessage: string) {
|
||||
if (
|
||||
typeof navigator === 'undefined'
|
||||
|| !navigator.clipboard
|
||||
|| typeof window === 'undefined'
|
||||
|| !('ClipboardItem' in window)
|
||||
) {
|
||||
throw new Error('当前浏览器不支持直接复制图片,请改用截图发送或下载')
|
||||
}
|
||||
|
||||
try {
|
||||
const ClipboardItemCtor = window.ClipboardItem as typeof ClipboardItem
|
||||
await navigator.clipboard.write([
|
||||
new ClipboardItemCtor({
|
||||
[blob.type || 'image/png']: blob,
|
||||
}),
|
||||
])
|
||||
} catch (error) {
|
||||
throw error instanceof Error ? error : new Error(fallbackMessage)
|
||||
}
|
||||
}
|
||||
|
||||
async function loadScreenshotPreview(nextDetail = detail.value) {
|
||||
const taskId = Number(nextDetail?.task?.taskId || 0)
|
||||
const shouldFetch = Boolean(taskId && (nextDetail?.session?.review?.capturedAt || nextDetail?.result?.screenshotReady))
|
||||
|
||||
if (!shouldFetch) {
|
||||
clearScreenshotPreview()
|
||||
return
|
||||
}
|
||||
|
||||
screenshotLoading.value = true
|
||||
|
||||
try {
|
||||
const blob = await fetchAdminTaskScreenshot(taskId)
|
||||
clearScreenshotPreview()
|
||||
screenshotBlob.value = blob
|
||||
screenshotUrl.value = URL.createObjectURL(blob)
|
||||
} catch {
|
||||
clearScreenshotPreview()
|
||||
} finally {
|
||||
screenshotLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function clearScreenshotPreview() {
|
||||
screenshotBlob.value = null
|
||||
|
||||
if (screenshotUrl.value) {
|
||||
URL.revokeObjectURL(screenshotUrl.value)
|
||||
screenshotUrl.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
function resolveSkuName(skuCode: string) {
|
||||
const normalized = String(skuCode || '').trim()
|
||||
const matched = suggestions.value.find((item) => String(item.skuCode || '').trim() === normalized)
|
||||
return matched?.skuCode || normalized
|
||||
}
|
||||
|
||||
function startPolling() {
|
||||
const tokenId = ++pollToken
|
||||
|
||||
const loop = async () => {
|
||||
if (tokenId !== pollToken || !task.value?.taskId) {
|
||||
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()
|
||||
}
|
||||
|
||||
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`)
|
||||
|
||||
return {
|
||||
form,
|
||||
suggestions,
|
||||
detail,
|
||||
detailLoading,
|
||||
createLoading,
|
||||
sessionLoading,
|
||||
roleConfirmLoading,
|
||||
redeemLoading,
|
||||
closeLoading,
|
||||
loginType,
|
||||
loginTypeLabel,
|
||||
loginTabs,
|
||||
session,
|
||||
task,
|
||||
order,
|
||||
orderItem,
|
||||
manualRequest,
|
||||
activityInfo,
|
||||
roleReady,
|
||||
roleConfirmed,
|
||||
hasSession,
|
||||
qrImage,
|
||||
qrFigureStyle,
|
||||
qrPreviewWidth,
|
||||
statusLabel,
|
||||
sessionNotice,
|
||||
roleFacts,
|
||||
resultFacts,
|
||||
canConfirmRole,
|
||||
canRedeem,
|
||||
redeemBlockedReason,
|
||||
redeemButtonLabel,
|
||||
initButtonLabel,
|
||||
scanInstruction,
|
||||
screenshotEmptyTitle,
|
||||
screenshotEmptyMessage,
|
||||
screenshotUrl,
|
||||
screenshotLoading,
|
||||
reviewReady,
|
||||
resultReady,
|
||||
canCreateTask,
|
||||
hasActiveTask,
|
||||
createTaskNow,
|
||||
loadSkuSuggestions,
|
||||
createSessionFlow,
|
||||
refreshSessionFlow,
|
||||
closeSessionFlow,
|
||||
switchLoginType,
|
||||
confirmRoleNow,
|
||||
redeemNow,
|
||||
closeTaskNow,
|
||||
resetCurrentTask,
|
||||
handleQrImageLoad,
|
||||
copyQrImage,
|
||||
copyScreenshot,
|
||||
downloadScreenshot,
|
||||
}
|
||||
}
|
||||
|
||||
function syncLoginTypeFromDetail(detail: AdminManualRedeemDetail, fallback: TencentLoginType = DEFAULT_LOGIN_TYPE) {
|
||||
const nextLoginType = String(detail.session?.loginType || detail.task.loginType || '').trim()
|
||||
if (nextLoginType === 'wx') {
|
||||
return 'wx'
|
||||
}
|
||||
|
||||
if (nextLoginType === 'qq') {
|
||||
return 'qq'
|
||||
}
|
||||
|
||||
return fallback
|
||||
}
|
||||
|
||||
function shouldKeepPolling(detail: AdminManualRedeemDetail | null) {
|
||||
if (!detail?.task?.taskId || !detail.session?.sessionId) {
|
||||
return false
|
||||
}
|
||||
|
||||
return ACTIVE_TASK_STATUSES.has(String(detail.task.status || '').trim())
|
||||
}
|
||||
|
||||
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 'closed':
|
||||
return '任务已关闭'
|
||||
default:
|
||||
return sessionStatus || taskStatus || '等待中'
|
||||
}
|
||||
}
|
||||
|
||||
function mergeDetail(current: AdminManualRedeemDetail | null, next: AdminManualRedeemDetail) {
|
||||
if (next.session === null) {
|
||||
return {
|
||||
...next,
|
||||
session: null,
|
||||
}
|
||||
}
|
||||
|
||||
if (!current?.session) {
|
||||
return next
|
||||
}
|
||||
|
||||
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,
|
||||
review: next.session.review ?? current.session.review ?? null,
|
||||
redeem: next.session.redeem ?? current.session.redeem ?? null,
|
||||
artifacts: next.session.artifacts || current.session.artifacts,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function shouldRefreshQrImage(currentSession: AdminManualRedeemDetail['session'], nextSession: AdminManualRedeemDetail['session']) {
|
||||
if (!currentSession || !nextSession || !nextSession.artifacts?.hasQrImage) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (!currentSession.qrImageBase64) {
|
||||
return true
|
||||
}
|
||||
|
||||
return Boolean(nextSession.qrUpdatedAt && nextSession.qrUpdatedAt !== currentSession.qrUpdatedAt)
|
||||
}
|
||||
Reference in New Issue
Block a user