Skip to content

查看商品的详情业务与订单模块(Day6) ​

本页对应 day6,共 7 段视频,约 3 小时 32 分。

学习目标 ​

  • 能够依据接口文档完成地址模块的新增、编辑、删除与列表渲染,并正确收集姓名、手机号、省市区编号、详细地址、标签、默认地址等参数。
  • 能够说明微信小程序页面跳转只能携带基础类型参数的限制,并通过传递地址 ID、再发请求获取详情的方式完成编辑业务。
  • 能够区分 onLoad 与 onShow 的触发时机,并选择合适生命周期实现地址列表在新增、编辑、删除后的数据刷新。
  • 能够配置微信小程序分包,说明主包与子包的打包规则、主包页面限制、子包间资源引用限制及独立分包不能使用全局资源的特点。
  • 能够分别使用 HBuilderX 和 vue-cli 创建 uni-app 项目,完成依赖安装、运行到微信开发者工具,并识别 pages.json、manifest.json、App.vue 等文件的作用。

知识点 ​

本节素材核对:先说明边界 ​

本节笔记只有两条业务目标:2.1 获取商品详情的数据并且渲染、2.2 完成加入购物车与立即购买的业务。笔记未附图片,也未附代码,因此本节没有可保留的图片 alt 与代码块。更关键的是,转写稿实际讲的是微信支付流程、地址模块增删改查、小程序分包、HBuilderX 创建 uni-app、vue-cli 创建 uni-app 等内容,并没有出现“商品详情数据渲染”“加入购物车”“立即购买”的具体接口、字段和调试过程。转写稿中“提酒饼单”应纠正为“提交订单”,“收购尾”应为“收尾”,“接辅优你”应理解为“接着讲 uni-app”,但这些都属于支付、地址和 uni-app 章节,不能移植到本节商品详情业务中。基于“不得编造”的要求,下面只按笔记已有业务目标写教程框架;接口路径、请求方法、参数字段、页面跳转地址必须以项目现有接口文档和代码为准,不能凭空写死。

2.1 获取商品详情的数据并且渲染 ​

为什么需要: 商品详情页是用户决策的核心页面。用户从商品列表、搜索、购物车等入口进入详情页后,必须先拿到当前商品的详情数据,才能展示商品图片、名称、价格、规格、库存、描述等信息。没有详情数据,页面只能显示静态骨架,加入购物车和立即购买也缺少商品标识,后续业务无法可靠发起。

怎么想: 先明确“详情页靠什么识别商品”。通常入口会携带商品 ID 或类似标识,但本节笔记没有给出字段名,不能假设具体参数。实现时应先查项目接口文档:详情接口需要什么入参、返回哪些字段、是否有规格和库存信息。然后按“进入页面 → 取入参 → 发请求 → 判状态 → 存数据 → 模板渲染 → 处理加载/失败/空数据”的顺序组织。不要把地址模块、支付参数、分包配置混进详情页逻辑。

怎么做: 第一步,在详情页生命周期中读取路由参数,具体用哪个生命周期要以项目现有写法为准;若项目使用小程序原生页面,常见是在页面加载阶段取参数,但本节素材未给出代码,不能补具体写法。第二步,调用项目已有的商品详情接口,请求方法、URL、字段名全部以接口文档为准。第三步,对返回结果做状态判断:成功则把详情数据保存到页面数据中,失败则给出提示并保留重试入口。第四步,模板中只渲染接口真实返回的字段;图片、价格、库存等字段若缺失,要有兜底展示。第五步,加入购物车、立即购买按钮应绑定同一个商品标识来源,避免一个按钮用路由参数、另一个按钮用页面数据导致不一致。

易错点: 一是把转写稿里的地址、支付、分包内容误写成商品详情实现,这是素材错配;二是凭经验编接口路径和字段名,导致联调失败;三是详情数据未返回就允许点击加购或立即购买;四是只处理成功分支,不处理加载中、失败、空数据;五是价格、库存等展示字段没有做空值兜底;六是入口参数名与接口参数名混用。

2.2 完成加入购物车与立即购买的业务 ​

为什么需要: 详情页最终要促成交易。加入购物车用于“先逛后买”,立即购买用于“直接结算”。两者都依赖商品详情中的商品标识和用户选择,但业务目的不同,不能简单复制同一个按钮逻辑。

怎么想: 先区分两个动作的落点:加入购物车通常需要把商品加入购物车数据,成功后给出提示,是否留在当前页要以项目现有交互为准;立即购买通常要进入订单确认或结算流程,具体跳转地址和参数必须查项目路由与接口文档。笔记没有给出接口字段,不能写“固定传商品 ID、数量、规格”这样的死代码;应查文档确认是否需要规格、数量、库存校验、token 等。

