uni-app优乐购商城小程序搭建
uni-app优乐购商城小程序搭建
一、新建项目
购物商城接口文档:https://www.showdoc.com.cn/128719739414963/2516997897914014
帮助文档如下:
uni-app官网:https://dcloud.io/
阿里巴巴字体图标官网:https://www.iconfont.cn/
微信小程序官网:https://developers.weixin.qq.com/miniprogram/dev/framework/
(1)uni-app框架:本项目用的是uni-app框架进行开发的,因为uni-app框架是使用 Vue.js 开发所有前端应用的框架,开发者编写一套代码,可发布到 iOS、Android、H5、以及各种小程序、快应用等多个平台。
(2)开发工具:用HBuilderX 来开发,因为uni-app项目都是用HbuildX来开发HBuilderX官网
(3)sass编译:为了方便编写scss/sass样式,建议安装 scss/sass 编译插件下载地址;进入之后点击——使用 HBuilderX 导入插件,登入HBuilderX,进行下载
(4)新建项目:
① 文件 ==》 新建 ==》 项目
② 填写项目的一些基本的信息

二、搭建目录结构
目录结构划分如下:
| 目录名 | 作用 |
| common | 存放公共样式 |
| components | 存放自定义组件 |
| page | 存放主包页面(tab页面) |
| subpkg1 | 子包1,存放子包页面(商品列表页面、商品列表页面、搜索页面) |
| subpkg2 | 子包2,存放子包页面(其他页面) |
| utils | 自己的帮助库 |

三、搭建项目页面
| 页面名称 | 名称 |
| 首页 | index |
| 分类页面 | category |
| 商品列表页面 | goods-list |
| 商品列表页面 | goods-detail |
| 购物车页面 | cart |
| 收藏页面 | collect |
| 订单页面 | order |
| 搜索页面 | search |
| 个人中心页面 | user |
| 意见反馈页面 | feedback |
| 登录页面 | login |
| 授权页面 | auth |
| 支付页面 | pay |
四、搭建tabBar
- 在项目根目录下,新建
icons文件夹,并将图标拷入改文件夹 - 修改
app.json文件,添加tabBar - 配置tabBar效果:pages.json文件 ⇒ 配置tabBar节点。代码和在微信开发者工具中的配置是一样的
代码如下:"tabBar": { "color": "#999", "selectedColor": "#ff2d4a", "backgroundColor": "", "position": "bottom", "borderStyle": "black", "list": [ { "pagePath": "pages/index/index", "text": "首页", "iconPath": "icons/home.png", "selectedIconPath": "icons/home-o.png" }, { "pagePath": "pages/category/index", "text": "分类", "iconPath": "icons/category.png", "selectedIconPath": "icons/category-o.png" }, { "pagePath": "pages/cart/index", "text": "购物车", "iconPath": "icons/cart.png", "selectedIconPath": "icons/cart-o.png" }, { "pagePath": "pages/user/index", "text": "我的", "iconPath": "icons/my.png", "selectedIconPath": "icons/my-o.png" } ] }
五、首页
- 首页的代码全部定义在:pages/home/home.vue中
- 结构定义在template标签中,逻辑定义在script标签中,样式定义在
1.获取轮播图的数据
(1)data中:定义存放轮播图的数组
(2)onLoad生命周期函数中:调用方法
(3)methods中:定义获取数据的方法
① 用uni.$http.get 来获取数据
② 用async和await来获取数据,代表值是一个promise对象
③ 判断状态码是否等于200,如果不是的话,调用uni.showToast提示他
④ 为data中数据赋值
export default {
data() {
return {
// 1. 轮播图的数据列表,默认为空数组
swiperList: [],
}
},
onLoad() {
// 2. 在小程序页面刚加载的时候,调用获取轮播图数据的方法
this.getSwiperList()
},
methods: {
// 3. 获取轮播图数据的方法
async getSwiperList() {
// 3.1 发起请求
const { data: res } = await uni.$http.get('/api/public/v1/home/swiperdata')
// 3.2 请求失败
if (res.meta.status !== 200) {
return uni.showToast({
title: '数据请求失败!',
duration: 1500,
icon: 'none',
})
}
// 3.3 请求成功,为 data 中的数据赋值
this.swiperList = res.message
},
},
}
.index-swiper {
swiper {
width: 750rpx;
height: 340rpx;
image {
width: 100%;
}
}
}
2.渲染轮播图
<!-- 轮播图 -->
<view class="index-swiper">
<swiper indicator-dots="true" autoplay="true" interval="3000" duration="1000">
<swiper-item v-for="(item,index) in swiperList" :key="index">
<image :src="item.image_src" mode="widthFix"></image>
</swiper-item>
</swiper>
</view>
首页效果 图如下:

六 、分类页面
分类这一块也是分的三大块 ,分别是搜索框和左侧菜单栏渲染以及右侧的内容区域。我们可以看到刚刚的首页也有搜索框 我们就可以把搜索框拆成一个组件。这样我们就可以少写一个组件。第一步 我们需要请求接口,将后台返回来的数据渲染到页面上 来完成分类的渲染。右侧数据来渲染我所有数据的children属性。第二部 右侧的数据,根据效果图 我们可以看到当我们点击左侧区域的时候右侧的数据会跟着我们变化而变化。
<view>
<lhm-header></lhm-header>
<!-- //左侧菜单 -->
<view class="index-cate">
<scroll-view class="left-menu" scroll-y="true">
<view class="cate1" :class="index===currentIndex?'active':''" v-for="(item,index) in leftMenuList"
:key="index" @tap="changeIndex(index)">
{{item}}
</view>
</scroll-view>
<!-- //右侧 -->
<scroll-view class="right-data" scroll-y="true">
<view class="cate2-group" v-for="(item,index) in rightDate" :key="item.cat_id">
<!-- //标题 -->
<view class="cate2-title">
/{{item.cat_name}}/
</view>
<!-- //产品列表 -->
<view class="cate3-list">
<navigator class="image-box" v-for="(item2,index2) in item.children" :key="item2.cat_id"
:url="'../../subpkg1/goods-list/goods-list?cid='+item2.cat_id">
<image mode="widthFix" :src="item2.cat_icon"></image>
<text>{{item2.cat_name}}</text>
</navigator>
</view>
</view>
</scroll-view>
</view>
</view>
获取数据:全部分类数据、左侧菜单数据、右侧菜单数据
return {
categories: [], //全部分类数据
leftMenuList: [], //左侧菜单数据1级
rightDate: [], //右侧数据2级+3级列表
currentIndex: 0,
scrollTop: 0
}
},
左侧数据和右侧数据分类:
onLoad() {
// this.getCategories();
//在发请求前,判断本地储存有没有旧数据
//没有旧数据就发送新的请求,有且没有过期就用旧数据
const Cates = uni.getStorageSync('cates');
if (!Cates) {
this.getCategories();
} else {
if ((Date.now() - Cates.time) > 1000 * 10) {
this.getCategories();
} else {
console.log('使用旧数据');
this.categories = Cates.data;
//左侧
this.leftMenuList = this.categories.map(e => e.cat_name);
//右侧数据
this.rightDate = this.categories[0].children;
}
}
分类页面效果图如下:

七、商品列表页面
商品列表页面就是 我们点击分类的时候 传过来的数据 然后我们在接受这个cid
需要在呢个onLoad生命周期函数里面 一个页面只能调用一次 页面加载的时候触发 可以在onLoad
的参数中获取打开当前页面的路径参数把这些传过来。
<view>
<!-- 搜索框 -->
<lhm-header></lhm-header>
<!-- tabs组件 -->
<lhm-tabs :tabs='tabsList' @tabsChange='handleTabsChange'></lhm-tabs>
<view class="goods-list">
<!-- <lhm-goods v-for="(item,index) in goodsList" :goods="item"></lhm-goods> -->
<lhm-goods v-for="(item,index) in newGoodsList" :key="item.goods_id" :goods="item"></lhm-goods>
</view>
</view>
页面生命周期函数,为上个页面传递参数:
//页面生命周期函数
//onLoad参数为上个页面传递的数据,参数类型为object(用于页面传参)
onLoad(e){
this.queryParam.cid=e.cid;
this.getGoodsList();
},
1.页面上拉加载
//页面上拉加载
//onReachBottom页面滚动到底部的事件,常用于下拉到底部时加载下一页数据
onReachBottom() {
//判断是否还有下一页数据,如当前页面大于总页数,则没有下一页
if(this.queryParam.pagenum>=this.totalPageCount) return uni.showToast({
title:'数据加载完毕'
})
//判断是否正在请求其他数据,如果是,则不发起额外请求
if(this.isloading)return;
//获取下一页数据
this.queryParam.pagenum++;
this.getGoodsList();
},
2.页面下拉刷新
//下拉刷新
onPullDownRefresh(){
//1重置数据
this.goodsList=[];
//2重置页码为1
this.queryParam.pagenum=1;
//3重新发请求
setTimeout(()=>{
//数据请求成功后去掉下拉 下拉后让他消失
this.getGoodsList(()=>uni.stopPullDownRefresh());
},1000)
},
切换 选项卡,价格升序和降序:
//切换选项卡
handleTabsChange(aid){
this.tabsList.forEach(v=> v.id==aid? v.isActive=true:v.isActive=false)
},
//获取商品列表 cb回调函数
async getGoodsList(cb) {
//打开节流阀
this.isloading=true;
const res = await this.$Https({
url: "/goods/search",
data:this.queryParam
});
console.log(res);
// this.goodsList = res.message.goods;
//关闭节流阀
this.isloading=false;
cb && cb();
//计算总页数
this.totalPageCount=Math.ceil(res.message.total/this.queryParam.pagesize);
//通过扩展运算符的形式,进行新旧数据的拼接
this.goodsList=[...this.goodsList,...res.message.goods];
console.log(this.goodsList);
}
},
computed:{
//价格升降序的方法
newGoodsList(){
console.log('computed方法被调用');
function compare(arg){
return function(a,b){
return a[arg] -b[arg];
}
}
if(this.tabsList[0].isActive){
return this.goodsList;
}else if(this.tabsList[1].isActive){
//正序排列
this.goodsList.sort(compare('goods_price'));
return this.goodsList;
}else{
//正序排列
this.goodsList.sort(compare('goods_price'));
//反转
this.goodsList.reverse();
return this.goodsList;
}
}
}
列表效果图如下:

