《Uniapp 实现 H5 聚合支付:支持微信支付和支付宝扫码支付》

前言:最近接到一个需求,做一个收银台,实现效果:生成一个商家支付码,用户无论是用微信还是支付宝扫码,都可以完成支付。遂采用Uniapp:跨平台开发框架,支持一次开发,多端部署。H5:通过网页实现支付功能,兼容性强,无需依赖原生环境。
实现步骤:1. 前端新建一个uniapp项目,开发完成后,打包发布成H5,会生成一个链接;2. 用链接生成一个二维码即可。

一、项目目录介绍

1.1 新建一个uniapp项目,运行到浏览器进行查看,以下是项目各目录介绍:

在这里插入图片描述

二、代码步骤详细讲解

2.1 支付页面样式

(1)数字键盘组件:咱们平常买东西扫码支付时,进入输入金额的支付页面,会看到下面有数字键盘,直接点击数字即可输入金额,而不是使用手机自带的键盘弹起,这里的键盘专门封装一个组件,直接引入到主页面,提升代码的可维护性。

  • keyboard.vue
<template>
	<view class="keyboard-box">
		<view class="left">
			<view class="item" @click="typing(1)">1</view>
			<view class="item" @click="typing(2)">2</view>
			<view class="item" @click="typing(3)">3</view>
			<view class="item" @click="typing(4)">4</view>
			<view class="item" @click="typing(5)">5</view>
			<view class="item" @click="typing(6)">6</view>
			<view class="item" @click="typing(7)">7</view>
			<view class="item" @click="typing(8)">8</view>
			<view class="item" @click="typing(9)">9</view>
			<view class="item zero" @click="typing(0)">0</view>
			<view class="item point" @click="typing('.')">.</view>
		</view>
		<view class="right">
			<view class="del" @click="typing('')">
				<image src="../static/img/payment/del_icon.png" mode="widthFix"></image>
			</view>
			<view class="confirm" :style="{backgroundColor: userAgentCurrent()==='wechat' ? '#09BA08' : userAgentCurrent()==='alipay' ? '#00AAEE' : '#266F9C',  opacity: opacity}" @click="confirm">
				<image :src="userAgentCurrent()==='wechat' ? wechatSrc : userAgentCurrent()==='alipay' ? alipaySrc : yunpaySrc" mode="widthFix"></image>
				<text>支付</text>
			</view>
		</view>
	</view>
</template>
<script>
export default {
	name: 'keyboard',
	props: {
		value: '',
		inter: {
			default: 5
		},
		decimal: {
			default: 2
		},
		opacity: {
			default: 1
		}
	},
	data() {
		return {
			/*value 在组件中的值*/
			val: '',
			aIllegal: ['00', '01', '02', '03', '04', '05', '06', '07', '08', '09', '0..', '.'],
			
			wechatSrc: require('../static/img/payment/wx_icon.png'),
			alipaySrc: require('../static/img/payment/Alipay_icon.png'),
			yunpaySrc: require('../static/img/payment/Yun_icon.png')
		}
	},
	created() {

	},
	methods: {
		confirm(){
			this.$emit('confirm')
		},

		/*输入*/
		typing(value) {
			/*如果是点击删除*/
			if (value === '') {
				this.del()
			}
			/*保存旧的值*/
			let oldValue = this.val
			/*获取新的值*/
			this.val = this.val + value
			/*检验新值, 如果没有通过检测, 恢复值*/
			if (!this.passCheck(this.val)) {
				this.val = oldValue
				return
			}
			/*为了让外界同步输入, 需要发送事件*/
			this.notify()
		},
		/*判读是否需要加0*/
		toCompletion() {
			let list = this.value.split('.')
			if (typeof list[1] === 'undefined') {
				if (this.val !== '') {
					this.val = this.val + '.'
					this.completion(this.decimal)
				}
			} else {
				if (list[1].length < this.decimal) {
					this.completion(this.decimal - list[1].length)
				}
			}
		},
		completion(len) {
			let v = ''
			for (let i = 0; i < len; i++) {
				v = v + '0'
			}
			this.val = this.val + v
		},
		notify() {
			this.$u.vuex('$money',this.val)
		},
		del() {
			/*删除值并不会触发值的校验, 所以需要手动再触发一次*/
			this.val = this.val.slice(0, -1)
			this.notify()
		},
		
		passCheck(val) {
			/*验证规则*/
			let aRules = [this.illegalInput, this.illegalValue, this.accuracy]
			return aRules.every(item => {
				return item(val)
			})
		},
		illegalInput(val) {
			if (this.aIllegal.indexOf(val) > -1) {
				return false
			}
			return true
		},
		/*非法值*/
		illegalValue(val) {
			if (parseFloat(val) != val) {
				return false
			}
			return true
		},
		/*验证精度*/
		accuracy(val) {
			let v = val.split('.')
			if (v[0].length > this.inter) {
				return false
			}
			if (v[1] && v[1].length > this.decimal) {
				return false
			}
			return true
		}
	}
}
</script>
<style scoped lang="scss">
.keyboard-box {
	display: flex;
	justify-content: space-between;
	position: fixed;
	bottom: 0;
	width: 100%;
	background-color: #eee;
	.left {
		display: flex;
		flex-wrap:wrap;
		width: 75%;
		.item {
			width: 33.33%;
			height: 120rpx;
			line-height: 120rpx;
			text-align: center;
			font-size: 52rpx;
			background-color: #fff;

			border-radius: 10rpx;
			border: 6rpx solid #F1F1F1;
			box-sizing: border-box;
			
			&:active{
				background-color: #f2f2f2;
			}
		}
		.zero {
			width: 66.66%;
			text-align: center;
		}
	}
	.right {
		width: 25%;
		.del {
			display: flex;
			align-items: center;
			justify-content: center;
			height: 120rpx;
			border-radius: 10rpx;
			border: 6rpx solid #F1F1F1;
			box-sizing: border-box;
			background-color: #fff;
			
			image{
				width: 52rpx;
				height: 36rpx;
			}
		}
		.confirm {
			display: flex;
			flex-direction: column;
			align-items: center;
			justify-content: center;
			height: 360rpx;
			// background-color: #09BA08;
			color: #fff;
			font-size: 40rpx;
			border-radius: 10rpx;
			border: 6rpx solid #F1F1F1;
			box-sizing: border-box;
			&:active {
				opacity: .3;
			}
			
			image{
				width: 96rpx;
				height: 78rpx;
			}
			
			text{
				padding-top: 12rpx;
			}
		}
	}
}
</style>

