Files
hhrs/pages/petrol/orderConfirm.vue
T

242 lines
11 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>
<!-- 券面 -->
<view class="ticket">
<view class="ticket-amt">200元 <text class="ticket-amt-sub">汽油电子券</text></view>
<view class="ticket-dsc">{{ voucherName }} | 收银台扫码核销</view>
<text class="ticket-wm">90天</text>
</view>
<!-- 订单要素 -->
<view class="card">
<view class="kv"><text class="k">券名称</text><text class="v">{{ voucherName }}</text></view>
<view class="kv"><text class="k">有效期</text><text class="v">购买日起 90 天</text></view>
<view class="kv"><text class="k">购买数量</text><text class="v">1 张</text></view>
<view class="kv"><text class="k">售价</text><text class="v">¥{{ priceText }}</text></view>
<view class="kv"><text class="k">购买账户</text><text class="v">{{ maskedPhone }}</text></view>
</view>
<!-- 支付方式 -->
<view class="card">
<view class="card-title">支付方式</view>
<view class="payrow">
<image class="pay-logo" src="https://zhihuiyou.oss-cn-shenzhen.aliyuncs.com/hhrs/hhrs_ysf_logo.png" mode="aspectFit" />
<text class="pay-name">云闪付 App 安全支付</text>
<view class="pay-radio"></view>
</view>
</view>
<view class="foot-note">券数量有限,售完即止;兑换后不可退换。</view>
<view class="footbar">
<view class="foot-price"><text class="foot-price-label">合计 </text>¥{{ priceText }}</view>
<view class="foot-btn" :class="{ disabled: submitting }" @click="onConfirmPay">{{ submitting ? '支付中…' : '确认支付' }}</view>
</view>
<!-- xuchao: 非云闪付环境拦截弹窗,必须点「我知道了」关闭(决策 11) -->
<view class="modal-mask" v-if="guideModal" @click.stop>
<view class="modal-box">
<view class="modal-text">{{ guideText }}</view>
<view class="modal-ok" @click="guideModal = false">我知道了</view>
</view>
</view>
</view>
</template>
<script>
import {
VOUCHER_NAME, VOUCHER_PRICE_TEXT, VOUCHER_GUIDE_TEXT, VOUCHER_CANCEL_TEXT,
bizOk, bizMsg, bizData
} from '@/common/petrolVoucher.js'
export default {
data() {
return {
phone: '',
orderNo: '',
submitting: false,
guideModal: false,
guideText: VOUCHER_GUIDE_TEXT,
voucherName: VOUCHER_NAME,
priceText: VOUCHER_PRICE_TEXT
}
},
computed: {
maskedPhone() {
if (!this.phone || this.phone.length !== 11) return this.phone || ''
return this.phone.slice(0, 3) + '****' + this.phone.slice(7)
}
},
onLoad(options) {
this.phone = uni.getStorageSync('phone') || ''
if (!this.phone) {
// xuchao: 未登录先看登录,回跳本流程从门店页重新进入(决策 15)
uni.redirectTo({ url: '/pages/index/login?redirect=' + encodeURIComponent('/pages/venue/detail?id=851219003520709&type=2') })
return
}
this.loadUpSdk() // xuchao: 运行时兜底注入(静态标签已在发布脚本注入产物 index.html,见 h5-release.sh)
if (options.orderNo) this.orderNo = options.orderNo
this.initFromServer()
},
methods: {
// xuchao: upsdk 运行时注入并返回就绪 Promise(同一加载只发起一次;超时/失败 resolve(false))。
// 不能依赖 index.html 静态标签——HBuilderX 命令行构建的 H5 模板不吃根 index.html(会丢标签)。
loadUpSdk(timeout = 9000) {
if (typeof window === 'undefined' || typeof document === 'undefined') return Promise.resolve(false)
if (window.upsdk) return Promise.resolve(true)
if (this._upSdkPromise) return this._upSdkPromise
const loading = new Promise(resolve => {
let done = false
const finish = v => { if (!done) { done = true; resolve(v) } }
const exists = document.querySelector('script[data-upsdk]')
if (exists) {
// main.js 已预热注入:轮询等 window.upsdk 出现(避免重复插标签)
const t0 = Date.now()
const timer = setInterval(() => {
if (window.upsdk) { clearInterval(timer); finish(true) }
else if (Date.now() - t0 > timeout) { clearInterval(timer); finish(false) }
}, 200)
return
}
const s = document.createElement('script')
s.src = 'https://open.95516.com/s/open/js/upsdk.js'
s.setAttribute('data-upsdk', '1')
s.onload = () => finish(!!window.upsdk)
s.onerror = () => finish(false)
document.head.appendChild(s)
setTimeout(() => finish(!!window.upsdk), timeout)
})
this._upSdkPromise = loading
return loading
},
// xuchao: 价格展示以服务端 latest 为准;已有待支付单自动续用;已发券直接跳券详情
initFromServer() {
this.$u.api.voucherLatest({ phone: this.phone }).then(res => {
if (!bizOk(res)) return
const d = bizData(res)
if (d.price) this.priceText = (d.price / 100).toFixed(2)
if (!this.orderNo && d.statusCode === 0 && d.orderNo) {
this.orderNo = d.orderNo
if (d.amount) this.priceText = (d.amount / 100).toFixed(2)
}
if (d.statusCode === 2) {
uni.redirectTo({ url: '/pages/petrol/voucherDetail?orderNo=' + (d.orderNo || '') })
}
}).catch(() => {})
},
onConfirmPay() {
if (this.submitting) return
// xuchao: 不做容器/白名单前置拦截,直接发起;云闪付控件在站外唤不起时
// 由 pay 的 fail 回调统一弹「去云闪付」指引模态(决策 11 修订:去白名单)
this.submitting = true
this.doPay()
},
doPay() {
const step = this.orderNo
? Promise.resolve({ orderNo: this.orderNo })
: this.$u.api.voucherCreate({ phone: this.phone }).then(res => {
if (!bizOk(res)) throw new Error(bizMsg(res) || '下单失败')
return bizData(res)
})
step.then(d => {
this.orderNo = d.orderNo
return this.$u.api.voucherPrepay({ orderNo: this.orderNo, phone: this.phone })
}).then(res => {
if (!bizOk(res)) throw new Error(bizMsg(res) || '发起支付失败')
const pre = bizData(res)
const tn = pre.extendMap && pre.extendMap.resData
if (!tn) throw new Error('未获取到支付凭证')
this.launchUnionPay(tn)
}).catch(err => {
this.submitting = false
uni.showToast({ title: (err && err.message) || '操作失败,请稍后再试', icon: 'none' })
})
},
// xuchao: 云闪付控件唤起。先等 SDK 就绪(修复首点无反应:脚本异步加载未完成时
// 旧逻辑只重试 4 秒且静默;就绪判定收敛到 loadUpSdk 的 Promise)。
async launchUnionPay(tn) {
const ready = await this.loadUpSdk()
if (!ready) {
this.submitting = false
// xuchao: 临时诊断(定位弹窗真因后移除):SDK 加载不出来时展示 UA 片段
this.guideText = '支付SDK未能加载[诊断] UA=' + (typeof navigator !== 'undefined' ? String(navigator.userAgent).slice(0, 120) : 'N/A')
this.guideModal = true
return
}
window.upsdk.pluginReady(() => {
window.upsdk.pay({
tn: tn,
success: () => {
uni.redirectTo({ url: '/pages/petrol/voucherDetail?orderNo=' + this.orderNo })
},
fail: (err) => {
// xuchao: 控件回调失败(含用户取消)。按 xuchao 要求文案定为取消提示;
// [诊断] 行临时保留采集真实错误码,区分"取消"与"容器不支持"后移除并分文案
this.submitting = false
this.guideText = VOUCHER_CANCEL_TEXT + '\n[诊断] ' + JSON.stringify(err || {})
this.guideModal = true
}
})
})
// 控件无异步结果时兜底:3 秒后恢复按钮(支付成功路径以回跳/轮询为准)
setTimeout(() => { this.submitting = false }, 3000)
},
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: 80px; }
.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; }
.ticket { position: relative; overflow: hidden; background: linear-gradient(135deg, #0d6fe0, #2f9bff); border-radius: 8px; margin: 12px; padding: 16px; color: #ffffff; }
.ticket-amt { font-size: 26px; font-weight: 700; }
.ticket-amt-sub { font-size: 13px; font-weight: 400; opacity: 0.9; margin-left: 4px; }
.ticket-dsc { font-size: 12px; opacity: 0.9; margin-top: 6px; }
.ticket-wm { position: absolute; right: 4px; bottom: -14px; font-size: 60px; font-weight: 800; opacity: 0.1; }
.card { background-color: #ffffff; border-radius: 8px; margin: 12px; padding: 4px 16px 8px; }
.card-title { font-size: 15px; font-weight: 600; color: #1a1818; padding: 12px 0 4px; }
.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; }
.payrow { display: flex; align-items: center; padding: 6px 0 12px; }
.pay-logo { width: 26px; height: 26px; border-radius: 5px; margin-right: 10px; flex-shrink: 0; }
.pay-name { flex: 1; font-size: 14px; color: #1a1818; }
.pay-radio { width: 18px; height: 18px; border-radius: 50%; background-color: #0088ff; position: relative; }
.pay-radio::after { content: ''; position: absolute; left: 5px; top: 5px; width: 8px; height: 8px; border-radius: 50%; background-color: #ffffff; }
.foot-note { font-size: 12px; color: #999999; margin: 4px 16px; line-height: 1.6; }
.footbar { position: fixed; left: 0; right: 0; bottom: 0; height: 64px; background-color: #ffffff; border-top: 1px solid #f0f0f0; display: flex; align-items: center; padding: 0 16px; z-index: 5; }
.foot-price { font-size: 20px; font-weight: 600; color: #ff4d4f; }
.foot-price-label { font-size: 12px; font-weight: 400; color: #666666; }
.foot-btn { flex: 1; margin-left: 14px; height: 44px; border-radius: 8px; background-color: #0088ff; color: #ffffff; font-size: 16px; display: flex; align-items: center; justify-content: center; }
.foot-btn.disabled { background-color: #c8cdd4; }
.modal-mask { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(0, 0, 0, 0.45); display: flex; align-items: center; justify-content: center; z-index: 99; }
.modal-box { width: 300px; background-color: #ffffff; border-radius: 12px; overflow: hidden; }
.modal-text { padding: 24px 20px 20px; font-size: 14px; color: #1a1818; line-height: 1.7; text-align: center; }
.modal-ok { height: 48px; border-top: 1px solid #f2f2f7; color: #0088ff; font-size: 16px; font-weight: 600; display: flex; align-items: center; justify-content: center; }
</style>