怎么做: 第一,确认加购接口和立即购买接口分别是什么,参数是否不同;第二,点击前做防重复提交,避免连续点击产生多条记录;第三,从详情数据中取接口要求的商品标识,若项目有规格选择,必须先让用户完成选择再发请求;第四,请求成功后按项目约定提示或跳转;第五,失败时展示后端返回信息,并恢复按钮可点击状态。若素材后续补充了代码,应原样保留代码块,只把图片 alt 改成中文说明。

易错点: 加购和立即购买共用一套参数导致字段缺失;未选规格就提交;重复点击造成重复加购;请求失败后按钮一直禁用;成功提示与页面跳转冲突;把支付流程提前写进详情页。调试时应记录:入口参数、请求入参、接口返回、状态判断、页面跳转结果。由于本节转写稿没有商品详情调试过程,不能虚构“某字段打印为某值”的记录;正确做法是后续补录真实调试日志后再写入教程。

3.1 展示立即购买与购物车的数据 ​

为什么需要:订单页不是孤立页面,它有两个典型入口:购物车和立即购买。用户从不同入口进来,订单页都要能展示对应数据,否则后续提交订单、选择地址和支付就无从谈起。

怎么想:先把“入口”和“页面数据”分开看。购物车进入时,数据来自购物车选中项;立即购买进入时,数据来自当前商品。转写稿里明确提到,订单页会从“购物车”和“立即购买”两个页面进入,因此订单页要围绕这两个入口组织展示逻辑。地址信息也属于订单页展示的一部分,但它的刷新时机要特别注意:转写稿中调试发现,地址获取写在 onLoad 里只会执行一次,从地址页返回订单页时不会重新执行,导致新增地址后订单页仍不刷新;后来改为 onShow,每次页面出现都重新获取地址列表。

怎么做:进入订单页后展示对应商品数据;地址列表的获取放在 onShow 中,调用类似 this.getUserAddress() 的方法,内部发 REQ_ADDRESS_LIST 请求。这样从地址管理页返回订单页时,地址数据会重新拉取。

易错点:不要把地址获取只写在 onLoad;onLoad 只执行一次,从地址页返回不会刷新。不要混淆两个入口的数据来源,订单页要统一承接。

3.2 获取收件人的地址信息 ​

为什么需要:提交订单前必须有收件人地址。首次登录或没有地址时,订单页要引导用户新增;已有地址时,要展示地址列表并允许选择、编辑、删除。

怎么想:地址模块本质是“增删改查”,和后台管理系统类似。地址列表页用 v-if 判断 addressList.length:没有收件人就展示“添加收货人”入口,有收件人就渲染列表。新增页收集姓名、手机号、省市区编号、详细地址、标签、是否默认等字段;保存时根据是否有 id 区分新增和更新。编辑跳转不能把整个对象带过去,因为小程序路由只能带简单类型,转写稿中明确说对象会被转成字符串,拿不到;所以只带 id,进入编辑页后再根据 id 发请求获取详情并展示。

怎么做:订单页在 onShow 中获取地址列表;地址列表页用 v-if 二选一;新增/编辑共用页面,在 onLoad 里判断 options.id 是否存在,有 id 就发 REQ_EDIT_ADDRESS 获取详情,没有就是新增;保存时调用新增或更新接口,成功后返回地址列表并重新拉取数据;删除后也重新获取列表。省市区用微信自带 picker,确定时才触发 change,从中取省、市、区编号。

易错点:参数名不能猜,必须看接口文档;编辑不能带对象,只能带 id;picker 取消不会触发确定逻辑;默认地址用 0/1 表示;新增和更新要区分,不能混用一个分支。

3.3 微信支付流程 ​

为什么需要:前端不能直接完成支付,必须由后台生成平台订单并向微信统一下单,前端只负责调起支付和确认结果。面试常问的不是单个 API,而是完整支付流程。

怎么想:按四步记。第一步,前端提交订单请求,获取订单号;后台 Java 生成平台订单、保存数据库,并向微信支付统一下单 API 提交订单,微信生成订单号返回后台。第二步,前端根据订单号向服务端获取支付信息,支付参数由后台加密后返回。第三步,前端调用 wx.requestPayment 调起支付,处理成功和失败。第四步,前端再发请求查询支付结果,确认商家账款是否到账,再做其他业务。

怎么做:前端发提交订单请求;拿到订单号后请求支付参数;调用 wx.requestPayment;支付结束后发查询请求确认结果。转写稿强调,支付参数不是前端拼的,是后台返回的;携带参数以接口文档为准。

易错点:不要以为“调个函数就完事”;第四步不能省;失败结果也要处理;参数不能凭感觉写;支付流程要能完整口述。

支付流程总览 ​

