464 lines
14 KiB
Vue
464 lines
14 KiB
Vue
<template>
|
||
<view class="container">
|
||
|
||
<!-- 内容区域 -->
|
||
<view class="content">
|
||
<!-- 金额区域 -->
|
||
<view class="amount-box">
|
||
<view class="amount-text">缴费金额</view>
|
||
<text class="amount"> <text style="font-size: 32rpx;"> ¥ </text>{{ amount }}</text>
|
||
<view class="countdown-wrap">
|
||
<text class="countdown">剩余支付时间 {{ countdownTime }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 支付方式列表 -->
|
||
<view class="tip">请选择支付方式</view>
|
||
<view class="pay-list">
|
||
<view class="pay-item" @click="selectPayType('wx')">
|
||
<view class="left">
|
||
<image src="https://wuye.ckdzkj.com/images/home/wechat.png" class="icon" mode="widthFix" />
|
||
<view class="pay-info">
|
||
<text class="pay-name">微信</text>
|
||
</view>
|
||
</view>
|
||
<view class="radio" :class="{ active: selectedType === 'wx' }" />
|
||
</view>
|
||
|
||
<view class="pay-item" @click="selectPayType('alipay')">
|
||
<view class="left">
|
||
<image src="https://wuye.ckdzkj.com/images/home/alipay.png" class="icon" mode="widthFix" />
|
||
<view class="pay-info">
|
||
<text class="pay-name">支付宝</text>
|
||
</view>
|
||
</view>
|
||
<view class="radio" :class="{ active: selectedType === 'alipay' }" />
|
||
</view>
|
||
</view>
|
||
|
||
|
||
</view>
|
||
|
||
<!-- 底部支付按钮 -->
|
||
<view class="btn-wrap">
|
||
<view class="pay-btn" @click="handlePayClick">
|
||
<text class="btn-text">立即支付 {{ amount }} 元</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, onUnmounted } from 'vue'
|
||
import { onLoad, onShow } from '@dcloudio/uni-app'
|
||
import { getUnionPayTn } from '/pageSubPack/api/apiSub.js'
|
||
import { getPushSingle } from '/pageSubPack/api/api.js'
|
||
|
||
// 银联商务全民付为 App 原生 UTS 插件,不支持小程序/H5,必须条件编译引入,否则其他端构建失败
|
||
// #ifdef APP-PLUS
|
||
import { upPayWithChannel, upPayRegistry, setPayEnviroment } from '@/uni_modules/xh-uppay'
|
||
// #endif
|
||
|
||
// 银联商务全民付收银台:微信 / 支付宝
|
||
|
||
const amount = ref('0.00')
|
||
const countdownTime = ref('10:00')
|
||
let timer = null
|
||
const payData = ref({})
|
||
|
||
onLoad(() => {
|
||
const data = uni.getStorageSync('payData')
|
||
if (data) {
|
||
payData.value = data
|
||
amount.value = Number(data.amount || 0).toFixed(2)
|
||
}
|
||
startCountdown()
|
||
})
|
||
|
||
onUnmounted(() => {
|
||
if (timer) clearInterval(timer)
|
||
})
|
||
|
||
const startCountdown = () => {
|
||
let [min, sec] = countdownTime.value.split(':')
|
||
let totalSec = parseInt(min) * 60 + parseInt(sec)
|
||
timer = setInterval(() => {
|
||
totalSec--
|
||
if (totalSec <= 0) {
|
||
clearInterval(timer)
|
||
uni.showToast({ title: '支付超时', icon: 'none' })
|
||
setTimeout(() => uni.navigateBack(), 1500)
|
||
return
|
||
}
|
||
const newMin = Math.floor(totalSec / 60).toString().padStart(2, '0')
|
||
const newSec = (totalSec % 60).toString().padStart(2, '0')
|
||
countdownTime.value = `${newMin}:${newSec}`
|
||
}, 1000)
|
||
}
|
||
|
||
const selectedType = ref('wx')
|
||
|
||
const selectPayType = (type) => {
|
||
selectedType.value = type
|
||
// #ifndef APP-HARMONY
|
||
uni.vibrateShort({ type: 'light' })
|
||
// #endif
|
||
}
|
||
|
||
// 银联商务渠道号:01 微信原生支付 / 02 支付宝支付 / 03 支付宝小程序 / 11 支付宝2.0
|
||
// 渠道号必须与后端下单的产品对应(响应里的 targetSys),对不上会静默失败:
|
||
// 微信 targetSys = WXPay → 01
|
||
// 支付宝 targetSys = Alipay 2.0 → 11(插件 Android 端 '11' 分支是空壳,未实现)
|
||
// 支付宝 targetSys = 支付宝小程序 → 03(需后端改下这个小程序产品才会生效)
|
||
const CHANNEL_MAP = { wx: '01', alipay: '03' }
|
||
|
||
// 传给后端的支付方式,沿用原有枚举 0=微信 / 1=支付宝
|
||
const PAY_TYPE_MAP = { wx: '0', alipay: '1' }
|
||
|
||
// 银联环境:0 正式 / 1 测试。必须与后端下单时所用的银联环境一致,
|
||
// 这是后端的配置项、不是前端构建环境;联调报「交易不存在/签名错误」多半是这里对不上。
|
||
// 注意:这里只对 iOS 生效,Android 要改插件 utssdk/app-android/AndroidManifest.xml 的 CurrentEnvironment(PROD/TEST)。
|
||
const UNIONPAY_ENV = process.env.NODE_ENV === 'development' ? 1 : 0
|
||
|
||
// 以下常量只在 iOS 生效(Android 的 upPayRegistry 是空实现),且每一项都必须与
|
||
// xh-uppay/utssdk/app-ios/info.plist 里 CFBundleURLTypes 的取值完全一致,
|
||
// 否则 iOS 上支付完成后回不到本应用。
|
||
const WX_APPID = 'wx84b02b2ec80715f4' // 需同步改 info.plist 的 weixin scheme(现在是 demo 值)
|
||
// 存疑:暂用项目里微信分享的 Universal Link,但银联商务支付链路可能要求用他们备案的域名,需与其确认
|
||
const WX_UNIVERSAL_LINK = 'https://verification.ckdzkj.com/uni-universallinks/__UNI__29C3D60/'
|
||
const SCHEME_CLOUD = 'unifyPayDemo' // info.plist 中 ucloud(云闪付回调,暂不用云闪付)
|
||
const SCHEME_ALI_MINI = 'qmfpppay' // info.plist 中 aliminipay(支付宝小程序回调)
|
||
const SCHEME_ALI = 'umsunifyalisafepay' // info.plist 中 alisafepay(支付宝回调)
|
||
|
||
const handlePayClick = () => {
|
||
handlePay()
|
||
}
|
||
|
||
// 账单明细 id:接口要求 string,多个账单逗号拼接,兼容单个 billId 的旧场景
|
||
const toBillIds = (billIds, billId) => {
|
||
if (Array.isArray(billIds) && billIds.length) {
|
||
return billIds.filter(Boolean).join(',')
|
||
}
|
||
if (typeof billIds === 'string' && billIds) {
|
||
return billIds
|
||
}
|
||
return billId ? String(billId) : ''
|
||
}
|
||
|
||
const handlePay = async () => {
|
||
try {
|
||
uni.showLoading({ title: '发起支付中' })
|
||
|
||
const billIds = toBillIds(payData.value.billIds, payData.value.billId)
|
||
if (!billIds) {
|
||
uni.hideLoading()
|
||
uni.showToast({ title: '账单信息异常,请返回重新选择', icon: 'none' })
|
||
return
|
||
}
|
||
|
||
// deviceCode/deviceType 仅水电表充值传;无值即物业费/停车费/垃圾处理费
|
||
const payload = {
|
||
amount: Number(amount.value),
|
||
orderDesc: (payData.value.type || '缴费') + '缴费',
|
||
residentUserId: payData.value.residentUserId || uni.getStorageSync('userId'),
|
||
payType: PAY_TYPE_MAP[selectedType.value],
|
||
billIds
|
||
}
|
||
if (payData.value.deviceCode) payload.deviceCode = payData.value.deviceCode
|
||
if (payData.value.deviceType) payload.deviceType = payData.value.deviceType
|
||
|
||
const res = await getUnionPayTn(payload)
|
||
console.log('支付接口返回:', JSON.stringify(res.data))
|
||
uni.hideLoading()
|
||
|
||
if (res.code !== 200 || !res.data) {
|
||
uni.showToast({ title: res.msg || '获取支付参数失败', icon: 'none' })
|
||
return
|
||
}
|
||
|
||
// 后端下发银联商务的 appPayRequest,整串 JSON 原样交给 SDK,不要拆开解析
|
||
const data = res.data
|
||
const payDataStr = typeof data === 'string'
|
||
? data
|
||
: (data.appPayRequest || data.appRequest || '')
|
||
|
||
if (!payDataStr || typeof payDataStr !== 'string' || !payDataStr.trim().startsWith('{')) {
|
||
console.error('未获取到有效的 appPayRequest,后端返回:', JSON.stringify(data))
|
||
uni.showToast({ title: '支付参数异常,请联系客服', icon: 'none' })
|
||
return
|
||
}
|
||
|
||
invokeUnionPay(payDataStr, CHANNEL_MAP[selectedType.value])
|
||
|
||
} catch (err) {
|
||
uni.hideLoading()
|
||
console.log('支付异常', err)
|
||
uni.showToast({ title: '支付请求异常', icon: 'none' })
|
||
}
|
||
}
|
||
|
||
// 银联商务综合支付:拉起微信 / 支付宝完成付款
|
||
const invokeUnionPay = (payData, channel) => {
|
||
// #ifdef APP-PLUS
|
||
ensureRegistered()
|
||
paying = true
|
||
upPayWithChannel({ payData, channel }, (code, info) => {
|
||
handlePayResult(String(code), info)
|
||
})
|
||
// #endif
|
||
|
||
// #ifndef APP-PLUS
|
||
uni.showToast({ title: '当前端暂不支持银联支付', icon: 'none' })
|
||
// #endif
|
||
}
|
||
|
||
// 注册和支付环境设置都只对 iOS 生效;Android 调 setPayEnviroment 只会刷警告,
|
||
// Android 的环境要改插件 utssdk/app-android/AndroidManifest.xml 里的 CurrentEnvironment。
|
||
let registered = false
|
||
const ensureRegistered = () => {
|
||
if (registered) return
|
||
registered = true
|
||
// #ifdef APP-PLUS
|
||
if (uni.getSystemInfoSync().platform === 'ios') {
|
||
setPayEnviroment(UNIONPAY_ENV)
|
||
upPayRegistry({
|
||
appId: WX_APPID,
|
||
link: WX_UNIVERSAL_LINK,
|
||
cloudUrl: SCHEME_CLOUD,
|
||
aliminipayUrl: SCHEME_ALI_MINI,
|
||
aliUrl: SCHEME_ALI
|
||
})
|
||
}
|
||
// #endif
|
||
}
|
||
|
||
let paying = false // 支付请求是否已发出
|
||
let payDone = false // 是否已给出结果提示,避免重复
|
||
|
||
// Android 端插件不触发回调,用户从微信/支付宝切回来时靠 onShow 兜底;
|
||
// iOS 有回调,交给 handlePayResult 处理,这里不插手。
|
||
onShow(() => {
|
||
if (!paying) return
|
||
if (uni.getSystemInfoSync().platform === 'android') {
|
||
paying = false
|
||
notifyPayPending()
|
||
}
|
||
})
|
||
|
||
// 结果码见 xh-uppay readme:0000 只表示「支付请求发送成功」,不代表付款成功,
|
||
// 真实结果以银联商务异步通知到后端的结果为准。所以这里不做成功判定,
|
||
// 统一引导用户回账单列表看实际状态。
|
||
const handlePayResult = (code, info) => {
|
||
console.log('银联支付回调', code, JSON.stringify(info))
|
||
paying = false
|
||
switch (code) {
|
||
case '0000':
|
||
case '2001': // 订单处理中,结果未知(有可能已支付成功)
|
||
notifyPayPending()
|
||
break
|
||
case '1000':
|
||
payDone = true
|
||
uni.showToast({ title: '支付已取消', icon: 'none' })
|
||
break
|
||
case '1001':
|
||
payDone = true
|
||
uni.showToast({ title: '支付参数错误', icon: 'none' })
|
||
break
|
||
case '1002':
|
||
payDone = true
|
||
uni.showToast({ title: '网络异常,请重试', icon: 'none' })
|
||
break
|
||
case '1003':
|
||
payDone = true
|
||
uni.showToast({ title: '未安装对应的支付客户端', icon: 'none' })
|
||
break
|
||
default:
|
||
payDone = true
|
||
uni.showToast({ title: (info && info.resultMsg) || '支付失败', icon: 'none' })
|
||
}
|
||
}
|
||
|
||
const notifyPayPending = () => {
|
||
if (payDone) return
|
||
payDone = true
|
||
uni.showToast({ title: '支付结果确认中,请稍后在账单中查看', icon: 'none' })
|
||
setTimeout(() => uni.navigateBack(), 2000)
|
||
}
|
||
|
||
// 支付成功流转:等后端提供「订单查询」接口后,查单确认已支付时再调用这里。
|
||
// 目前 SDK 的结果码不足以判定付款成功,所以暂时没有调用点。
|
||
const paySuccess = () => {
|
||
sendPaySuccessPush()
|
||
uni.showToast({ title: '支付成功', icon: 'success' })
|
||
uni.setStorageSync('paySuccess', true)
|
||
setTimeout(() => {
|
||
uni.navigateTo({
|
||
url: '/pageSubPack/payment-list/addPaymentSuccess?type=pay'
|
||
})
|
||
}, 1500)
|
||
}
|
||
|
||
const sendPaySuccessPush = () => {
|
||
const userId = uni.getStorageSync('userId')
|
||
if (userId) {
|
||
getPushSingle({
|
||
userId: userId,
|
||
channel: 'resident',
|
||
title: '缴费成功',
|
||
content: `您已成功缴纳${payData.value.type || ''}费用 ¥${amount.value}元`
|
||
}).catch(err => console.log('推送发送失败', err))
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
/* 全局背景 */
|
||
.container {
|
||
display: flex;
|
||
flex-direction: column;
|
||
width: 100%;
|
||
min-height: 100vh;
|
||
background-color: #F5F6F9;
|
||
padding: 0 30rpx;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
/* 状态栏 */
|
||
.status-bar {
|
||
width: 100%;
|
||
height: var(--status-bar-height);
|
||
}
|
||
|
||
/* 内容区域 */
|
||
.content {
|
||
/* flex: 1; */
|
||
}
|
||
|
||
/* 金额卡片 */
|
||
.amount-box {
|
||
background: #ffffff;
|
||
border-radius: 20rpx;
|
||
padding: 50rpx 0;
|
||
text-align: center;
|
||
margin: 30rpx 0;
|
||
}
|
||
|
||
.amount-text {
|
||
font-size: 28rpx;
|
||
color: #666;
|
||
margin-bottom: 16rpx;
|
||
}
|
||
|
||
.amount {
|
||
display: block;
|
||
font-size: 52rpx;
|
||
font-weight: bold;
|
||
color: #333;
|
||
}
|
||
|
||
/* 倒计时样式 */
|
||
.countdown-wrap {
|
||
display: inline-block;
|
||
margin-top: 24rpx;
|
||
padding: 8rpx 24rpx;
|
||
background: #F4F4F4;
|
||
border-radius: 30rpx;
|
||
}
|
||
|
||
.countdown {
|
||
font-size: 28rpx;
|
||
color: #000;
|
||
}
|
||
|
||
/* 支付方式卡片 */
|
||
.pay-list {
|
||
background: #ffffff;
|
||
border-radius: 20rpx;
|
||
padding: 0 30rpx;
|
||
margin-bottom: 30rpx;
|
||
}
|
||
|
||
.pay-item {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 36rpx 0;
|
||
border-bottom: 1rpx solid #f2f2f2;
|
||
}
|
||
|
||
.pay-item:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.left {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.icon {
|
||
width: 56rpx;
|
||
height: 56rpx;
|
||
margin-right: 24rpx;
|
||
}
|
||
|
||
.pay-name {
|
||
font-size: 32rpx;
|
||
color: #333;
|
||
font-weight: 500;
|
||
}
|
||
|
||
/* 单选按钮 — 对勾样式 */
|
||
.radio {
|
||
width: 44rpx;
|
||
height: 44rpx;
|
||
border-radius: 50%;
|
||
border: 3rpx solid #d0d0d0;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.radio.active {
|
||
background-color: #2F77FD;
|
||
border-color: #2F77FD;
|
||
}
|
||
|
||
.radio.active::after {
|
||
content: '';
|
||
width: 12rpx;
|
||
height: 22rpx;
|
||
border: 4rpx solid #fff;
|
||
border-left: 0;
|
||
border-top: 0;
|
||
transform: rotate(45deg) translate(-2rpx, -2rpx);
|
||
}
|
||
|
||
/* 提示 */
|
||
.tip {
|
||
font-size: 32rpx;
|
||
color: #333;
|
||
font-weight: 500;
|
||
text-align: left;
|
||
padding: 20rpx 0;
|
||
}
|
||
|
||
/* 底部按钮区域 */
|
||
.btn-wrap {
|
||
padding: 30rpx 0 60rpx;
|
||
}
|
||
|
||
/* 底部支付按钮 */
|
||
.pay-btn {
|
||
width: 100%;
|
||
height: 96rpx;
|
||
line-height: 96rpx;
|
||
background: #2F77FD;
|
||
color: #fff;
|
||
text-align: center;
|
||
font-size: 34rpx;
|
||
font-weight: bold;
|
||
border-radius: 8rpx;
|
||
box-shadow: 0 6rpx 20rpx rgba(47, 119, 253, 0.3);
|
||
}
|
||
</style> |