小兔鲜儿 UniApp 项目 - 第四周综合笔记
小兔鲜儿 UniApp 项目 - 第四周综合笔记第四周学习内容购物车模块和订单模块。购物流的核心是将用户选好的商品从SKU弹窗加入到购物车列表在列表中管理数量和选中状态最后结算生成订单。订单流则涵盖了从填写订单、支付到查看详情、管理列表的完整链路。一、购物车模块购物车模块围绕几个核心操作展开加入商品、展示列表、修改数量、切换选中、计算金额、跳转结算。1.1 整体数据流从商品详情到购物车结算的完整链路商品详情页 SKU 弹窗 -- 用户选择规格 数量 -- 点击加入购物车 或 立即购买 -- 调用 /member/cart (POST) 写入后端 -- 跳转或停留在当前页 -- 购物车页 onShow 时调用 /member/cart (GET) 拉取列表 -- 用户操作改数量、改选中、删除 -- 调用 /member/cart/:skuId (PUT) 或 (DELETE) 同步后端 -- 点击去结算将选中商品带入订单填写页这个过程中有几个关键点加入购物车和查看购物车用的是同一个接口地址/member/cart但请求方法不同POST vs GET购物车页用onShow而非onLoad触发数据获取保证每次切回 tab 都刷新数量修改和选中状态修改共用同一个 PUT 接口通过传不同 body 参数区分1.2 加入购物车的核心逻辑加入购物车发生在商品详情页的 SKU 弹窗里。用户选好规格、定好数量后点击加入购物车按钮触发add-cart事件事件回调中拿到 SKU 组件返回的当前选中数据调用接口完成添加。接口很简单只需要两个参数参数说明skuId当前选中规格组合的 ID来自事件参数ev._idcount购买数量来自事件参数ev.buy_num// src/services/cart.ts export const postMemberCartAPI (data: { skuId: string; count: number }) { return http({ method: POST, url: /member/cart, data }) }// 在商品详情页的 SKU 事件回调中 const onAddCart async (ev: SkuPopupEvent) { await postMemberCartAPI({ skuId: ev._id, count: ev.buy_num }) uni.showToast({ title: 添加成功 }) isShowSku.value false // 关闭弹窗 }这里的ev不是普通的 DOM 事件对象而是 SKU 组件内部封装的事件载荷包含了当前选中 SKU 的完整信息。这个设计的好处是使用方不需要自己去组件里翻数据组件直接告诉你需要的一切。1.3 购物车列表的数据管理购物车列表的数据获取和展示有几个设计要点为什么用 onShow购物车是 tabBar 页面从其他 tab 切换过来时不会重新创建不触发 onLoad但每次展示时都需要最新的数据。所以数据获取放在onShow里onShow(() { if (memberStore.profile) { getMemberCartData() } })登录判断前置在请求列表之前先判断memberStore.profile是否存在避免未登录状态下发出无效请求。模板层也做了对应的条件渲染——未登录显示引导登录的占位已登录才展示列表。CartItem 类型的关键字段export type CartItem { skuId: string // SKU ID后续所有修改操作都要用到 name: string // 商品名称 picture: string // 缩略图 count: number // 当前数量 nowPrice: number // 当前价格可能比加入时变动 stock: number // 库存上限步进器的 max 值 selected: boolean // 是否被选中影响结算 attrsText: string // 规格文字如黄色小象 140cm isEffective: boolean // 是否还有效商品可能下架 }注意区分price加入时的价格和nowPrice当前价格——结算时用的是nowPrice因为商品价格可能在用户加入购物车后发生变化。1.4 步进器组件与数量修改数量修改复用了 SKU 插件自带的vk-data-input-number-box步进器组件。为了获得类型提示需要补充类型声明文件。步进器组件有几个关键属性属性作用v-model双向绑定当前数量值min最小值购物车场景固定为 1max最大值取item.stock库存上限index标识符传入item.skuId在回调中用于定位是哪个商品index是这个组件的关键设计列表中有多个商品时数量变化事件需要知道是哪个商品触发的。通过index传递 skuId在onChangeCount回调中通过ev.index拿到。const onChangeCount (ev: InputNumberBoxEvent) { putMemberCartBySkuIdAPI(ev.index, { count: ev.value }) }修改接口/member/cart/:skuId(PUT) 支持部分更新——只传count就只改数量只传selected就只改选中状态。这种设计符合 RESTful 最佳实践。1.5 选中与全选的联动逻辑选中状态的管理涉及三个层次单品选中点击商品前的勾选框取反当前item.selected同步调用 PUT 接口。全选计算用计算属性isSelectedAll判断是否所有商品都选中。注意边界情况——列表为空时every()返回true所以需要cartList.value.length 前置判断const isSelectedAll computed(() { return cartList.value.length cartList.value.every((v) v.selected) })全选切换点击全选按钮时先取反得到目标状态然后遍历所有商品设置selected最后调用全选接口/member/cart/selected(PUT) 同步后端。这里有一个前后端协同的细节前端先乐观更新立即修改本地数据的 selected 状态再异步调用后端接口。这样用户点击后界面马上响应不需要等待网络。如果后端失败理论上应该回滚但这个项目没有做——因为这种场景下失败的几率很低。1.6 结算金额的计算结算信息全部依赖计算属性因为金额需要随选中状态和数量变化自动更新计算属性依赖作用selectedCartListcartList 每项的selected找出所有被选中的商品selectedCartListCountselectedCartList 每项的count计算总件数selectedCartListMoneyselectedCartList 每项的count和nowPrice计算总金额const selectedCartListMoney computed(() { return selectedCartList.value .reduce((sum, item) sum item.count * item.nowPrice, 0) .toFixed(2) // 保留两位小数 })toFixed(2)是因为 JavaScript 浮点数运算可能产生99.0000000001这种结果保留两位小数确保显示正常。1.7 购物车组件化拆分小程序有一个限制跳转到 tabBar 页面时会关闭所有其他非 tabBar 页面。这意味着从商品详情页直接switchTab到购物车后用户无法通过返回按钮回到商品详情页。解决方案把购物车业务逻辑抽取为独立组件CartMain.vue然后创建两个页面分别引用它pages/cart/ cart.vue -- tabBar 页面引用 CartMain cart2.vue -- 普通页面引用 CartMain可作为 navigator 跳转目标 components/ CartMain.vue -- 核心业务组件从商品详情页跳转时用cart2.vue普通页有返回按钮从底部 tabBar 进入时用cart.vue。两个页面只是壳真正的逻辑都在CartMain组件里避免了代码重复。二、订单模块订单模块是整个购物流的终点。从购物车结算、商品详情立即购买、订单详情再次购买这三种方式最终都汇聚到填写订单页。2.1 三种订单来源的统一处理填写订单页需要兼容三种入口入口来源传递参数调用接口购物车结算购物车底部去结算无后端根据登录用户查询购物车选中商品/member/order/pre立即购买商品详情 SKU 弹窗立即购买skuId count/member/order/pre/now再次购买订单详情再次购买orderId/member/order/repurchase/:id统一处理的核心思路根据页面参数判断走哪个分支。const getMemberOrderPreData async () { if (query.count query.skuId) { // 路径1立即购买有 skuId 和 count 参数 const res await getMemberOrderPreNowAPI({ count: query.count, skuId: query.skuId }) orderPre.value res.result } else if (query.orderId) { // 路径2再次购买有 orderId 参数 const res await getMemberOrderRepurchaseByIdAPI(query.orderId) orderPre.value res.result } else { // 路径3购物车结算无特殊参数 const res await getMemberOrderPreAPI() orderPre.value res.result } }这种参数分发模式是处理多入口页面的常用手法避免了为每种入口单独创建页面。2.2 收货地址的跨页面选择收货地址的选择涉及两个页面地址管理页选择和填写订单页展示。为了让两个页面共享选中的地址数据使用了独立的 Pinia Store// stores/modules/address.ts export const useAddressStore defineStore(address, () { const selectedAddress refAddressItem() const changeSelectedAddress (val: AddressItem) { selectedAddress.value val } return { selectedAddress, changeSelectedAddress } })工作流程用户在填写订单页点击地址区域跳转到地址管理页地址管理页中用户点击某个地址调用addressStore.changeSelectedAddress(item)存入 Storeuni.navigateBack()返回填写订单页填写订单页通过计算属性读取 Store 中的选中地址地址选择时有一个细节地址管理页中每个地址项同时有点击选中和点击修改两个操作。修改是一个navigator组件如果不阻止事件冒泡点击修改会先触发 navigator 跳转然后冒泡到外层的taponChangeAddress(item)导致既跳转了又选中了。所以需要tap.stop() {}来阻止冒泡。2.3 提交订单的流程提交订单时前端组装的数据结构await postMemberOrderAPI({ addressId: selecteAddress.value?.id, // 选中的地址 ID buyerMessage: buyerMessage.value, // 订单备注 deliveryTimeType: activeDelivery.value.type, // 配送时间类型 goods: orderPre.value!.goods.map((v) ({ // 商品列表 count: v.count, skuId: v.skuId })), payChannel: 2, // 固定微信支付 payType: 1, // 固定在线支付 })提交成功后接口返回{ id: string }订单 ID然后用uni.redirectTo跳转到订单详情页。这里用redirectTo而非navigateTo的原因是提交订单后当前填写页已经没有用了关闭它避免用户返回时看到一个已提交的订单页。2.4 订单状态体系订单有 6 种状态不同状态下可执行的操作不同待付款(1) -- 支付 -- 待发货(2) -- 商家发货 -- 待收货(3) -- 确认收货 -- 待评价(4) -- 评价 -- 已完成(5) | | 取消 取消不可取消 | 已取消(6)为了代码可读性用枚举替代魔法数字export enum OrderState { DaiFuKuan 1, // 待付款 DaiFaHuo 2, // 待发货 DaiShouHuo 3, // 待收货 DaiPingJia 4, // 待评价 YiWanCheng 5, // 已完成 YiQuXiao 6, // 已取消 }各状态下的可操作按钮状态可执行操作待付款去支付、取消订单待发货再次购买、开发环境模拟发货待收货再次购买、确认收货待评价再次购买、去评价、删除订单已完成再次购买、删除订单已取消再次购买、删除订单2.5 自定义导航栏的滚动动画订单详情页的导航栏有一个渐进显隐的动画效果页面刚打开时导航栏透明向下滚动到一定距离后导航栏背景和文字逐渐显现。这个效果只在微信小程序端可用利用了小程序的animateAPI。核心实现步骤通过getCurrentPages()获取当前页面实例在onReady中调用页面实例的animate方法绑定动画到 scroll-view 的滚动位置onReady(() { pageInstance.animate( .navbar, [{ backgroundColor: transparent }, { backgroundColor: #f8f8f8 }], 1000, { scrollSource: #scroller, startScrollOffset: 0, // 从顶部开始 endScrollOffset: 50, // 滚动 50px 后动画完成 timeRange: 1000, }, ) // 标题和返回按钮的动画同理只是变化的属性是 color })导航栏左上角的按钮还有一个逻辑判断通过pages.length 1判断是否有上一页。有上一页显示返回箭头没有则显示首页图标。这是因为从支付成功页redirectTo过来后页面栈只有当前一个页面没有可返回的页面。由于 uni-app 的类型声明不包含animate方法需要使用类型断言或扩展类型type PageInstance Page.PageInstance WechatMiniprogram.Page.InstanceMethodsany const pageInstance pages.at(-1) as PageInstance2.6 支付流程与开发环境处理微信支付在生产环境和开发环境需要走不同的路径生产环境调用 /pay/wxPay/miniPay 获取支付参数 -- 调用 wx.requestPayment() 唤起微信支付 开发环境调用 /pay/mock 模拟支付 -- 直接修改订单状态为已支付切换逻辑通过import.meta.env.DEV判断const onOrderPay async () { if (import.meta.env.DEV) { await getPayMockAPI({ orderId: query.id }) } else { const res await getPayWxPayMiniPayAPI({ orderId: query.id }) await wx.requestPayment(res.result) } uni.redirectTo({ url: /pagesOrder/payment/payment?id${query.id} }) }这种通过环境变量区分行为的模式在项目中多处使用模拟支付、模拟发货避免了手动注释切换代码的麻烦。2.7 支付倒计时的处理待付款状态下订单有一个支付倒计时后端返回countdown字段单位秒。使用 uni-ui 的uni-countdown组件展示uni-countdown :secondorder.countdown :show-dayfalse :show-colonfalse timeuponTimeup /倒计时结束时触发timeup事件需要在前端主动将订单状态改为已取消。这里前端主动修改状态是为了界面即时响应——后端可能不会实时推送状态变更。2.8 订单列表的多 Tab 分页架构订单列表页的设计有几个层次Tab 切换层使用swiper 游标实现滑动切换activeIndex同时控制游标位置和当前 swiper-item。列表组件层每个 Tab 对应一个OrderList子组件子组件接收orderStateprops内部独立管理自己的数据和分页状态。这种设计的优势是每个 Tab 的数据相互隔离。用户从全部切换到待付款再切回来时全部的数据还在不需要重新加载。各 Tab 的分页状态也互不影响。列表项的关键信息展示订单状态文字 下单时间商品缩略图 名称 规格文字总件数和实付金额根据状态显示对应操作按钮去支付 / 再次购买 / 确认收货2.9 订单模块其他操作的触发条件不同订单操作的触发条件有严格的状态限制由后端约束前端也需要做对应判断// 取消订单仅待付款 // 接口PUT /member/order/:id/cancel // 删除订单仅待评价、已完成、已取消 // 接口DELETE /member/order // 确认收货仅待收货 // 接口PUT /member/order/:id/receipt // 模拟发货仅开发环境 待发货 // 接口GET /member/order/consignment/:id // 获取物流仅待收货、待评价、已完成 // 接口GET /member/order/:id/logistics在模板中通过v-if配合枚举判断来按需显示按钮这是状态机模式在前端 UI 渲染中的典型应用。三、关键知识点总结3.1 页面通信方式汇总场景方式原因立即购买传参URL query 参数跨页面参数简单skuId count再次购买传参URL query 参数跨页面参数简单orderId选中收货地址Pinia Store跨页面 需返回上一页购物车组件复用抽取公共组件同一业务逻辑被两个页面使用3.2 常见易错点onShowvsonLoadtabBar 页面的数据刷新用onShow因为切换 tab 不触发onLoadredirectTovsnavigateTo提交订单、支付成功后用redirectTo因为当前页面已无用乐观更新修改选中状态时先改本地数据再调接口保证 UI 即时响应全选判断边界列表为空时every()返回true需要额外判断length事件冒泡地址选中项内有 navigator 时需要tap.stop阻止冒泡四、常见报错速查问题原因解决加入购物车后返回列表不刷新购物车用了onLoadtabBar 页改用onShow步进器改数量无效或改错商品未传index或取值错误传:indexitem.skuId回调中用ev.index全选勾选状态异常列表为空时every()返回 true加cartList.value.length 前提条件提交订单提示无地址Store 中未存储选中地址检查地址管理页是否正确调用了changeSelectedAddress订单详情导航栏动画不生效H5 端或非微信小程序条件编译包裹仅在 MP-WEIXIN 下使用倒计时结束状态不变后端未主动更新在timeup事件中手动修改前端 orderState再次购买页面显示购物车数据未接收orderId参数defineProps中补充orderId?: string点击修改地址同时选中了地址事件冒泡navigator 上加tap.stop