微信小程序支付看起来只是“调一个函数”,但转写稿里反复强调:面试常问的不是某个 API 的拼写,而是支付的完整流程。本节笔记把流程拆成四步:提交订单拿订单号 → 获取支付参数 → wx.requestPayment → 发请求确认商家账款到账,再做其他业务。转写稿里的“提酒饼单”应纠正为“提交订单”,“账况”应纠正为“账款”。这套流程不只小程序会用到,PC 端、支付宝支付也大体是“下单—取支付参数—拉起支付—确认结果”的思路。调试时不要猜字段,要按接口文档和后台约定来。

第一步:提交订单,获取订单号 ​

为什么需要:订单号是后续业务的“主线”。没有订单号,前端无法向服务端要支付参数,后台也无法对账、确认到账。转写稿明确说,商家后台(如 Java 后台)要生成自己平台下的订单,数据库要保存订单信息;同时后台还要向微信支付统一下单接口发请求,微信侧会返回信息,后台再加密生成支付参数给前端。

怎么想:前端不要把“提交订单”和“获取支付参数”混成一步。先让后台生成平台订单,拿到订单号;再拿订单号去换支付参数。前端只负责按流程串起来。

怎么做:发提交订单请求,携带参数按文档来;请求成功后从返回结果中取订单号。转写稿提醒:项目实际携带参数和文档不一定完全一样,但要以能对接后台为准,不能自己瞎猜。

易错点:把订单号当成支付参数;参数名写错导致后台收不到;以为前端能自己生成订单号。转写稿说“参数是根据文档”,所以字段要以接口约定为准。

第二步:获取支付参数 ​

为什么需要:wx.requestPayment 需要支付参数,但这些参数不能由前端随便拼。转写稿说,后台向微信系统发请求后,会返回并加密支付参数;前端拿到订单号后,再找服务端要支付参数。

怎么想:第二步的核心是“用订单号换支付参数”。前端不负责算签名、不负责生成支付参数,只负责请求服务端并接收结果。

怎么做:拿第一步的订单号发请求给服务端,获取支付参数;把返回的支付参数保存好,准备传给 wx.requestPayment。转写稿提到“昨天要的那 5 个支付参数”是后台返回的,具体字段以文档和后台为准。

易错点:把“统一下单”和“获取支付参数”混为一谈;前端自己拼参数;忽略后台返回的加密/签名结果。调试时先打印服务端返回结构,确认字段齐全再调支付。

第三步:调用 wx.requestPayment ​

为什么需要:这是真正拉起微信收银台、让用户输入密码完成支付的 API。转写稿说,前端这一步就是调 wx.requestPayment,剩下是传参数,并处理支付成功、支付失败。

怎么想:前端在这一步只做“拉起支付 + 结果回调处理”。支付是否最终到账,不能只凭前端回调下结论,要接第四步。

怎么做:调用 wx.requestPayment,传入第二步拿到的支付参数;在成功、失败回调里分别做提示和后续跳转。转写稿说“输入密码,支付成功、支付失败都要处理”,不要只写成功分支。

易错点:API 名写错;支付参数传错;失败回调不处理;把 wx.requestPayment 的成功回调直接当成商家已确认到账。转写稿特别提醒第四步不能忘。

第四步:发请求确认商家账款到账,再做其他业务 ​

为什么需要:转写稿明确说,最后要发请求确认“商家账款是不是已经到账”,确认后再做其他业务。这样避免前端显示支付成功,但后台订单/账款状态还没确认,就直接发货或改业务状态。

怎么想:把前端支付回调当成“用户侧支付动作结束”,再把服务端确认当成“业务侧最终依据”。两步分开,逻辑才稳。

怎么做:支付回调后,拿订单号发请求给服务端确认账款到账;根据服务端返回结果再做后续业务。转写稿说“再去做其他业务”,说明第四步是业务继续执行的前置条件。

易错点:省掉第四步;只凭前端成功提示就发货;确认请求不带订单号;不处理确认失败。转写稿还提到支付不能随意真机测试,所以更要先把流程记牢、按文档调试,别靠猜。

地址模块的整体套路:先明确页面流转 ​

为什么需要: 用户第一次进入订单页时可能没有收货地址,必须先能新增;新增成功后还要回到地址列表看到数据;已有地址则要能修改。这个模块本质就是“增、查、改”的闭环。

怎么想: 转写稿里反复强调,地址模块和后台管理系统的增删改查很像:没有地址时展示“添加收货人”,有地址时展示地址列表;点击新增进入新增页,点击编辑进入同一个表单页但带上地址 ID。页面跳转用 navigator,路径按项目目录走,例如地址列表跳到 pages/address/add/add。

怎么做: 先建地址列表页和新增/编辑页,配好路由与页面标题;列表页用条件判断二选一:地址数组有长度就渲染列表,没有就展示空状态和新增按钮。