(2)主页面:把数字键盘引入到主页面,以及主页面其他的样式设计,这个页面可根据自己的需求重新设计样式。

  • payment/index.vue
<template>
	<view class="content">
		<view class="card">
			<view class="title_bg">
				<image src="../../static/img/payment/home_icon.png" mode="widthFix"></image>
				<view class="title">
					<text>{{ $merchantName }}</text>
					<text>{{ $merchantName }}</text>
				</view>
			</view>
			<view class="title2">金额</view>
			<view class="amount_input">
				<view class="cursor"  v-if="!$money"></view>
				<view class="input_num" v-if="$money">{{ $money }}</view>
				<view class="input" v-else>请输入金额</view>
				<view class="dot"></view>
			</view>
			
			<view class="remark" @click="getRemark">添加付款备注</view>
		</view>
		<text class="remark_txt" v-if="remarkTxt">{{remarkTxt}}</text>
		<view class="tips">
			请核对以上金额、收款商户等信息与交易一致;经您确认支付的款 项,支付机构无法追回亦无赔偿义务。
		</view>
		
		<keyboard @confirm="submit" ref="keyboard" :opacity="opacity"></keyboard>
		
		
		<!-- 备注弹框 -->
		<uni-popup ref="popup" type="center" :animation="false">
			<view class="remark_pop">
				<view class="title">请填写付款说明</view>
				<view class="textarea_con">
					<textarea class="textarea" maxlength="30" v-model="remark" placeholder="请输入付款说明" placeholder-style="color: #a0a0a0;font-size: 14px;"></textarea>
				</view>
				<view class="btn">
					<button class="cancel_btn" @click="cancel">取消</button>
					<button class="confirm_btn" @click="confirm">确定</button>
				</view>
			</view>
		</uni-popup>
	</view>
</template>


<script>
import keyboard from '@/components/keyboard.vue'

export default {
	data() {
		return {
			remark: '',
			remarkTxt: '',
			opacity: 1,
		}
	},
	components: {
		keyboard
	},
	methods: {
		confirm(){
			this.$refs.popup.close()
			this.remarkTxt = this.remark
		},
		cancel(){
			this.$refs.popup.close()
		},
		getRemark(){
			this.remark = ''
			this.$refs.popup.open()
		},
	}
}
</script>