八、商品详情页面detail页面
在详情页⾯可以根据商品id来渲染页⾯
在详情页有加⼊ 加⼊购物车 :是根据效果来定义⼀个需要的数组 ⽤⼀个数组来接收,给点击购物车绑定⼀个点击事件 当点击后将数据保存
到数组⾥⾯并保存到本地存储⾥⾯,这样的话可以让数据不丢失 通过wx.setStorageSync ⽅法将数据保存 取的话可以使⽤
wx.getStorageSync ⽅法获取本地存储的数据
这⾥要注意 添加购物车要判断商品id是否⼀致,当id⼀致时就不会往数组⾥⾯添加,只会增加对应的商品数量
商品页面我们需要接受我们这个列表页面传过来得这个goods_id
还是一样的方法在这个onLoad来接受
return {
//向服务器接口发送请求时发送的数据
queryParam: {
goods_id: 0
},
goodsInfo: [], //商品信息
}
},
//onLoad参数为上个页面传递的数据,参数类型为object(用于页面传参)
onLoad(e) {
this.queryParam.goods_id = e.goods_id;
this.getGoodsInfo();
},
1.预览图片
//预览图片
preview(index) {
uni.previewImage({
current: index,
urls: this.goodsInfo.pics.map(x => x.pics_big)
})
},
gotoCart() {
uni.switchTab({
url: '/pages/cart/cart'
})
},
2.加入购物车
//加入购物车
addCart() {
//从本地缓存中取出购物车 数组类型
let cart = uni.getStorageSync('cart') || [];
//在购物车中查询是否已经加入该商品
let index = cart.findIndex(v => v.goods_id == this.goodsInfo.goods_id);
//如果没有,将该商品加入,并添加数量和选中状态属性
if (index == -1) {
this.goodsInfo.cartNum = 1;
this.goodsInfo.checked = true;
cart.push(this.goodsInfo);
} else {
//如果有,将该商品数量加一
cart[index].cartNum++;
}
//将购物车数据写入缓存中
uni.setStorageSync('cart', cart);
//弹出消息
uni.showToast({
title: "加入成功"
});
console.log(cart);
},
商品详情效果如下:

九、购物车页面cart
添加到自己喜欢的商品以后 他就会到这个购物车里面 我们还有进行渲染就可以了
购车车功能有:跳转到购物车页⾯可以对保存的数据进⾏渲染,在购物车页⾯,需要实现对商品全选,反选。我们需要在data里面定义 cartlist: uni.getStorageSync("cart") || [],就是我们获取的商品在本地存储里面的数据checked: false 是否为true false。我们复制一个卡片 进行渲染就行了 我们就可以看到我们加入的购物车的内容,全选反选我们在want里面复制checkbox组件 绑定value属性 因为这里面不能使用v-modle ,点击事件 为change事件 checkAll。
<view>
<view v-if='cart.length>0'>
<lhm-address :address='address'></lhm-address>
<view class="cart-title">
<text class="iconfont icon-gouwuche">购物车</text>
</view>
<!-- 购物车列表 -->
<view>
<view v-for='(item,index) in cart' :key='index'>
<lhm-goods :goods="item" :showCheck='true' :showNum='true' :allowLongTap='true'
@changeNum="changeNum" @changeChecked="changeChecked"></lhm-goods>
</view>
</view>
<lhm-settel :cartData="cart" :showAllCheck='true' :buttonText="'结算'" @handleAllChecked="handleAllChecked"></lhm-settel>
</view>
<view class="cart-default" v-else>
<view class="default-image">
<image class="cart-image" src="../../static/cart_empty.png" mode="widthFix"></image>
</view>
<view class="default-text">
购物车空空如也~
</view>
</view>
</view>
全选:
allChecked() {
return this.cartData.every(v => v.checked)
}
1.获取地址
点击添加收货按钮的时候 ,我们需要在微信小程序里面找到api并找到开放接口 ,开放接口里面有个设置,将wx.getsetting复制到页面上 它主要是获取用户的当前设置。返回值中只会出现小程序已经向用户请求过的权限,并进行判断如果我的res里面的用户授权结果中的scope.address为true的话 就会获取用户收货地址。调起用户编辑收货地址原生界面,并在编辑完成后返回用户选择的地址。
<view class="address-button" v-if="!address.userName">
<button type="primary" size="mini" @click="getAddress" open-type="openSetting">+添加收货地址</button>
</view>
async getAddress(){
//1获取收货地址
//返回值res是一个数组:第一项为错误对象;第二项为成功之后的收货地址对象
const [err,succ]=await uni.chooseAddress();
// const succ=await uni.chooseAddress();
console.log(succ);
//2用户成功的选择了收货地址
if(succ && succ.errMsg === 'chooseAddress:ok'){
uni.setStorageSync('address',succ);
}
//3用户没有授权
if(err && err.errMsg === 'chooseAddress:fail auth deny'){
}
2.修改商品数量
//修改购物车相应商品数量
changeNum(e) {
let index = this.cart.findIndex(v => v.goods_id == e.goods_id);
if (e.cartNum === 0) {
this.cart.splice(index, 1);
} else {
console.log(e.cartNum);
this.cart[index].cartNum = e.cartNum;
}
console.log(this.cart);
uni.setStorageSync('cart', this.cart);
},
changeChecked(e) {
let index = this.cart.findIndex(v => v.goods_id == e.goods_id);
this.cart[index].checked = e.checked;
uni.setStorageSync('cart', this.cart);
},
handleAllChecked(e){
this.cart.forEach(v=>v.checked=e);
}
}
购物车效果图如下:

十、我的页面
1.登录:在我的页面中有一个登录鉴权 ,当我点击登录鉴权按钮的时候 需要获取我的头像和基本信息。我们需要获取token值 以此我们需要请求接口里面传的encryptedData,rawData,iv,signature,code。 因此我们需要来获取这些参数。获取用户信息。uni.getUserProfile 页面产生点击事件(例如 button 上 bindtap 的回调中)后才可调用,每次请求都会弹出授权窗口,用户同意后返回 userInfo。该接口用于替换 wx.getUserInfo,并将我的个人信息存储到本地存储中去。接下来 我们需要使用wx.login来获取登录凭证 code,通过凭证进而换取用户登录态信息,并将接口调用成功的回调函数 返回来的数据 进行结构赋值,请求接口,将我需要的参数来传进去,我们这里需要手动存储token值,将token值存储到本地存储中去。这样登录就完成啦。
// 一键登录
async getUserInfo(e) {
//1、获取用户信息,并缓存
const {
encryptedData,
rawData,
iv,
signature,
userInfo
} = await getUserProfile();
uni.setStorageSync('userinfo', userInfo);
//2、获取用户登录后的code
const {
code
} = await login();
//3、发送请求,获取用户的token
const loginParams = {
encryptedData,
rawData,
iv,
signature,
code
}; //请求参数
// const {
// token
// } = await this.$Https({
// url: '/users/wxlogin',
// data: loginParams,
// method: 'post'
// });
// console.log(res);
const token = 'token'; //虚拟token
//4、将token存入本地缓存
uni.setStorageSync('token', token);
//5、跳转回上一页面
let page = uni.getStorageSync('page');
uni.removeStorageSync('page');
if (page === 'pay') {
uni.navigateTo({
url: '../../subpkg2/pay/pay'
}),
uni.showToast({
title: "登录成功!"
})
} else if (page === 'user') {
uni.switchTab({
url: '../../pages/user/user'
})
}
}
1.点击订单跳转到订单页面
点击我的页面中的全部订单 待付款 代收款 会跳到另一个页面,需要渲染数据 ,需要请求接口,接口里面传的参数值为type 他为number类型 所以我们得到type之后 需要将他进行一个数据类型转换。
<view class="order">
<!-- 2.我的订单 -->
<view class="dida">我的订单</view>
<!-- 3.订单详情 -->
<view class="order-box" @click="gotoOrder(0)">
<view class="iconfont icon-dingdanjihe"></view>
<view class="text">全部订单</view>
</view>
<view class="order-box" @click="gotoOrder(1)">
<view class="iconfont icon-daifukuan"></view>
<view class="text">待付款</view>
</view>
<view class="order-box" @click="gotoOrder(2)">
<view class="iconfont icon-fukuantongzhi"></view>
<view class="text">待收货</view>
</view>
<view class="order-box">
<view class="iconfont icon-shouhuodizhixiawu"></view>
<view class="text">退款/退货</view>
</view>
</view>
//跳转到订单查询页
gotoOrder(num) {
uni.navigateTo({
url: '../../subpkg2/order/order?num=' + num
})
}
2.退出登录
<view class="exit" @tap="logout">
<view class="text">退出登录</view>
<view class="more">></view>
</view>
//退出登录
async logout() {
const [err, succ] = await uni.showModal({
title: '提示',
content: '确定退出登录吗?'
});
// console.log(res);
//点击确认后,清除缓存的token、address、userinfo
if (succ && succ.confirm) {
uni.removeStorageSync('token');
uni.removeStorageSync('address');
uni.removeStorageSync('userinfo');
uni.reLaunch({
url: '../../subpkg2/login/login'
})
}
我的页面效果如下:

十一、学习心得
uniapp具有许多特点和优势,使其成为移动应用开发的理想选择。首先,它具有跨平台的能力,可以同时开发多个平台的应用,极大地提高了开发效率。其次,uniapp使用Vue.js作为基础,可以充分利用Vue的生态系统和社区资源。还有,uniapp支持原生插件的集成,可以方便地使用设备功能和第三方服务。此外,uniapp还提供了丰富的组件和模板,可以快速构建复杂的界面和交互。
学习uniapp的过程中,我感受到了它的便利和强大。它是一个基于Vue.js的跨平台开发框架,可以同时开发iOS、Android、H5等多个平台的应用。通过uniapp,我可以使用熟悉的Vue语法来开发移动应用,节省了学习不同平台特定技术的时间和精力。
十二、总结
经过这一个学期对Uni-app的学习,我对Uni-app有了更深入、全面的认识。同时,Uni-app的发展也是非常迅速的,未来它会有更加广泛的应用前景。uniapp是一框强大的框架,在学习的过程中受到了很多的启发,希望我这篇文章能对一些有需要的人获得帮助,在此我也给大家分享一些在学习中受到的启发,学习uniapp需要多多练习,通过多次的练习来不断提高自己的技术水平,在学习的过程中也要多思考问题,找到适合自己的学习方式,来帮助自己更加高效的学习。在未来的学习和实践中,我相信我可以更加深入地了解Uni-app。