易错点: 不要把所有页面都塞进主包路由里乱配;转写稿提醒“子包页面必须从主包路由中移出去”,但本节先聚焦地址业务,路由路径要和实际目录一致。新增页和编辑页可以复用同一个页面,靠 options.id 区分。

2.1 新增收件人业务:先看接口,再收集字段 ​

为什么需要: 新增地址不是随便拼参数,字段必须和后台接口一致,否则后台收不到。笔记给出的接口是:

js
/mall-api/userAddress/save  POST
参数解释:
{
   name:'',//新增收件人的名字,
   phone:''//收件人的手机号码
   provinceCode:'',//省的区域编号
   cityCode:'',市
   districtCode:'',//区
   address:'',//详细地址
   tagName:'',标签
   isDefault:0,//默认地址
}

怎么想: 转写稿的推导是:先打开接口文档确认请求方式、字段名,再决定页面收集什么。姓名、手机号用输入框;省市区不能用三个输入框,而用微信小程序自带的 picker,mode="region",在 change 事件里拿 event.detail.code 得到省、市、区编号,拿 event.detail.value 得到中文用于展示;详细地址用输入框;标签用 radio 收集“家/公司”;默认地址用 switch,但 switch 的 event.detail.value 是布尔值,要转成 0/1。

怎么做: 页面数据先收集到 data,保存按钮触发请求,参数就是上面对象;请求成功 code === 200 后 wx.navigateBack() 回地址列表。转写稿调试时特别提醒:picker 只在点“确定”时触发 change,取消不触发;可以在事件里 console.log(e.detail) 确认 code 和 value 都有。

易错点: 字段名不能猜,provinceCode/cityCode/districtCode 是编号,不是汉字;isDefault 要传 0/1,不能直接传布尔值;picker 是微信小程序自带组件,不是 Vant 组件,不要写错组件名。

2.2 地址列表数据渲染:onShow 拉取,v-if 二选一 ​

为什么需要: 新增成功返回列表后,列表必须重新请求数据,否则页面还是旧数据。转写稿里实际调试发现:写在 onLoad 里只执行一次,新增返回后不会刷新;改成 onShow 后,每次页面出现都会拉一次地址列表。

怎么想: 列表页要判断“有没有地址”:有就 wx:for 渲染每一项,展示地址、姓名、手机号、标签、默认标签;没有就展示空状态和“新建地址”按钮。默认标签根据 item.isDefault === 1 控制显示。

怎么做: 在 onShow 里调用获取地址列表的 GET 接口,把返回数组存到 addressList;模板里用 wx:if="{{addressList.length}}" 和 wx:else 做二选一。渲染时地址、姓名、手机号分别取 item.address、item.name、item.phone。

易错点: 不要把列表请求只放 onLoad;wx:for 要写 wx:key;默认标签不要用真值判断,按接口约定判断 isDefault == 1 更稳。

2.3 修改收件人:路由只传 ID,再回查详情 ​

为什么需要: 编辑时要先展示已有数据,再允许修改。但微信小程序路由跳转不能像 Vue 后台那样直接传对象,转写稿明确说:路由只能带简单类型,传对象会变成字符串,取不出来。

怎么想: 编辑按钮跳转同一个新增页,只带 id:url="/pages/address/add/add?id={{item.id}}"。进入页面后在 onLoad(options) 里判断:有 id 说明是编辑,没有就是新增。编辑时用 ID 发请求拿详情,回填表单;保存时根据是否有 id 决定调更新接口还是新增接口。

怎么做: 回填省市区时,详情里通常有省市区名称,需要拼成展示字符串,例如“省/市/区”;picker 的 value 用于展示,code 用于提交。保存成功后统一返回地址列表。

易错点: 不要把整个地址对象塞进路由参数;编辑页不要只回填汉字而忘了保存省市区编号;保存按钮要区分新增和更新,不能一律调新增接口。

超2M的直接后果:先判断要不要分包 ​

为什么需要: 微信小程序项目上线时,主包体积不能超过2M。转写稿里说得很直接:超出2M,微信不需要审核代码,直接打回。更麻烦的是,真机测试时如果包体超了,也可能测不了,所以不能等上线才发现。

怎么想: 先看体积,再决定拆不拆。开发者工具预览时,二维码旁边能看到代码包体积;转写稿演示时当前项目约462K,这种就不需要分包。只有页面多、图片等本地资源多,主包接近或超过2M时,才用分包把一部分页面挪出去,让主包降下来。

怎么做: 在开发者工具里打开预览,先确认主包体积;超2M就按后面的规则拆包,拆完重新编译,再看主包是否缩小,最后做真机测试。

易错点: 不要等项目写完才想分包;真机测不了先查包体;也不要为了分包而分包,小项目硬拆只会增加路由维护成本。

