前面的ok了, 绑定与角色
This commit is contained in:
@@ -0,0 +1,357 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
import TencentAuthCard from '@/components/tencent/TencentAuthCard.vue'
|
||||
import TencentRedeemPanel from '@/components/tencent/TencentRedeemPanel.vue'
|
||||
import TencentResultPanel from '@/components/tencent/TencentResultPanel.vue'
|
||||
import { useClaimPage } from '@/composables/useClaimPage'
|
||||
|
||||
const route = useRoute()
|
||||
const token = computed(() => String(route.params.token || '').trim())
|
||||
|
||||
const {
|
||||
detailLoading,
|
||||
roleConfirmLoading,
|
||||
sessionLoading,
|
||||
redeemLoading,
|
||||
loginType,
|
||||
loginTypeLabel,
|
||||
loginTabs,
|
||||
task,
|
||||
order,
|
||||
orderItem,
|
||||
hasSession,
|
||||
qrImage,
|
||||
qrFigureStyle,
|
||||
qrPreviewWidth,
|
||||
statusLabel,
|
||||
session,
|
||||
sessionNotice,
|
||||
roleFacts,
|
||||
resultFacts,
|
||||
roleConfirmed,
|
||||
roleReady,
|
||||
canConfirmRole,
|
||||
canRedeem,
|
||||
redeemBlockedReason,
|
||||
redeemButtonLabel,
|
||||
initButtonLabel,
|
||||
scanInstruction,
|
||||
screenshotEmptyTitle,
|
||||
screenshotEmptyMessage,
|
||||
screenshotUrl,
|
||||
showScreenshot,
|
||||
createSessionFlow,
|
||||
reloadSessionPage,
|
||||
closeSessionFlow,
|
||||
switchLoginType,
|
||||
confirmRoleNow,
|
||||
redeemNow,
|
||||
handleQrImageLoad,
|
||||
} = useClaimPage(token.value)
|
||||
|
||||
const qrPreviewVisible = ref(false)
|
||||
const screenshotPreviewVisible = ref(false)
|
||||
|
||||
const currentLoginTabLabel = computed(
|
||||
() => loginTabs.find((tab) => tab.value === loginType.value)?.label ?? loginTabs[0].label,
|
||||
)
|
||||
|
||||
const helperText = computed(() => {
|
||||
if (!task.value) {
|
||||
return '正在加载领取任务'
|
||||
}
|
||||
|
||||
if (task.value.requiresSupportReview) {
|
||||
return `订单 ${order.value?.platformOrderId || '-'} · 登录后请联系人工客服继续确认和兑换`
|
||||
}
|
||||
|
||||
return `订单 ${order.value?.platformOrderId || '-'} · 任务 ${task.value.taskNo}`
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="claim-page">
|
||||
<section class="workspace">
|
||||
<div class="page-toolbar">
|
||||
<div>
|
||||
<div class="brand-chip">订单领取</div>
|
||||
<p class="helper-copy">{{ helperText }}</p>
|
||||
</div>
|
||||
<div class="toolbar-actions">
|
||||
<div class="status-badge">{{ statusLabel }}</div>
|
||||
<RouterLink class="back-link" to="/tx/browser">调试页</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="order-card">
|
||||
<div class="order-meta">
|
||||
<article>
|
||||
<span>订单号</span>
|
||||
<strong>{{ order?.platformOrderId || '-' }}</strong>
|
||||
</article>
|
||||
<article>
|
||||
<span>商品</span>
|
||||
<strong>{{ orderItem?.skuName || '-' }}</strong>
|
||||
</article>
|
||||
<article>
|
||||
<span>数量</span>
|
||||
<strong>{{ orderItem?.quantity || 0 }}</strong>
|
||||
</article>
|
||||
<article>
|
||||
<span>任务号</span>
|
||||
<strong>{{ task?.taskNo || '-' }}</strong>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div v-if="detailLoading" class="empty-block loading-block">
|
||||
<strong>领取信息加载中</strong>
|
||||
<p>正在校验链接并同步任务状态。</p>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div class="layout-grid">
|
||||
<TencentAuthCard
|
||||
:current-login-tab-label="currentLoginTabLabel"
|
||||
:has-session="hasSession"
|
||||
:init-button-label="initButtonLabel"
|
||||
:login-tabs="loginTabs"
|
||||
:login-type="loginType"
|
||||
:login-type-label="loginTypeLabel"
|
||||
:qr-figure-style="qrFigureStyle"
|
||||
:qr-image="qrImage"
|
||||
:scan-instruction="scanInstruction"
|
||||
:session-status="session?.status || ''"
|
||||
:session-id="session?.sessionId || ''"
|
||||
:session-loading="sessionLoading"
|
||||
:session-notice="sessionNotice"
|
||||
@create-session="createSessionFlow"
|
||||
@close-session="closeSessionFlow"
|
||||
@open-qr-preview="qrPreviewVisible = true"
|
||||
@qr-image-load="handleQrImageLoad"
|
||||
@reload-session="reloadSessionPage"
|
||||
@switch-login-type="switchLoginType"
|
||||
/>
|
||||
|
||||
<div class="control-column">
|
||||
<TencentRedeemPanel
|
||||
:can-redeem="canRedeem"
|
||||
:can-confirm-role="canConfirmRole"
|
||||
confirm-action-label="确认当前角色并继续"
|
||||
:confirm-action-loading="roleConfirmLoading"
|
||||
:confirm-action-visible="!roleConfirmed && !task?.requiresSupportReview"
|
||||
:confirm-readonly="true"
|
||||
:hide-redeem-form="true"
|
||||
:login-type-label="loginTypeLabel"
|
||||
:max-attempts="6"
|
||||
:redeem-blocked-reason="redeemBlockedReason"
|
||||
:redeem-button-label="redeemButtonLabel"
|
||||
:redeem-code="''"
|
||||
:redeem-loading="redeemLoading"
|
||||
:role-confirmed="roleConfirmed"
|
||||
:role-facts="roleFacts"
|
||||
:role-ready="roleReady"
|
||||
:status-label="statusLabel"
|
||||
@confirm-role="confirmRoleNow"
|
||||
@redeem="redeemNow"
|
||||
@update:max-attempts="() => undefined"
|
||||
@update:redeem-code="() => undefined"
|
||||
@update:role-confirmed="() => undefined"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TencentResultPanel
|
||||
:result-facts="resultFacts"
|
||||
:screenshot-empty-message="screenshotEmptyMessage"
|
||||
:screenshot-empty-title="screenshotEmptyTitle"
|
||||
:screenshot-url="screenshotUrl"
|
||||
:show-screenshot="showScreenshot"
|
||||
@open-screenshot-preview="screenshotPreviewVisible = true"
|
||||
/>
|
||||
</template>
|
||||
</section>
|
||||
|
||||
<el-dialog
|
||||
v-model="qrPreviewVisible"
|
||||
align-center
|
||||
class="preview-dialog"
|
||||
:width="qrPreviewWidth"
|
||||
>
|
||||
<div class="qr-preview-body">
|
||||
<img
|
||||
class="preview-image qr-preview-image"
|
||||
:src="qrImage"
|
||||
:alt="`Claim ${loginTypeLabel} QR Preview`"
|
||||
/>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
v-model="screenshotPreviewVisible"
|
||||
align-center
|
||||
class="preview-dialog"
|
||||
width="1080px"
|
||||
>
|
||||
<img class="preview-image" :src="screenshotUrl" alt="Claim Screenshot Preview" />
|
||||
</el-dialog>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.claim-page {
|
||||
min-height: 100vh;
|
||||
padding: 24px;
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(73, 136, 255, 0.12), transparent 32%),
|
||||
radial-gradient(circle at top right, rgba(87, 194, 150, 0.12), transparent 28%),
|
||||
linear-gradient(180deg, #f4f8fc 0%, #eef4fb 100%);
|
||||
}
|
||||
|
||||
.workspace {
|
||||
max-width: 1240px;
|
||||
margin: 0 auto;
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.page-toolbar,
|
||||
.toolbar-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.brand-chip,
|
||||
.status-badge,
|
||||
.back-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 42px;
|
||||
padding: 0 18px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
border: 1px solid rgba(86, 108, 138, 0.12);
|
||||
color: #27405e;
|
||||
box-shadow: 0 12px 32px rgba(30, 49, 78, 0.08);
|
||||
}
|
||||
|
||||
.brand-chip {
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.back-link {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.helper-copy {
|
||||
margin: 8px 0 0;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.order-card {
|
||||
padding: 18px 22px;
|
||||
border-radius: 28px;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border: 1px solid rgba(148, 163, 184, 0.18);
|
||||
box-shadow: 0 22px 60px rgba(30, 41, 59, 0.08);
|
||||
}
|
||||
|
||||
.order-meta {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.order-meta article {
|
||||
padding: 14px 16px;
|
||||
border-radius: 20px;
|
||||
background: linear-gradient(180deg, rgba(248, 250, 252, 0.9), rgba(241, 245, 249, 0.92));
|
||||
border: 1px solid rgba(148, 163, 184, 0.16);
|
||||
}
|
||||
|
||||
.order-meta span {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.order-meta strong {
|
||||
font-size: 16px;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.layout-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.35fr) minmax(300px, 0.95fr);
|
||||
gap: 18px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.control-column {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.empty-block {
|
||||
padding: 28px;
|
||||
border-radius: 28px;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border: 1px solid rgba(148, 163, 184, 0.18);
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.loading-block strong {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.preview-dialog :deep(.el-dialog__body) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.preview-image {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.qr-preview-body {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 20px;
|
||||
background: linear-gradient(180deg, #f8fbff 0%, #eef5ff 100%);
|
||||
}
|
||||
|
||||
.qr-preview-image {
|
||||
max-width: 100%;
|
||||
max-height: min(78vh, 920px);
|
||||
}
|
||||
|
||||
@media (max-width: 980px) {
|
||||
.claim-page {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.page-toolbar,
|
||||
.toolbar-actions {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.order-meta,
|
||||
.layout-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,374 +1,89 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
import TencentAuthCard from '@/components/tencent/TencentAuthCard.vue'
|
||||
import TencentRedeemPanel from '@/components/tencent/TencentRedeemPanel.vue'
|
||||
import TencentResultPanel from '@/components/tencent/TencentResultPanel.vue'
|
||||
import { useClaimPage } from '@/composables/useClaimPage'
|
||||
import { fetchClaimDetail } from '@/services/claim'
|
||||
|
||||
import ClaimTencentView from './ClaimTencentView.vue'
|
||||
import KuaishouCloudClaimView from './KuaishouCloudClaimView.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const token = computed(() => String(route.params.token || '').trim())
|
||||
const loading = ref(true)
|
||||
const errorMessage = ref('')
|
||||
const flowType = ref('tencent_claim')
|
||||
|
||||
const {
|
||||
detailLoading,
|
||||
roleConfirmLoading,
|
||||
sessionLoading,
|
||||
redeemLoading,
|
||||
loginType,
|
||||
loginTypeLabel,
|
||||
loginTabs,
|
||||
task,
|
||||
order,
|
||||
orderItem,
|
||||
hasSession,
|
||||
qrImage,
|
||||
qrFigureStyle,
|
||||
qrPreviewWidth,
|
||||
statusLabel,
|
||||
session,
|
||||
sessionNotice,
|
||||
roleFacts,
|
||||
resultFacts,
|
||||
roleConfirmed,
|
||||
roleReady,
|
||||
canConfirmRole,
|
||||
canRedeem,
|
||||
redeemBlockedReason,
|
||||
redeemButtonLabel,
|
||||
initButtonLabel,
|
||||
scanInstruction,
|
||||
screenshotEmptyTitle,
|
||||
screenshotEmptyMessage,
|
||||
screenshotUrl,
|
||||
showScreenshot,
|
||||
createSessionFlow,
|
||||
reloadSessionPage,
|
||||
closeSessionFlow,
|
||||
switchLoginType,
|
||||
confirmRoleNow,
|
||||
redeemNow,
|
||||
handleQrImageLoad,
|
||||
} = useClaimPage(token.value)
|
||||
async function bootstrap() {
|
||||
loading.value = true
|
||||
errorMessage.value = ''
|
||||
|
||||
const qrPreviewVisible = ref(false)
|
||||
const screenshotPreviewVisible = ref(false)
|
||||
try {
|
||||
const response = await fetchClaimDetail(token.value)
|
||||
flowType.value = String(response.data.flowType || response.data.task.executorKey || 'tencent_claim').trim()
|
||||
} catch (error) {
|
||||
errorMessage.value = error instanceof Error ? error.message : '读取领取信息失败'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const currentLoginTabLabel = computed(
|
||||
() => loginTabs.find((tab) => tab.value === loginType.value)?.label ?? loginTabs[0].label,
|
||||
const isKuaishouCloudFlow = computed(
|
||||
() => flowType.value === 'kuaishou_cloud' || flowType.value === 'kuaishou_ct_assisted',
|
||||
)
|
||||
|
||||
const helperText = computed(() => {
|
||||
if (!task.value) {
|
||||
return '正在加载领取任务'
|
||||
}
|
||||
|
||||
if (task.value.requiresSupportReview) {
|
||||
return `订单 ${order.value?.platformOrderId || '-'} · 登录后请联系人工客服继续确认和兑换`
|
||||
}
|
||||
|
||||
return `订单 ${order.value?.platformOrderId || '-'} · 任务 ${task.value.taskNo}`
|
||||
onMounted(() => {
|
||||
void bootstrap()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="claim-page">
|
||||
<section class="workspace">
|
||||
<div class="page-toolbar">
|
||||
<div>
|
||||
<div class="brand-chip">订单领取</div>
|
||||
<p class="helper-copy">{{ helperText }}</p>
|
||||
</div>
|
||||
<div class="toolbar-actions">
|
||||
<div class="status-badge">{{ statusLabel }}</div>
|
||||
<RouterLink class="back-link" to="/tx/browser">调试页</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="order-card">
|
||||
<div class="order-meta">
|
||||
<article>
|
||||
<span>订单号</span>
|
||||
<strong>{{ order?.platformOrderId || '-' }}</strong>
|
||||
</article>
|
||||
<article>
|
||||
<span>商品</span>
|
||||
<strong>{{ orderItem?.skuName || '-' }}</strong>
|
||||
</article>
|
||||
<article>
|
||||
<span>数量</span>
|
||||
<strong>{{ orderItem?.quantity || 0 }}</strong>
|
||||
</article>
|
||||
<article>
|
||||
<span>任务号</span>
|
||||
<strong>{{ task?.taskNo || '-' }}</strong>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div v-if="detailLoading" class="empty-block loading-block">
|
||||
<strong>领取信息加载中</strong>
|
||||
<p>正在校验链接并同步任务状态。</p>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div class="layout-grid">
|
||||
<TencentAuthCard
|
||||
:current-login-tab-label="currentLoginTabLabel"
|
||||
:has-session="hasSession"
|
||||
:init-button-label="initButtonLabel"
|
||||
:login-tabs="loginTabs"
|
||||
:login-type="loginType"
|
||||
:login-type-label="loginTypeLabel"
|
||||
:qr-figure-style="qrFigureStyle"
|
||||
:qr-image="qrImage"
|
||||
:scan-instruction="scanInstruction"
|
||||
:session-status="session?.status || ''"
|
||||
:session-id="session?.sessionId || ''"
|
||||
:session-loading="sessionLoading"
|
||||
:session-notice="sessionNotice"
|
||||
@create-session="createSessionFlow"
|
||||
@close-session="closeSessionFlow"
|
||||
@open-qr-preview="qrPreviewVisible = true"
|
||||
@qr-image-load="handleQrImageLoad"
|
||||
@reload-session="reloadSessionPage"
|
||||
@switch-login-type="switchLoginType"
|
||||
/>
|
||||
|
||||
<div class="control-column">
|
||||
<TencentRedeemPanel
|
||||
:can-redeem="canRedeem"
|
||||
:can-confirm-role="canConfirmRole"
|
||||
confirm-action-label="确认当前角色并继续"
|
||||
:confirm-action-loading="roleConfirmLoading"
|
||||
:confirm-action-visible="!roleConfirmed && !task?.requiresSupportReview"
|
||||
:confirm-readonly="true"
|
||||
:hide-redeem-form="true"
|
||||
:login-type-label="loginTypeLabel"
|
||||
:max-attempts="6"
|
||||
:redeem-blocked-reason="redeemBlockedReason"
|
||||
:redeem-button-label="redeemButtonLabel"
|
||||
:redeem-code="''"
|
||||
:redeem-loading="redeemLoading"
|
||||
:role-confirmed="roleConfirmed"
|
||||
:role-facts="roleFacts"
|
||||
:role-ready="roleReady"
|
||||
:status-label="statusLabel"
|
||||
@confirm-role="confirmRoleNow"
|
||||
@redeem="redeemNow"
|
||||
@update:max-attempts="() => undefined"
|
||||
@update:redeem-code="() => undefined"
|
||||
@update:role-confirmed="() => undefined"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TencentResultPanel
|
||||
:result-facts="resultFacts"
|
||||
:screenshot-empty-message="screenshotEmptyMessage"
|
||||
:screenshot-empty-title="screenshotEmptyTitle"
|
||||
:screenshot-url="screenshotUrl"
|
||||
:show-screenshot="showScreenshot"
|
||||
@open-screenshot-preview="screenshotPreviewVisible = true"
|
||||
/>
|
||||
</template>
|
||||
<main v-if="loading" class="claim-shell loading-shell">
|
||||
<section class="shell-card">
|
||||
<strong>页面加载中</strong>
|
||||
<p>正在识别领取流程类型。</p>
|
||||
</section>
|
||||
|
||||
<el-dialog
|
||||
v-model="qrPreviewVisible"
|
||||
align-center
|
||||
class="preview-dialog"
|
||||
:width="qrPreviewWidth"
|
||||
>
|
||||
<div class="qr-preview-body">
|
||||
<img
|
||||
class="preview-image qr-preview-image"
|
||||
:src="qrImage"
|
||||
:alt="`Claim ${loginTypeLabel} QR Preview`"
|
||||
/>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
v-model="screenshotPreviewVisible"
|
||||
align-center
|
||||
class="preview-dialog"
|
||||
width="1080px"
|
||||
>
|
||||
<img class="preview-image" :src="screenshotUrl" alt="Claim Screenshot Preview" />
|
||||
</el-dialog>
|
||||
</main>
|
||||
|
||||
<main v-else-if="errorMessage" class="claim-shell error-shell">
|
||||
<section class="shell-card">
|
||||
<strong>无法打开领取页</strong>
|
||||
<p>{{ errorMessage }}</p>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<KuaishouCloudClaimView v-else-if="isKuaishouCloudFlow" :token="token" />
|
||||
<ClaimTencentView v-else />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.claim-page {
|
||||
.claim-shell {
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 24px;
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(73, 136, 255, 0.12), transparent 32%),
|
||||
radial-gradient(circle at top right, rgba(87, 194, 150, 0.12), transparent 28%),
|
||||
linear-gradient(180deg, #f4f8fc 0%, #eef4fb 100%);
|
||||
radial-gradient(circle at top left, rgba(74, 222, 128, 0.12), transparent 28%),
|
||||
linear-gradient(180deg, #f8fafc 0%, #eef4fb 100%);
|
||||
}
|
||||
|
||||
.workspace {
|
||||
max-width: 1240px;
|
||||
margin: 0 auto;
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.page-toolbar,
|
||||
.toolbar-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.brand-chip,
|
||||
.status-badge,
|
||||
.back-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 42px;
|
||||
padding: 0 18px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
border: 1px solid rgba(86, 108, 138, 0.12);
|
||||
color: #27405e;
|
||||
box-shadow: 0 12px 32px rgba(30, 49, 78, 0.08);
|
||||
}
|
||||
|
||||
.brand-chip {
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.back-link {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.helper-copy {
|
||||
margin: 8px 0 0;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.order-card {
|
||||
padding: 18px 22px;
|
||||
.shell-card {
|
||||
width: min(520px, 100%);
|
||||
padding: 28px;
|
||||
border-radius: 28px;
|
||||
background: rgba(255, 255, 255, 0.94);
|
||||
border: 1px solid rgba(86, 108, 138, 0.1);
|
||||
box-shadow: 0 22px 70px rgba(30, 49, 78, 0.08);
|
||||
}
|
||||
|
||||
.order-meta {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.order-meta article {
|
||||
padding: 14px;
|
||||
border-radius: 16px;
|
||||
background: #f7f9fc;
|
||||
}
|
||||
|
||||
.order-meta span {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: #708096;
|
||||
}
|
||||
|
||||
.order-meta strong {
|
||||
display: block;
|
||||
margin-top: 6px;
|
||||
color: #1f324a;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.layout-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 420px) minmax(0, 1fr);
|
||||
gap: 18px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.control-column {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.empty-block {
|
||||
min-height: 220px;
|
||||
border-radius: 18px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
border: 1px solid rgba(148, 163, 184, 0.16);
|
||||
box-shadow: 0 24px 70px rgba(15, 23, 42, 0.08);
|
||||
text-align: center;
|
||||
padding: 24px;
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
border: 1px dashed #d5dfec;
|
||||
}
|
||||
|
||||
.loading-block strong {
|
||||
color: #2c4058;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.loading-block p {
|
||||
margin: 8px 0 0;
|
||||
color: #6b7a91;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.preview-image {
|
||||
.shell-card strong {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
margin-bottom: 10px;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
:deep(.preview-dialog .el-dialog) {
|
||||
border-radius: 28px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:deep(.preview-dialog .el-dialog__body) {
|
||||
padding: 0;
|
||||
background: #f5f7fb;
|
||||
}
|
||||
|
||||
.qr-preview-body {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.qr-preview-image {
|
||||
width: auto;
|
||||
max-width: 100%;
|
||||
image-rendering: -moz-crisp-edges;
|
||||
image-rendering: crisp-edges;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
@media (max-width: 1140px) {
|
||||
.layout-grid,
|
||||
.order-meta {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.claim-page {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.page-toolbar,
|
||||
.toolbar-actions {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.shell-card p {
|
||||
margin: 0;
|
||||
color: #475569;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,487 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
|
||||
import { showError, showSuccess } from '@/lib/feedback'
|
||||
import { fetchClaimDetail, verifyKuaishouCloudClaimTicket } from '@/services/claim'
|
||||
import type { ClaimDetailData } from '@/types/claim'
|
||||
|
||||
const props = defineProps<{
|
||||
token: string
|
||||
}>()
|
||||
|
||||
const loading = ref(true)
|
||||
const submitting = ref(false)
|
||||
const errorMessage = ref('')
|
||||
const detail = ref<ClaimDetailData | null>(null)
|
||||
const ticketCode = ref('')
|
||||
const expandedPanels = ref(['guide'])
|
||||
let pollTimer = 0
|
||||
|
||||
const flow = computed(() => detail.value?.kuaishouCloudFulfillment || null)
|
||||
const order = computed(() => detail.value?.order || null)
|
||||
const orderItem = computed(() => detail.value?.orderItem || null)
|
||||
const task = computed(() => detail.value?.task || null)
|
||||
const isCompleted = computed(() => flow.value?.consume.status === 'success' || task.value?.status === 'completed')
|
||||
const isWaitingSupport = computed(
|
||||
() => flow.value?.ticket.status === 'verified' && !isCompleted.value && flow.value?.dispatch.status !== 'success',
|
||||
)
|
||||
const canSubmitTicket = computed(() => {
|
||||
if (!flow.value?.binding.bindUrl) {
|
||||
return false
|
||||
}
|
||||
|
||||
return !['completed', 'manual_review', 'closed', 'expired'].includes(String(task.value?.status || '').trim())
|
||||
})
|
||||
const bindButtonLabel = computed(() => {
|
||||
if (flow.value?.ticket.status === 'verified') {
|
||||
return '继续打开绑定链接'
|
||||
}
|
||||
|
||||
return '校验后继续绑定'
|
||||
})
|
||||
const pageStatusText = computed(() => {
|
||||
if (isCompleted.value) {
|
||||
return '流程已完成'
|
||||
}
|
||||
|
||||
if (flow.value?.consume.status === 'failed') {
|
||||
return '核销异常'
|
||||
}
|
||||
|
||||
if (flow.value?.dispatch.status === 'success') {
|
||||
return '已发货'
|
||||
}
|
||||
|
||||
if (flow.value?.ticket.status === 'verified') {
|
||||
return '等待客服确认'
|
||||
}
|
||||
|
||||
if (flow.value?.binding.prepareStatus === 'ready') {
|
||||
return '等待填写核销码'
|
||||
}
|
||||
|
||||
return '等待系统准备'
|
||||
})
|
||||
|
||||
async function loadDetail(options: { silent?: boolean } = {}) {
|
||||
if (!options.silent) {
|
||||
loading.value = true
|
||||
}
|
||||
|
||||
errorMessage.value = ''
|
||||
|
||||
try {
|
||||
const response = await fetchClaimDetail(props.token)
|
||||
detail.value = response.data
|
||||
if (!ticketCode.value && response.data.kuaishouCloudFulfillment?.ticket.code) {
|
||||
ticketCode.value = response.data.kuaishouCloudFulfillment.ticket.code
|
||||
}
|
||||
syncPolling()
|
||||
} catch (error) {
|
||||
errorMessage.value = error instanceof Error ? error.message : '读取领取信息失败'
|
||||
stopPolling()
|
||||
} finally {
|
||||
if (!options.silent) {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function submitTicket() {
|
||||
const normalizedTicketCode = ticketCode.value.trim()
|
||||
if (!normalizedTicketCode) {
|
||||
showError('请先粘贴快手小店核销码')
|
||||
return
|
||||
}
|
||||
|
||||
submitting.value = true
|
||||
|
||||
try {
|
||||
const response = await verifyKuaishouCloudClaimTicket(props.token, {
|
||||
ticketCode: normalizedTicketCode,
|
||||
})
|
||||
detail.value = response.data
|
||||
ticketCode.value = response.data.kuaishouCloudFulfillment?.ticket.code || normalizedTicketCode
|
||||
showSuccess('核销码校验成功,正在跳转绑定页')
|
||||
syncPolling()
|
||||
|
||||
const bindUrl = String(response.data.kuaishouCloudFulfillment?.binding.bindUrl || '').trim()
|
||||
if (bindUrl) {
|
||||
window.setTimeout(() => {
|
||||
window.location.assign(bindUrl)
|
||||
}, 600)
|
||||
}
|
||||
} catch (error) {
|
||||
showError(error instanceof Error ? error.message : '核销码校验失败')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openBindUrl() {
|
||||
const bindUrl = String(flow.value?.binding.bindUrl || '').trim()
|
||||
if (!bindUrl) {
|
||||
showError('当前还没有可用绑定链接')
|
||||
return
|
||||
}
|
||||
|
||||
window.open(bindUrl, '_blank', 'noopener,noreferrer')
|
||||
}
|
||||
|
||||
function stopPolling() {
|
||||
if (pollTimer) {
|
||||
window.clearInterval(pollTimer)
|
||||
pollTimer = 0
|
||||
}
|
||||
}
|
||||
|
||||
function syncPolling() {
|
||||
const taskStatus = String(task.value?.status || '').trim()
|
||||
const shouldPoll = Boolean(flow.value) && !['completed', 'manual_review', 'failed', 'closed', 'expired'].includes(taskStatus)
|
||||
|
||||
if (!shouldPoll) {
|
||||
stopPolling()
|
||||
return
|
||||
}
|
||||
|
||||
if (pollTimer) {
|
||||
return
|
||||
}
|
||||
|
||||
pollTimer = window.setInterval(() => {
|
||||
void loadDetail({ silent: true })
|
||||
}, 4000)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
void loadDetail()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
stopPolling()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="kuaishou-claim-page">
|
||||
<section class="hero-card">
|
||||
<div class="hero-copy">
|
||||
<span class="eyebrow">快手小店客户领取</span>
|
||||
<h1>先获取核销码,再继续绑定游戏资源</h1>
|
||||
<p>
|
||||
订单 {{ order?.platformOrderId || '-' }} · 商品 {{ orderItem?.skuName || '-' }}。
|
||||
你只需要按图片指引找到快手小店核销码,提交成功后页面会自动跳转到绑定链接。
|
||||
</p>
|
||||
</div>
|
||||
<div class="hero-status">
|
||||
<span class="status-label">当前状态</span>
|
||||
<strong>{{ pageStatusText }}</strong>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div v-if="loading" class="placeholder-card">
|
||||
<strong>页面加载中</strong>
|
||||
<p>正在检查领取链接和当前任务进度。</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="errorMessage" class="placeholder-card error-card">
|
||||
<strong>无法继续处理</strong>
|
||||
<p>{{ errorMessage }}</p>
|
||||
</div>
|
||||
|
||||
<template v-else-if="detail && flow">
|
||||
<section class="info-grid">
|
||||
<article class="info-card">
|
||||
<span>核销码校验</span>
|
||||
<strong>{{ flow.ticket.status === 'verified' ? '已通过' : '待提交' }}</strong>
|
||||
<p>{{ flow.ticket.code || '还没有提交核销码' }}</p>
|
||||
</article>
|
||||
<article class="info-card">
|
||||
<span>绑定链接</span>
|
||||
<strong>{{ flow.binding.prepareStatus === 'ready' ? '已准备' : '准备中' }}</strong>
|
||||
<p>{{ flow.binding.bindPreparedAt || '系统准备完成后即可继续' }}</p>
|
||||
</article>
|
||||
<article class="info-card">
|
||||
<span>发货状态</span>
|
||||
<strong>{{ flow.dispatch.status === 'success' ? '已发货' : '等待客服' }}</strong>
|
||||
<p>{{ flow.dispatch.dispatchAt || flow.dispatch.note || '客服确认绑定信息无误后会执行发货' }}</p>
|
||||
</article>
|
||||
<article class="info-card">
|
||||
<span>快手核销</span>
|
||||
<strong>{{ flow.consume.status === 'success' ? '已完成' : flow.consume.status === 'failed' ? '异常' : '待完成' }}</strong>
|
||||
<p>{{ flow.consume.errorMessage || flow.consume.consumedAt || '发货和退号完成后会自动核销' }}</p>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="form-card">
|
||||
<div class="card-head">
|
||||
<div>
|
||||
<h2>第 1 步:复制并粘贴快手小店核销码</h2>
|
||||
<p>核销码会使用这笔订单对应的快手店铺进行校验,校验通过后才会继续绑定。</p>
|
||||
</div>
|
||||
<el-button
|
||||
v-if="flow.ticket.status === 'verified'"
|
||||
round
|
||||
type="primary"
|
||||
plain
|
||||
@click="openBindUrl"
|
||||
>
|
||||
{{ bindButtonLabel }}
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<label class="field-block">
|
||||
<span>核销码</span>
|
||||
<el-input
|
||||
v-model="ticketCode"
|
||||
maxlength="120"
|
||||
placeholder="请粘贴快手小店里的核销码"
|
||||
:disabled="submitting || !canSubmitTicket"
|
||||
@keyup.enter="submitTicket"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div class="action-row">
|
||||
<el-button
|
||||
round
|
||||
type="primary"
|
||||
:loading="submitting"
|
||||
:disabled="!canSubmitTicket"
|
||||
@click="submitTicket"
|
||||
>
|
||||
{{ bindButtonLabel }}
|
||||
</el-button>
|
||||
<span class="action-tip">
|
||||
校验成功后会自动跳转到绑定页;绑定完成后请等待客服继续处理。
|
||||
</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="isWaitingSupport || flow.dispatch.status === 'success' || isCompleted" class="state-card">
|
||||
<h2>第 2 步:完成绑定后等待客服处理</h2>
|
||||
<p v-if="isWaitingSupport">核销码已经通过校验。请在外部绑定页完成绑定,然后等待客服确认绑定信息并执行发货。</p>
|
||||
<p v-else-if="flow.dispatch.status === 'success' && !isCompleted">客服已经执行发货,系统正在进行退号和快手核销收口。</p>
|
||||
<p v-else>所有步骤都已经完成,没有检测到异常,可以回到快手小店查看最终状态。</p>
|
||||
</section>
|
||||
|
||||
<section class="guide-card">
|
||||
<div class="card-head">
|
||||
<div>
|
||||
<h2>图文指引</h2>
|
||||
<p>如果你不确定核销码在哪里,可以展开下面的步骤图对照操作。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-collapse v-model="expandedPanels">
|
||||
<el-collapse-item name="guide" title="展开核销码获取步骤">
|
||||
<div class="guide-grid">
|
||||
<figure
|
||||
v-for="(imageUrl, index) in flow.guideImages"
|
||||
:key="imageUrl"
|
||||
class="guide-figure"
|
||||
>
|
||||
<img :src="imageUrl" :alt="`快手核销码步骤 ${index + 1}`" />
|
||||
<figcaption>步骤 {{ index + 1 }}</figcaption>
|
||||
</figure>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<div v-else class="placeholder-card error-card">
|
||||
<strong>流程数据不完整</strong>
|
||||
<p>当前领取链接没有查到可用的快手 Cloud 履约上下文,请联系客服处理。</p>
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.kuaishou-claim-page {
|
||||
min-height: 100vh;
|
||||
padding: 24px;
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(255, 182, 132, 0.24), transparent 28%),
|
||||
radial-gradient(circle at bottom right, rgba(98, 176, 255, 0.18), transparent 34%),
|
||||
linear-gradient(180deg, #fff7ef 0%, #f6fbff 100%);
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.hero-card,
|
||||
.placeholder-card,
|
||||
.form-card,
|
||||
.state-card,
|
||||
.guide-card,
|
||||
.info-card {
|
||||
border-radius: 28px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.16);
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
box-shadow: 0 24px 70px rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
|
||||
.hero-card {
|
||||
max-width: 1180px;
|
||||
margin: 0 auto 18px;
|
||||
padding: 28px;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.4fr) 240px;
|
||||
gap: 20px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 34px;
|
||||
padding: 0 14px;
|
||||
border-radius: 999px;
|
||||
background: rgba(251, 146, 60, 0.14);
|
||||
color: #b45309;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.hero-copy h1,
|
||||
.form-card h2,
|
||||
.state-card h2,
|
||||
.guide-card h2 {
|
||||
margin: 14px 0 10px;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.hero-copy p,
|
||||
.card-head p,
|
||||
.state-card p,
|
||||
.placeholder-card p,
|
||||
.action-tip {
|
||||
color: #475569;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.hero-status {
|
||||
padding: 20px;
|
||||
border-radius: 24px;
|
||||
background: linear-gradient(180deg, #101828 0%, #1f3f5b 100%);
|
||||
color: #f8fafc;
|
||||
}
|
||||
|
||||
.hero-status strong {
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
font-size: 26px;
|
||||
}
|
||||
|
||||
.status-label,
|
||||
.info-card span,
|
||||
.field-block span {
|
||||
color: #94a3b8;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.placeholder-card,
|
||||
.form-card,
|
||||
.state-card,
|
||||
.guide-card {
|
||||
max-width: 1180px;
|
||||
margin: 0 auto 18px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.error-card {
|
||||
border-color: rgba(239, 68, 68, 0.18);
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
max-width: 1180px;
|
||||
margin: 0 auto 18px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.info-card {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.info-card strong {
|
||||
display: block;
|
||||
margin: 8px 0 10px;
|
||||
font-size: 19px;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.info-card p {
|
||||
margin: 0;
|
||||
color: #475569;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.card-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.field-block {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.action-row {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.state-card {
|
||||
border-style: dashed;
|
||||
}
|
||||
|
||||
.guide-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.guide-figure {
|
||||
margin: 0;
|
||||
padding: 14px;
|
||||
border-radius: 24px;
|
||||
background: linear-gradient(180deg, rgba(248, 250, 252, 0.96), rgba(241, 245, 249, 0.92));
|
||||
border: 1px solid rgba(148, 163, 184, 0.14);
|
||||
}
|
||||
|
||||
.guide-figure img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.guide-figure figcaption {
|
||||
margin-top: 10px;
|
||||
color: #475569;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (max-width: 980px) {
|
||||
.kuaishou-claim-page {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.hero-card,
|
||||
.info-grid,
|
||||
.guide-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.card-head {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user