<style lang="scss">
page {
	background-color: #F5F6F8;
}
.content {
	padding-top: 24rpx;

	.card{
		width: 94%;
		height: 422rpx;
		background: #FFFFFF;
		border-radius: 8rpx 8rpx 8rpx 8rpx;
		display: flex;
		flex-direction: column;
		
		.title_bg{
			width: 100%;
			height: 150rpx;
			background-image: url('@/static/img/payment/title_bg.png');
			background-repeat: no-repeat;
			background-size: 100% 100%;
			display: flex;
			align-items: center;
			padding-left: 28rpx;
			
			image{
				width: 68rpx;
				height: 68rpx;
			}
			
			.title{
				display: flex;
				flex-direction: column;
				justify-content: space-evenly;
				padding-left: 24rpx;
				
				text:first-child{
					font-weight: 800;
					font-size: 32rpx;
					color: #232323;
				}
				
				text:last-child{
					font-weight: 400;
					font-size: 22rpx;
					color: #666666;
					padding-top: 12rpx;
				}
			}
		}
		
		.title2{
			font-weight: 500;
			font-size: 32rpx;
			color: #232323;
			padding: 24rpx 0 20rpx 28rpx;
		}
		
		.amount_input{
			height: 112rpx;
			display: flex;
			align-items: center;
			justify-content: space-between;
			padding: 0 42rpx 0 28rpx;
			
			.cursor {
				width: 4rpx;
				height: 50rpx;
				background-color: #5E79CB;
				margin-right: 10rpx;
				animation: flicker 1.2s infinite;
				@keyframes flicker {
					0%,
					100% {
						opacity: 0;
					}
			
					50% {
						opacity: 1;
					}
				}
			}
			
			.input{
				flex: 1;
				font-weight: 500;
				font-size: 40rpx;
				color: #999999;
			}
			.input_num{
				flex: 1;
				font-weight: 500;
				font-size: 40rpx;
				color: #232323;
			}
			
			.dot{
				width: 82rpx;
				height: 42rpx;
				text-align: left;
				font-weight: 400;
				font-size: 32rpx;
				color: #999999;
			}
		}
		
		.remark{
			font-weight: 500;
			font-size: 26rpx;
			color: #5E79CB;
			text-align: right;
			padding-right: 42rpx;
		}
	}
	
	.remark_txt{
		width: 92%;
		font-weight: 5400;
		font-size: 22rpx;
		color: #a4a4a4;
		line-height: 38rpx;
		text-align: left;
		margin-top: 20rpx;
	}
	
	.tips{
		width: 92%;
		font-weight: 500;
		font-size: 24rpx;
		color: #999999;
		line-height: 38rpx;
		text-align: left;
		margin-top: 20rpx;
	}
	
	
	
	.remark_pop{
		width: 620rpx;
		height: 460rpx;
		background-color: #fff;
		border-radius: 20rpx;
		display: flex;
		flex-direction: column;
		align-items: center;
		justify-content: space-between;
		
		.title{
			line-height: 100rpx;
			font-size: 28rpx;
			font-weight: 700;
		}
		
		.textarea_con{
			width: 90%;
			margin: 0 auto;
			height: 200rpx;
			border: 2rpx solid #c4c4c4;
			border-radius: 10rpx;
			padding: 32rpx;
			
			.textarea{
				width: 100%;
				height: 100%;
			}
		}
		
		
		.btn{
			display: flex;
			align-items: center;
			justify-content: space-between;
			width: 100%;
			height: 100rpx;
			margin-top: 32rpx;
			
			
			button{
				width: 50%;
				height: 100rpx;
				line-height: 100rpx;
				text-align: center;
				font-size: 28rpx;
				font-weight: 500;
				background-color: #fff;
				border-radius: none;
			}
			
			.cancel_btn{
				color: #373737;
			}
			
			.confirm_btn{
				color: #5E79CB;
			}
		}
	}
}
</style>

2.2 获取授权code

2.2.1 判断支付环境

:首次进入页面,在入口页面可判断当前环境,如果是微信内部打开,则调用微信授权登录,如果是支付宝内部打开,则调用支付宝授权。

  • 新建 userAgent.js
export const userAgentCurrent = function (){
	// 获取用户代理字符串
	const userAgent = navigator.userAgent.toLowerCase();
	
	// 判断是否在微信客户端
	if (userAgent.indexOf('micromessenger') !== -1) {
	  console.log('当前环境在微信客户端');
		return 'wechat'
	} 
	// 判断是否在支付宝客户端
	else if (userAgent.indexOf('alipay') !== -1) {
	  console.log('当前环境在支付宝客户端');
		return 'alipay'
	}
	// 判断是否在云闪付客户端
	else if (userAgent.indexOf('mybank') !== -1) {
	  console.log('当前环境在云闪付客户端');
		return 'yunpay'
	}
	// 其他情况
	else {
	  console.log('当前环境在其他客户端');
		return 'wechat'
	}
}
  • 引入到入口文件 App.vue
<script>
	import { wxAuthorize } from '@/utils/wxauth.js'
	import { aliAuthorize } from '@/utils/AlipayAuth.js'
	export default {
		onLaunch: function() {
			console.log('App Launch')
			
			switch (this.userAgentCurrent()){
				case "wechat":
					wxAuthorize()   // 微信网页授权
					break
				case "alipay":
					aliAuthorize()  // 支付宝授权
					break
				case "yunpay":
					console.log('当前是浏览器/云闪付')
					break
			}
		},
	}
</script>
2.2.2 微信授权