分包的基本规则:主包与子包怎么划分 ​

为什么需要: 原来所有页面都放在一个“篮子”里,资源一多主包就超。分包就是把部分页面按目录拆到子包,主包只保留必须最先加载的内容。

怎么想: 微信的规则是:在 app.json 里通过 subPackages 声明分包结构;被 subPackages 配置路径覆盖的目录打进子包,路径外的目录仍进主包。也就是说,先规划目录,再配路由。

怎么做: 例如把商品页放 packageA,详情页放 packageB:

json
{
  "pages": [
    "pages/index/index",
    "pages/category/category",
    "pages/cart/cart",
    "pages/my/my"
  ],
  "subPackages": [
    {
      "root": "packageA",
      "pages": ["pages/goods/goods"]
    },
    {
      "root": "packageB",
      "pages": ["pages/detail/detail"],
      "independent": true
    }
  ],
  "tabBar": {
    "list": [
      { "pagePath": "pages/index/index", "text": "首页" },
      { "pagePath": "pages/category/category", "text": "分类" },
      { "pagePath": "pages/cart/cart", "text": "购物车" },
      { "pagePath": "pages/my/my", "text": "我的" }
    ]
  }
}

易错点: root 不能是另一个子包内的子目录,子包目录不要重名;页面挪到子包后,要从主包 pages 里删掉旧路由;跳转路径要加上子包 root,否则会找不到页面。

tabBar为什么必须放主包 ​

为什么需要: tabBar 是启动入口,用户一进小程序就要用,必须随主包加载。转写稿明确提醒:tabBar 务必放在主包里面。

怎么想: 首页、分类、购物车、我的这类 tabBar 页面不能拆进子包;可以拆的是商品详情、地址管理等非 tabBar 页面。

怎么做: 在 app.json 的 tabBar.list 中保留主包页面路径,同时这些页面继续写在主包 pages 里;拆包时只挪其他页面。

易错点: 把 tabBar 页面拆进子包后,入口找不到页面;改跳转时只改了一处,tabBar 路径没改;主包 pages 删了但 tabBar 还指向旧路径。

独立分包:子包不能用全局静态资源 ​

为什么需要: 独立分包可以相对独立运行,不依赖主包。正因为它独立,所以不能使用主包的全局静态资源,否则运行时会缺失。

怎么想: 转写稿用“孩子独立了就不能再随便用家里的东西”作比喻。普通分包可以引用主包资源,但独立分包不能依赖主包;笔记也强调“子包不能用全局静态资源!!!”。

怎么做: 在 subPackages 的某个分包对象里加 "independent": true,与 root、pages 平级;该分包需要的样式、图片等资源放到自己目录里,不要指望 app.wxss 等全局资源。

易错点: 调试时把全局样式放在 app.wxss,独立分包页面样式失效,不是没编译,而是独立分包拿不到全局资源;子包之间不能相互引用,但普通分包可以 require 主包,独立分包不能依赖主包。

配置完必须回归:路由、体积、真机一起查 ​

怎么做: 拆包后重新编译,先看主包体积是否下降;再逐页点跳转,确认子包路径正确;检查主包 pages 是否删掉旧路由;最后真机测试。转写稿里拆出子包后主包体积缩小,说明分包生效。

易错点: 只配 subPackages 不删主包旧路由,容易路由重复或跳错;跳转仍写旧路径会 404;独立分包缺样式;tabBar 页面被误拆。上线前一定确认主包小于2M,否则微信会直接打回。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

课堂画面 6

代码示例 ​

代码一:地址列表页(渲染 + 编辑/删除入口) ​

js
// utils/request.js
const BASE_URL = 'https://你的接口域名'

export function request(url, method = 'GET', data = {}) {
  return new Promise((resolve, reject) => {
    wx.request({
      url: BASE_URL + url,
      method,
      data,
      header: {
        // 地址类接口都需要带 token 识别当前账号
        token: wx.getStorageSync('token') || ''
      },
      success: res => resolve(res.data),
      fail: reject
    })
  })
}
js
// utils/api.js
import { request } from './request'

// 获取当前账号下所有收件人地址(GET)
export const REQ_ADDRESS_LIST = () => request('/mall-api/userAddress/findUserAddress', 'GET')

