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' import { useSessionPolling, useSessionQrDisplay, useSessionRoleFacts, useSessionResultFacts, buildRedeemBlockedReason, resolveTaskStatusLabel, syncLoginTypeFromDetail, mergeSessionDetail, shouldRefreshQrImage, shouldKeepPolling, DEFAULT_LOGIN_TYPE, } from './shared' export function useAdminManualRedeemPage() { const route = useRoute() const router = useRouter() // ── Form state (admin-only) ──────────────────────────────────────── const form = reactive({ proofValue: '', skuCode: '', remark: '', }) const suggestions = ref([]) // ── Core state ──────────────────────────────────────────────────── const detail = ref(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 loginType = ref(DEFAULT_LOGIN_TYPE) // ── Screenshot state (admin-only) ────────────────────────────────── const screenshotBlob = ref(null) const screenshotUrl = ref('') const screenshotLoading = 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 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 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', ) // ── Polling ──────────────────────────────────────────────────────── const { pollWarningMessage, startPolling, resetPolling, resetPollingWarning, handleSilentPollingError, } = useSessionPolling({ hasSession: computed(() => Boolean(task.value?.taskId && hasSession.value)), shouldKeepPolling: computed(() => shouldKeepPolling(detail.value)), onPollTick: () => 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 loginTabs = [ { value: 'qq' as const, label: 'QQ账号登录' }, { value: 'wx' as const, label: '微信账号登录' }, ] const statusLabel = computed(() => resolveTaskStatusLabel(task.value?.status, session.value?.status, 'admin'), ) const initButtonLabel = computed(() => `生成${loginTypeLabel.value}二维码`) const scanInstruction = computed( () => `请让客户使用${loginTypeLabel.value}扫码,并在手机上确认登录`, ) useSessionRoleFacts({ activityInfo, statusLabel, roleFactDefaults: { nickname: '等待客户登录' }, }) const proofValue = computed(() => manualRequest.value?.proofValue || order.value?.platformOrderId || '-') const { resultFacts } = useSessionResultFacts({ order, orderItem, result, resultFactOverrides: { firstLabel: '唯一凭据', firstValue: proofValue, }, }) // Admin overrides roleFacts to include "所在大区" and exclude "任务状态" const adminRoleFacts = 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 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(() => buildRedeemBlockedReason({ detail, hasSession, loginTypeLabel, redeemLoading, roleReady, roleConfirmed, sessionNotice, context: 'admin', }), ) 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)) // ── Watchers ─────────────────────────────────────────────────────── watch(qrImage, () => { // QR image natural width reset handled by useSessionQrDisplay }) watch( () => currentTaskId.value, (taskId, previousTaskId) => { if (!taskId || taskId === previousTaskId) { return } void loadDetail(taskId) }, { immediate: true }, ) onMounted(() => { void loadSkuSuggestions() }) onBeforeUnmount(() => { resetPolling() clearScreenshotPreview() }) // ── Helpers ──────────────────────────────────────────────────────── function restartPollingIfNeeded(nextDetail = detail.value) { resetPolling() if (shouldKeepPolling(nextDetail)) { startPolling() } } function resolveSkuName(skuCode: string) { const normalized = String(skuCode || '').trim() const matched = suggestions.value.find( (item) => String(item.skuCode || '').trim() === normalized, ) return matched?.skuCode || normalized } // ── Screenshot helpers (admin-only) ─────────────────────────────── 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 = '' } } 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) } } // ── API flows ───────────────────────────────────────────────────── 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 = mergeSessionDetail( detail.value, response.data, (currentSession, nextSession) => ({ review: nextSession.review ?? currentSession.review ?? null, }), ) loginType.value = syncLoginTypeFromDetail(response.data, loginType.value) if (shouldRefreshQrImage(currentSession, response.data.session)) { const fullResponse = await fetchAdminManualRedeemDetail(task.value.taskId!) detail.value = mergeSessionDetail( detail.value, fullResponse.data, (currentSession, nextSession) => ({ review: nextSession.review ?? currentSession.review ?? null, }), ) 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: {}, }) } 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 } 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: adminRoleFacts, 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, } }