:我这里微信授权的时候有两种情况,其中一个厂家不需要我们获取微信的code,用厂家自己获取到的code进行授权即可,另外一个厂家需要自己去微信官方获取code进行授权,小伙伴们可根据自己的情况改写:微信H5授权

  • wxauth.js
// 微信授权-openId
import { baseUrl } from "../common/config";
// 微信公众号授权
export const wxAuthorize = function () {
	let link = window.location.href;
	let params = getUrlParams(link);  // 地址解析
	console.log('params', params);
	
	if (params.openid && params.appid && params.merchName) {  // 富有获取openid
		uni.$u.vuex('$openId', params.openid)
		uni.$u.vuex('$wxAppId', params.appid)
		uni.$u.vuex('$merchantName', params.merchName)
		let sn = getUrlSn(link)
		uni.$u.vuex('$termSn', sn)
	}else if (params.code) {  // hk-微信授权获取jscode
		let sn = getUrlSn(link)
		uni.$u.vuex('$termSn', sn)
		
		if (sn) {
			// 调用后台接口,授权
			uni.request({
				url: `${baseUrl}/order/initiateTrans/getOpenid`,  // 后端获取openid
				method: 'POST',
				data: {
					transType: 1, //交易类型 1微信 2支付宝
					termSn: sn, //终端号
					code: params.code //支付宝授权码
				},
				success: (response) => {
					const res = response.data
					if(res.code === 200){
						if(res.data.openId){
							// 海科
							uni.$u.vuex('$openId', res.data.openId)
							uni.$u.vuex('$merchantName', res.data.merchName)
						}else if(res.data.redirectUri){
							// 富有
							window.location.href = res.data.redirectUri
						}
					}else if(res.msg){
						uni.showToast({
							title: res.msg,
							icon: 'none'
						})
					}
				},
				fail: (err) => {
					console.log(err, 'err');
					uni.showToast({
						title: err.errMsg,
						icon: 'none'
					})
				}
			});
		}
		
	} else {
		let appid = 'xxx自己的微信公众平台的appid';
		let uri = encodeURIComponent(link);
		let authURL = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${appid}&redirect_uri=${uri}&response_type=code&scope=snsapi_base&state=123#wechat_redirect`;
		window.location.href = authURL;
	}
}


// 解析url
function getUrlParams(link) {
	let decodeLink = decodeURIComponent(link)
  const params = {};
  const regex = /[?&]+([^=&]+)=([^&]*)/g;
  let match;
  while ((match = regex.exec(decodeLink)) !== null) {
    params[decodeURIComponent(match[1])] = decodeURIComponent(match[2]);
  }
	
	for (let key in params) {
		if (params.hasOwnProperty(key)) {
			// 检查值是否以#/结尾
			if (params[key].endsWith('#/')) {
				// 如果是,去掉#/
				params[key] = params[key].slice(0, -2);
			}
		}
	}
  return params;
}

// 获取sn
function getUrlSn(url){
	// 使用URL对象解析URL
	var urlObj = new URL(url);
	// 获取路径名部分
	var pathname = urlObj.pathname;
	
	// 使用字符串方法提取所需部分
	var sn = pathname.split('/')[pathname.split('/').length - 1];
	return sn;
}



2.2.3 支付宝授权

支付宝H5授权

注:aliPayCode.js 这个文件是个附件,会放到后面。【其实根据官方文档写不需要用到这个js,但是我用官方写法没有生效,遂用这个】

  • AlipayAuth.js
// 支付宝授权-openId
import store from "../store";
import { baseUrl } from "../common/config";
import { getAuthCode } from './aliPayCode.js'

// 支付宝授权
export const aliAuthorize = function () {
	let link = window.location.href;
	let sn = getUrlSn(link)
	uni.$u.vuex('$termSn', sn)
	
	getAuthCode ({
		appId :  '2021xxxxxxxx9508', // 自己的支付宝开放平台appid
		scopes : ['auth_base'],
	},function(res){
		let authCode = res.authCode
		
		if(store.state.$termSn && authCode){
			getOpenidByAli(store.state.$termSn, authCode)
		}
	});
}

function getOpenidByAli(sn, authCode){
	// 调用后台接口,授权
	uni.request({
		url: `${baseUrl}/order/initiateTrans/getOpenid`,  // 后端获取openid
		method: 'POST',
		data: {
			transType: 2, //交易类型
			termSn: sn, //终端号
			code: authCode //支付宝授权码
		},
		success: (response) => {
			const res = response.data
			console.log(res, 'res');
			if(res.code === 200){
				// 存储起来openid
				uni.$u.vuex('$merchantName', res.data.merchName)
				uni.$u.vuex('$openId', res.data.openId)
			}else if(res.msg){
				uni.showToast({
					title: res.msg,
					icon: 'none'
				})
			}
		},
		fail: (err) => {
			console.log(err, 'err');
			uni.showToast({
				title: err.msg,
				icon: 'none'
			})
		}
	});
}

