主题
弹出支付窗口与生成二维码(Lesson22 2/3)
本页对应 Lesson22,共 5 段视频,约 1 小时 13 分。
学习目标
- 能使用
qrcode的toDataURL将支付链接转换为 Base64 图片,并正确渲染到支付弹窗的img中。 - 能实现订单支付状态轮询查询,识别
205支付中与200支付成功,并在成功后跳转支付成功页。 - 能在支付成功、关闭支付弹窗、切换路由等场景下清理轮询定时器,避免重复请求和内存泄漏。
- 能搭建支付成功路由页面,并完成“查看订单”“继续购物”等跳转交互。
- 能说明用户提交订单、服务端生成未支付订单、扫码支付、服务端更新订单状态、前端轮询获取结果的前后端支付流程。
- 能配置路由懒加载与图片懒加载,验证路由按需加载,并使用
vue-lazyload与v-lazy为图片提供加载占位图。
知识点
全局挂载 MessageBox:让支付页能直接 this.$alert
为什么需要:支付弹窗在多个页面都可能用到。如果每个组件都 import { MessageBox } from 'element-ui',代码重复,调用方式也不统一。把 MessageBox 挂到 Vue 原型上后,组件里可以直接写 this.$alert、this.$msgbox、this.$message。
怎么想:转写稿里反复出现“先引入、再挂到原型、组件里直接用”的思路。MessageBox.alert 是 MessageBox 的 alert 方法,Message 是轻提示组件,二者不是同一个东西,要分别挂。
怎么做:在 src/index.js 中注册:
js
import {Button, Message,MessageBox, Row} from 'element-ui';
Vue.prototype.$msgbox = MessageBox;
Vue.prototype.$alert = MessageBox.alert;
Vue.prototype.$message = Message;易错点:只挂 $msgbox 不挂 $alert,组件里写 this.$alert 会报“不是函数”;挂载要放在创建 Vue 实例之前;MessageBox 和 Message 大小写别写错。转写稿里还提到支付成功后可调用 MessageBox.close() 关闭弹窗,这也是基于全局 MessageBox 的能力。
支付按钮先阻止默认跳转
为什么需要:笔记模板里是 <a href="" target="_blank">立即支付</a>。如果直接点击,浏览器可能按链接行为跳转或新开窗口,弹窗还没处理完页面就跑了。
怎么想:这里点击不是为了跳链接,而是为了弹二维码。所以要先阻止默认行为,再执行 payHandler。
怎么做:保留笔记写法:
vue
<template>
<div class="submit">
<a @click.prevent="payHandler" class="btn" href="" target="_blank">立即支付</a>
</div>
</template>易错点:不写 .prevent 时,href="" 仍可能触发页面刷新;方法名要和 methods 中一致;如果后续改成按钮,也要保证点击只触发一次弹窗逻辑。
用 $alert 承载二维码 HTML
为什么需要:二维码本质是一张图片,MessageBox 默认把 message 当纯文本。如果直接传 <img ...>,用户看到的是一串 HTML 字符串,不是二维码。
怎么想:Element UI 的 MessageBox 提供 dangerouslyUseHTMLString,开启后 message 才按 HTML 片段处理。转写稿里还演示了用 QRCode.toDataURL 把支付地址转成 base64,再拼到 img 的 src 上;笔记里先用 /images/1.jpg 作为静态占位图。
怎么做:在 src/pages/Pay/index.vue 中:
vue
<script>
methods:{
payHandler(){
this.$alert('<img width="200" src="/images/1.jpg" alt="微信支付二维码"/>', '用微信扫描下方二维码完成支付!', {
// 是否将 message 属性作为 HTML 片段处理
dangerouslyUseHTMLString: true,
// 是否居中布局
center:true,
// 是否显示取消按钮
showCancelButton:true,
// 完成按钮的文本内容
confirmButtonText:"已完成微信支付",
// 取消按钮的文本内容
cancelButtonText:"支付遇到问题",
// MessageBox 是否显示右上角关闭按钮
showClose:false,
callback(action){
if(action === "confirm"){
console.log("已完成支付")
}else{
console.log("支付遇到问题")
}
}
});
}
},
</script>易错点:忘记 dangerouslyUseHTMLString: true 是最常见错误;图片路径要能被项目访问;该配置会解析 HTML,不要拼接用户输入,避免 XSS。转写稿调试时还遇到“模板字符串拼接图片地址”的问题,实际把 base64 放进 src 时要用模板字符串或字符串拼接,不能直接把变量塞进普通字符串里。
按钮文案、关闭行为与回调
为什么需要:支付弹窗不是普通提示,用户可能已完成支付,也可能遇到问题,需要两个明确出口。
怎么想:用 showCancelButton 打开取消按钮,用 confirmButtonText、cancelButtonText 改文案;showClose:false 隐藏右上角关闭,让用户只能从“已完成微信支付”或“支付遇到问题”离开;callback(action) 中 action 为 confirm 或 cancel。
怎么做:笔记代码已经在 callback 中区分 confirm 和取消分支,分别打印“已完成支付”“支付遇到问题”。转写稿进一步演示:确认后可跳转支付成功页,取消可用 this.$message 提示“如遇到问题请联系客服”。
易错点:callback 的参数不是布尔值,而是 action 字符串;showClose:false 后不要指望右上角关闭;支付成功跳转前要关闭弹窗,否则弹窗可能还挂在页面上。转写稿里还强调:如果弹窗后开启轮询查询支付状态,成功、取消、离开路由、组件销毁时都要清定时器,避免重复请求。这里的“心跳查询”就是定时向服务端查询订单状态,不是“心脏查询”。
先下载 qrcode 模块
为什么需要:支付页要把后端返回的支付链接变成二维码,靠手写编码不现实,直接用成熟模块最稳。转写稿里明确是在 npm 上找到 qrcode,先下载再使用。
怎么想:二维码本质上是“把地址转成可扫描的图形”。扫码后手机拿到的是地址,再跳转到这个地址。这里要生成的是支付链接 this.payInfo.codeUrl 对应的二维码,不是随便一个网址。
怎么做:进入自己的项目目录再安装,避免装到外层目录导致同事拷走项目后缺模块。
shell
cnpm install qrcode易错点:转写稿反复提醒“进入到你自己的项目里面安装”。如果在外层目录安装,项目里实际没有这个依赖,后面引入会报错。
用 toDataURL 把支付地址转成 base64
为什么需要:二维码图片要显示在弹窗里,最方便的形式是 base64 图片地址。转写稿里说 toDataURL 就是做“转换”的,类似图片上传预览时把图片转成 base64 的思路。
怎么想:不要一上来就写弹窗,先验证 QRCode.toDataURL(...) 返回什么。转写稿的调试过程是先打印,发现它返回的是 Promise,不是直接字符串;Promise 的成功值才是 base64 地址。因此要加 async,用 await 取成功值。
怎么做:在 src->pages->Pay->index.vue 中使用。使用前要引入 qrcode 模块,再在支付处理函数里生成 base64。笔记代码如下:
vue
<script>
async payHandler(){
const base64Url = await QRCode.toDataURL(this.payInfo.codeUrl);
this.$alert(`<img width="200" src="${base64Url}"/>`, '用微信扫描下方二维码完成支付!', {
// 是否将 message 属性作为 HTML 片段处理
dangerouslyUseHTMLString: true,
// 是否居中布局
center:true,
// 是否显示取消按钮
showCancelButton:true,
// 完成按钮的文本内容
confirmButtonText:"已完成微信支付",
// 取消按钮的文本内容
cancelButtonText:"支付遇到问题",
// MessageBox 是否显示右上角关闭按钮
showClose:false,
callback(action){
if(action === "confirm"){
console.log("已完成支付")
}else{
console.log("支付遇到问题")
}
}
});
}
</script>易错点:第一,忘记 await,拿到的是 Promise,不是 base64;第二,函数没写 async,await 会报错;第三,转写稿里先写百度地址测试,确认能出二维码后,必须换成 this.payInfo.codeUrl,否则用户扫出来跳的不是支付地址。
用 $alert 展示二维码并处理按钮回调
为什么需要:二维码生成后要立刻让用户看到并扫码,弹窗是最直接的交互。转写稿里通过 $alert 弹出二维码,并给出“已完成微信支付”“支付遇到问题”两个操作。
怎么想:message 里放的是 HTML 图片标签,不是纯文本,所以必须开启 dangerouslyUseHTMLString: true。图片地址是变量,必须用模板字符串拼接。转写稿调试时发现直接写冒号识别不了,最后改成模板字符串:<img width="200" src="${base64Url}"/>。
怎么做:按笔记配置 center 居中、showCancelButton 显示取消按钮、confirmButtonText 和 cancelButtonText 设置按钮文案、showClose:false 隐藏右上角关闭,再在 callback(action) 中判断 confirm 和取消分支。
易错点:不写 dangerouslyUseHTMLString 时,弹窗会把 <img ...> 当普通文字显示;拼接字符串时引号写错会导致图片不显示;callback 里要根据 action === "confirm" 区分“已完成支付”,否则会把取消也当成完成。转写稿里后续还会接支付状态查询和跳转,但本节先把二维码稳定生成并展示出来。
生成支付二维码
为什么需要:用户提交订单后进入支付页,页面要拿到订单号、总价和支付地址;二维码的作用就是把支付地址转成图片,让用户扫码进入支付。
怎么想:转写稿里说得很直白:二维码本质是“把地址转成二维码”,扫码后跳到一个地址。qrcode 的 toDataURL 会把地址转成 base64 图片数据,且返回 Promise,所以要用 await 取结果,再赋给 img 的 src。
怎么做:
bash
cnpm i qrcodejs
import QRCode from 'qrcode'
async createQrCode() {
// this.pay.codeUrl 来自支付页接口返回的订单信息
const url = await QRCode.toDataURL(this.pay.codeUrl)
this.qrCode = url
}html
<img :src="qrCode" alt="支付二维码" />转写稿的调试过程是:先写 https://www.baidu.com 生成二维码,手机能扫出百度地址,证明二维码只是地址载体;再把地址换成接口返回的 codeUrl。中间还遇到模板里直接拼冒号不识别的问题,后来改用模板字符串/绑定解决。
易错点:toDataURL 是异步的,不能直接当同步值用;图片要用 :src 绑定;二维码地址必须用接口返回的支付地址,不要写死百度;生成前要确认支付页已拿到订单信息。
支付成功页与入口
为什么需要:扫码支付成功后要给用户明确反馈,并提供“查看订单”“继续购物”等下一步入口。
怎么想:成功页本身是静态页,重点是路由配置、图片/图标资源和跳转入口。转写稿中先搭成功路由,再放对号图标或图片,最后处理“已完成微信支付”和“支付遇到问题”。
怎么做:新建支付成功组件,在路由中注册;成功页放对号图标、提示文案、查看订单和继续购物按钮。点击“已完成微信支付”可跳转到成功页或订单页;点击“支付遇到问题”给提示,例如联系客服。转写稿还提到关闭弹窗可用 MessageBox.close() 一类方法。
易错点:图片资源路径要放对;静态页也要配路由;图标兼容性别太差;跳转前要确认订单状态,不能只靠按钮文案判断。
心跳查询支付状态
为什么需要:用户扫码后,支付结果由微信回调服务端改变,前端并不知道;所以前端要不断查询订单状态。转写稿里的“心脏查询”应纠正为“心跳查询”。
怎么想:查询接口返回 code:205 表示支付中,200 表示支付成功。轮询到成功就跳转成功页;失败则走失败提示。响应拦截器里不能把 205 当异常拦掉。
怎么做:
js
// 示意:按项目实际 API 封装替换
async queryStatus() {
const res = await getOrderStatus(this.orderId)
if (res.code === 205) {
// 支付中,继续等
}
if (res.code === 200) {
this.$router.push('/pay-success')
}
}易错点:把 205 当错误;漏传 orderId;忘记引入查询 API;成功后仍继续轮询;拦截器统一报错导致“支付中”被中断。
定时器清理与并发问题
为什么需要:轮询不能一直跑。用户离开路由、关闭弹窗、支付成功或失败后都要停,否则会产生大量无效请求。
怎么想:转写稿先用 setInterval 每秒查一次,但发现慢网下上一次请求没回来,下一次又发出,可能重复请求。后来改成 setTimeout 递归:请求完成后再等一段时间发下一次,205 继续递归,200 停止。
怎么做:
js
queryByTimer() {
this.timer = setTimeout(async () => {
const res = await getOrderStatus(this.orderId)
if (res.code === 205) {
this.queryByTimer()
} else if (res.code === 200) {
clearTimeout(this.timer)
this.$router.push('/pay-success')
}
}, 1000)
}同时在 beforeRouteLeave、点击“已完成微信支付”、点击“支付遇到问题”、支付成功时清理定时器。
易错点:只在一个地方清理;递归后忘记清;205 判断写错;离开页面后定时器还在跑。
支付流程回顾
用户提交订单 → 服务端生成一条“未支付”订单记录 → 支付页调用接口获取订单号、总价、支付地址 → 用户扫码支付 → 微信不管成功失败都会调用服务端接口 → 服务端把“未支付”改成“支付成功/支付失败” → 前端心跳查询看到状态变化后跳转。前端不直接改订单状态。
路由懒加载
为什么需要:转写稿中打包文件约 1.4M,用户只看首页也会加载很多暂时不用的页面,浪费带宽。路由懒加载让每个路由单独成包,首屏只加载当前页面。
怎么做:
js
// 原来:import Home from '@/views/Home.vue'
const Home = () => import('@/views/Home.vue')易错点:某个页面过大时懒加载也会慢,必要时再拆分;不要所有路由都同步 import。
图片懒加载
为什么需要:图片未加载完会出现大白板,体验差。先放一张本地 loading 图占位,真实图片加载完再替换。
怎么做:Vue2 项目安装与 Vue2 契合的 vue-lazyload 1.x 版本,不要装 3.x;入口安装并配置 loading 图,模板把 :src 改成 v-lazy。
js
import VueLazyload from 'vue-lazyload'
Vue.use(VueLazyload, {
loading: require('./assets/loading.png')
})html
<img v-lazy="imgUrl" alt="商品图片" />易错点:版本装错;v-lazy 仍写成 :src;loading 图路径错;懒加载主要提升体验,不是减少服务端图片请求。
成功路由界面:先让支付结果有落点
为什么需要:用户扫码支付成功后,不能一直停在二维码弹窗里,需要跳到明确的成功页,给出“支付成功”反馈,并提供“查看订单”“继续购物”入口。
怎么想:先搭静态成功页并配置路由,再让支付页在确认支付成功后 push 到该路由。
怎么做:在 src/pages/PaySuccess/index.vue 创建组件,路由中配置 /paySuccess 指向它。笔记代码如下,图片保留为注释形式:
vue
<template>
<div class="paysuccess">
<div class="success">
<h3>
<span style="font-size: 30px;color:#71b247" class="iconfont icon-weixinzhifu"></span>
<!-- <img src="./images/right.png" width="48" height="48" alt="支付成功对勾图标">-->
恭喜您,支付成功啦!
</h3>
<div class="paydetail">
<p class="button">
<a href="myOrder.html" class="btn-look">查看订单</a>
<a href="index.html" class="btn-goshop">继续购物</a>
</p>
</div>
</div>
</div>
</template>
<script>
export default {
name: "PaySuccess"
}
</script>易错点:路由 path 要和 this.$router.push("/paySuccess") 一致;组件名、目录大小写要一致;笔记里 a href 是静态链接,若项目是单页路由,要按项目现有路由方式处理,不能想当然。
查询支付状态接口:前端要知道“扫完没有”
为什么需要:用户何时扫码、何时支付成功,前端无法凭弹窗判断,只能向服务端查询订单支付状态。
怎么想:微信支付回调会更新服务端订单状态,前端定时查询;205 表示支付中,200 表示成功。
怎么做:在 src/api/payment.js 增加查询接口:
js
// 查询订单的支付状态: /payment/weixin/queryPayStatus/{orderId}
export const getQueryPayStatus = orderId=>sphRequest.get(`/payment/weixin/queryPayStatus/${orderId}`);易错点:orderId 从 this.$route.params.orderId 取;不要把 205 当失败;接口返回的是响应体,页面里用 status.code 判断。
支付页轮询与弹窗:成功后再跳转
为什么需要:二维码展示后要持续查询支付结果,成功就关弹窗、清定时器、跳成功页。
怎么想:转写稿调试时先用定时查询,发现若请求未返回就发下一次,可能重复请求;因此用 setTimeout 递归:本次查询完成后再安排下一次。
怎么做:payHandler 生成二维码并弹窗,payTimeOut 每秒查询一次:205 继续递归,否则提示成功、关闭弹窗、清定时器、跳转。
js
methods:{
payTimeOut(){
this.timer = setTimeout(async ()=>{
const status =await getQueryPayStatus(this.$route.params.orderId);
if(status.code === 205){
this.payTimeOut();
}else{
console.log("status",status);
this.$message.success("支付成功!");
this.$msgbox.close();// 关闭弹窗
clearTimeout(this.timer);
this.$router.push("/paySuccess");
}
},1000);
},
async payHandler(){
const base64Url = await QRCode.toDataURL(this.payInfo.codeUrl);
this.$alert(`<img width="200" src="${base64Url}"/>`, '用微信扫描下方二维码完成支付!', {
// 是否将 message 属性作为 HTML 片段处理
dangerouslyUseHTMLString: true,
// 是否居中布局
center:true,
// 是否显示取消按钮
showCancelButton:true,
// 完成按钮的文本内容
confirmButtonText:"已完成微信支付",
// 取消按钮的文本内容
cancelButtonText:"支付遇到问题",
// MessageBox 是否显示右上角关闭按钮
showClose:false,
callback:(action)=>{
clearTimeout(this.timer);
if(action === "confirm"){
console.log("已完成支付");
this.$router.push("/paySuccess")
}else{
console.log("支付遇到问题");
this.$message.warning("如果遇到问题,可以联系客服!")
}
}
});
this.payTimeOut();
// this.timer = setInterval(async ()=>{
// const status =await getQueryPayStatus(this.$route.params.orderId);
// console.log("status",status);
// this.$message.success("支付成功!");
// this.$msgbox.close();// 关闭弹窗
// clearInterval(this.timer);
// this.$router.push("/paySuccess");
// },200);
}
},易错点:$alert 要加 dangerouslyUseHTMLString:true 才能渲染 base64 图片;成功、确认、取消都要清 timer;转写稿调试中特别提醒,若允许直接切换路由,还要在离开组件前清理定时器,避免后台继续请求。
响应拦截器:放行 205 支付中
为什么需要:项目统一拦截异常,但 205 是“支付中”,不是异常;若被拦截,轮询拿不到状态。
怎么想:200 和 205 都返回 response.data,其他 code 再提示异常并阻断。
怎么做:修改 src/request/sphRequest.js:
js
sphRequest.interceptors.response.use(response => {
nprogress.done();// 结束进度条
// token异常
if(response.data.code === 208){
// 清空token
// 跳转至登陆界面
store.commit("user/OUT_LOG");
Message.error("token超时,请重新登陆")
return new Promise(()=>{});
}
// 1- 增加判断code是否为205
if(response.data.code === 200 || response.data.code === 205){
return response.data;// 返回响应体
}
// if(response.data.code === 205){
// return new Promise(()=>{});
// }
Message.error("异常:"+response.data.message);
return new Promise(()=>{});
}, (err) => {
nprogress.done();// 结束进度条
alert(err);
return new Promise(() => {
});// 返回一个状态为pending的Promise痊
});易错点:205 若走异常分支,页面会一直提示异常;返回 new Promise(()=>{}) 会阻断后续流程;不要漏掉 nprogress.done()。转写稿中控制台一直输出 205,正说明订单仍在支付中,放行后 payTimeOut 才能继续递归查询。
为什么需要路由懒加载
路由懒加载要解决的问题,是首屏一次性把太多页面代码都下载下来。转写稿里先打开浏览器控制台看 Network,刷新项目后看到首屏只加载了几个文件;再往下看自己的包,发现其中一个包大约 1.4 兆,里面不只是首页,还包括搜索、订单、支付成功等很多页面资源。也就是说,用户可能只想看首页,但其他页面的代码也被提前加载了,这部分请求就是无效的,会浪费带宽和加载时间。
所以路由懒加载的第一个优点是:减少网络请求、降低请求文件的大小。它不是把所有页面都塞进首屏包,而是把每个路由页面单独拆成一个 chunk,用户访问哪个路由,再加载哪个路由对应的文件。转写稿里也明确说了缺点:如果某一个文件过大,会影响用户体验。比如某个页面本身写得特别大,点进去时才加载,就会明显慢一些;还举例说一个文件 20 兆就会很卡。因此懒加载不是“越拆越好”,而是要让首屏轻,同时避免单个页面 chunk 过大。
怎么想:先观察首屏,再改路由
做路由懒加载之前,先不要急着改代码。转写稿里的调试顺序是:打开 Network,刷新页面,观察首屏加载了哪些资源、自己的包有多大;再判断这些资源里是否包含当前页面用不到的页面代码。如果首屏包很大,且里面混了很多路由页面,就适合做路由懒加载。
思路是:原来用静态 import 直接引入页面组件,项目启动或首屏加载时就会把这些模块一起打进去;改成动态 import() 后,构建工具会把每个页面拆成独立文件,路由切换到该页面时再请求对应文件。转写稿里说“把每一个路由都单独读写成一个 D”,这里应理解为单独打包成一个 chunk。改完后再次刷新 Network,观察首屏包是否变小;再点击搜索、订单等路由,观察是否新增对应 chunk;回到首页时,已经加载过的 chunk 不会重复加载。
怎么做:把静态引入改成动态 import
本节笔记给出的位置是 src/router/index.js,代码如下:
js
const Home = ()=>import("@/pages/Home");
const Login = ()=>import("@/pages/Login");
const Register = ()=>import("@/pages/Register");
const Search = ()=>import("@/pages/Search");
const Detail = ()=>import("@/pages/Detail");
const AddCartSuccess = ()=>import("@/pages/AddCartSuccess");
const Cart = ()=>import("@/pages/Cart");
const Trade = ()=>import("@/pages/Trade");
const Pay = ()=>import("@/pages/Pay");
const PaySuccess = ()=>import("@/pages/PaySuccess");这些常量后面仍然作为路由配置里的 component 使用,只是组件从“直接引入”变成了“返回 import 的函数”。转写稿里强调:不要再用直接 import 的方式,而是写成 const Home = () => import("@/pages/Home") 这种形式;一共改了 10 个页面。改的时候要注意路径别名 @ 指向 src,不要把 @/pages/... 写错;也不要只改一部分路由,否则没改的页面仍然会进首屏包。
调试:看首屏包和按需 chunk
改完后转写稿里的验证过程是:刷新首页,能感觉到比之前快一点;再看 Network,首屏加载包的大小已经不再是原来那个 1.4 兆左右的大包,而是变小了。接着点击搜索,Network 里会新增搜索页面对应的 chunk;再回到首页,已经加载过的资源不会重复加载。最后进入订单页,也会加载 Pay 下面的 chunk。这个调试过程说明懒加载生效了:首屏只加载首页需要的代码,其他页面按路由访问时再加载。
易错点与边界
第一,路由懒加载不是图片懒加载。转写稿后面才讲图片懒加载,两者解决的不是同一个问题。第二,懒加载后如果某个页面文件过大,用户点进去仍然会等,所以单个 chunk 不能无限大,必要时还要继续拆分页面内部的大模块。第三,改完要清缓存或强制刷新看 Network,避免浏览器缓存干扰判断。第四,不要漏改路由,也不要把动态 import 写成普通函数调用;它返回的是 Promise,交给路由和构建工具处理即可。第五,首屏仍然需要核心依赖和公共代码,懒加载只是把非首屏路由拆出去,不是所有资源都延迟加载。
课堂画面





