主题
完成提交订单-使用store与将两个参数合并为一个(Lesson22 1/3)
本页对应 Lesson22,共 5 段视频,约 1 小时 34 分。
学习目标
- 能够为“提交订单”按钮绑定点击事件,收集交易号与订单请求体,调用提交订单接口,并根据返回的订单号跳转到支付页面。
- 能够将交易号与请求体两个参数合并为一个参数传递给提交订单接口,并通过网络面板验证请求成功。
- 能够在不使用 store 的情况下,在页面内直接调用提交订单 API 完成订单提交,并验证提交结果。
- 能够新建并配置支付页面路由,复用支付页样式与公共图片资源,正确展示订单号和应付金额。
- 能够调用“获取订单支付信息”接口,接收订单号参数并将返回数据渲染到支付页面。
- 能够使用 Element UI 的 MessageBox 实现支付弹窗,配置标题、按钮文本、居中布局和隐藏关闭按钮,并通过回调区分“已完成支付”和“支付遇到问题”。
知识点
目标与整体流程
这一节要完成的是:在结算页点击“提交订单”,收集订单数据,调用提交订单接口,接口成功后进入订单成功支付界面。转写稿里先把目标说清楚:不是“点一下直接跳页面”,而是跳转前必须真正完成提交。整体推导是:给按钮加单击事件 → 在事件中收集收件人、电话、地址、支付方式、备注、商品清单等数据 → 调用接口 → 成功后拿订单号进入支付页。
为什么需要:提交订单是交易闭环的关键一步,数据不齐或参数结构不对,后端就无法生成订单。
怎么想:先找接口文档,再写 API,再决定是否用 Vuex 封装,最后在组件里绑定事件。转写稿强调“写不下去就先找接口”,这是正确顺序。
怎么做:按下面 API、store、组件三层落地。
易错点:不能只写跳转不提交;不能把 tradeNo 和 body 的结构传错。
先定义提交订单接口
src->api->order.js 中新增提交订单方法。接口地址里 tradeNo 拼在路径上,其余订单数据放在请求体 body 中。
js
// 提交订单:/order/auth/submitOrder?tradeNo={tradeNo} post
export const postSubmitOrder = (tradeNo,body)=>{
return sphRequest.post(`/order/auth/submitOrder?tradeNo=${tradeNo}`,body)
}为什么需要:组件不直接拼接口地址,统一放在 api 层,后续维护和排查更方便。
怎么想:看文档确认请求方式为 POST,tradeNo 是路径查询参数,其他字段是请求体。
怎么做:用模板字符串拼接 tradeNo,第二个参数传 body。
易错点:把 tradeNo 放进 body 或把 body 拼到 URL 上,都会导致后端取不到数据。
在 store 中封装 action
src->store->order.js 中增加异步 action,组件通过 dispatch 调用。
js
import {getTradeList, postSubmitOrder} from "@/api/order";
const actions = {
// 提交订单
async postSubmitOrderAsync(context,{tradeNo,body}){
const result = await postSubmitOrder(tradeNo,body);
return result;
}
}
export default {
namespaced:true,
actions,
state,
mutations
}为什么需要:订单提交属于异步业务,放进 store 后组件只负责收集数据,接口调用和结果返回集中在 action 中。
怎么想:action 第二个参数用对象解构 {tradeNo, body},因为调用方需要同时传交易号和请求体。
怎么做:await postSubmitOrder(tradeNo,body),并把 result 返回给组件。
易错点:模块开了 namespaced:true,调用时必须写 "order/postSubmitOrderAsync";解构参数时如果 dispatch 传成 {tradeNo, body} 之外的结构,就会出现 body 为 undefined。转写稿中调试时就是通过 Network 看请求,发现参数结构不一致后调整传参。
在结算页绑定点击并收集数据
src->store->Trade->index.vue 中给“提交订单”加点击事件,方法里从默认地址和 store 的交易信息中取数据。
vue
<template>
<a @click.prevent="submitOrder" href="##" class="subBtn">提交订单</a>
</template>
<script>
methods:{
async submitOrder(){
// console.log(this.addressDefault);
const {consignee,phoneNum:consigneeTel,fullAddress:deliveryAddress} = this.addressDefault;
const {orderComment} = this;
const result = await this.$store.dispatch("order/postSubmitOrderAsync",{
tradeNo:this.$store.state.order.tradeInfo.tradeNo,
body:{
// 收件人姓名
consignee,
// 收件人电话
consigneeTel,
// 收件地址
deliveryAddress,
// 支付方式
paymentWay:"ONLINE",
// 订单备注
orderComment,
// 存储多个商品对象的数组
orderDetailList:this.$store.state.order.tradeInfo.detailArrayList
}
});
console.log(result);
}
},
</script>为什么需要:页面上的地址、备注、商品清单分散在不同位置,提交前必须组装成接口要求的六个字段。
怎么想:默认地址用 addressDefault 解构,并重命名成接口字段;交易号、商品清单从 order.tradeInfo 取;支付方式固定 "ONLINE"。
怎么做:@click.prevent 阻止默认跳转,async/await 等待提交结果。转写稿中提交成功后出现“不能重复提交”,说明订单已生成;此时 result 中可拿到订单号,后续支付页需要用它。
易错点:phoneNum 要映射为 consigneeTel,fullAddress 要映射为 deliveryAddress;orderDetailList 不是随便的数组,而是 tradeInfo.detailArrayList;地址默认项为空时先判空,否则解构会报错;当前示例只 console.log(result),实际还应携带订单号跳转支付页。
先明确本节目标:把提交订单的参数收拢成一个对象
这一节要解决的不是“页面怎么提交”,而是提交订单调用链里的参数形态:接口需要 tradeNo 拼在 URL 上,同时还需要一整个订单请求体。最初容易写成“接口接收两个参数:交易号和 body”,但这样从页面到 store 再到 api,每层都要同时传两份数据,维护起来很别扭。转写稿里的思路是:既然页面本来就要收集一个订单对象,那就把这个对象一路传下去,在 api 层从对象里取 tradeNo 拼路径,其余字段继续作为请求体发送。也就是“将两个参数合并为一个”。
为什么需要这样做?因为提交订单的业务数据是一个整体:收件人、电话、地址、支付方式、备注、商品清单都在同一个对象里,tradeNo 只是其中用于拼 URL 的一个字段。如果 api 层拆成两个参数,页面和 store 都要重复维护“交易号”和“请求体”的对应关系,一旦字段改名或漏传,很难定位。怎么想?先看接口文档:提交订单是 POST,tradeNo 拼接在路径中,其他数据放在请求体里。那就让 api 只接收一个 body,URL 用 body.tradeNo,请求体也传 body。怎么做?看下面三段代码。
API 层:只接收一个 body,从 body 中取 tradeNo
js
// 提交订单:/order/auth/submitOrder?tradeNo={tradeNo} post
export const postSubmitOrder = body=>{
return sphRequest.post(`/order/auth/submitOrder?tradeNo=${body.tradeNo}`,body)
}为什么需要这样写?因为调用方不再需要记住“第一个参数是交易号、第二个参数是请求体”,只准备一个订单对象即可。怎么想?body.tradeNo 专门用于模板字符串拼 URL;第二个参数 body 是整个请求体。转写稿里也提到,请求体里多带一个 tradeNo 一般不影响,后端不用而已,所以不必为了“干净”再删一次。易错点:body 不能为空,否则 body.tradeNo 会报错;模板字符串的反引号和 ${} 不能写错;不要出现 tradeNo 变量未定义却直接拼接的情况。调试时如果请求地址里出现 undefined,优先检查页面 dispatch 时有没有把 tradeNo 放进对象。
store 层:action 只负责透传一个参数
js
// 提交订单
async postSubmitOrderAsync(context,body){
const result = await postSubmitOrder(body);
return result;
},为什么需要这层?页面不直接依赖 api,而是通过 store 的 action 发请求,后续加 loading、统一错误处理都方便。怎么想?既然 api 已经合并为一个参数,action 也只需要 context 和 body,把 body 原样交给 postSubmitOrder,再把结果返回。怎么做?保持 async/await,拿到 result 后 return result。易错点:不要还写成旧签名 postSubmitOrderAsync(context, tradeNo, body);不要忘了 return,否则页面拿不到提交结果;context 当前没用到也不能删,因为 action 的第一个参数约定是上下文。
页面层:收集订单数据,dispatch 时传一个完整对象
js
async submitOrder(){
// console.log(this.addressDefault);
const {consignee,phoneNum:consigneeTel,fullAddress:deliveryAddress} = this.addressDefault;
const {orderComment} = this;
const result = await this.$store.dispatch("order/postSubmitOrderAsync",{
tradeNo:this.$store.state.order.tradeInfo.tradeNo,
// 收件人姓名
consignee,
// 收件人电话
consigneeTel,
// 收件地址
deliveryAddress,
// 支付方式
paymentWay:"ONLINE",
// 订单备注
orderComment,
// 存储多个商品对象的数组
orderDetailList:this.$store.state.order.tradeInfo.detailArrayList
});
console.log(result);
}为什么需要这样收集?提交订单按钮触发后,必须把当前默认地址、备注、交易信息和商品清单一次性交给后端。怎么想?地址对象里的字段名和接口字段不一致,所以用解构重命名:phoneNum 改成 consigneeTel,fullAddress 改成 deliveryAddress;备注从组件 this.orderComment 取;交易号和商品清单从 store.state.order.tradeInfo 取。怎么做?dispatch 的第二个参数直接传一个对象,这个对象同时包含 tradeNo 和请求体字段,正好对应 api 层“一个 body”。易错点:addressDefault 必须有值,否则解构会失败;paymentWay 按接口要求写 "ONLINE";商品数组字段是 detailArrayList,不要写成别的名字;orderComment 如果页面没有绑定,会是空值,但不影响提交。转写稿里调试时出现过请求异常,排查顺序就是先看网络面板里的请求方式、请求地址和请求体,再回到参数对象逐项核对。
验证与调试:看请求、看返回值、看是否重复提交
改完后点击提交订单,打开浏览器网络面板,确认请求方式是 POST,地址是 /order/auth/submitOrder?tradeNo=...,请求体里有收件人、电话、地址、支付方式、备注和商品数组。转写稿中提到,提交成功后会返回订单号,后续支付页需要用到;如果再次提交相同订单,后端可能提示“不能重复提交”,这反过来说明上一次提交已经成功。易错点:不要只看控制台输出,必须看网络请求;如果 URL 里 tradeNo 是 undefined,回到页面 dispatch 对象检查;如果 store action 报参数错误,检查是否还按旧的两个参数调用。这样合并后,调用链更短,字段来源也更清楚。
一、先明确目标:提交订单按钮要做什么
为什么需要:提交订单不是“点一下跳页面”这么简单。转写稿里反复强调,先有目标才有思路:用户点击“提交订单”按钮后,要把订单相关数据提交到服务器,成功后再进入订单成功/支付界面。
怎么想:按业务流程拆成三步:给提交订单按钮增加单击事件 → 在事件中收集订单相关数据 → 调用提交订单接口,成功后进入支付界面。
怎么做:在 Trade/index.vue 中找到“提交订单”按钮,绑定单击事件,例如 @click="submitOrder";在方法里完成数据收集和接口调用。转写稿中明确说:“不能说你写个跳转就行,跳转之前必须先完成提交操作。”
易错点:不要只写路由跳转而不调接口;也不要把“提交订单”和“支付”混成一步。提交成功只代表订单已生成,支付是后续页面的事。
二、找接口与组织参数
为什么需要:组件里写不下去时,通常是因为还没确认接口。转写稿的做法是先去接口文档找“提交订单”接口,再回到代码里写调用。
怎么想:提交订单接口是 POST 请求,需要交易编号、收件人姓名、收件人电话、收件地址、支付方式、订单备注、商品清单等数据。转写稿提到接口文档里交易编号可能要求拼接在路径中,其他数据放请求体;后续又把参数合并成一个对象传给 API 方法,减少参数个数。学习时以本节笔记代码的调用方式为准,同时对照 postSubmitOrder 内部实现确认路径和 body 的处理。
怎么做:从 addressDefault 中解构收件人信息,并用别名映射成接口字段:phoneNum:consigneeTel、fullAddress:deliveryAddress;orderComment 从组件数据取;tradeNo 和 orderDetailList 从结算页存入 store 的 tradeInfo 中取。支付方式固定为 "ONLINE"。
易错点:字段名不能想当然。转写稿里专门调试过:页面里的电话、地址字段和接口字段不完全一样,所以要用解构重命名;orderDetailList 对应的是 detailArrayList,不要写成 detailList;paymentWay 只支持在线支付时不要乱改。
三、在组件内直接调用接口(未使用 store action)
为什么需要:转写稿里说,store 之前帮忙做的主要是“调用 API”。现在不通过 store 封装提交动作,而是在组件内自己收集数据、自己调接口,逻辑更直接。
怎么想:注意“未使用 store”指的是不通过 store 里的 action/dispatch 去提交,而不是完全不读 store。交易编号和商品清单仍来自结算页写入的 tradeInfo,否则组件里拿不到。
怎么做:笔记代码保留如下:
js
async submitOrder(){
const {consignee,phoneNum:consigneeTel,fullAddress:deliveryAddress} = this.addressDefault;
const {orderComment} = this;
const result = await postSubmitOrder({
tradeNo:this.$store.state.order.tradeInfo.tradeNo,
// 收件人姓名
consignee,
// 收件人电话
consigneeTel,
// 收件地址
deliveryAddress,
// 支付方式
paymentWay:"ONLINE",
// 订单备注
orderComment,
// 存储多个商品对象的数组
orderDetailList:this.$store.state.order.tradeInfo.detailArrayList
});
console.log(result);
}易错点:确保 postSubmitOrder 已从 API 模块导入;方法要加 async 并用 await;console.log(result) 只用于调试,调通后应删除或替换成后续跳转逻辑;不要重复提交,转写稿中“不能重复提交”是后端业务校验,不是前端随便能跳过的。
四、调试与提交成功后的跳转
为什么需要:接口调通不代表流程完整。转写稿中通过浏览器 Network 面板检查请求方式、请求地址、请求体,确认返回 201 后,data 里是订单号。
怎么想:点击提交后先看网络请求;如果报错,优先检查请求方式、地址、body 参数是否齐全;如果提示“不能重复提交”,说明订单已生成,是正常业务提示。
怎么做:调试时保留 console.log(result) 查看返回;成功后按转写稿思路进入支付界面,并把订单号通过 query 或 params 传给支付页,因为支付页后续查询支付信息需要 orderId。
易错点:订单号在 result.data 中,不要拿错层级;支付页没有 orderId 就查不到支付信息;调试完记得清理多余输出,避免控制台干扰。
先封装支付接口:把请求地址从页面里抽出来
**为什么需要:**支付页要拿订单支付信息,如果直接在组件里写完整 URL,页面和接口会耦合,后面改地址、加拦截都不好维护。
**怎么想:**订单相关接口放 api/order.js,支付相关接口单独放 api/payment.js,页面只调用函数。
**怎么做:**新建 src/api/payment.js,按笔记保留 getCreateNative。
**易错点:**接口路径里的 {orderId} 是路径参数,调用时必须传真实订单号,否则会请求成 undefined。
js
import {sphRequest} from "@/request";
// 获取订单支付信息:/payment/weixin/createNative/{orderId}
export const getCreateNative = (orderId)=>sphRequest.get(`/payment/weixin/createNative/${orderId}`);搭支付页路由:让订单号跟着 URL 进页面
**为什么需要:**提交订单成功后要进入支付页,支付页还必须知道“支付哪一单”。
**怎么想:**把订单号放在路由参数里,支付页通过 this.$route.params.orderId 取。
**怎么做:**在 src/router/index.js 增加 /pay/:orderId.html,组件指向 Pay。
**易错点:**路由写的是 params,支付页却用 query 取,就会拿到 undefined;跳转地址也要拼上 .html。
js
{
path:"/pay/:orderId.html",
component:Pay
}支付页取数并渲染:mounted 里拿 orderId 调接口
**为什么需要:**支付页要显示订单号、应付金额,这些不能写死,必须从后端支付信息接口取。
**怎么想:**页面挂载后调 getCreateNative(orderId),把返回的 data 存到 payInfo,模板里渲染 payInfo.orderId、payInfo.totalFee。
**怎么做:**按笔记保留 Pay/index.vue,图片补中文 alt。
**易错点:**图片相对路径要确保 ./images/ 下真有对应文件;接口返回结构要看 result.data,不要直接拿 result。
vue
<template>
<!--中间内容-->
<div class="pay-main">
<div class="pay-container">
<div class="checkout-tit">
<h4 class="tit-txt">
<span class="success-icon"></span>
<span class="success-info">订单提交成功,请您及时付款,以便尽快为您发货~~</span>
</h4>
<div class="paymark">
<span class="fl">请您在提交订单<em class="orange time">4小时</em>之内完成支付,超时订单会自动取消。
订单号:<em>{{payInfo.orderId}}</em>
</span>
<span class="fr"><em class="lead">应付金额:</em><em class="orange money">¥{{payInfo.totalFee}}</em></span>
</div>
</div>
<div class="checkout-info">
<h4>重要说明:</h4>
<ol>
<li>尚品汇商城支付平台目前支持<span class="zfb">支付宝</span>支付方式。</li>
<li>其它支付渠道正在调试中,敬请期待。</li>
<li>为了保证您的购物支付流程顺利完成,请保存以下支付宝信息。</li>
</ol>
<h4>支付宝账户信息:(很重要,<span class="save">请保存!!!</span>)</h4>
<ul>
<li>支付帐号:11111111</li>
<li>密码:111111</li>
<li>支付密码:111111</li>
</ul>
</div>
<div class="checkout-steps">
<div class="step-tit">
<h5>支付平台</h5>
</div>
<div class="step-cont">
<ul class="payType">
<li><img src="./images/pay2.jpg" alt="支付宝支付方式图标"></li>
<li><img src="./images/pay3.jpg" alt="微信支付方式图标"></li>
</ul>
</div>
<div class="hr"></div>
<div class="payshipInfo">
<div class="step-tit">
<h5>支付网银</h5>
</div>
<div class="step-cont">
<ul class="payType">
<li><img src="./images/pay10.jpg" alt="网银图标1"></li>
<li><img src="./images/pay11.jpg" alt="网银图标2"></li>
<li><img src="./images/pay12.jpg" alt="网银图标3"></li>
<li><img src="./images/pay13.jpg" alt="网银图标4"></li>
<li><img src="./images/pay14.jpg" alt="网银图标5"></li>
<li><img src="./images/pay15.jpg" alt="网银图标6"></li>
<li><img src="./images/pay16.jpg" alt="网银图标7"></li>
<li><img src="./images/pay17.jpg" alt="网银图标8"></li>
<li><img src="./images/pay18.jpg" alt="网银图标9"></li>
<li><img src="./images/pay19.jpg" alt="网银图标10"></li>
<li><img src="./images/pay20.jpg" alt="网银图标11"></li>
<li><img src="./images/pay21.jpg" alt="网银图标12"></li>
<li><img src="./images/pay22.jpg" alt="网银图标13"></li>
</ul>
</div>
</div>
<div class="hr"></div>
<div class="submit">
<a class="btn" href="paysuccess.html" target="_blank">立即支付</a>
</div>
<div class="otherpay">
<div class="step-tit">
<h5>其他支付方式</h5>
</div>
<div class="step-cont">
<span><a href="weixinpay.html" target="_blank">微信支付</a></span>
<span>中国银联</span>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import {getCreateNative} from "@/api/payment";
export default {
name: "Pay",
data(){
return {
payInfo:{}
}
},
mounted(){
getCreateNative(this.$route.params.orderId).then(result=>{
console.log(result)
this.payInfo = result.data;
})
}
}
</script>提交订单后跳转:用后端返回的订单号拼支付地址
**为什么需要:**提交订单接口成功后,后端会返回订单号;支付页必须拿这个订单号才能查支付信息。
怎么想:submitOrder 里先收集收件人、电话、地址、支付方式、备注、商品清单,再 await postSubmitOrder(...),拿到 result.data 后跳转。
**怎么做:**按笔记保留跳转代码:this.$router.push("/pay/"+result.data+".html")。
**易错点:**不要写死订单号;转写稿调试时接口异常,是因为 API 封装把 tradeNo 和 body 拆成两个参数,而调用处传参不匹配,后来合并成一个对象才通。转写稿里的“包底/保底”应纠正为 body,“IDD”是 orderId。
js
async submitOrder(){
const {consignee,phoneNum:consigneeTel,fullAddress:deliveryAddress} = this.addressDefault;
const {orderComment} = this;
const result = await postSubmitOrder({
tradeNo:this.$store.state.order.tradeInfo.tradeNo,
// 收件人姓名
consignee,
// 收件人电话
consigneeTel,
// 收件地址
deliveryAddress,
// 支付方式
paymentWay:"ONLINE",
// 订单备注
orderComment,
// 存储多个商品对象的数组
orderDetailList:this.$store.state.order.tradeInfo.detailArrayList
});
this.$router.push("/pay/"+result.data+".html")
}调试复盘:从 201、undefined 到支付页有数据
**为什么需要:**这一节最容易卡在“订单提交了但支付页没数据”。
**怎么想:**先看网络请求:提交订单是否 201、返回 data 是否订单号;再看路由是否把订单号带进支付页;最后看 mounted 是否用 params.orderId 调支付接口。
**怎么做:**转写稿中“不能重复提交”是后端校验;换商品重新结算后能拿到新订单号。支付页请求 /payment/weixin/createNative/{orderId} 成功后再渲染。
**易错点:**转写稿里的“攀姆”是 params,“夸瑞”是 query”,“直播界面”是支付界面;params路由和query` 路由取法不同。图片路径也不能在弹窗里直接写模板相对路径,转写稿中 404 就是路径按当前 URL 解析导致的。
先明确目标:点击“立即支付”后弹出支付窗口
为什么需要:提交订单后,用户需要看到支付入口。转写稿里明确目标是:点击“立即支付”按钮后弹出一个支付窗口,窗口里有二维码图片、提示标题,以及“已完成微信支付”“支付遇到问题”两个操作按钮,并且不希望用户点右上角叉号关闭。
怎么想:不要自己从零写弹窗,优先用 Element UI 的 MessageBox。先想清楚弹窗要展示什么:HTML 图片、标题、按钮文案、是否居中、是否显示取消按钮、是否显示右上角关闭按钮,以及点击按钮后的回调。
怎么做:在 src/pages/Pay/index.vue 的支付按钮上绑定点击事件,在 payHandler 中调用 this.$alert:
vue
<template>
<div class="submit">
<a @click.prevent="payHandler" class="btn" href="" target="_blank">立即支付</a>
</div>
</template>
<script>
methods:{
payHandler(){
this.$alert('<img width="200" src="/images/1.jpg"/>', '用微信扫描下方二维码完成支付!', {
// 是否将 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 不开,<img> 会被当普通文本显示;开了就表示内容按 HTML 渲染,只能放可信内容,转写稿也提醒注意 XSS。callback 里判断的是 confirm,取消会走 else。图片路径不要写当前目录相对路径,弹窗渲染时容易 404,笔记中使用的是 /images/1.jpg 这种绝对路径。
全局注册 MessageBox,解决 $alert 不是函数
为什么需要:转写稿里第一次直接写 this.$alert,控制台报“this.$alert is not a function”。原因是 Vue 实例上还没有挂载 $alert,组件里不能直接凭空调用。
怎么想:Element UI 的 MessageBox 需要先引入,再把 MessageBox.alert 挂到 Vue.prototype.$alert。这样所有组件里都能用 this.$alert。同时笔记里也挂了 $msgbox 和 $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;易错点:只 import MessageBox 但没有挂 Vue.prototype.$alert,仍会报不是函数;$alert 对应的是 MessageBox.alert,不是把 MessageBox 整个当函数调用。转写稿中出现的“买几个”应理解为 MessageBox,“财除所”是弹出框,“靠拍子”是 callback。
调试细节:图片 404、关闭报 Promise 异常
为什么需要:弹窗能出来后还要处理真实调试问题。转写稿里图片一开始用相对路径,弹窗打开后 404;点击关闭时控制台又出现 Promise 异常。
怎么想:图片 404 说明弹窗里的 src 是按当前页面路径解析的,改成以 / 开头的静态资源地址更稳;关闭报错说明 $alert 返回 Promise,用户点取消/关闭时 Promise 进入失败状态,没有处理就会提示未捕获异常。可以用 callback 统一处理,也可以补 catch。
怎么做:保留笔记中的 /images/1.jpg,并确认静态资源真实存在;按钮结果用 callback(action) 区分:
js
callback(action){
if(action === "confirm"){
console.log("已完成支付")
}else{
console.log("支付遇到问题")
}
}易错点:不要写 ./images/1.jpg 这类相对路径;showClose:false 只是去掉右上角叉号,不代表不会产生取消动作;callback 中不要把 confirm 拼错,转写稿里“开锁/car尔”实际是 cancel。支付弹窗只是交互层,后续支付状态仍需接口确认,不能只靠前端按钮判断。
课堂画面





代码示例
js
// src/api/order.js
import { sphRequest } from "@/request";
// 提交订单:/order/auth/submitOrder?tradeNo={tradeNo} post
export const postSubmitOrder = (tradeNo, body) => {
return sphRequest.post(`/order/auth/submitOrder?tradeNo=${tradeNo}`, body);
};
// src/store/order.js
import { postSubmitOrder } from "@/api/order";
const actions = {
// 提交订单
async postSubmitOrderAsync(context, { tradeNo, body }) {
const result = await postSubmitOrder(tradeNo, body);
return result;
}
};
export default {
namespaced: true,
actions
// state、mutations 沿用项目已有代码
};
// src/pages/Trade/index.vue 中提交订单方法
methods: {
async submitOrder() {
const {
consignee,
phoneNum: consigneeTel,
fullAddress: deliveryAddress
} = this.addressDefault;
const { orderComment } = this;
const result = await this.$store.dispatch("order/postSubmitOrderAsync", {
tradeNo: this.$store.state.order.tradeInfo.tradeNo,
body: {
// 收件人姓名
consignee,
// 收件人电话
consigneeTel,
// 收件地址
deliveryAddress,
// 支付方式
paymentWay: "ONLINE",
// 订单备注
orderComment,
// 存储多个商品对象的数组
orderDetailList: this.$store.state.order.tradeInfo.detailArrayList
}
});
this.$router.push("/pay/" + result.data + ".html");
}
}要点解释:
postSubmitOrder第一个参数是tradeNo,第二个参数是请求体body。- store 中
postSubmitOrderAsync接收{ tradeNo, body },调用接口后把结果返回给组件。 paymentWay: "ONLINE"表示在线支付,笔记中明确只支持该方式。orderDetailList来自tradeInfo.detailArrayList,不要写错字段名。- 提交成功后用
result.data作为订单号跳转:/pay/订单号.html。
js
// src/api/order.js
// 提交订单:/order/auth/submitOrder?tradeNo={tradeNo} post
export const postSubmitOrder = body => {
return sphRequest.post(
`/order/auth/submitOrder?tradeNo=${body.tradeNo}`,
body
);
};
// src/store/order.js
const actions = {
// 提交订单
async postSubmitOrderAsync(context, body) {
const result = await postSubmitOrder(body);
return result;
}
};
// src/pages/Trade/index.vue
async submitOrder() {
const {
consignee,
phoneNum: consigneeTel,
fullAddress: deliveryAddress
} = this.addressDefault;
const { orderComment } = this;
const result = await this.$store.dispatch("order/postSubmitOrderAsync", {
tradeNo: this.$store.state.order.tradeInfo.tradeNo,
// 收件人姓名
consignee,
// 收件人电话
consigneeTel,
// 收件地址
deliveryAddress,
// 支付方式
paymentWay: "ONLINE",
// 订单备注
orderComment,
// 存储多个商品对象的数组
orderDetailList: this.$store.state.order.tradeInfo.detailArrayList
});
console.log(result);
}要点解释:
- 合并参数后,
postSubmitOrder只接收一个body对象。 - 接口地址中的
tradeNo从body.tradeNo取,不再单独传第二个参数。 - store 的 action 第二个参数直接就是
body,不要再写成{ tradeNo, body }。 - 组件 dispatch 时也直接传一个大对象,字段全部平铺。
- 这是“将两个参数合并为一个”的关键改动,容易和上一版参数结构混用。
vue
// src/api/payment.js
import { sphRequest } from "@/request";
// 获取订单支付信息:/payment/weixin/createNative/{orderId}
export const getCreateNative = orderId =>
sphRequest.get(`/payment/weixin/createNative/${orderId}`);
// src/router/index.js
{
path: "/pay/:orderId.html",
component: Pay
}
// src/index.js
import { Button, Message, MessageBox, Row } from "element-ui";
Vue.prototype.$msgbox = MessageBox;
Vue.prototype.$alert = MessageBox.alert;
Vue.prototype.$message = Message;
// src/pages/Pay/index.vue
<template>
<div class="pay-main">
<div class="pay-container">
<div class="checkout-tit">
<h4 class="tit-txt">
<span class="success-icon"></span>
<span class="success-info">订单提交成功,请您及时付款,以便尽快为您发货~~</span>
</h4>
<div class="paymark">
<span class="fl">
请您在提交订单<em class="orange time">4小时</em>之内完成支付,超时订单会自动取消。
订单号:<em>{{ payInfo.orderId }}</em>
</span>
<span class="fr">
<em class="lead">应付金额:</em>
<em class="orange money">¥{{ payInfo.totalFee }}</em>
</span>
</div>
</div>
<div class="checkout-steps">
<div class="submit">
<a @click.prevent="payHandler" class="btn" href="" target="_blank">立即支付</a>
</div>
</div>
</div>
</div>
</template>
<script>
import { getCreateNative } from "@/api/payment";
export default {
name: "Pay",
data() {
return {
payInfo: {}
};
},
mounted() {
getCreateNative(this.$route.params.orderId).then(result => {
console.log(result);
this.payInfo = result.data;
});
},
methods: {
payHandler() {
this.$alert(
'<img width="200" src="/images/1.jpg"/>',
"用微信扫描下方二维码完成支付!",
{
// 是否将 message 属性作为 HTML 片段处理
dangerouslyUseHTMLString: true,
// 是否居中布局
center: true,
// 是否显示取消按钮
showCancelButton: true,
// 完成按钮的文本内容
confirmButtonText: "已完成微信支付",
// 取消按钮的文本内容
cancelButtonText: "支付遇到问题",
// MessageBox 是否显示右上角关闭按钮
showClose: false,
callback(action) {
if (action === "confirm") {
console.log("已完成支付");
} else {
console.log("支付遇到问题");
}
}
}
);
}
}
};
</script>要点解释:
- 支付页路由必须配置
/pay/:orderId.html,组件里通过this.$route.params.orderId取订单号。 getCreateNative返回支付信息,mounted中赋值给payInfo,模板再渲染订单号和金额。this.$alert必须在src/index.js中注册,否则会报this.$alert is not a function。- 弹窗里要显示
<img>,必须开启dangerouslyUseHTMLString: true。 showClose: false去掉右上角关闭按钮;callback(action)中action为confirm或cancel。- 图片建议用
/images/1.jpg这类绝对路径,避免相对路径导致 404。
常见错误
现象:点击提交订单报
Cannot destructure property 'consignee' of 'this.addressDefault'。→ 原因:未选择地址或默认地址计算属性为空,直接解构。→ 修法:提交前校验addressDefault,为空时提示用户选择地址并中断提交。现象:接口返回异常或订单提交失败,
tradeNo为undefined。→ 原因:postSubmitOrder、store action、组件 dispatch 三处参数契约不一致({tradeNo, body}与合并对象混用)。→ 修法:统一为单一body对象,并确保body.tradeNo在 API、action、组件三处同名同层级。现象:提交成功但未进入支付页,或支付页
orderId为undefined。→ 原因:未使用提交接口返回的订单号,或路由参数/查询参数未按约定传递。→ 修法:从提交结果中取订单号,跳转/pay/:orderId.html或 query 传参,并在支付页读取后校验。现象:点击“立即支付”报
this.$alert is not a function。→ 原因:Element UI 的MessageBox未注册到 Vue 原型。→ 修法:在入口注册Vue.prototype.$alert = MessageBox.alert,并确认组件内调用的是已注册方法。现象:弹窗二维码图片 404。→ 原因:弹窗 HTML 中的图片使用
./images/1.jpg相对路径,运行时按当前页面 URL 解析。→ 修法:改用可访问的绝对路径/公共资源路径,并限制图片尺寸、居中显示。现象:关闭支付弹窗控制台出现
Uncaught (in promise) cancel。→ 原因:$alert返回 Promise,取消/关闭分支未处理 rejection。→ 修法:使用callback或.catch()处理取消/关闭,区分“已完成支付”和“支付遇到问题”。现象:连续点击提交订单,后端提示不能重复提交。→ 原因:按钮未禁用、未加 loading 或防抖,重复发起请求。→ 修法:提交期间禁用按钮/加 loading,成功后跳转,失败后恢复。
练习
按“组件收集数据 → API 请求 → 返回订单号 → 跳转支付页”的顺序,画出提交订单到支付页的数据流,并标出
tradeNo与orderId的来源。将
postSubmitOrder重构为只接收一个body对象,并同步修改 API、store action、组件 dispatch 三处,保证参数契约一致。在支付页增加订单号缺失兜底:当路由参数
orderId不存在时给出提示,不发起支付信息请求。实现支付弹窗的“已完成支付 / 支付遇到问题”两个分支,分别输出对应提示,并处理关闭弹窗时的 Promise 异常。
将支付弹窗中的二维码图片改为可访问路径,设置居中、固定宽高,并验证关闭按钮隐藏后只能通过两个操作按钮结束弹窗。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 完成提交订单 | 38:18 |
| 2 | 将两个参数合并为一个 | 04:10 |
| 3 | 提交订单未使用store | 03:18 |
| 4 | 完成支付界面 | 22:39 |
| 5 | 弹出支付窗口 | 25:48 |
小结
提交订单链路的核心是参数契约一致,API、store、组件三处不能各写一套结构。
订单号是支付页的关键入参,必须从提交结果中取得并显式传递、读取前校验。
store 不是提交订单的必要条件;用与不用都可以,但接口调用与数据契约不能变。
支付页的数据请求应放在路由参数就绪后执行,并对缺失参数和接口异常做兜底。
弹窗类组件要先完成全局注册,再处理确认、取消、关闭三类分支,避免未捕获异常。
弹窗内 HTML 的资源路径按运行时 URL 解析,相对路径容易 404,应使用稳定可访问路径。
提交订单必须做防重复处理,按钮禁用/loading 与成功跳转要形成闭环。
支付流程要区分“提交订单成功”“支付信息获取成功”“用户确认支付”三个状态,不能混为一个跳转动作。