// 获取sn
function getUrlSn(url){
	// 使用URL对象解析URL
	var urlObj = new URL(url);
	// 获取路径名部分
	var pathname = urlObj.pathname;
	
	// 使用字符串方法提取所需部分
	var sn = pathname.split('/')[pathname.split('/').length - 1];
	return sn;
}

2.3 微信支付

前提:注意!需要提前开通微信的 JSAPI支付哦 !开通步骤参考官方:微信支付

/* 微信支付 */
wxPay(){
	// 获取微信支付参数-预下单
	uni.request({
		url: `${baseUrl}/order/initiateTrans/preCreateOrder`,  // 后端获取openid
		method: 'POST',
		data: {
			termSn: this.$termSn, //终端号
			transAmount: parseFloat(this.$money) * 100, //交易金额(分)
			transType: 1, //交易类型 1微信 2支付宝
			remark: this.remark, //备注
			appid: this.$wxAppId,
			openId: this.$openId //openid
		},
		success: (response) => {
			uni.hideLoading()
			const res = response.data
			console.log(res, 'res');
			this.opacity = 1
			if(res.code === 200){
				this.wxPayDataTemp = JSON.stringify(res.data)
				if (typeof WeixinJSBridge == "undefined"){
				  if( document.addEventListener ){
						document.addEventListener('WeixinJSBridgeReady', this.onBridgeReady, false);
				  }else if (document.attachEvent){
						document.attachEvent('WeixinJSBridgeReady', this.onBridgeReady); 
						document.attachEvent('onWeixinJSBridgeReady', this.onBridgeReady);
				  }
				}else{
					this.onBridgeReady();
				}
			}else if(res.msg){
				uni.showToast({
					title: res.msg,
					icon: 'none'
				})
			}
		},
		fail: (err) => {
			uni.hideLoading()
			console.log(err, 'err');
			uni.showToast({
				title: err.msg,
				icon: 'none'
			})
		}
	});
},

// 微信jsapi支付
onBridgeReady(){
	let data = JSON.parse(this.wxPayDataTemp)
	WeixinJSBridge.invoke(
		'getBrandWCPayRequest', {
			 "appId": data.sdkAppid,     //公众号ID,由商户传入     
			 "timeStamp": data.sdkTimestamp,         //时间戳,自1970年以来的秒数     
			 "nonceStr": data.sdkNoncestr, //随机串     
			 "package": data.sdkPackage,     
			 "signType": data.sdkSigntype,         //微信签名方式:     
			 "paySign": data.sdkPaysign //微信签名 
		},
		function(res){
			if(res.err_msg == "get_brand_wcpay_request:ok" ){
				// 使用以上方式判断前端返回,微信团队郑重提示:
				//res.err_msg将在用户支付成功后返回ok,但并不保证它绝对可靠。
				uni.showToast({
					title: '支付成功',
					icon: 'none'
				})
				this.$u.vuex('$money', null)
			}else if(res.err_msg == 'get_brand_wcpay_request:cancel'){
				uni.showToast({
					title: '支付取消',
					icon: 'none'
				})
			}else{
				uni.showToast({
					title: '支付失败',
					icon: 'none'
				})
			}
		}
	); 
},

2.4 支付宝支付

前提:注意!需要提前申请支付账号,获取到appid,才能调用支付,详细步骤查看官方接入准备:支付宝支付 H5开放文档

// 支付宝预下单
aliPay(){
	// 调用后台接口,授权
	uni.request({
		url: `${baseUrl}/order/initiateTrans/preCreateOrder`,  // 后端获取openid
		method: 'POST',
		data: {
			termSn: this.$termSn, //终端号
			transAmount: parseFloat(this.$money) * 100, //交易金额(分)
			transType: 2, //交易类型 1微信 2支付宝
			remark: this.remark, //备注
			openId: this.$openId //openid
		},
		success: (response) => {
			uni.hideLoading()
			const res = response.data
			console.log(res, 'res');
			if(res.code === 200){
				this.tradeNO = res.data.reservedTransactionId
				this.tradePay()  // 调支付宝支付-传入订单号
			}else if(res.msg){
				this.opacity = 1
				uni.showToast({
					title: res.msg,
					icon: 'none'
				})
			}
		},
		fail: (err) => {
			uni.hideLoading()
			this.opacity = 1
			console.log(err, 'err');
			uni.showToast({
				title: err.msg,
				icon: 'none'
			})
		}
	});
 },

