Files
hhrs/pages/petrol/voucherDetail.vue
T

262 lines
10 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="page">
<view class="nav-bar">
<view class="nav-back" @click="goBack">
<u-icon name="arrow-left" color="#1a1818" size="40"></u-icon>
</view>
<text class="nav-title">券详情</text>
</view>
<!-- 发券中(statusCode=1) -->
<view class="issuing" v-if="status === 1">
<view class="spinner"></view>
<view class="issuing-t">支付成功,券发放中…</view>
<view class="issuing-d">通常 10 秒内到账,请稍后刷新查看券码</view>
<view class="btn-line" @click="refresh()">手动刷新</view>
</view>
<!-- 已发券(statusCode=2):二维码 + 订单信息 -->
<block v-else-if="status === 2">
<view class="qrbox">
<!-- 核销码为图片URL时直接展示 -->
<image v-if="isQrImage" class="qr-img" :src="qrText" mode="widthFix" />
<!-- 核销码为字符串时前端渲染二维码(每60秒向服务端换取新码) -->
<view v-else id="petrol-qr" class="qr-canvas"></view>
<view class="qr-tip">核销码每 60 秒自动刷新,请到店后出示给收银员扫码</view>
</view>
<view class="card">
<view class="card-title">订单信息</view>
<view class="kv"><text class="k">券名称</text><text class="v">{{ voucherName }}</text></view>
<view class="kv"><text class="k">有效期至</text><text class="v">{{ voucher.validUntil || '-' }}</text></view>
<view class="kv"><text class="k">实付金额</text><text class="v">¥{{ amountText }}</text></view>
<view class="kv"><text class="k">支付时间</text><text class="v">{{ fmtTime(voucher.payTime) }}</text></view>
<view class="kv"><text class="k">订单号</text><text class="v">{{ voucher.orderNo }}</text></view>
</view>
</block>
<!-- 待支付(0):续付入口 -->
<view class="state-card" v-else-if="status === 0">
<view class="state-t">订单待支付</view>
<view class="state-d">实付金额 ¥{{ amountText }},订单将在下单 30 分钟后自动关闭</view>
<view class="btn-main" @click="goConfirm">继续支付</view>
</view>
<!-- 已退款(3/4) -->
<view class="state-card" v-else-if="status === 3 || status === 4">
<view class="state-t">{{ status === 4 ? '退款处理中' : '已退款' }}</view>
<view class="state-d">很抱歉,本券发放失败,支付金额已原路退回{{ status === 4 ? '处理中,到账请稍后查询' : '' }}。如有疑问请致电客服。</view>
<view class="btn-main" @click="goConfirm">重新购买</view>
</view>
<!-- 已关闭(5) -->
<view class="state-card" v-else-if="status === 5">
<view class="state-t">订单已关闭</view>
<view class="state-d">该待支付订单已超时关闭,可重新下单购买。</view>
<view class="btn-main" @click="goConfirm">重新购买</view>
</view>
<!-- 加载中 / 无单 -->
<view class="state-card" v-else>
<view class="state-t">{{ status === null ? '加载中…' : '暂无券订单' }}</view>
<view class="state-d" v-if="status === null">正在获取订单状态</view>
<view class="state-d" v-else>您还没有购买汽油券,可返回门店页购买</view>
<view class="btn-line" v-if="status !== null" @click="refresh()">刷新</view>
</view>
<!-- 使用说明(有券或发券中都展示) -->
<block v-if="status === 2 || status === 1">
<view class="card">
<view class="card-title">使用规则</view>
<view class="ol-item" v-for="(t, i) in useRules" :key="'r' + i">{{ i + 1 }}、{{ t }}</view>
</view>
<view class="card">
<view class="card-title">使用流程</view>
<view class="ol-item" v-for="(t, i) in useFlow" :key="'f' + i">{{ i + 1 }}、{{ t }}</view>
</view>
</block>
</view>
</template>
<script>
import {
VOUCHER_NAME, VOUCHER_USE_RULES, VOUCHER_USE_FLOW,
bizOk, bizMsg, bizData
} from '@/common/petrolVoucher.js'
export default {
data() {
return {
orderNo: '',
phone: '',
voucher: {},
status: null, // null=加载中;含义对齐后端 statusCode
qrText: '', // xuchao: 实时核销码(60 秒滚动,来自 /voucher/checkCode)
pollTimer: null,
pollLeft: 3,
checkTimer: null,
voucherName: VOUCHER_NAME,
useRules: VOUCHER_USE_RULES,
useFlow: VOUCHER_USE_FLOW
}
},
computed: {
amountText() {
const f = this.voucher.amount
return f ? (f / 100).toFixed(2) : ''
},
isQrImage() {
return typeof this.qrText === 'string' && /^https?:\/\//i.test(this.qrText)
}
},
onLoad(options) {
this.phone = uni.getStorageSync('phone') || ''
this.orderNo = options.orderNo || ''
if (!this.phone) {
uni.redirectTo({ url: '/pages/index/login?redirect=' + encodeURIComponent('/pages/petrol/voucherDetail?orderNo=' + this.orderNo) })
return
}
this.refresh(true)
},
onUnload() {
this.stopPoll()
this.stopCheckTimer()
},
methods: {
refresh(first) {
this.$u.api.voucherLatest({ phone: this.phone }).then(res => {
if (!bizOk(res)) {
uni.showToast({ title: bizMsg(res) || '查询失败', icon: 'none' })
return
}
const d = bizData(res)
if (!d || d.statusCode === undefined || d.statusCode === -1) {
this.status = -1
this.voucher = {}
return
}
this.voucher = d
this.status = d.statusCode
if (this.status === 1) {
// xuchao: 发券中轮询(支付回跳后 5 秒 ×3 次;手动刷新不限量)
if (first && this.pollLeft > 0) this.startPoll()
} else {
this.stopPoll()
}
if (this.status === 2) {
this.loadCheckCode()
} else {
this.stopCheckTimer()
}
}).catch(() => {
if (first) uni.showToast({ title: '网络异常,请稍后重试', icon: 'none' })
})
},
startPoll() {
this.stopPoll()
this.pollTimer = setInterval(() => {
if (this.pollLeft <= 0) { this.stopPoll(); return }
this.pollLeft -= 1
this.refresh(false)
}, 5000)
},
stopPoll() {
if (this.pollTimer) { clearInterval(this.pollTimer); this.pollTimer = null }
},
// xuchao: 取实时核销码并渲染;CNPC 侧 60 秒滚动,前端每 55 秒预取刷新
loadCheckCode() {
const orderNo = this.voucher.orderNo || this.orderNo
if (!orderNo) return
this.$u.api.voucherCheckCode({ orderNo, phone: this.phone }).then(res => {
if (!bizOk(res)) { uni.showToast({ title: bizMsg(res) || '获取核销码失败', icon: 'none' }); return }
const d = bizData(res)
this.qrText = d.checkCode || ''
this.$nextTick(() => this.renderQr())
this.startCheckTimer()
}).catch(() => {})
},
startCheckTimer() {
this.stopCheckTimer()
this.checkTimer = setInterval(() => this.loadCheckCode(), 55000)
},
stopCheckTimer() {
if (this.checkTimer) { clearInterval(this.checkTimer); this.checkTimer = null }
},
// xuchao: 核销码字符串→二维码渲染;qrcode.min.js 为本地静态资源(static/js/),动态注入一次
renderQr() {
if (this.isQrImage) return
if (!this.qrText) return
const draw = () => {
if (typeof document === 'undefined' || typeof window.QRCode === 'undefined') return
const el = document.getElementById('petrol-qr')
if (!el) return
el.innerHTML = ''
// eslint-disable-next-line no-new
new window.QRCode(el, {
text: this.qrText,
width: 200, height: 200, colorDark: '#1a1818', colorLight: '#ffffff',
correctLevel: window.QRCode.CorrectLevel.H
})
}
if (typeof window !== 'undefined' && window.QRCode) { draw(); return }
if (typeof document === 'undefined') return
const s = document.createElement('script')
s.src = '/static/js/qrcode.min.js'
s.onload = draw
document.head.appendChild(s)
},
goConfirm() {
uni.navigateTo({ url: '/pages/petrol/orderConfirm' + (this.status === 0 ? '?orderNo=' + this.voucher.orderNo : '') })
},
fmtTime(t) {
if (!t) return '-'
return String(t).replace('T', ' ').slice(0, 16)
},
goBack() {
const pages = getCurrentPages()
if (pages.length > 1) {
uni.navigateBack()
} else {
uni.reLaunch({ url: '/pages/index/index' })
}
}
}
}
</script>
<style>
.page { min-height: 100vh; background-color: #f2f2f7; padding-bottom: 24px; }
.nav-bar { display: flex; align-items: center; height: 56px; background-color: #ffffff; border-bottom: 1px solid #f0f0f0; position: relative; }
.nav-back { position: absolute; left: 12px; top: 0; bottom: 0; display: flex; align-items: center; }
.nav-title { flex: 1; text-align: center; font-size: 17px; font-weight: 600; color: #1a1818; padding: 0 56px; }
.qrbox { position: relative; background-color: #ffffff; border-radius: 8px; margin: 12px; padding: 28px 20px 18px; text-align: center; }
.qr-canvas { display: inline-block; }
.qr-canvas img, .qr-canvas canvas { display: block; }
.qr-img { width: 200px; }
.qr-tip { font-size: 12px; color: #999999; margin-top: 12px; }
.card { background-color: #ffffff; border-radius: 8px; margin: 12px; padding: 4px 16px 12px; }
.card-title { font-size: 15px; font-weight: 600; color: #1a1818; padding: 14px 0 2px; }
.kv { display: flex; justify-content: space-between; align-items: center; font-size: 13px; padding: 11px 0; border-bottom: 1px solid #f2f2f7; }
.kv:last-child { border-bottom: none; }
.k { color: #999999; flex-shrink: 0; margin-right: 12px; }
.v { color: #1a1818; text-align: right; word-break: break-all; }
.ol-item { font-size: 13px; color: #666666; line-height: 1.8; }
.issuing { background-color: #ffffff; border-radius: 8px; margin: 12px; padding: 46px 20px 26px; text-align: center; }
.spinner { width: 44px; height: 44px; border: 3px solid #dbe9ff; border-top-color: #0088ff; border-radius: 50%; margin: 0 auto 16px; animation: spin 1s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
.issuing-t { font-size: 16px; color: #1a1818; font-weight: 600; margin-bottom: 6px; }
.issuing-d { font-size: 12px; color: #999999; line-height: 1.7; margin-bottom: 18px; }
.state-card { background-color: #ffffff; border-radius: 8px; margin: 12px; padding: 30px 20px; text-align: center; }
.state-t { font-size: 17px; font-weight: 600; color: #1a1818; margin-bottom: 8px; }
.state-d { font-size: 13px; color: #999999; line-height: 1.7; margin-bottom: 20px; }
.btn-main { height: 44px; border-radius: 8px; background-color: #0088ff; color: #ffffff; font-size: 15px; display: flex; align-items: center; justify-content: center; }
.btn-line { height: 42px; border-radius: 8px; border: 1px solid #0088ff; color: #0088ff; font-size: 15px; display: flex; align-items: center; justify-content: center; background-color: #ffffff; margin: 0 30px; }
.issuing .btn-line { margin: 0; }
</style>