交易订单页面

  • 先搞定静态组件,就三个动作
- 复制粘贴'静态组件'

- 路由注册一下

- 访问path测试
### src.router.routers.js
......
import Trade from '@/pages/Trade'
export default [
	{
		name:"trade",
		path: "/trade",
		component: Trade,
		meta: {
			show: true
		}
	},
	......
	
- 测试: http://localhost:8080/#/trade
  • 修改一下购物车页面的结算链接
### ShopCart.index.vue
......
<div class="sumbtn">
    <!-- <a class="sum-btn" href="###" target="_blank">结算</a> -->
    <!--只是作简单的跳转,直接router-link-->
    <router-link to='/trade' class="sum-btn">结算</router-link>
</div>
  • 先搞定交易页面收货地址,开始老套路
- 配置请求
- vuex三连环
- 组件内渲染数据
### api.index.js
......
export const reqAddressInfo = ()=>requests({url:'/user/userAddress/auth/findUserAddressList',method:'get'})

### trade.index.js
......
import { reqAddressInfo } from "@/api"

const state = {
	address:[]
}

const actions = {
	async getUserAddress({commit}){
		var res = await reqAddressInfo()
		if(res.code == 200){
			commit('GETUSERADDRESS',res.data) // res.data就是收货地址
			return 'ok'
		}else{
			return Promise.reject(new Error('获取用户收货地址失败'))
		}
	}
}

### Trade.index.vue
......
<script>
  export default {
    name: 'Trade',
	mounted(){
		this.$store.dispatch('getUserAddress') // 派发请求
	}
  }
</script>


- 正常响应的数据是这样
	{code: 200, message: "成功",…}
        code: 200
        data: [{id: 5, userAddress: "北京市昌平区1000", userId: 2, provinceId: 1, consignee: "test110",…}]
        	0: {id: 5, userAddress: "北京市昌平区1000", userId: 2, provinceId: 1, consignee: "test110",…}
        message: "成功"
        ok: true
  • 组件数据的渲染稍后再处理,先搞定一个交易订单的请求
### api.index.js
......
export const reqOrderInfo = ()=>requests({url:'/order/auth/trade',method:'get'})

### trade.index.js
......
import { reqAddressInfo,reqOrderInfo } from "@/api"

const state = {
	......
	orderInfo:{} // 初始化数据,返回值是一个对象,数据有点多,这里就不作展示(直接到仓库去看...)
}

const actions = {
	......
	async getOrderInfo({commit}){
		var res = await reqOrderInfo()
		if(res.code == 200){
			commit('GETORDERINFO',res.data)
			return 'ok'
		}else{
			return Promise.reject(new Error('获取用户订单信息失败'))
		}
	}
}

const mutations = {
	......
	GETORDERINFO(state,orderInfo){
		state.orderInfo = orderInfo
	}
}

### Trade.index.vue
......
mounted() {
    ......
    this.$store.dispatch('getOrderInfo'); // 派发请求
},

  • 收货地址交易订单数据在组件内的渲染
### Trade.index.vue
......
<template>
	<div class="trade-container">
		......
			<div class="address clearFix" v-for="(address,index) in addressInfo" :key="address.id">
				<!--是否有selected样式,由isDefault决定-->
				<span class="username" :class="{selected:address.isDefault == 1}">{{address.consignee}}</span>
				<!--绑定排它事件: 先统一xx,然后单独项再开小灶-->
				<p @click="changeDefault(address,addressInfo)">
					<span class="s1">{{address.fullAddress}}</span>
					<span class="s2">{{address.phoneNum}}</span>
					<!--是否有selected样式,由isDefault决定-->
					<span class="s3" v-show="address.isDefault == 1">默认地址</span>
				</p>
			</div>
			
		......
		<div class="trade">
			......
				寄送至:
				<!--用户点击哪个地址,通过计算属性渲染结果-->
				<span>{{userDefaultAddress.fullAddress}}</span>
				收货人:<span>{{userDefaultAddress.consignee}}</span>
				<span>{{userDefaultAddress.phoneNum}}</span>
			</div>
		</div>
		......
	</div>
</template>

<script>
	import {mapState} from 'vuex'
	
	export default {
		name: 'Trade',
		mounted() {
			......
		},
		computed:{
			// 映射仓库数据
			...mapState({addressInfo:state=>state.trade.address}),
			// find会遍历item,然后找出符合条件的'项'
			userDefaultAddress(){
				return this.addressInfo.find(item=>item.isDefault == 1) || {} // 返回{}避免网络请求失败,而出现vue警告
			}
		},
		methods:{
			changeDefault(address,addressInfo){
				// 排他操作
				addressInfo.forEach(item => item.isDefault = 0)
				address.isDefault = 1
			}
		}
	}
</script>

- 用户点击哪个地址,通过计算属性渲染结果,其实还可以这么做

	- 初始化一个data,然后在changeDefault方法里面,把 address项传给data,结构中直接使用data来渲染数据
  • 订单信息数据的渲染
<template>
	<div class="trade-container">
		......
				<!--开始渲染-->
				<ul class="list clearFix" v-for="(order,index) in orderInfo.detailArrayList" :key="order.skuId">
					<li>
						<!--这里要加入style样式,否则图片太大-->
						<img :src="order.imgUrl" alt="" style="width: 100px;height: 100px;">
					</li>
					<li>
						<p>
							{{order.skuName}}</p>
						<h4>7天无理由退货</h4>
					</li>
					<li>
						<h3>¥{{order.orderPrice}}.00</h3>
					</li>
					<li>X{{order.skuNum}}</li>
					<li>有货</li>
				</ul>
				
			</div>
			<div class="bbs">
				<h5>买家留言:</h5>
				<!--获取买家留言-->
				<textarea placeholder="建议留言前先与商家沟通确认" class="remarks-cont" v-model="msg"></textarea>

			</div>
		......
		</div>
		<div class="money clearFix">
			<ul>
				<li>
					<b><i>{{orderInfo.totalNum}}</i>件商品,总商品金额</b>
					<span>¥{{orderInfo.totalAmount}}.00</span>
				</li>
				......
			</ul>
		</div>
		<div class="trade">
			<div class="price">应付金额: <span>¥{{orderInfo.totalAmount}}.00</span></div>
			......
	</div>
</template>

<script>
	import {mapState} from 'vuex'
	
	export default {
		name: 'Trade',
		data(){
			return {
				msg:'' // 初始化数据
			}
		},
		mounted() {
			......
		},
		computed:{
			...mapState({
				addressInfo:state=>state.trade.address,
				orderInfo:state=>state.trade.orderInfo // 映射订单数据信息
			}),
			......
		},
		methods:{
			......
		}
	}
</script>

trade组件最后一个功能,提交订单按钮的逻辑实现

  • 本次请求以后,数据不再放在vuex,而是使用类似全局事件总线$bus的逻辑
### api.index.js
......
// 订单号参数 和 订单信息对象参数
export const reqSubmitOrder = (tradeNo,data)=>requests({url:`/order/auth/submitOrder?tradeNo=${tradeNo}`,method:'post'})

### main.js
......
import * as API from '@/api'
......
new Vue({
 ......
  beforeCreate(){
	  Vue.prototype.$bus = this;
	  Vue.prototype.$API = API; // 新增 $API属性
  }
}).$mount('#app')

### trade.index.vue
......
......
<div class="sub clearFix">
	<!-- <router-link class="subBtn" to="/pay">提交订单</router-link> -->
	<!--绑定事件-->
	<a class="subBtn" @click="submitOrder">提交订单</a>
</div>
......
<script>
	import {mapState} from 'vuex'
	
	export default {
		name: 'Trade',
		data(){
			return {
				......
				orderId:'' // 初始化
			}
		},
		......
		methods:{
			......
			async submitOrder(){
			
				let {tradeNo} = this.orderInfo; // 获取订单号
				let data = { // 构造data对象
				  "consignee": this.userDefaultAddress.consignee,   // 收件人的名字
				  "consigneeTel": this.userDefaultAddress.phoneNum,  // 收件人的手机号
				  "deliveryAddress": this.userDefaultAddress.fullAddress,  // 收件人的地址
				  "paymentWay": "ONLINE",   // 支付方式
				  "orderComment": this.msg,   // 买家的留言信息
				  "orderDetailList": this.orderInfo.detailArrayList,   // 商品清单
				};
				var res = await this.$API.reqSubmitOrder(tradeNo,data) // 发请求
				
				if(res.code == 200 ){
					this.orderId = res.data; // 响应成功就保存订单号并跳转
					this.$router.push('/pay?orderId=' + this.orderId)
				}else{
					alert(res.data)
				}
				
			}
		}
	}
</script>

支付模块

  • 先搞定静态组件,然后router注册一下
import Pay from '@/pages/Pay'

export default [
	......
	{ // 注册
		name:"pay",
		path: "/pay",
		component: Pay,
		meta: {
			show: true
		}
	},
  • 获取支付信息: 当Pay页面挂载完毕以后,立即向后端发请求,获取支付信息(先配置请求接口)
### api.index.js
......
export const reqPayInfo = (orderId)=>requests({url:`/payment/weixin/createNative/${orderId}`,method:'get'})

### Pay.index.vue
......
<script>
  export default {
    name: 'Pay',
	data(){
		return {
			payInfo:{} // 初始化
		}
	},
	computed:{
		orderId(){ // 由于提交订单经常会失败,这里干脆写死...
			return this.$route.query.orderId || 'xxx-yyy-zzz'
		}
	},
	mounted(){ // vue生命周期函数,尽量不要使用async,有坑
		this.getPayInfo()
	},
	methods:{
		async getPayInfo(){ // 在这里实现异步
			var res = await this.$API.reqPayInfo(this.orderId);
			if(res.code == 200){
				this.payInfo = res.data
			}else{
				alert(res.data)
			}
		}
	}
  }
</script>

引入element-ui组件库

- npm install element-ui
  • 如果需要按需引入,在上述基础上,可以这么搞
- npm install babel-plugin-component -D

### babel.config.js

module.exports = { // 修改配置文件,注意要重启serve,否则element-ui无法生效
  presets: [
    '@vue/cli-plugin-babel/preset'
  ],
   plugins: [ // 新增
      [
        "component",
        {
          "libraryName": "element-ui",
          "styleLibraryName": "theme-chalk"
        }
      ]
    ]
}

  • main.js注册为全局组件
### main.js
......
import { Button } from 'element-ui' // 导入按钮,测试用
Vue.component(Button.name,Button)
  • 测试
### Pay.index.vue
.......
<template>
  <div class="pay-main">
    <!--有了-->
	<el-button type="primary">测试按钮</el-button>  
  • 项目中,我们要使用MessageBox 弹框这个组件,文档参考地址
- https://element.eleme.cn/2.5/#/zh-CN/component/message-box
  • 第二种按需引入的方式,绑定到Vue.prototype上面
### main.js
......
import { Button,MessageBox } from 'element-ui' // 导入
Vue.component(Button.name,Button) // 第一种方式
Vue.prototype.$msgbox = MessageBox // 第二种方式
Vue.prototype.$alert = MessageBox.alert
  • 测试.点击按钮,弹窗
### Pay.index.vue
......
<div class="submit">
	<!-- <router-link class="btn" to="/paysuccess">立即支付</router-link> -->
	<a class="btn" @click="open">立即支付</a> <!--绑定点击事件-->
</div>
......
methods: {
			......
			open() {
				this.$alert('<strong>这是 <i>HTML</i> 片段</strong>', 'HTML 片段', {
					dangerouslyUseHTMLString: true,
					center:true, // 文字居中
					showCancelButton:true, // 显示'取消'按钮
					cancelButtonText:'支付遇到问题', // 更改按钮文本文字
					confirmButtonText:'已支付成功',
					showClose:false // 隐藏`X`项
				});
			}
		}
  • 接下来要做的事情,就是把弹窗中的测试文本,修改为二维码

    • 如果订单提交成功,payInfo有以下字段
    codeUrl:"weixin://wxpay/bizpayurl?pr=ZuHaocUzz"
    
    • 前面的操作中,如果订单提交失败,这里可以写死codeUrl,我们作测试用
    ### Pay.index.vue
    .....
    open() {
    		this.$alert('<strong>这是 <i>HTML</i> 片段</strong>', 'HTML 片段', {
    			......
    		});
    		this.payInfo.codeUrl = "weixin://wxpay/bizpayurl?pr=ZuHaocUzz" // 加这句
    	}
    
  • 生成二维码相关的库,我们使用qrcode来实现

- 安装: npm i qrcode

### Pay.index.vue
......
<script>
	import QRCode from 'qrcode' // 引入
	
	export default {
		......
		methods: {
			......
			open() {
				......
				this.payInfo.codeUrl = "weixin://wxpay/bizpayurl?pr=ZuHaocUzz"
				
				let res = QRCode.toDataURL(this.payInfo.codeUrl)
				// 返回的是一个Promise对象
				// Promise {<fulfilled>: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAJQA…sSoGAUnI1mBQIKFIou1Lgf8LubvkqQm+8AAAAAElFTkSuQmCC'}
				console.log(res)
			}
		}
	}
</script>

- 老套路,加上 async 和 await 直接获取结果

......
async open() { // 加 async 和 await 直接获取结果 
	......
	this.payInfo.codeUrl = "weixin://wxpay/bizpayurl?pr=ZuHaocUzz"
	
	let res = await QRCode.toDataURL(this.payInfo.codeUrl)
	// 浏览器打开链接,就是一张二维码图片
	console.log(res) // data:image/png;base64,iVBORw0KGgoAAA......
}
	
  • 最终代码
### Pay.index.vue
......
......
methods: {
	.......
	async open() {
		
		this.payInfo.codeUrl = "weixin://wxpay/bizpayurl?pr=ZuHaocUzz"
		let url = await QRCode.toDataURL(this.payInfo.codeUrl) // 获取二维码地址
		
		// 把 url 传给 img 显示
		this.$alert(`<img src="${url}"></img>`, '请你微信支付', {
			dangerouslyUseHTMLString: true,
			center:true,
			showCancelButton:true,
			cancelButtonText:'支付遇到问题',
			confirmButtonText:'已支付成功',
			showClose:false
		});
	}
}
  • 二维码出现的时候,用户应完成支付,而用户何时支付,这个时间是不确定的,所以我们需要每隔一段时间去问服务器,用户是否完成支付
- 后端接口: 

    ### api.index.js
    ......
    // 获取订单的支付状态
    export const reqPayStatus = (orderId)=>requests({url:`/payment/weixin/queryPayStatus/${orderId}`,method:'get'})

### Pay.index.vue
......
<script>
	
	export default {
		......
		data() {
			return {
				......
				timer:null, // 初始化数据
				code:''
			}
		},
		computed: {
			orderId() { // 之前的数据
				return this.$route.query.orderId || 'xxx-yyy-zzz'
			}
		},
		methods: {
			......
			async open() {
				
				......
				// 使用定时器,每隔1s就询问服务器,用户是否已完成支付
				if(!this.timer){
					this.timer = setInterval(async ()=>{
						let res = await this.$API.reqPayStatus(this.orderId)
						if(res.code == 200){ // 200响应,说明用户完成支付了
							clearInterval(this.timer) // 清除计时器并关闭弹窗,同时跳转到'支付成功'页
							this.timer = null
							this.code = res.code
							this.$msgbox.close()
							this.$router.push('/paysuccess')
						}
					},1000) // 如果后端返回的不是200,就一直重复下去...
				}
			}
		}
	}
</script>
  • 然后,配置一下PaySuccess静态组件
### router.routers.js
......
import PaySuccess from '@/pages/PaySuccess'

export default [
	.......
	{
		name:"paysuccess",
		path: "/paysuccess",
		component: PaySuccess,
		meta: {
			show: true
		}
	},

messageBox弹窗的按钮逻辑处理

  • 该弹窗有两个按钮
    • 支付遇到问题
    • 已成功支付
- 当用户点击'支付遇到问题'的时候,我们弹窗提示消息,并清除计时器,最后关闭 messageBox
- 当用户点击'已成功支付'的时候,我们清除计时器,关闭 messageBox,然后跳转到'/paysuccess'
### Pay.index.vue
......
methods: {
	......
	async open() {
		
		......
		this.$alert(`<img src="${url}"></img>`, '请你微信支付', {
			......
			// action 是用户的行为:比如'confirm'或者'cancel'
			// instance 是 messageBox实例对象,通过它可以调用该实例的一些属性和方法
			// done就是function对象,关闭弹窗
			// 文档地址: https://element.eleme.cn/2.8/#/zh-CN/component/message-box
			beforeClose: (action,instance,done) => {
				if(action == 'cancel'){
					alert('请联系管理员')
					clearInterval(this.timer)
					this.timer = null
					done()
				}else{
					// if(this.code == 200){ // 为了测试,先不判断 200 状态码
						clearInterval(this.timer)
						this.timer = null
						done()
						this.$router.push('/paysuccess')
					// }
				}
			}
		});
		
		if(!this.timer){
			......
		}
	}
}