// 支付宝jsapi支付
tradePay() {
	this.opacity = 1
	
	if (window.AlipayJSBridge) {
		AlipayJSBridge.call("tradePay", {
			tradeNO: this.tradeNO
		}, function (data) {
			if ("9000" == data.resultCode) {
				uni.showToast({
					title: '支付成功',
					icon: 'none'
				})
				this.$u.vuex('$money', null)
			}else if ("6001" == data.resultCode) {
				uni.showToast({
					title: '支付取消',
					icon: 'none'
				})
			}else{
				uni.showToast({
					title: '支付失败',
					icon: 'none'
				})
			}
		});
	}else {
		uni.showToast({
			title: '请到支付宝打开',
			icon: 'none'
		})
	}
},

三、全部代码

3.1 附上完整代码
  • pages/payment/index.vue
<template>
	<view class="content">
		<view class="card">
			<view class="title_bg">
				<image src="../../static/img/payment/home_icon.png" mode="widthFix"></image>
				<view class="title">
					<text>{{ $merchantName }}</text>
					<text>{{ $merchantName }}</text>
				</view>
			</view>
			<view class="title2">金额</view>
			<view class="amount_input">
				<view class="cursor"  v-if="!$money"></view>
				<view class="input_num" v-if="$money">{{ $money }}</view>
				<view class="input" v-else>请输入金额</view>
				<view class="dot"></view>
			</view>
			
			<view class="remark" @click="getRemark">添加付款备注</view>
		</view>
		<text class="remark_txt" v-if="remarkTxt">{{remarkTxt}}</text>
		<view class="tips">
			请核对以上金额、收款商户等信息与交易一致;经您确认支付的款 项,支付机构无法追回亦无赔偿义务。
		</view>
		
		<keyboard @confirm="submit" ref="keyboard" :opacity="opacity"></keyboard>
		
		
		<!-- 备注弹框 -->
		<uni-popup ref="popup" type="center" :animation="false">
			<view class="remark_pop">
				<view class="title">请填写付款说明</view>
				<view class="textarea_con">
					<textarea class="textarea" maxlength="30" v-model="remark" placeholder="请输入付款说明" placeholder-style="color: #a0a0a0;font-size: 14px;"></textarea>
				</view>
				<view class="btn">
					<button class="cancel_btn" @click="cancel">取消</button>
					<button class="confirm_btn" @click="confirm">确定</button>
				</view>
			</view>
		</uni-popup>
	</view>
</template>


<script>
import { baseUrl } from '../../common/config'
import keyboard from '@/components/keyboard.vue'
import { getAuthCode } from '../../utils/aliPayCode.js'

