️作者简介:大家好,我是亦世凡华、渴望知识储备自己的一名在校大学生
个人主页:亦世凡华、
系列专栏:uni-app
座右铭:人生亦可燃烧,亦可腐败,我愿燃烧,耗尽所有光芒。
引言
⚓经过web前端的学习,相信大家对于前端开发有了一定深入的了解,今天我开设了uni-app专栏,主要想从移动端开发方向进一步发展,而对于我来说写移动端博文的第二站就是uni-app开发,希望看到我文章的朋友能对你有所帮助。
今天开始使用 vue3 + uni-app 搭建一个电商购物的小程序,因为文章会将项目的每一个地方代码的书写都会讲解到,所以本项目会分成好几篇文章进行讲解,我会在最后一篇文章中会将项目代码开源到我的GitHub上,大家可以自行去进行下载运行,希望本文章对有帮助的朋友们能多多关注本专栏,学习更多前端uni-app知识。然后开篇先简单介绍一下本项目用到的技术栈都有哪几个方面(阅读此次项目实践文章能够学习到的技术):
uni-app:跨平台的应用开发框架,基于vue.js可以一套代码同时构建运行在多个平台。
pnpm:高性能、轻量级npm替代品,帮助开发人员更加高效地处理应用程序的依赖关系。
vue3:vue.js最新版本的用于构建用户界面的渐进式JavaScript框架。
typescript:JavaScript的超集,提供了静态类型检查,使得代码更加健壮。
pinia:vue3构建的Vuex替代品,具有响应式能力,提供非常简单的 API,进行状态管理。
uni-ui:基于vue.js和uni-app的前端UI组件库,开发人员可以快速地构建跨平台应用程序。
如果是第一次接触uni-app并且想学习uni-app的朋友,我是不建议直接从此次实战项目开始看起,可以先阅读一下我以前的基础文章:什么是uniapp?如何开发uniapp?按部就班的学习可以让学习变得更轻松更容易上手哦,闲话少说我们直接开始今天的uni-app实战篇
目录
地址模块静态搭建
新建地址功能实现
修改/删除地址功能实现
SKU模块搭建
购物车模块搭建
填写订单
接下来实现地址模块的静态搭建,地址模块需要借助两个页面,一个是地址信息的展示另一个是新建或修改地址信息,地址信息这一块我们也将其放置在分包页面当中进行展示,首先我们要新建两个分包页面,如下:
因为address-form要进行新建页面和修改页面两部分内容,所以这里新建页面不需要设置标题后面根据传参动态设置标题即可。
收获地址的静态结构布局如下,修改地址这一块传递query参数,新建地址不需要传递:
小王子
13111111111
默认
广东省 广州市 天河区 程序员
修改
小公主
13222222222
默认
北京市 北京市 顺义区 程序员
修改
暂无收货地址
新建地址
新建和修改地址的页面设计如下:
根据query传递过来的参数动态设置修改和新建页面的显示:
// 获取query数据
const query = defineProps<{
id?: string
}>()
// 动态设置标题
uni.setNavigationBarTitle({ title: query.id ? '修改地址' : '新建地址' })
最终呈现的效果如下:
接下来开始实现新建地址的功能,首先我们编写相应的接口函数用来收集表单数据进行新建地址,根据后端返回给我们的数据编写相应的ts类型,然后在前端接口中进行类型的限制:
根据需要的参数,在新建地址页面设置响应式ref数据用来获取相应的表单数据,如下:
// 表单数据
const form = ref({
receiver: '', // 收货人
contact: '', // 联系方式
fullLocation: '', // 省市区(前端展示)
provinceCode: '', // 省份编码(后端参数)
cityCode: '', // 城市编码(后端参数)
countyCode: '', // 区/县编码(后端参数)
address: '', // 详细地址
isDefault: 0, // 默认地址,1为是,0为否
})
正常的输入框直接使用 v-model 进行数据的双向绑定即可:
关于获取所在地区的数据在上文讲解个人信息模块的时候已经讲解过了,这里也简单提一下,通过change函数监听用户选择的数据,将文字进行前端页面展示,数字进行后端参数传递:
// 收集所在地区的事件处理函数
const onRegionChange: UniHelper.RegionPickerOnChange = (ev) => {
// 省市区前端展示需要
form.value.fullLocation = ev.detail.value.join(' ')
// 省市区后端参数需要
const [provinceCode, cityCode, countyCode] = ev.detail.code!
// 将获取到的参数进行一个合并
Object.assign(form.value, { provinceCode, cityCode, countyCode })
}
设置默认地址的switch按钮也是采用change函数进行监听,根据boolean类型返回数字1或0:
// 收集是否默认收获地址
const onSwitchChange: UniHelper.SwitchOnChange = (ev) => {
form.value.isDefault = ev.detail.value ? 1 : 0
}
接下来给按钮设置点击函数用于表单数据的提交,然后进行页面的跳转:
// 提交表单
const onSubmit = async () => {
// 新建地址请求
await postMemberAddressAPI(form.value)
// 成功提示
uni.showToast({ icon: 'success', title: '添加成功' })
// 返回上一页
setTimeout(() => {
uni.navigateBack()
}, 400)
}
进行页面跳转之后接下来就需要进行地址页面数据的渲染了,老生常谈的事先编写相应接口函数:
编写完相应的接口函数之后,接下来我们需要调用该函数然后通过onShow页面展示的时候调用:
通过v-for遍历数据,然后通过查找语法进行页面的展示:
最终呈现的结果如下:
接下来实现修改地址的功能,这里我们也先编写相应的接口函数,修改地址需要传入相应的id值:
/**
* 获取收获地址详情接口
* @param id 地址id(路径参数)
*/
export const getMemberAddressByIdAPI = (id: string) => {
return http({
method: 'GET',
url: `/member/address/${id}`,
})
}
在修改地址页面中调用接口函数获取地址的详情数据,并将数据合并到表单当中:
// 获取收获地址详情数据
const getMemberAddressByIdData = async () => {
if (query.id) {
const res = await getMemberAddressByIdAPI(query.id)
// 把数据合并到表单中
Object.assign(form.value, res.result)
}
}
// 初始化调用(页面加载)
onLoad(() => {
getMemberAddressByIdData()
})
接下来我们需要对表单的提交按钮再进行处理,首先我们先编写相应的修改地址的API:
/**
* 获取收获地址详情接口
* @param id 地址id(路径参数)
* @param data 表单数据(请求体参数)
*/
export const puttMemberAddressByIdAPI = (id: string, data: AddressParams) => {
return http({
method: 'PUT',
url: `/member/address/${id}`,
data,
})
}
编写完接口函数之后,我们就可以根据当前页面是否有query参数来判断是修改还是新增:
最终呈现的结果如下:
接下来对修改地址页面进行表单的校验规则,如下我们定义相应的规则:
// 定义校验规则
const rules: UniHelper.UniFormsRules = {
receiver: { rules: [{ required: true, errorMessage: '请输入收货人姓名' }] },
contact: {
rules: [
{ required: true, errorMessage: '请输入联系方式' },
{ pattern: /^1[3-9]\d{9}$/, errorMessage: '手机号格式不正确' },
],
},
fullLocation: { rules: [{ required: true, errorMessage: '请选择所在地区' }] },
address: { rules: [{ required: true, errorMessage: '请选择详细地址' }] },
}
接下来借助uniapp中的uni-form进行相应的表单校验:
拿到相应的表单实例之后,调用表单校验函数进行验证,通过trycatch进行报错提示:
// 存储表单组件实例
const formRef = ref()
// 提交表单
const onSubmit = async () => {
try {
await formRef.value?.validate?.()
if (query.id) {
// 修改地址的API
await puttMemberAddressByIdAPI(query.id, form.value)
} else {
// 新建地址请求
await postMemberAddressAPI(form.value)
}
// 成功提示
uni.showToast({ icon: 'success', title: query.id ? '修改成功' : '添加成功' })
// 返回上一页
setTimeout(() => {
uni.navigateBack()
}, 400)
} catch (error) {
uni.showToast({ icon: 'error', title: '请填写完整信息' })
}
}
如果什么数据都没有填入就进行表单提交的话,呈现的效果如下图所示:
接下来实现删除地址功能的实现,这里我们借助uniapp给我们提供的uni-swipe-action组件进行删除业务的实现,首先我们先编写相应的删除功能的接口函数:
/**
* 删除收获地址
* @param id 地址id(路径参数)
*/
export const deleteMemberAddressByIdAPI = (id: string) => {
return http({
method: 'DELETE',
url: `/member/address/${id}`,
})
}
接下来将我们要进行删除业务的功能换上相应的组件,在组件中存放要删除按钮的插槽:
通过点击函数设置删除按钮的回调:
// 删除收获地址
const onDeleteAddress = (id: string) => {
// 二次确认
uni.showModal({
content: '确认删除?',
success: async (res) => {
if (res.confirm) {
// 根据id删除收获地址
await deleteMemberAddressByIdAPI(id)
// 重新获取数据列表
getMemberAddressData()
}
},
})
}
最终呈现的结果如下:
SKU模块展示了购买某种商品时给我们呈现的选择样式的相关界面,uniapp插件市场也为其提供了相应的插件进行使用:插件市场选择 ,这里我们选择了vue3项目,找下载量最高的插件进行下载
进行该插件进行相应的下载:
下载完插件根据作者给我们的使用提示进行插件的部署:
配置好文件之后,记下来我们在商品详情组件引入该组件:
通过 v-model 进行双向数据绑定来显示SKU页面的显示与隐藏,当打开SKU页面的时候根据传入数据的不同来切换不同的按钮状态,mode属性就是改变其按钮模式的:
// 是否显示SKU组件
const isShowSku = ref(false)
// 商品信息
const localdata = ref({} as SkuPopupLocaldata)
// 设置按钮模式
enum SkuMode {
Both = 1,
Cart = 2,
Buy = 3,
}
const mode = ref(SkuMode.Both)
// 打开Sku弹窗修改按钮模式
const openSkuPopup = (val: SkuMode) => {
// 显示SKU组件
isShowSku.value = true
// 修改按钮模式
mode.value = val
}
当我们点击选择的时候,两个按钮都同时显示:
这里我们通过之前设置的函数传递对应的数值来显示不同按钮下显示的不同按钮模式:
最终呈现的效果如下:
接下来实现,当我们点击商品选择的时候,原本的请选择商品规格的文字就会变成我们选择的文字,这里我们通过设置SKU组件的ref实例获取组件实例之后,通过selectArr属性数据替代原本的文字数据:
// SKU组件的实例
const skuPopupRef = ref()
// 计算被选中的值
const selectArrText = computed(() => {
return skuPopupRef.value?.selectArr?.join(' ').trim() || '请选择商品规格'
})
然后在选择按钮文字处通过插值语法动态设置其对应的文字内容:
这里我们也可以通过 actived-style 属性设置其颜色、边框和背景等相关颜色,使其更适配当前主题
最终呈现的效果如下:
接下来实现加入购物车功能的实现,在SKU组件中调用@add-cart设置其加入购物车的回调:
// 加入购物车的事件处理函数
const onAddCart = async (ev: SkuPopupEvent) => {
await postMemberCartAPI({ skuId: ev._id, count: ev.buy_num })
uni.showToast({ icon: 'none', title: '添加成功' })
isShowSku.value = false
}
还是老生常谈的东西,关于购物车界面有两种情况的展示,一种是用户未登录状态另一种是用户已登录状态,这里需要我们先通过判断仓库中是否有用户信息来进行v-if和v-else展示,具体如下:
接下来开始编写接口函数来获取购物车中的数据:
/**
* 获取购物车列表
*/
export const getMemberCartAPI = () => {
return http({
method: 'GET',
url: '/member/cart',
})
}
在购物车组件中调用该接口函数获取相应的购物车数据,将获取到的数据存储到ref数据当中:
// 获取购物车数据
const getMemberCartData = async () => {
const res = await getMemberCartAPI()
cartList.value = res.result
}
// 初始化调用(页面显示)
onShow(() => {
if (memberStore.profile) {
getMemberCartData()
}
})
下面就是通过插值语法将获取到的数据进行一个展示了,如下:
最终呈现的效果如下:
删除商品也很简单,调用相应的接口函数,如下:
/**
* 删除/清空购物车单品
* @param data 请求头参数 ids SKUID 的集合
*/
export const deleteMemberCartAPI = (data: { ids: string[] }) => {
return http({
method: 'DELETE',
url: '/member/cart',
data,
})
}
然后给删除按钮设置点击事件,如此就可以进行商品的删除了,如下:
// 点击删除按钮
const onDeleteCart = (skuid: string) => {
// 二次确认
uni.showModal({
content: '是否删除',
success: async (res) => {
if (res.confirm) {
// 后端删除
await deleteMemberCartAPI({ ids: [skuid] })
// 重新获取列表
getMemberCartData()
}
},
})
}
接下来开始设置数据点击框和商品选择框的数据的交互,首先我们先编写相应的接口函数:
/**
* 修改购物车单品
* @param skuId SKUID
* @param data selected 选中状态 count 商品数量
*/
export const putMemberCartBySkuIdAPI = (
skuId: string,
data: { selected?: boolean; count?: number },
) => {
return http({
method: 'PUT',
url: `/member/cart/${skuId}`,
data,
})
}
/**
* 购物车全选/取消全选
* @param data selected 是否选中
*/
export const putMemberCartSelectedAPI = (data: { selected: boolean }) => {
return http({
method: 'PUT',
url: '/member/cart/selected',
data,
})
}
通过调用相关接口函数以及设置计算属性来得到改变选择框的状态:
// 修改商品数量
const onChageCount = (ev: InputNumberBoxEvent) => {
putMemberCartBySkuIdAPI(ev.index, { count: ev.value })
}
// 修改选中状态-单品修改
const onChangeSelected = (item: CartItem) => {
// 前端数据更新-是否选中取反
item.selected = !item.selected
// 后端数据更新
putMemberCartBySkuIdAPI(item.skuId, { selected: item.selected })
}
// 计算全选状态
const isSelectedAll = computed(() => {
return cartList?.value?.length && cartList?.value.every((v) => v.selected)
})
// 修改选中状态-全选修改
const onChangeSelectedAll = () => {
// 全选状态取反
const _isSelectedAll = !isSelectedAll.value
// 前端数据更新
cartList?.value?.forEach((item) => {
item.selected = _isSelectedAll
})
// 后端数据更新
putMemberCartSelectedAPI({ selected: _isSelectedAll })
}
最终呈现的效果如下:
接下来实现购物车底部结算信息内容功能的搭建,功能实现很简单,这里仅需要借助computed计算属性计算出当前商品的选中、总数以及总金额,然后通过插值语法进行一个数据的展示即可:
// 计算选中单品列表
const selectedCartList = computed(() => {
return cartList.value.filter((v) => v.selected)
})
// 计算选中总数
const selectedCartListCount = computed(() => {
return selectedCartList.value.reduce((sum, item) => sum + item.count, 0)
})
// 计算选中总金额
const selectedCartListMoney = computed(() => {
return selectedCartList.value
.reduce((sum, item) => sum + item.count * item.nowPrice, 0)
.toFixed(2)
})
接下来给商品结算按钮设置回调函数,如果没有选中商品就提示一下用户,如果选中商品了这里也简单的提示一下用户:
// 结算按钮回调函数
const gotoPayment = () => {
if (selectedCartListCount.value === 0) {
return uni.showToast({ icon: 'none', title: '请选择商品' })
}
// 跳转到结算页
uni.showToast({ icon: 'none', title: '等待完成' })
}
最终呈现的结果如下:
接下来实现填写订单相关业务的实现,首先我们需要再创建一个分包页面用于展示填写订单的页面展示,创建完分包之后,接下来就需要将我们之前购物车的结算按钮的跳转链接改一改了:
设置完跳转链接接下来就需要编写相应的接口函数,根据后端返回的数据编写ts类型:
接下来编写填写订单页面,有些基本的html代码在这就不再讲解了,直接给出代码:
张三 13333333333
广东省 广州市 天河区 黑马程序员3
请选择收货地址
{{ item.name }}
{{ item.attrsText }}
{{ item.payPrice }}
{{ item.price }}
x{{ item.count }}
配送时间
{{ activeDelivery.text }}
订单备注
商品总价:
{{ orderPre?.summary.totalPrice.toFixed(2) }}
运费:
{{ orderPre?.summary.postFee.toFixed(2) }}
{{ orderPre?.summary.totalPayPrice.toFixed(2) }}
提交订单
最终呈现的结果如下:
接下来实现收获地址的选择与修改,因为我们之前的地址管理模块仅仅是将地址信息进行了一个展示,并没有设置修改方面的内容,这里我们需要单独再写一个管理地址信息的仓库用于后面填写订单选择地址信息:
import type { AddressItem } from '@/types/address'
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useAddressStore = defineStore('address', () => {
const selectedAddress = ref()
const changeSelectedAddress = (val: AddressItem) => {
selectedAddress.value = val
}
return {
selectedAddress,
changeSelectedAddress,
}
})
在地址管理组件中定义一个修改收获地址的回调函数:
// 修改收货地址
const changeAddress = (item: AddressItem) => {
// 修改收货地址
const addressStore = useAddressStore()
addressStore.changeSelectedAddress(item)
// 返回上一页
uni.navigateBack()
}
给当前地址的设置好点击事件,这里我们需要给原本修改的地方设置一下阻止冒泡行为:
最后我们在填写订单组件的地方设置好地址数据,如果仓库中有数据就采用仓库中原本的数据,没有的话采用默认地址:
const addressStore = useAddressStore()
// 收获地址
const selectedAddress = computed(() => {
return addressStore.selectedAddress || orderPre.value?.userAddresses.find((v) => v.isDefault)
})
这里进行插值语法进行数据的呈现
最终呈现的结果如下:
接下来我们需要实现在商品详情页面中点击立即购买直接跳转到填写订单页面并展示当前的产品信息,这里我们仍然需要编写相应的接口函数:
/**
* 填写订单-获取立即购买订单
*/
export const getMemberOrderPreNowAPI = (data: {
skuId: string
count: string
addressId?: string
}) => {
return http({
method: 'GET',
url: '/member/order/pre/now',
data,
})
}
SKU模块有单独的一个属性用来进行立即购买使用的:
该回调函数进行路由的跳转,携带当前商品的一些参数,至于地址是处于可选的参数:
// 立即购买
const onBuyNow = (ev: SkuPopupEvent) => {
uni.navigateTo({
url: `/subpackage/create/create?skuId=${ev._id}&count=${ev.buy_num}&addressId=${address.value?.id}`,
})
}
至于商品详情页的地址选择,这里之前设置的一个静态的地址选择组件,这里需要我们将之前地址管理模块的具体地址数据进行渲染当前组件,然后通过子传父组件,将选择好的组件传递给父组件,父组件拿到子组件传递过来的数据进行页面的内容显示和作为query参数传递给填写订单组件当中,关于商品详情地址组件的具体代码如下:
配送至
{{ item.receiver }} {{ item.contact }}
{{ item.address }}
新建地址
确定
父组件拿到当前的子组件传递过来的内容:
// 获取当前选择地址
let address = ref()
const handleSelect = (val: any) => {
address.value = val
popup.value?.close()
}
接下来需要在填写订单组件中调用上文写到的接口函数,这里需要判断当前组件的是否能够接收到query参数,不能的话就正常采用之前的方式:
// 获取订单信息
const orderPre = ref()
const getMemberOrderPreData = async () => {
if (query.skuId && query.count) {
const res = await getMemberOrderPreNowAPI({
count: query.count,
skuId: query.skuId,
addressId: query.addressId,
})
orderPre.value = res.result
} else {
const res = await getMemberOrderPreAPI()
orderPre.value = res.result
}
}
收获地址这里也需要进行判断一下,如果有query参数就直接采用当前地址的第一个数据即可:
const addressStore = useAddressStore()
// 收获地址
const selectedAddress = computed(() => {
if (query.skuId && query.count) {
return orderPre.value?.userAddresses[0]
} else {
return addressStore.selectedAddress || orderPre.value?.userAddresses.find((v) => v.isDefault)
}
})
最终实现的效果如下:
接下来开始实现提交订单按钮的功能,在订单按钮处进行一个判断,如果当前没有订单的话就禁止
接下来编写相应的提交订单的接口函数:
/**
* 提交订单
* @param data 请求参数
*/
export const postMemberOrderAPI = (data: OrderCreateParams) => {
return http<{ id: string }>({
method: 'POST',
url: '/member/order',
data,
})
}
在提交订单按钮的回调事件中调用该接口函数,获取当前的订单id并跳转到订单详情页:
// 提交订单
const onOrderSubmit = async () => {
// 没有收货地址提醒
if (!selectedAddress.value?.id) {
return uni.showToast({ icon: 'none', title: '请选择收货地址' })
}
// 发送请求
const res = await postMemberOrderAPI({
addressId: selectedAddress.value?.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
uni.redirectTo({ url: `/subpackage/detail/detail?id=${res.result.id}` })
}
最终呈现的效果如下:
本项目地址管理页面、购物车页面以及订单页面的一些基本功能的搭建就讲解到这,下一篇文章将继续讲解项目其他页面操作,关注博主学习更多前端uni-app知识,您的支持就是博主创作的最大动力!