// 删除收件人(URL 以接口文档为准)
export const REQ_DELETE_ADDRESS = id => request(`/mall-api/userAddress/delete/${id}`, 'GET')
html
<!-- pages/address/address.wxml -->
<view class="address-list">
  <!-- 二选一:有收件人渲染列表,没有收件人展示空态 -->
  <block wx:if="{{addressList.length}}">
    <view class="list-item" wx:for="{{addressList}}" wx:key="id">
      <view class="item-top">
        <text class="tag" wx:if="{{item.isDefault === 1}}">默认</text>
        <text class="tag-name">{{item.tagName}}</text>
      </view>
      <view class="item-address">{{item.address}}</view>
      <view class="item-info">
        <text>{{item.name}}</text>
        <text>{{item.phone}}</text>
      </view>
      <view class="item-op">
        <view bindtap="editAddress" data-id="{{item.id}}">编辑</view>
        <view bindtap="deleteAddress" data-id="{{item.id}}">删除</view>
      </view>
    </view>
  </block>
  <view wx:else class="empty">还没有收货地址,快去新建一个吧</view>

  <navigator url="/pages/address/add/add" class="add-btn">新建地址</navigator>
</view>
js
// pages/address/address.js
import { REQ_ADDRESS_LIST, REQ_DELETE_ADDRESS } from '../../utils/api'

Page({
  data: {
    addressList: []
  },

  // 用 onShow 而不是 onLoad:从新增/编辑页返回后要重新拉一次
  onShow() {
    this.getUserAddress()
  },

  async getUserAddress() {
    const res = await REQ_ADDRESS_LIST()
    if (res.code === 200) {
      this.setData({ addressList: res.data })
    }
  },

  // 编辑:只带 id 过去,不带整个对象(小程序路由只能传字符串)
  editAddress(e) {
    const id = e.currentTarget.dataset.id
    wx.navigateTo({ url: `/pages/address/add/add?id=${id}` })
  },

  // 删除成功后重新拉列表
  async deleteAddress(e) {
    const id = e.currentTarget.dataset.id
    const res = await REQ_DELETE_ADDRESS(id)
    if (res.code === 200) {
      this.getUserAddress()
    }
  }
})

要点:

  • 列表与空态用 wx:if="{{addressList.length}}" / wx:else 二选一,判断依据是接口返回的数据本身,不要自己再造一个标志位。
  • 数据请求放在 onShow:onLoad 只在页面第一次创建时执行一次,从新增页返回时不会重新执行,导致新增完地址回来列表不刷新(转写稿里专门验证过这个问题)。
  • data-id="{{item.id}}" 取值必须走 e.currentTarget.dataset.id,写 e.target 在点击内部文本时会取不到。
  • 默认标签的条件是 item.isDefault === 1,接口返回 0/1,不要用字符串 '1' 比较。
  • wx:for 必须配 wx:key="id",否则列表更新会错乱。

代码二:新增 / 编辑地址页(省市区 picker + switch + 新增更新二合一) ​

js
// utils/api.js 追加
import { request } from './request'

// 新增收件人(笔记给出的正式接口)
export const REQ_ADD_ADDRESS = data => request('/mall-api/userAddress/save', 'POST', data)

// 根据 id 获取地址详情,用于编辑回显(URL 以接口文档为准)
export const REQ_ADDRESS_DETAIL = id => request(`/mall-api/userAddress/${id}`, 'GET')

// 修改地址(URL 以接口文档为准)
export const REQ_UPDATE_ADDRESS = data => request('/mall-api/userAddress/update', 'POST', data)
html
<!-- pages/address/add/add.wxml -->
<view class="add-address">
  <view class="form-item">
    <text class="label">收货人</text>
    <input placeholder="请填写收货人姓名" model:value="{{name}}" />
  </view>

  <view class="form-item">
    <text class="label">手机号</text>
    <input type="number" placeholder="请填写收件人手机号" model:value="{{phone}}" />
  </view>

  <!-- 用的是微信自带的 picker,不是 vant 组件 -->
  <view class="form-item">
    <text class="label">省市区</text>
    <picker mode="region" bindchange="onRegionChange">
      <view class="picker-value">{{region || '请选择省市区'}}</view>
    </picker>
  </view>

  <view class="form-item">
    <text class="label">详细地址</text>
    <input placeholder="街道、门牌号" model:value="{{address}}" />
  </view>

  <view class="form-item">
    <text class="label">标签</text>
    <radio-group model:value="{{tagName}}">
      <label><radio value="家" />家</label>
      <label><radio value="公司" />公司</label>
    </radio-group>
  </view>

  <view class="form-item">
    <text class="label">默认地址</text>
    <switch bindchange="onDefaultChange" checked="{{isDefault === 1}}" />
  </view>

  <button type="primary" bindtap="save">保存</button>
</view>
js
// pages/address/add/add.js
import { REQ_ADD_ADDRESS, REQ_ADDRESS_DETAIL, REQ_UPDATE_ADDRESS } from '../../../utils/api'