export default {
	data() {
		return {
			remark: '',
			remarkTxt: '',
			opacity: 1,
			tradeNO: null,
			wxPayDataTemp: null, // 微信支付数据暂存
		}
	},

	components: {
		keyboard
	},

	methods: {
		confirm(){
			this.$refs.popup.close()
			this.remarkTxt = this.remark
		},
		cancel(){
			this.$refs.popup.close()
		},
		getRemark(){
			this.remark = ''
			this.$refs.popup.open()
		},
		

		submit() {
			if (!this.$money) {
				uni.showToast({
					title: '请先输入金额',
					icon: 'error'
				})
				return false
			}
			this.opacity = 0.3
			uni.showLoading({
				mask: true
			})
			
			switch (this.userAgentCurrent()){
				case "wechat":
					this.wxPay()
					break
				case "alipay":
					this.aliPay()
					break
				case "yunpay":
					this.yunPay()
					break
			}
		},
		
		/*
		 
		 微信支付
		 
		*/
		wxPay(){
			// 获取微信支付参数-预下单
			uni.request({
				url: `${baseUrl}/order/initiateTrans/preCreateOrder`,  // 后端获取openid
				method: 'POST',
				data: {
					termSn: this.$termSn, //终端号
					transAmount: parseFloat(this.$money) * 100, //交易金额(分)
					transType: 1, //交易类型 1微信 2支付宝
					remark: this.remark, //备注
					appid: this.$wxAppId,
					openId: this.$openId //openid
				},
				success: (response) => {
					uni.hideLoading()
					const res = response.data
					console.log(res, 'res');
					this.opacity = 1
					if(res.code === 200){
						this.wxPayDataTemp = JSON.stringify(res.data)
						if (typeof WeixinJSBridge == "undefined"){
						  if( document.addEventListener ){
								document.addEventListener('WeixinJSBridgeReady', this.onBridgeReady, false);
						  }else if (document.attachEvent){
								document.attachEvent('WeixinJSBridgeReady', this.onBridgeReady); 
								document.attachEvent('onWeixinJSBridgeReady', this.onBridgeReady);
						  }
						}else{
							this.onBridgeReady();
						}
					}else if(res.msg){
						uni.showToast({
							title: res.msg,
							icon: 'none'
						})
					}
				},
				fail: (err) => {
					uni.hideLoading()
					console.log(err, 'err');
					uni.showToast({
						title: err.msg,
						icon: 'none'
					})
				}
			});
		},
		
		// 微信jsapi支付
		onBridgeReady(){
			let data = JSON.parse(this.wxPayDataTemp)
			WeixinJSBridge.invoke(
				'getBrandWCPayRequest', {
					 "appId": data.sdkAppid,     //公众号ID,由商户传入     
					 "timeStamp": data.sdkTimestamp,         //时间戳,自1970年以来的秒数     
					 "nonceStr": data.sdkNoncestr, //随机串     
					 "package": data.sdkPackage,     
					 "signType": data.sdkSigntype,         //微信签名方式:     
					 "paySign": data.sdkPaysign //微信签名 
				},
				function(res){
					if(res.err_msg == "get_brand_wcpay_request:ok" ){
						// 使用以上方式判断前端返回,微信团队郑重提示:
						//res.err_msg将在用户支付成功后返回ok,但并不保证它绝对可靠。
						uni.showToast({
							title: '支付成功',
							icon: 'none'
						})
						this.$u.vuex('$money', null)
					}else if(res.err_msg == 'get_brand_wcpay_request:cancel'){
						uni.showToast({
							title: '支付取消',
							icon: 'none'
						})
					}else{
						uni.showToast({
							title: '支付失败',
							icon: 'none'
						})
					}
				}
			); 
		},
		
		
		/* 
		 
		 支付宝支付
		 
		*/
	 
		// 支付宝预下单
		aliPay(){
			// 调用后台接口,授权
			uni.request({
				url: `${baseUrl}/order/initiateTrans/preCreateOrder`,  // 后端获取openid
				method: 'POST',
				data: {
					termSn: this.$termSn, //终端号
					transAmount: parseFloat(this.$money) * 100, //交易金额(分)
					transType: 2, //交易类型 1微信 2支付宝
					remark: this.remark, //备注
					openId: this.$openId //openid
				},
				success: (response) => {
					uni.hideLoading()
					const res = response.data
					console.log(res, 'res');
					if(res.code === 200){
						this.tradeNO = res.data.reservedTransactionId
						this.tradePay()  // 调支付宝支付-传入订单号
					}else if(res.msg){
						this.opacity = 1
						uni.showToast({
							title: res.msg,
							icon: 'none'
						})
					}
				},
				fail: (err) => {
					uni.hideLoading()
					this.opacity = 1
					console.log(err, 'err');
					uni.showToast({
						title: err.msg,
						icon: 'none'
					})
				}
			});
		 },
		
		// 支付宝jsapi支付
		tradePay() {
			this.opacity = 1
			
			if (window.AlipayJSBridge) {
				AlipayJSBridge.call("tradePay", {
					tradeNO: this.tradeNO
				}, function (data) {
					if ("9000" == data.resultCode) {
						uni.showToast({
							title: '支付成功',
							icon: 'none'
						})
						this.$u.vuex('$money', null)
					}else if ("6001" == data.resultCode) {
						uni.showToast({
							title: '支付取消',
							icon: 'none'
						})
					}else{
						uni.showToast({
							title: '支付失败',
							icon: 'none'
						})
					}
				});
			}else {
				uni.showToast({
					title: '请到支付宝打开',
					icon: 'none'
				})
			}
		},

		
		// 云闪付支付
		yunPay(){
			setTimeout(()=>{
				uni.hideLoading()
				uni.showToast({
					title: '云闪付支付',
					icon: 'success',
					duration: 3000
				})
				this.opacity = 1
			}, 2000)
		},
		
	}
}
</script>

