840 lines
20 KiB
Vue
840 lines
20 KiB
Vue
<script setup lang="ts">
|
||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||
import QRCode from 'qrcode'
|
||
import { ElMessageBox } from 'element-plus'
|
||
import {
|
||
CircleCheck,
|
||
CircleClose,
|
||
Clock,
|
||
Loading,
|
||
RefreshRight,
|
||
Warning,
|
||
} from '@element-plus/icons-vue'
|
||
|
||
import { showError, showSuccess } from '@/lib/feedback'
|
||
import {
|
||
confirmKuaishouCloudClaimRole,
|
||
fetchClaimDetail,
|
||
redeemKuaishouCloudClaim,
|
||
verifyKuaishouCloudClaimTicket,
|
||
} from '@/services/claim'
|
||
import type { ClaimDetailData } from '@/types/claim'
|
||
|
||
const props = defineProps<{
|
||
token: string
|
||
}>()
|
||
|
||
const loading = ref(true)
|
||
const submitting = ref(false)
|
||
const refreshingRole = ref(false)
|
||
const confirmingRole = ref(false)
|
||
const redeeming = ref(false)
|
||
const errorMessage = ref('')
|
||
const detail = ref<ClaimDetailData | null>(null)
|
||
const ticketCode = ref('')
|
||
const qrCodeDataUrl = ref('')
|
||
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 roleName = computed(() => flow.value?.role.name || flow.value?.binding.roleName || '')
|
||
const roleId = computed(() => flow.value?.role.rid || flow.value?.binding.roleId || '')
|
||
const isTicketVerified = computed(() => flow.value?.ticket.status === 'verified')
|
||
const isBindUrlExpired = computed(() => {
|
||
const expiresAt = String(flow.value?.binding.bindExpiresAt || '').trim()
|
||
if (!expiresAt) {
|
||
return false
|
||
}
|
||
|
||
const expiresTime = Date.parse(expiresAt)
|
||
return Number.isFinite(expiresTime) && expiresTime <= Date.now()
|
||
})
|
||
const isBindingPrepared = computed(
|
||
() =>
|
||
flow.value?.binding.prepareStatus === 'ready' &&
|
||
Boolean(String(flow.value?.binding.bindUrl || '').trim()) &&
|
||
!isBindUrlExpired.value,
|
||
)
|
||
const isBindingPreparing = computed(() => flow.value?.binding.prepareStatus === 'pending')
|
||
const canEnterBindingStep = computed(() => isTicketVerified.value)
|
||
const isRoleReady = computed(() => Boolean(roleName.value || roleId.value))
|
||
const isRoleConfirmed = computed(() => String(task.value?.status || '').trim() === 'role_confirmed')
|
||
const isDispatched = computed(() => String(flow.value?.dispatch.status || '').trim() === 'success')
|
||
const isCompleted = computed(
|
||
() =>
|
||
String(task.value?.status || '').trim() === 'completed' ||
|
||
String(flow.value?.consume.status || '').trim() === 'success',
|
||
)
|
||
const hasRedeemResult = computed(() => {
|
||
const status = String(task.value?.status || '').trim()
|
||
return (
|
||
isDispatched.value ||
|
||
['dispatched_pending_return', 'completed', 'manual_review', 'failed'].includes(status)
|
||
)
|
||
})
|
||
const canSubmitTicket = computed(
|
||
() =>
|
||
!['completed', 'manual_review', 'closed', 'expired'].includes(
|
||
String(task.value?.status || '').trim(),
|
||
) && !submitting.value,
|
||
)
|
||
const currentStep = computed(() => {
|
||
if (hasRedeemResult.value) {
|
||
return 4
|
||
}
|
||
|
||
if (isRoleConfirmed.value) {
|
||
return 3
|
||
}
|
||
|
||
if (canEnterBindingStep.value) {
|
||
return 2
|
||
}
|
||
|
||
return 1
|
||
})
|
||
|
||
const progressText = computed(() => {
|
||
if (hasRedeemResult.value) {
|
||
return '兑换结果已生成'
|
||
}
|
||
if (isRoleConfirmed.value) {
|
||
return '角色已确认,等待兑换'
|
||
}
|
||
if (isTicketVerified.value) {
|
||
return isBindingPrepared.value ? '请完成扫码绑定' : '绑定链接刷新中,请稍候'
|
||
}
|
||
return '等待提交核销码'
|
||
})
|
||
const resultTitle = computed(() => {
|
||
if (isCompleted.value) {
|
||
return '兑换成功'
|
||
}
|
||
|
||
if (isDispatched.value) {
|
||
return '兑换请求已提交'
|
||
}
|
||
|
||
return '结果已记录'
|
||
})
|
||
const resultDescription = computed(() => {
|
||
if (isCompleted.value) {
|
||
return '当前兑换流程已经完成。发货、退号和核销结果会保存在后台任务界面。'
|
||
}
|
||
|
||
return '你的兑换请求已经提交。发货、退号和核销结果会由系统保存在后台任务界面,无需在此页面等待。'
|
||
})
|
||
|
||
async function generateQRCode(url: string) {
|
||
if (!url) {
|
||
qrCodeDataUrl.value = ''
|
||
return
|
||
}
|
||
|
||
try {
|
||
qrCodeDataUrl.value = await QRCode.toDataURL(url, {
|
||
width: 280,
|
||
margin: 2,
|
||
color: {
|
||
dark: '#0f172a',
|
||
light: '#ffffff',
|
||
},
|
||
})
|
||
} catch (error) {
|
||
qrCodeDataUrl.value = ''
|
||
console.error('生成二维码失败:', error)
|
||
}
|
||
}
|
||
|
||
async function applyDetail(nextDetail: ClaimDetailData) {
|
||
detail.value = nextDetail
|
||
if (!ticketCode.value && nextDetail.kuaishouCloudFulfillment?.ticket.code) {
|
||
ticketCode.value = nextDetail.kuaishouCloudFulfillment.ticket.code
|
||
}
|
||
await generateQRCode(String(nextDetail.kuaishouCloudFulfillment?.binding.bindUrl || '').trim())
|
||
}
|
||
|
||
async function loadDetail(options: { silent?: boolean } = {}) {
|
||
if (!options.silent) {
|
||
loading.value = true
|
||
}
|
||
|
||
errorMessage.value = ''
|
||
|
||
try {
|
||
const response = await fetchClaimDetail(props.token)
|
||
await applyDetail(response.data)
|
||
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,
|
||
})
|
||
await applyDetail(response.data)
|
||
showSuccess('核销码验证通过,系统已开始准备绑定资源')
|
||
syncPolling()
|
||
} catch (error) {
|
||
showError(error instanceof Error ? error.message : '核销码验证失败')
|
||
} finally {
|
||
submitting.value = false
|
||
}
|
||
}
|
||
|
||
async function refreshRole() {
|
||
refreshingRole.value = true
|
||
try {
|
||
await loadDetail({ silent: true })
|
||
if (isRoleReady.value) {
|
||
showSuccess('角色信息已刷新')
|
||
} else {
|
||
showError(
|
||
flow.value?.role.errorMessage || '暂时还没有识别到角色信息,请完成绑定后稍等片刻再试',
|
||
)
|
||
}
|
||
} finally {
|
||
refreshingRole.value = false
|
||
}
|
||
}
|
||
|
||
async function confirmRole() {
|
||
confirmingRole.value = true
|
||
try {
|
||
const response = await confirmKuaishouCloudClaimRole(props.token)
|
||
await applyDetail(response.data)
|
||
showSuccess('角色已确认,进入下一步')
|
||
syncPolling()
|
||
} catch (error) {
|
||
showError(error instanceof Error ? error.message : '确认角色失败')
|
||
} finally {
|
||
confirmingRole.value = false
|
||
}
|
||
}
|
||
|
||
async function confirmRedeem() {
|
||
try {
|
||
await ElMessageBox.confirm(
|
||
'兑换后不可取消,也不可退货。请确认角色和商品信息完全正确。',
|
||
'确认兑换',
|
||
{
|
||
confirmButtonText: '确认兑换',
|
||
cancelButtonText: '取消',
|
||
type: 'warning',
|
||
center: true,
|
||
},
|
||
)
|
||
} catch {
|
||
return
|
||
}
|
||
|
||
redeeming.value = true
|
||
try {
|
||
const response = await redeemKuaishouCloudClaim(props.token)
|
||
await applyDetail(response.data)
|
||
showSuccess('兑换请求已提交')
|
||
syncPolling()
|
||
} catch (error) {
|
||
showError(error instanceof Error ? error.message : '兑换失败')
|
||
} finally {
|
||
redeeming.value = false
|
||
}
|
||
}
|
||
|
||
function openBindUrl(useCurrentPage = false) {
|
||
const bindUrl = String(flow.value?.binding.bindUrl || '').trim()
|
||
if (!bindUrl) {
|
||
showError('绑定链接还没准备好,请稍后刷新')
|
||
return
|
||
}
|
||
|
||
if (useCurrentPage) {
|
||
window.location.assign(bindUrl)
|
||
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) && !hasRedeemResult.value && !['closed', 'expired'].includes(taskStatus)
|
||
|
||
if (!shouldPoll) {
|
||
stopPolling()
|
||
return
|
||
}
|
||
|
||
if (pollTimer) {
|
||
return
|
||
}
|
||
|
||
pollTimer = window.setInterval(() => {
|
||
void loadDetail({ silent: true })
|
||
}, 4000)
|
||
}
|
||
|
||
function formatDateTime(value: string | null) {
|
||
if (!value) {
|
||
return '-'
|
||
}
|
||
|
||
try {
|
||
return new Date(value).toLocaleString('zh-CN', {
|
||
year: 'numeric',
|
||
month: '2-digit',
|
||
day: '2-digit',
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
second: '2-digit',
|
||
})
|
||
} catch {
|
||
return value
|
||
}
|
||
}
|
||
|
||
onMounted(() => {
|
||
void loadDetail()
|
||
})
|
||
|
||
onBeforeUnmount(() => {
|
||
stopPolling()
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<main class="claim-page">
|
||
<section class="header-card">
|
||
<div class="header-copy">
|
||
<span class="eyebrow">快手 Cloud 客户领取</span>
|
||
<h1>{{ orderItem?.skuName || '商品领取' }}</h1>
|
||
<p>订单号:{{ order?.platformOrderId || '-' }}</p>
|
||
</div>
|
||
<div v-if="!loading && !errorMessage" class="step-indicator">
|
||
<div
|
||
v-for="step in 4"
|
||
:key="step"
|
||
class="step-dot"
|
||
:class="{ active: currentStep >= step }"
|
||
>
|
||
{{ step }}
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<div v-if="loading" class="content-card loading-card">
|
||
<el-icon class="large-icon spinning"><Loading /></el-icon>
|
||
<p>正在读取当前领取进度...</p>
|
||
</div>
|
||
|
||
<div v-else-if="errorMessage" class="content-card error-card">
|
||
<el-icon class="large-icon error"><CircleClose /></el-icon>
|
||
<p>{{ errorMessage }}</p>
|
||
</div>
|
||
|
||
<template v-else-if="detail && flow">
|
||
<section class="summary-card">
|
||
<div class="summary-item">
|
||
<span>当前进度</span>
|
||
<strong>{{ progressText }}</strong>
|
||
</div>
|
||
<div class="summary-item">
|
||
<span>当前角色</span>
|
||
<strong>{{ roleName || '待识别' }}</strong>
|
||
</div>
|
||
<div class="summary-item">
|
||
<span>角色 ID</span>
|
||
<strong>{{ roleId || '-' }}</strong>
|
||
</div>
|
||
</section>
|
||
|
||
<section v-if="currentStep === 1" class="content-card">
|
||
<h2>第 1 步:提交核销码</h2>
|
||
<p class="muted">
|
||
请先从快手小店复制核销码。验证通过后,系统会自动准备 Cloud
|
||
绑定资源,不需要再让客服手工点“准备绑定资源”。
|
||
</p>
|
||
|
||
<el-input
|
||
v-model="ticketCode"
|
||
placeholder="粘贴快手核销码"
|
||
size="large"
|
||
:disabled="!canSubmitTicket"
|
||
@keyup.enter="submitTicket"
|
||
/>
|
||
|
||
<el-button
|
||
type="primary"
|
||
size="large"
|
||
:loading="submitting"
|
||
:disabled="!canSubmitTicket"
|
||
@click="submitTicket"
|
||
>
|
||
验证核销码并继续
|
||
</el-button>
|
||
|
||
<div class="status-banner info">
|
||
<el-icon><Clock /></el-icon>
|
||
<span>{{
|
||
isBindingPreparing
|
||
? '绑定资源会在核销通过后自动准备。'
|
||
: '核销完成后会自动进入扫码绑定步骤。'
|
||
}}</span>
|
||
</div>
|
||
|
||
<div v-if="flow.guideImages.length > 0" class="guide-section">
|
||
<el-collapse>
|
||
<el-collapse-item title="查看核销码图文指引" name="guide">
|
||
<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>
|
||
</div>
|
||
</section>
|
||
|
||
<section v-else-if="currentStep === 2" class="content-card">
|
||
<h2>第 2 步:扫码绑定角色</h2>
|
||
<p class="muted">
|
||
核销码已验证通过。请扫码或直接打开绑定页面,绑定你自己的游戏角色。绑定信息如果一开始不对,可以重新绑定后再点下一步。
|
||
</p>
|
||
|
||
<div v-if="isBindingPrepared && qrCodeDataUrl" class="qr-block">
|
||
<img :src="qrCodeDataUrl" alt="绑定二维码" class="qr-code" />
|
||
<div class="action-row">
|
||
<el-button type="primary" size="large" @click="openBindUrl()">
|
||
新窗口打开绑定页
|
||
</el-button>
|
||
<el-button size="large" plain @click="openBindUrl(true)"> 当前页面打开 </el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-else class="status-banner info">
|
||
<el-icon class="spinning"><Loading /></el-icon>
|
||
<span>系统正在准备绑定资源,请稍后自动刷新。</span>
|
||
</div>
|
||
|
||
<p v-if="flow.role.errorMessage" class="muted warning-line">
|
||
{{ flow.role.errorMessage }}
|
||
</p>
|
||
<p v-else class="muted warning-line">
|
||
如果当前角色不正确,请重新打开绑定页完成重新绑定,然后刷新角色信息。
|
||
</p>
|
||
<p class="muted meta-line">
|
||
最近刷新:{{ formatDateTime(flow.role.refreshedAt) }};链接有效期:{{
|
||
formatDateTime(flow.binding.bindExpiresAt)
|
||
}}
|
||
</p>
|
||
<p v-if="flow.binding.bindProbeMessage" class="muted meta-line">
|
||
链接检测:{{ flow.binding.bindProbeStatus || '-' }} / {{ flow.binding.bindProbeMessage }}
|
||
</p>
|
||
|
||
<div class="action-row">
|
||
<el-button size="large" plain :loading="refreshingRole" @click="refreshRole">
|
||
<el-icon><RefreshRight /></el-icon>
|
||
刷新角色信息
|
||
</el-button>
|
||
<el-button
|
||
type="primary"
|
||
size="large"
|
||
:loading="confirmingRole"
|
||
:disabled="!isBindingPrepared || !isRoleReady"
|
||
@click="confirmRole"
|
||
>
|
||
我已完成绑定,下一步
|
||
</el-button>
|
||
</div>
|
||
</section>
|
||
|
||
<section v-else-if="currentStep === 3" class="content-card">
|
||
<h2>第 3 步:确认兑换信息</h2>
|
||
<p class="muted">请再次确认角色和商品信息,确认无误后再继续兑换。</p>
|
||
|
||
<div class="info-grid">
|
||
<div class="info-item">
|
||
<span>角色名称</span>
|
||
<strong>{{ roleName || '-' }}</strong>
|
||
</div>
|
||
<div class="info-item">
|
||
<span>角色 ID</span>
|
||
<strong>{{ roleId || '-' }}</strong>
|
||
</div>
|
||
<div class="info-item">
|
||
<span>商品名称</span>
|
||
<strong>{{ orderItem?.skuName || '-' }}</strong>
|
||
</div>
|
||
<div class="info-item">
|
||
<span>商品数量</span>
|
||
<strong>{{ orderItem?.quantity || 0 }}</strong>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="status-banner warning">
|
||
<el-icon><Warning /></el-icon>
|
||
<span>兑换后不可取消,也不可退货。</span>
|
||
</div>
|
||
|
||
<el-button type="primary" size="large" :loading="redeeming" @click="confirmRedeem">
|
||
确认兑换
|
||
</el-button>
|
||
</section>
|
||
|
||
<section v-else class="content-card">
|
||
<div class="result-header">
|
||
<el-icon class="large-icon success"><CircleCheck /></el-icon>
|
||
<div>
|
||
<h2>{{ resultTitle }}</h2>
|
||
<p class="muted">{{ resultDescription }}</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="info-grid">
|
||
<div class="info-item">
|
||
<span>结果时间</span>
|
||
<strong>{{ formatDateTime(flow.dispatch.dispatchAt) }}</strong>
|
||
</div>
|
||
<div class="info-item">
|
||
<span>商品名称</span>
|
||
<strong>{{ orderItem?.skuName || '-' }}</strong>
|
||
</div>
|
||
<div class="info-item">
|
||
<span>角色名称</span>
|
||
<strong>{{ roleName || '-' }}</strong>
|
||
</div>
|
||
<div class="info-item">
|
||
<span>角色 ID</span>
|
||
<strong>{{ roleId || '-' }}</strong>
|
||
</div>
|
||
<div class="info-item">
|
||
<span>订单号</span>
|
||
<strong>{{ order?.platformOrderId || '-' }}</strong>
|
||
</div>
|
||
<div class="info-item">
|
||
<span>购买数量</span>
|
||
<strong>{{ orderItem?.quantity || 0 }}</strong>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="status-banner success">
|
||
<el-icon><CircleCheck /></el-icon>
|
||
<span>客户侧操作已经完成,后续履约结果请以后台任务界面为准。</span>
|
||
</div>
|
||
</section>
|
||
</template>
|
||
|
||
<div v-else class="content-card error-card">
|
||
<el-icon class="large-icon error"><CircleClose /></el-icon>
|
||
<p>流程数据不完整,请联系客服处理</p>
|
||
</div>
|
||
</main>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.claim-page {
|
||
min-height: 100vh;
|
||
padding: 24px;
|
||
background:
|
||
radial-gradient(circle at top right, rgba(59, 130, 246, 0.18), transparent 28%),
|
||
linear-gradient(180deg, #f8fafc 0%, #eef4fb 100%);
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 18px;
|
||
}
|
||
|
||
.header-card,
|
||
.summary-card,
|
||
.content-card {
|
||
width: 100%;
|
||
max-width: 680px;
|
||
background: rgba(255, 255, 255, 0.92);
|
||
border: 1px solid rgba(148, 163, 184, 0.16);
|
||
border-radius: 20px;
|
||
box-shadow: 0 18px 40px rgba(15, 23, 42, 0.08);
|
||
backdrop-filter: blur(12px);
|
||
}
|
||
|
||
.header-card {
|
||
padding: 24px 28px;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
gap: 16px;
|
||
}
|
||
|
||
.header-copy h1,
|
||
.content-card h2 {
|
||
margin: 0;
|
||
color: #0f172a;
|
||
}
|
||
|
||
.header-copy p,
|
||
.muted {
|
||
margin: 0;
|
||
color: #64748b;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.eyebrow {
|
||
display: inline-flex;
|
||
margin-bottom: 8px;
|
||
color: #2563eb;
|
||
font-size: 13px;
|
||
font-weight: 700;
|
||
letter-spacing: 0.08em;
|
||
}
|
||
|
||
.step-indicator {
|
||
display: flex;
|
||
gap: 10px;
|
||
}
|
||
|
||
.step-dot {
|
||
width: 34px;
|
||
height: 34px;
|
||
border-radius: 999px;
|
||
background: #e2e8f0;
|
||
color: #94a3b8;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.step-dot.active {
|
||
background: linear-gradient(135deg, #2563eb 0%, #0ea5e9 100%);
|
||
color: #ffffff;
|
||
}
|
||
|
||
.summary-card {
|
||
padding: 18px 22px;
|
||
display: grid;
|
||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||
gap: 14px;
|
||
}
|
||
|
||
.summary-item,
|
||
.info-item {
|
||
padding: 16px;
|
||
border-radius: 16px;
|
||
background: #f8fafc;
|
||
border: 1px solid #e2e8f0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
}
|
||
|
||
.summary-item span,
|
||
.info-item span {
|
||
color: #64748b;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.summary-item strong,
|
||
.info-item strong {
|
||
color: #0f172a;
|
||
font-size: 17px;
|
||
}
|
||
|
||
.content-card {
|
||
padding: 28px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 18px;
|
||
}
|
||
|
||
.loading-card,
|
||
.error-card {
|
||
align-items: center;
|
||
justify-content: center;
|
||
text-align: center;
|
||
min-height: 240px;
|
||
}
|
||
|
||
.large-icon {
|
||
font-size: 44px;
|
||
}
|
||
|
||
.large-icon.success {
|
||
color: #16a34a;
|
||
}
|
||
|
||
.large-icon.error {
|
||
color: #dc2626;
|
||
}
|
||
|
||
.spinning {
|
||
animation: spin 1s linear infinite;
|
||
}
|
||
|
||
.status-banner {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
padding: 14px 16px;
|
||
border-radius: 14px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.status-banner.info {
|
||
background: #eff6ff;
|
||
color: #1d4ed8;
|
||
}
|
||
|
||
.status-banner.success {
|
||
background: #f0fdf4;
|
||
color: #166534;
|
||
}
|
||
|
||
.status-banner.warning {
|
||
background: #fff7ed;
|
||
color: #c2410c;
|
||
}
|
||
|
||
.status-banner.error {
|
||
background: #fef2f2;
|
||
color: #b91c1c;
|
||
}
|
||
|
||
.guide-grid,
|
||
.info-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
gap: 14px;
|
||
}
|
||
|
||
.guide-figure {
|
||
margin: 0;
|
||
background: #f8fafc;
|
||
border: 1px solid #e2e8f0;
|
||
border-radius: 16px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.guide-figure img {
|
||
width: 100%;
|
||
display: block;
|
||
}
|
||
|
||
.guide-figure figcaption {
|
||
padding: 10px 12px 14px;
|
||
color: #475569;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.qr-block {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 16px;
|
||
padding: 22px;
|
||
border-radius: 18px;
|
||
background: linear-gradient(180deg, #eff6ff 0%, #f8fafc 100%);
|
||
border: 1px solid #dbeafe;
|
||
}
|
||
|
||
.qr-code {
|
||
width: 280px;
|
||
height: 280px;
|
||
padding: 12px;
|
||
background: #ffffff;
|
||
border-radius: 18px;
|
||
}
|
||
|
||
.action-row {
|
||
display: flex;
|
||
gap: 12px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.warning-line {
|
||
color: #b45309;
|
||
}
|
||
|
||
.meta-line {
|
||
font-size: 13px;
|
||
}
|
||
|
||
.result-header {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 14px;
|
||
}
|
||
|
||
@keyframes spin {
|
||
from {
|
||
transform: rotate(0deg);
|
||
}
|
||
|
||
to {
|
||
transform: rotate(360deg);
|
||
}
|
||
}
|
||
|
||
@media (max-width: 720px) {
|
||
.claim-page {
|
||
padding: 16px;
|
||
}
|
||
|
||
.header-card,
|
||
.summary-card,
|
||
.content-card {
|
||
max-width: 100%;
|
||
}
|
||
|
||
.header-card {
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
}
|
||
|
||
.summary-card,
|
||
.guide-grid,
|
||
.info-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.qr-code {
|
||
width: min(280px, 100%);
|
||
height: auto;
|
||
}
|
||
|
||
.action-row {
|
||
width: 100%;
|
||
flex-direction: column;
|
||
}
|
||
}
|
||
</style>
|