Page({
  data: {
    id: '',            // 有 id = 编辑,没有 id = 新增
    name: '',
    phone: '',
    provinceCode: '',
    cityCode: '',
    districtCode: '',
    address: '',
    tagName: '家',
    isDefault: 0,
    region: ''         // 只用于页面展示,如 "北京市/北京市/昌平区"
  },

  onLoad(options) {
    // 路由参数只有 onLoad 能拿到
    if (options.id) {
      this.setData({ id: options.id })
      this.getAddressDetail()
    }
  },

  // 编辑时先按 id 查详情做回显
  async getAddressDetail() {
    const res = await REQ_ADDRESS_DETAIL(this.data.id)
    if (res.code === 200) {
      const d = res.data
      this.setData({
        name: d.name,
        phone: d.phone,
        provinceCode: d.provinceCode,
        cityCode: d.cityCode,
        districtCode: d.districtCode,
        address: d.address,
        tagName: d.tagName,
        isDefault: d.isDefault,
        // 汉字区域名要自己拼,接口只给了编号
        region: `${d.provinceName}/${d.cityName}/${d.districtName}`
      })
    }
  },

  // picker 确认时才触发:e.detail.code 是省市区编号,e.detail.value 是汉字
  onRegionChange(e) {
    const [p, c, d] = e.detail.code
    this.setData({
      provinceCode: p,
      cityCode: c,
      districtCode: d,
      region: e.detail.value.join('/')
    })
  },

  // switch 的 e.detail.value 是布尔值,接口要 0/1
  onDefaultChange(e) {
    this.setData({ isDefault: e.detail.value ? 1 : 0 })
  },

  async save() {
    const { id, name, phone, provinceCode, cityCode, districtCode, address, tagName, isDefault } = this.data

    if (!name || !phone || !provinceCode || !address) {
      wx.showToast({ title: '请完善地址信息', icon: 'none' })
      return
    }

    const params = { name, phone, provinceCode, cityCode, districtCode, address, tagName, isDefault }

    // 有 id 走更新,没有 id 走新增
    const res = id
      ? await REQ_UPDATE_ADDRESS({ ...params, id })
      : await REQ_ADD_ADDRESS(params)

    if (res.code === 200) {
      wx.navigateBack()
    }
  }
})

要点:

  • 新增与编辑共用一个页面,靠 onLoad(options) 里有没有 id 区分,保存时也按 this.data.id 分流,不要写两套页面。
  • 小程序路由跳转只能携带字符串,把整个地址对象塞进 url 会被转成 [object Object],所以只传 id,进页面再查详情回显。
  • 提交参数必须严格按文档字段名:name / phone / provinceCode / cityCode / districtCode / address / tagName / isDefault,字段名带错后台收不到。
  • picker 的 bindchange 只在点“确定”时触发,取消不触发;e.detail.code 是编号数组(用来提交),e.detail.value 是汉字数组(只用来展示),两者不能混。
  • switch 拿到的是布尔值,isDefault 需要自己转成 0 / 1;回显时用 checked="{{isDefault === 1}}"。
  • 保存成功后用 wx.navigateBack() 返回,配合列表页的 onShow 才能看到最新数据。

代码三:分包配置(app.json) ​

json
{
  "pages": [
    "pages/index/index",
    "pages/category/category",
    "pages/cart/cart",
    "pages/user/user"
  ],
  "subPackages": [
    {
      "root": "packageA",
      "pages": [
        "pages/goods/detail",
        "pages/order/order",
        "pages/address/address",
        "pages/address/add/add"
      ]
    }
  ],
  "window": {
    "navigationBarTitleText": "商城",
    "navigationBarBackgroundColor": "#ffffff",
    "navigationBarTextStyle": "black"
  },
  "tabBar": {
    "list": [
      { "pagePath": "pages/index/index", "text": "首页" },
      { "pagePath": "pages/category/category", "text": "分类" },
      { "pagePath": "pages/cart/cart", "text": "购物车" },
      { "pagePath": "pages/user/user", "text": "我的" }
    ]
  }
}

要点:

  • tabBar 里的页面必须留在主包 pages 中,放到分包里会直接报错(转写稿强调的第一条)。
  • 分包页面路径是相对 root 写的,不带前导 /、也不重复写 packageA;主包 pages 里要把已经移到分包的路径删掉,否则重复注册报错。
  • 移入分包后,所有 wx.navigateTo 的跳转地址要同步改成 /packageA/pages/xxx/xxx,否则点了跳不过去。
  • 分包之间不能互相引用 js / 模板 / 静态资源,但都可以引用主包资源;独立分包("independent": true)连全局 app.wxss、全局组件都用不了,需要自己带样式。
  • 项目体积超过 2M 时,真机预览和上线都会被直接打回,必须在开发阶段就拆包,别等提审才发现。