<style lang="scss">
page {
	background-color: #F5F6F8;
}
.content {
	padding-top: 24rpx;

	.card{
		width: 94%;
		height: 422rpx;
		background: #FFFFFF;
		border-radius: 8rpx 8rpx 8rpx 8rpx;
		display: flex;
		flex-direction: column;
		
		.title_bg{
			width: 100%;
			height: 150rpx;
			background-image: url('@/static/img/payment/title_bg.png');
			background-repeat: no-repeat;
			background-size: 100% 100%;
			display: flex;
			align-items: center;
			padding-left: 28rpx;
			
			image{
				width: 68rpx;
				height: 68rpx;
			}
			
			.title{
				display: flex;
				flex-direction: column;
				justify-content: space-evenly;
				padding-left: 24rpx;
				
				text:first-child{
					font-weight: 800;
					font-size: 32rpx;
					color: #232323;
				}
				
				text:last-child{
					font-weight: 400;
					font-size: 22rpx;
					color: #666666;
					padding-top: 12rpx;
				}
			}
		}
		
		.title2{
			font-weight: 500;
			font-size: 32rpx;
			color: #232323;
			padding: 24rpx 0 20rpx 28rpx;
		}
		
		.amount_input{
			height: 112rpx;
			display: flex;
			align-items: center;
			justify-content: space-between;
			padding: 0 42rpx 0 28rpx;
			
			.cursor {
				width: 4rpx;
				height: 50rpx;
				background-color: #5E79CB;
				margin-right: 10rpx;
				animation: flicker 1.2s infinite;
				@keyframes flicker {
					0%,
					100% {
						opacity: 0;
					}
			
					50% {
						opacity: 1;
					}
				}
			}
			
			.input{
				flex: 1;
				font-weight: 500;
				font-size: 40rpx;
				color: #999999;
			}
			.input_num{
				flex: 1;
				font-weight: 500;
				font-size: 40rpx;
				color: #232323;
			}
			
			.dot{
				width: 82rpx;
				height: 42rpx;
				text-align: left;
				font-weight: 400;
				font-size: 32rpx;
				color: #999999;
			}
		}
		
		.remark{
			font-weight: 500;
			font-size: 26rpx;
			color: #5E79CB;
			text-align: right;
			padding-right: 42rpx;
		}
	}
	
	.remark_txt{
		width: 92%;
		font-weight: 5400;
		font-size: 22rpx;
		color: #a4a4a4;
		line-height: 38rpx;
		text-align: left;
		margin-top: 20rpx;
	}
	
	.tips{
		width: 92%;
		font-weight: 500;
		font-size: 24rpx;
		color: #999999;
		line-height: 38rpx;
		text-align: left;
		margin-top: 20rpx;
	}
	
	
	
	.remark_pop{
		width: 620rpx;
		height: 460rpx;
		background-color: #fff;
		border-radius: 20rpx;
		display: flex;
		flex-direction: column;
		align-items: center;
		justify-content: space-between;
		
		.title{
			line-height: 100rpx;
			font-size: 28rpx;
			font-weight: 700;
		}
		
		.textarea_con{
			width: 90%;
			margin: 0 auto;
			height: 200rpx;
			border: 2rpx solid #c4c4c4;
			border-radius: 10rpx;
			padding: 32rpx;
			
			.textarea{
				width: 100%;
				height: 100%;
			}
		}
		
		
		.btn{
			display: flex;
			align-items: center;
			justify-content: space-between;
			width: 100%;
			height: 100rpx;
			margin-top: 32rpx;
			
			
			button{
				width: 50%;
				height: 100rpx;
				line-height: 100rpx;
				text-align: center;
				font-size: 28rpx;
				font-weight: 500;
				background-color: #fff;
				border-radius: none;
			}
			
			.cancel_btn{
				color: #373737;
			}
			
			.confirm_btn{
				color: #5E79CB;
			}
		}
	}
}
</style>

  • main.js
import Vue from "vue"
import App from "./App"

import store from '@/store';
import uView from "uview-ui";

let vuexStore = require("@/store/$u.mixin.js");
import './utils/filters.js'

Vue.config.productionTip = false
App.mpType = "app"

// 区分微信/支付宝环境
import { userAgentCurrent } from '@/utils/userAgent.js'
Vue.prototype.userAgentCurrent = userAgentCurrent

Vue.mixin(vuexStore);
Vue.use(uView);


const app = new Vue({
	store,
  ...App,
})
app.$mount()
  • filters.js
import Vue from 'vue'

// 金额保留两位小数
Vue.filter('$money', function(val) {
	if (typeof val == 'string') {
		val = val.toString().replace(/\$|\,/g, '');
	}
	if (isNaN(val)) {
		val = "0";
	}
	let sign = (val == (val = Math.abs(val)));
	val = Math.floor(val * 100 + 0.50000000001);
	let cents = val % 100;
	val = Math.floor(val / 100).toString();
	if (cents < 10) {
		cents = "0" + cents
	}
	for (var i = 0; i < Math.floor((val.length - (1 + i)) / 3); i++) {
		val = val.substring(0, val.length - (4 * i + 3)) + ',' + val.substring(val.length - (4 * i + 3));
	}

	return (((sign) ? '' : '-') + val + '.' + cents);
})

3.2 上线发布

(1)上线发布时,找到 manifest.json -> Web配置,填写页面标题、路由模式 hash 或 history 皆可,hash 打包出来的链接后面会拼一个 # ,运行的路径可以随便定义一个,避免与其他域名重复。比如定义pay,域名是https://www.abc.com,则打包出来的链接就是 https://www.abc.com/pay/

在这里插入图片描述
(2)打包发行:依次点击发行 -> 网站PC Web…然后发行即可。打包之后可以把打包的 /unpackage/dist 文件夹放到域名的服务器上,即可访问改H5页面。

在这里插入图片描述

四、页面效果展示

4.1 微信内部打开:

在这里插入图片描述

4.1 支付宝内部打开:

在这里插入图片描述

五、附件

注:附上支付宝H5授权所需要的 js包和项目内引用到的图片,附件好像在文章顶部展示,大家自行查看哈!

祝大家开心每一天~

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值