代码示例
代码一:支付页 —— 生成二维码 + 弹窗 + 心跳轮询(src/pages/Pay/index.vue)
vue
<template>
<div class="submit">
<a @click.prevent="payHandler" class="btn" href="" target="_blank">立即支付</a>
</div>
</template>
<script>
import QRCode from 'qrcode';
import { getQueryPayStatus } from '@/api/payment';
export default {
name: "Pay",
data() {
return {
// 支付信息由服务端接口返回(订单号、总价格、codeUrl 等)
payInfo: {},
timer: null
}
},
methods: {
// 心跳查询:递归 setTimeout,等上一次请求回来再等 1 秒发下一次
payTimeOut() {
this.timer = setTimeout(async () => {
const status = await getQueryPayStatus(this.$route.params.orderId);
if (status.code === 205) { // 205 = 支付中,继续轮询
this.payTimeOut();
} else { // 其他(200)= 支付成功
this.$message.success("支付成功!");
this.$msgbox.close(); // 关闭弹窗
clearTimeout(this.timer);
this.$router.push("/paySuccess");
}
}, 1000);
},
async payHandler() {
// 将支付链接转成 base64 图片地址
const base64Url = await QRCode.toDataURL(this.payInfo.codeUrl);
this.$alert(`<img width="200" src="${base64Url}"/>`, '用微信扫描下方二维码完成支付!', {
// 是否将 message 属性作为 HTML 片段处理
dangerouslyUseHTMLString: true,
// 是否居中布局
center: true,
// 是否显示取消按钮
showCancelButton: true,
// 完成按钮的文本内容
confirmButtonText: "已完成微信支付",
// 取消按钮的文本内容
cancelButtonText: "支付遇到问题",
// MessageBox 是否显示右上角关闭按钮
showClose: false,
callback: (action) => {
clearTimeout(this.timer); // 关弹窗前必须先清定时器
if (action === "confirm") {
this.$router.push("/paySuccess");
} else {
this.$message.warning("如果遇到问题,可以联系客服!");
}
}
});
this.payTimeOut(); // 弹窗出现后开始心跳
}
},
beforeDestroy() {
clearTimeout(this.timer); // 离开路由时兜底清理
}
}
</script>关键行 / 易错点
await QRCode.toDataURL(...):toDataURL返回的是 Promise,不加await拿到的是 Promise 对象,src里就是[object Promise]。- 模板字符串
`<img src="${base64Url}"/>`是必须的:转写稿中强调“得拼接”,直接写src="base64Url"只会把变量名当字符串。 dangerouslyUseHTMLString: true不开,<img>会被当成纯文本转义输出。- 用
setTimeout递归而不是setInterval:setInterval不会等上一次请求返回,网络慢时会连发多次请求(转写稿实测“成功了 3 次”)。 clearTimeout要在三个地方清:callback(点确认/取消)、支付成功分支、beforeDestroy(切路由)。- 弹窗关闭用
this.$msgbox.close(),需要在src/index.js里挂了Vue.prototype.$msgbox = MessageBox。
代码二:支付状态接口 + 响应拦截器放行 205(src/api/payment.js、src/request/sphRequest.js)
js
// ---------------- src/api/payment.js ----------------
import sphRequest from '@/request/sphRequest';
// 查询订单的支付状态: /payment/weixin/queryPayStatus/{orderId}
export const getQueryPayStatus = orderId => sphRequest.get(`/payment/weixin/queryPayStatus/${orderId}`);js
// ---------------- src/request/sphRequest.js(响应拦截器部分)----------------
sphRequest.interceptors.response.use(response => {
nprogress.done();// 结束进度条
// token 异常
if (response.data.code === 208) {
// 清空token
// 跳转至登陆界面
store.commit("user/OUT_LOG");
Message.error("token超时,请重新登陆")
return new Promise(() => {});
}
// 1- 增加判断code是否为205:200 成功、205 支付中,都放行
if (response.data.code === 200 || response.data.code === 205) {
return response.data;// 返回响应体
}
Message.error("异常:" + response.data.message);
return new Promise(() => {});
}, (err) => {
nprogress.done();// 结束进度条
alert(err);
return new Promise(() => {
});// 返回一个状态为pending的Promise
});关键行 / 易错点
code === 205必须加入放行白名单:否则拦截器会走Message.error并返回 pending Promise,业务层的await getQueryPayStatus(...)永远拿不到status,status.code === 205判断也永远不成立。return new Promise(() => {})是“挂起”写法:返回一个永远 pending 的 Promise,后续.then不执行,用于中断业务链。- 接口路径里的
{orderId}要用模板字符串替换成真实订单号,this.$route.params.orderId对应路由:orderId参数。
代码三:支付成功页 + 路由懒加载(src/pages/PaySuccess/index.vue、src/router/index.js)
vue
<!-- ---------------- src/pages/PaySuccess/index.vue ---------------- -->
<template>
<div class="paysuccess">
<div class="success">
<h3>
<span style="font-size: 30px;color:#71b247" class="iconfont icon-weixinzhifu"></span>
<!-- <img src="./images/right.png" width="48" height="48"> -->
恭喜您,支付成功啦!
</h3>
<div class="paydetail">
<p class="button">
<a href="myOrder.html" class="btn-look">查看订单</a>
<a href="index.html" class="btn-goshop">继续购物</a>
</p>
</div>
</div>
</div>
</template>
<script>
export default {
name: "PaySuccess"
}
</script>js
// ---------------- src/router/index.js(路由懒加载写法)----------------
const Home = () => import("@/pages/Home");
const Login = () => import("@/pages/Login");
const Register = () => import("@/pages/Register");
const Search = () => import("@/pages/Search");
const Detail = () => import("@/pages/Detail");
const AddCartSuccess = () => import("@/pages/AddCartSuccess");
const Cart = () => import("@/pages/Cart");
const Trade = () => import("@/pages/Trade");
const Pay = () => import("@/pages/Pay");
const PaySuccess = () => import("@/pages/PaySuccess");关键行 / 易错点
- 懒加载必须写成 箭头函数返回
import():const Home = () => import("@/pages/Home")。写成const Home = import(...)会立刻执行,等于没懒加载。 - 支付成功页里“查看订单 / 继续购物”是静态
href,实际项目里换成<router-link>才走前端路由(转写稿中留给学生自己完成)。 - 懒加载的取舍:好处是首屏只加载当前路由的 chunk,包体从 1.4M 拆分下来;坏处是单个 chunk 过大时,点进去那一刻才下载,会有明显等待。
常见错误
- 现象:弹窗中二维码图片破图或不显示;原因:
src拼接错误,或异步生成base64Url后未正确放入模板字符串;修法:await QRCode.toDataURL(...)后用模板字符串拼到<img src="${base64Url}">。 - 现象:支付成功后仍反复请求订单状态;原因:轮询定时器未在成功、关闭弹窗、离开路由时清理;修法:保存 timer,在成功/取消/关闭/
beforeDestroy/路由离开时统一clearTimeout。 - 现象:订单状态为 205 时被提示异常且不再轮询;原因:响应拦截器只放行 200,把 205 当异常;修法:拦截器放行 200 和 205,业务层再判断 205 继续查询。
- 现象:轮询短时间内发出多次请求;原因:
setInterval或固定延时递归在请求未返回时重入;修法:改为“请求完成后再延时发起下一次”的单链路轮询,并在成功时终止。 - 现象:点击“已完成微信支付”后跳转报错或
this不是组件;原因:callback写成普通函数,this指向变化;修法:使用箭头函数,或提前保存组件实例。 - 现象:路由懒加载后仍打包成一个大文件;原因:路由仍用静态
import,或只改了部分路由;修法:统一改为const X = () => import("@/pages/X")。 - 现象:
v-lazy不生效或图片不替换;原因:未安装/未注册vue-lazyload,或 Vue2 项目误装不兼容版本;修法:在项目根目录安装 Vue2 兼容版本,Vue.use注册并配置loading图。
练习
- 排查并修复“支付弹窗中二维码破图”的问题,要求说明问题定位点和最终修法。
- 模拟接口持续返回 205,改造轮询逻辑,确保支付成功后只跳转一次且弹窗关闭。
- 在支付页实现离开路由或关闭弹窗时停止轮询,并验证控制台不再发请求。
- 将现有 10 个页面路由改为懒加载,观察首屏加载资源体积变化。
- 在搜索页商品图片上接入图片懒加载,配置本地 loading 占位图并验证弱网效果。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 生成二维码 | 11:12 |
| 2 | 完成支付 | 35:45 |
| 3 | 支付流程 | 06:29 |
| 4 | 路由懒加载 | 07:11 |
| 5 | 图片懒加载 | 12:22 |
小结
- 支付弹窗的核心不是“显示二维码”,而是二维码生成、状态轮询、弹窗关闭、路由跳转形成闭环。
- 205 应视为“支付中”而非异常,拦截器与业务层职责要分清。
- 轮询必须可终止:成功、失败、取消、关闭弹窗、离开页面都要清理。
- 固定间隔轮询容易重入,请求完成后再延时更稳。
- 支付成功跳转前应同时处理提示、关闭弹窗、清理定时器。
- 路由懒加载能降低首屏资源体积,但单页过大仍会影响体验。
- 图片懒加载主要提升弱网体验,不会减少图片最终加载量。
- 第三方依赖应在项目根目录安装,并注意 Vue2 插件版本兼容。