Files
property-resident-side/property-uniapp-project/pageSubPack/payment-list/yinlianPayTn.vue
2026-09-21 14:07:37 +08:00

464 lines
14 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="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>