常见错误 ​

  1. 现象:新增/编辑地址后返回地址列表,页面数据不刷新。
    原因:地址列表请求写在 onLoad 中,而 onLoad 只在页面首次加载时执行一次。
    修法:把获取地址列表的逻辑放到 onShow 中,保证每次页面显示都重新拉取数据。

  2. 现象:从地址列表点击“编辑”跳转后,编辑页拿不到完整地址对象,字段变成字符串或直接丢失。
    原因:小程序路由跳转只能携带简单类型参数,不能像 Vue 路由那样直接传对象。
    修法:跳转时只传地址 id,进入编辑页后再根据 id 发请求获取详情并回显。

  3. 现象:编辑页点击保存后,有时新增一条数据,有时又没更新成功。
    原因:保存按钮没有区分“新增”和“更新”,无论有没有 id 都调用同一个接口。
    修法:保存前判断当前是否有地址 id:有 id 调更新接口,无 id 调新增接口;成功后返回列表并刷新。

  4. 现象:把商品详情页拆到分包后,点击商品提示页面不存在或跳转失败。
    原因:页面已移到子包,但主包 pages 路由未删除、跳转路径仍按主包路径写。
    修法:从主包 pages 中移除该页面,在 subpackages 中配置子包 root 和 pages,并把跳转路径改成“子包根目录/页面路径”。

  5. 现象:配置独立分包后,子包页面样式错乱或使用全局资源不生效。
    原因:独立分包不能使用主包的全局静态资源/全局样式。
    修法:独立分包需要的样式和资源自行放入子包内,不依赖主包全局资源。

  6. 现象:微信支付只调用了 wx.requestPayment,支付成功后直接发业务请求,后续出现订单状态不一致。
    原因:缺少“查询/确认支付结果”步骤,前端只依赖支付弹窗成功回调。
    修法:严格按“提交订单取订单号 → 获取支付参数 → wx.requestPayment → 请求后台确认支付结果”四步执行,确认到账后再做后续业务。

  7. 现象:地址表单里省市区选择后,页面只显示了编号,或者保存后编辑回显没有“省/市/区”文字。
    原因:只收集了 code,没有把选择器返回的省市区名称拼成展示文本并保存。
    修法:在 picker 确认事件中同时保存省市区编号和拼接后的展示文本,编辑回显时用展示文本渲染。

  8. 现象:HBuilderX 运行 uni-app 项目到微信小程序时启动失败,或找不到微信开发者工具。
    原因:未配置微信开发者工具路径,或微信开发者工具未开启服务端口。
    修法:在 HBuilderX 的运行设置中配置微信开发者工具安装路径,并在微信开发者工具“设置 → 安全”中打开服务端口。

练习 ​

  1. 地址列表刷新:实现地址列表页,要求新增地址成功返回后、编辑地址成功返回后、删除地址成功后,列表都能自动展示最新数据。说明为什么选择该生命周期。

  2. 地址编辑回显:从地址列表点击编辑,只携带地址 id 跳转到编辑页;编辑页根据 id 请求详情并回显姓名、手机号、省市区、详细地址、标签、默认地址状态。保存时区分新增与更新。

  3. 地址删除:在地址列表实现删除按钮,删除前拿到当前地址 id,调用删除接口;删除成功后重新获取地址列表。写出关键事件参数取值路径。

  4. 分包改造:将商品详情页从主包拆到子包,完成 app.json 分包配置,并修正商品列表跳转路径。同时说明 tabBar 页面为什么不能放入子包。

  5. 支付流程口述:不看笔记,按顺序写出微信小程序支付的四个步骤,并说明每一步前端/后台分别做什么、为什么第四步不能省略。

原视频对照 ​

#视频时长
1地址模块页面创建与路由跳转25:50
2完成添加新增用户的业务31:33
3地址列表数据的渲染11:55
4微信小程序地址模块业务完成34:05
5微信小程序的分包21:56
6hb工具创建uniapp项目的使用35:40
7vue-cli创建uniapp项目51:46

小结 ​

  1. 地址模块本质就是“增删改查”:新增、列表渲染、编辑回显、删除后刷新,和后台管理系统思路一致,但受小程序路由和生命周期限制。

  2. 小程序页面跳转不要传对象,传 id 后再查详情,是编辑类业务的通用做法。

  3. 列表页数据刷新优先考虑 onShow;需要接收路由参数时用 onLoad,两者职责要分清。

  4. 微信支付要记完整流程,不是只记 wx.requestPayment:提交订单、获取支付参数、发起支付、确认支付结果,四步缺一不可。

  5. 分包的核心目的是控制主包体积;tabBar 页面必须放主包,子包页面路由和跳转路径要同步修改。

  6. 独立分包不能依赖主包全局资源,需要的样式和静态资源要自己带。

  7. uni-app 底层是 Vue,Vue 语法、小程序组件/API 都能对应使用,但不要把 Vue 和小程序写法混写成“大杂烩”,统一用一种语法。

  8. 接口字段不要猜,以接口文档为准;文档不清晰时先沟通确认,再写收集和提交逻辑。