商城小程序

项目介绍

1、商品首页

 2、分类页面

 3、商品列表

 

 4、商品详情

 

 5、购物车

6、支付页面

7、授权页面

 

 8、订单页面

9、搜索页面

 

10、个人中心

 11、收藏页面

12、意见反馈

 

 

首先我们先在app.json配置文件中把以上的页面都添加好,整个项目的颜色是使用的蕃茄红,所以在app.wxss中也先进行一个颜色配置。

此时app.wxss的代码

page{
  --themeColor:#e23b46;
}
image{
  width:100%;
}

app.json的代码

{
  "pages": [
    "pages/index/index",

    "pages/goods_list/index",
    
    "pages/goods_detail/index",
    
    "pages/cart/index",
    
    "pages/collect/index",
    
    "pages/order/index",
    
    "pages/search/index",
    
    "pages/user/index",
    
    "pages/feedback/index",
    
    "pages/login/index",
    
    "pages/auth/index",
    
    "pages/pay/index",

    "pages/category/index"
   
  ],
  "window": {
    "backgroundTextStyle": "dark",
    "navigationBarBackgroundColor": "#eb4450",
    "navigationBarTitleText": "Weixin",
    "navigationBarTextStyle": "white"
  },
  "tabBar": {
    "list": [{
      "pagePath": "pages/index/index",
      "text": "首页",
      "iconPath": "icons/home.jpg",
      "selectedIconPath": "icons/home1.jpg"
    },
   { "pagePath": "pages/category/index",
    "text": "分类",
    "iconPath": "icons/category.jpg",
    "selectedIconPath": "icons/category1.jpg"
  },
 { "pagePath": "pages/cart/index",
  "text": "购物车",
  "iconPath": "icons/cart.jpg",
  "selectedIconPath": "icons/cart1.jpg"
},
{"pagePath": "pages/user/index",
"text": "我的",
"iconPath": "icons/my.jpg",
"selectedIconPath": "icons/my1.jpg"
}
  ]
  
  },
  "style": "v2",
  "sitemapLocation": "sitemap.json"
}

tips:

"style": "v2"表示启用新版的组件样式。

使用v2版本的组件样式默认情况和v1不同,可以先清除组件默认样式,使它与v1版本相同。

v2版本button默认display: block;要设置宽高可更改为display: inline-block;背景色可正常设置。

关键字sitemapLocation的作用主要是设置是否允许微信能否搜索小程序内的页面

官方说明:

微信现已开放小程序内搜索,开发者可以通过 sitemap.json 配置,或者管理后台页面收录开关来配置其小程序页面是否允许微信索引。当开发者允许微信索引时,微信会通过爬虫的形式,为小程序的页面内容建立索引。当用户的搜索词条触发该索引时,小程序的页面将可能展示在搜索结果中。 爬虫访问小程序内页面时,会携带特定的 user-agent:mpcrawler 及场景值:1129。需要注意的是,若小程序爬虫发现的页面数据和真实用户的呈现不一致,那么该页面将不会进入索引中。

# sitemap 配置

小程序根目录下的 sitemap.json 文件用于配置小程序及其页面是否允许被微信索引,文件内容为一个 JSON 对象,如果没有 sitemap.json ,则默认为所有页面都允许被索引;
 

3、引入字体 图标

1、打开阿里巴巴字体图标库(网址:https://www.iconfont.cn/ )使用GitHub账号登陆

把想要的图标都添加到购物车里

      点击图标新建项目

 font class是指通过类的方式来使用这些图标,需要把他们引入到项目去

选择查看在线链接,链接如下图所示,然后把这个代码复制到网页里打开,出现了许多代码

然后把所有的代码都复制

 

然后回到项目打开styles文件夹目录,新增一个文件iconfont.wxss,把代码都放里面

然后在app.wxss(全局样式文件)中导入

@import "./styles/iconfont.wxss";

然后您index.wxml中测试

<view>首页

<text class="iconfont icon-shoucang1></text>

</view>

显示结果


 1、实现首页功能

首页分为四大块内容

1、搜索框

您看搜索框有好几个页面都需要用到,为了方便复用,所以这里我们使用组件的形式来写搜索框

step1新建一个名为components的文件夹,然后右键选择新建一个component,取名为SearchInput

 SearchInput.wxml

一点击搜索框,就会跳转到搜索页面去。

<view class="a">

 <navigator url="/pages/search/index" open-type="navigate" >

搜索 </navigator>

</view>

tips:

navigator的open-type属性 可选值 'navigate'、'redirect'、'switchTab',对应于wx.navigateTo、wx.redirectTo、wx.switchTab的功能

open-type="navigate"等价于API的 wx.navigateTo 而wx.navigateTo的url是需要跳转的应用内非 tabBar 的页面的路径

open-type="redirect"等价于API的 wx.redirectTo 而wx.redirectTo的url是需要跳转的应用内非 tabBar 的页面的路径

open-type="switchTab"等价于API的 wx.switchTab而wx.switchTab的url是需要跳转到 tabBar 页面,并关闭其他所有非 tabBar 页面

最后一个switchTab事件触发以后 把前面的页面都关闭了

获取用户权限open-type="getUserInfo"

<button open-type="getUserInfo" @getuserinfo="bindGetUserInfo" @click="getUserInfo1">获取权限</button>

SearchInput.wxss代码:

.a {

  height: 100rpx;
  
  padding: 5rpx;
  
  background-color: #eb4450;
  
  }
  
  .a navigator {
  
  height: 40%;
  
  display: flex;
  
  justify-content: center;
  
  align-items: center;
  
  background-color: #fff;
  
  border-radius: 15rpx;
  
  color: #666;
  
  }

此时index/wxml代码:

<view class="pyg.index">
<!--搜索框 开始-->
<SearchInput>  </SearchInput>
<!--搜索框 结束-->

</view>

在index.json中引入搜索框组件

{
  "component": true,
  "usingComponents": {
    "SearchInput":"../../components/SearchInput/SearchInput"
  },
  "navigationBarTitleText":"优购首页"
}

2、轮播图、导航栏、楼层

step1 获取轮播图数据

首先我们先新建一个文件夹request,建立index.js文件

其中代码如下

export const request=(params)=>{
  //定义公共的URL
  const baseUrl="https://api-hmugo-web.itheima.net/api/public/v1"
  return new Promise((resolve,reject)=>{
    wx.request({
      ...params,
      url:baseUrl+params.url,
      success:(result)=>{
        resolve(result);
      },
      fail:(err)=>{
        reject(err);
      }
    });
  })
}

index.js代码如下:

//引入用来发送请求的方法,一定要把路径补全
import{request}from "../../request/index.js";
Page({
data:{
//轮播图数组
swiperList:[],
//导航数组
catesList:[],
//分类导航数组
floorList:[]
},
//这是页面开始加载时候就会触发的一个事件
onLoad:function(options){
this.getSwiperList();
this.getCateList();
this.getFloorList();
},
//获取轮播图数据方法
getSwiperList(){
  request({url:"/home/swiperdata"})

  .then(result=>{
  
  this.setData({
  
  swiperList:result.data.message
  
  })
  
  })
},

//获取分类导航数据方法
getCateList(){
  request({url:"/home/catitems"})

  .then(result=>{
  
  this.setData({
  
  catesList:result.data.message,
  
  })
  
  })
},

//获取分类导航楼层数据方法
getFloorList(){
  request({url:"/home/floordata"})

  .then(result=>{
  
  this.setData({
  
  floorList:result.data.message
  
  })
  
  })
},
})

此时index.wxml代码如下:

<view class="pyg.index">
  <!--搜索框 开始-->
  <SearchInput> </SearchInput>
  <!--搜索框 结束-->

  <!--轮播图 开始-->
<view class="index_swiper">
  <!--
1、swiper标签存在默认的宽度和高度100%*150px
2、image标签也存在默认的宽度和高度320px*240px
(所以如果您不在wxss里写自适应的代码,切换到其他机型图片还是会像那个样子
3、设计图片和轮播图
1、原图的宽高 750*340
让图片的高度自适应 宽度等于100%
4、介绍一个图片标签
mode属性 渲染模式
widthFix 让图片的标签宽高和图片标签的内容的宽高都等比例的发生变化
  -->
<swiper autoplay indicator-dots circular>
<swiper-item
wx:for="{{swiperList}}"
wx:key="goods_id">
<navigator>
  <image mode="widthFix" src="{{item.image_src}}"></image>
</navigator>

</swiper-item>
</swiper>


 </view>


  <!--轮播图 结束-->
 <!--导航 开始-->
 <view class="index_cate">
  <navigator
  wx:for="{{catesList}}"
  wx:key="name"
  
  >
  <image mode="widthFix" src="{{item.image_src}}">
    </image>
    </navigator>
  
 </view>


  <!--导航 结束-->

  <!--页面楼层 开始-->
  <view class="index_floor">
    <view class="floor_group" wx:for="{{floorList}}" wx:for-item="item1" wx:for-index="index1" wx:key="floor_title">
      <!-- 标题 -->
      <view class="floor_title">
        <image src="{{item1.floor_title.image_src}}" mode="widthFix"></image>
      </view>
      <!-- 内容 -->
      <view class="floor_list">
        <navigator wx:for="{{item1.product_list}}" wx:for-item="item2" wx:for-index="index2" wx:key="name">
          <image src="{{item2.image_src}}" mode="{{index2===0?'widthFix':'scaleToFill'}}"></image>
        </navigator>
      </view>
    </view>
  </view>

  <!--页面楼层 结束-->
</view>

 index.wxss代码:

.index_swiper swiper {
  width: 750rpx;
  height: 340rpx;
}
.index_swiper swiper image {
  width: 100%;
}
.index_cate {
  display: flex;
}
.index_cate navigator {
  padding: 20rpx;
  flex: 1;
}
.index_cate navigator image {
  width: 100%;
}
.index_floor .floor_group .floor_title {
  padding: 10rpx 0;
}
.index_floor .floor_group .floor_title image {
  width: 100%;
}
.index_floor .floor_group .floor_list navigator {
  float: left;
  width: 30%;
}
.index_floor .floor_group .floor_list navigator:nth-last-child(-n+4) {
  height: 24.52711207vw;
  border-left: 10rpx solid #fff;
}
.index_floor .floor_group .floor_list navigator:nth-child(2),
.index_floor .floor_group .floor_list navigator:nth-child(3) {
  border-bottom: 10rpx solid #fff;
}
.index_floor .floor_group .floor_list navigator image {
  width: 100%;
  height: 100%;
}

此时首页效果如图 

 

 2、分类页面(category)

 

首先我们先打开category的index.json文件,添加搜索框组件

{
  
  "usingComponents": {
    "SearchInput":"../../components/SearchInput/SearchInput"
  },
  "navigationBarTitleText":"商品分类"
}

首先我们先看后台封装的数据,分为左侧菜单和右侧菜单

 

 

 1、获取接口数据

// pages/category/index.js
import{request}from "../../request/index.js";
Page({

  /**
   * 页面的初始数据
   */
  data: {
//左侧的菜单数据
leftMenuList:[],
//右侧的商品数据
rightContent:[],

  },
  //接口的返回数据
  Cates:[],

  /**
   * 生命周期函数--监听页面加载
   */
  onLoad: function (options) {

this.getCates();
  },


  //获取分类数据

/*
request成功以后,触发then方法,里面返回值res
*/

  getCates(){
    request({
      url:"/categories"
    })
    .then(res=>{
     this.Cates=res.data.message;
     /*
map是遍历数组的意思
*/

     //构造左侧的大菜单数据
     let leftMenuList=this.Cates.map(v=>v.cat_name);
     //构造右侧的商品数据
     let rightContent=this.Cates[0].children;
     this.setData({
       leftMenuList,
       rightContent
     })
    })
  }

 
})

 此时我们写category的 index.wxml代码

您能发现左右两侧菜单都能进行滚动,在小程序中提供了一个很好用的组件来实现滚动效果 

<view class="cates">
<SearchInput></SearchInput>
<view class="cates_container">
<!--左侧菜单 scroll-y="true"是允许纵向滚动的意思 后面的true可省略不写-->
<scroll-view scroll-y="true" class="left_menu">


</scroll-view>

<!--右侧商品-->
<scroll-view scroll-top="{{scrollTop}}" scroll-y="true" class="right_content">

</scroll-view>
</view>

</view>

 这里的思路是把左右两个菜单各看成一个容器盒子

  category的index.wxml

<view class="cates">
<SearchInput></SearchInput>
<view class="cates_container">
<!--左侧菜单 scroll-y="true"是允许纵向滚动的意思 后面的true可省略不写-->
<scroll-view scroll-y="true" class="left_menu">
<view
class="menu_item"
wx:for="{{leftMenuList}}"
wx:key="*this"
>
{{item}}
</view>


</scroll-view>

<!--右侧商品 goods_group是个大容器,里面分为两个容器,一个标题,一个内容-->
<scroll-view scroll-top="{{scrollTop}}" scroll-y="true" class="right_content">
<!--因为右侧菜单数据需要做双层循环,所以这里需要用wx:for-index|item 因为在右边菜单标题数据中找不到唯一的属性值,所以这里加不加key值都可以 -->
<view class="goods_group"
wx:for="{{rightContent}}"
wx:for-item="item1"
wx:for-index="index1"
>
<!--右侧标题部分 因为这里的cat_name具有唯一性,所以使用这个属性名 且效果图是有两个斜杠-->
<view class="goods_title">
<text class="delimiter">/</text>
<text class="title">{{item1.cat_name}}</text>
<text class="delimiter">/</text>
</view>
<!--每一款商品都是一个超链接,所以这里要用到navigator标签-->
<view class="goods_list">
<navigator 
wx:for="{{item1.children}}"
wx:for-index="index2"
wx:for-item="item2"
wx:key="cat_id"
>
<!--您看效果图此时应该要有图片了,图片的属性关键词就是cat_icon-->

<image mode="widthFix" src="{{item2.cat_icon}}"></image>
<!--图片下的文字,其关键词是cat_name-->
<view class="goods_name">{{item2.cat_name}}</view>

</navigator>

</view>
</view>
</scroll-view>
</view>

</view>

 category的 index.wxss

page {
  height: 100%;
}
.cates {
  height: 100%;
}
.cates_container {
  height: calc(100vh - 90rpx);
  display: flex;
}
.cates_container .left_menu {
  flex: 2;
 
}
.cates_container .left_menu .active{
  color:#e23b46;
  border-left:5rpx solid currentColor;
}
.cates .cates_container .left_menu .menu_item{
height:80rpx;
display: flex;
justify-content: center;
align-items: center;
font-size:30rpx;
}
.cates_container .right_content {
  flex:5;
}

.cates_container .right_content .goods_group .goods_list{
  display:flex;
  flex-wrap:wrap;
}
.cates_container .right_content .goods_group .goods_title  {
  height:80rpx;
  display:flex;
  justify-content:center;
  align-items:center;
}
.cates_container .right_content .goods_group .goods_title .delimiter {
  color:#ccc;
  padding:0 10rpx;
}
.cates_container .right_content .goods_group .goods_list navigator{
  width:33.33%;
  text-align:center;
}
.cates_container .right_content .goods_group .goods_list navigator image{
  width:50%;
  text-align:center;
}

 p62 点击菜单切换商品内容 

实现点击左侧菜单能有点击效果

首先在index.wxss中写

.cates_container .left_menu .active{
  color:#e23b46;
  border-left:5rpx solid currentColor;
}

然后看左边菜单栏,给谁添加这个类呢,得做个判断,我把左侧菜单的值变成一个索引

然后在category index.js中的data中添加被点击的左侧的菜单

// pages/category/index.js
import{request}from "../../request/index.js";
Page({

  /**
   * 页面的初始数据
   */
  data: {
//左侧的菜单数据
leftMenuList:[],
//右侧的商品数据
rightContent:[],
//被点击的左侧的菜单索引
currentIndex:0

  },
  //接口的返回数据
  Cates:[],

  /**
   * 生命周期函数--监听页面加载
   */
  onLoad: function (options) {


  },


  //获取分类数据

/*
request成功以后,触发then方法,里面返回值res
*/

  getCates(){
    request({
      url:"/categories"
    })
    .then(res=>{
     this.Cates=res.data.message;
     /*
map是遍历数组的意思
*/

     //构造左侧的大菜单数据
     let leftMenuList=this.Cates.map(v=>v.cat_name);
     //构造右侧的商品数据
     let rightContent=this.Cates[0].children;
     this.setData({
       leftMenuList,
       rightContent
     })
    })
  }

 
})

  并且在category index.wxml中的左侧菜单一栏中添加判断,并且给左侧菜单添加一下点击事件

<!--左侧菜单-->
<scroll-view scroll-y="true" class="left_menu">
<!--当index等于此时是否在激活的左侧菜单索引的状态中,是的话变成active的样式,否的话就是空,没有任何变化-->
<view 
class="menu_item {{index===currentIndex?'active':''}}"

wx:for="{{leftMenuList}}"
wx:key="*this"
//给左侧菜单绑定一个点击事件
bindtap="handleItemTap"
//点击的时候还需要传递一些参数过去
data-index="{{index}}"

>
{{item}}


</view>

</scroll-view>

 然后在category的index.js中添加左侧菜单的点击事件


  //左侧菜单的点击事件
  handleItemTap(e){
   /*
   1 获取被点击的标题身上的索引
   2 给data中的currentIndex赋值
   */
  const{index}=e.currentTarget.dataset;
/*
此时右侧原本的Cates[0],应该变成Cates[index]随机左侧点击的数据变化而变化
根据不同的索引来渲染右侧的商品内容
*/
 let rightContent=this.Cates[index].children;
/*
这个this.setData是赋值操作,把左侧拿到的数据进行一个能渲染的代码
*/
  this.setData({
    currentIndex:index
/*
这时也要把右侧的数据扔到这里面来
*/
     rightContent
  })

 
  }

此时左侧菜单已经可以出现激活选中的效果,这时右侧内容要随着左边点击的数据变化而变化

因为每次您在右侧商品栏下滑时候,然后左侧重新点一个分类,虽然会切换内容,可是商品也是从上次的位置显示,没有重新置顶显示商品,这个用户体验就不好。 

 

在category index.wxml中,此处添加scroll-top里写一个变量,而不能写死一个数值0

<!--右侧商品-->
<scroll-view scroll-top="{{scrollTop}}" scroll-y="true" class="right_content">

 然后看到index.js中的data里添加一条

data: {

//左侧的菜单数据

leftMenuList:[],

//右侧的商品数据

rightContent:[],

//被点击的左侧的菜单索引

currentIndex:0,

//右侧内容的滚动条距离顶部的距离,一开始先等于0

scrollTop:0

},

然后在index.js中的左侧菜单点击事件中添加


  //左侧菜单的点击事件
  handleItemTap(e){
   /*
   1 获取被点击的标题身上的索引
   2 给data中的currentIndex赋值
   */
  const{index}=e.currentTarget.dataset;
/*
此时右侧原本的Cates[0],应该变成Cates[index]随机左侧点击的数据变化而变化
根据不同的索引来渲染右侧的商品内容
*/
 let rightContent=this.Cates[index].children;
/*
这个this.setData是赋值操作,把左侧拿到的数据进行一个能渲染的代码
*/
  this.setData({
    currentIndex:index,
/*
这时也要把右侧的数据扔到这里面来
*/
     rightContent,
/*设置右侧内容的scroll-view标签的距离顶部的距离
*/
    scrollTop:0
  })

 
  }

此时就已经完善了这个bug

因为这个页面的数据量太大,所以这里我们使用缓存技术 

// pages/category/index.js
import{request}from "../../request/index.js";
Page({

  /**
   * 页面的初始数据
   */
  data: {
//左侧的菜单数据
leftMenuList:[],
//右侧的商品数据
rightContent:[],
//被点击的左侧的菜单索引
currentIndex:0

  },
  //接口的返回数据
  Cates:[],

  /**
   * 生命周期函数--监听页面加载
   */
  onLoad: function (options) {
/*
1、先判断以下本地存储中没有旧的数据
{time:Date.now(),data:[...]}
2、没有旧数据,直接发送新请求
3、有旧数据且该数据没有过期,就使用本地存储中的旧数据即可
小程序也有本地存储技术
*/
//1、获取本地存储中的数据
const Cates=wx.getStorageSync("cates");
//2、判断
if(!Cates){
  //不存在 发送请求获取数据
  this.getCates();
}else{
  //有旧数据,定义过去事件为10s 1000是毫秒 *10 就是10秒的意思
  if(Date.now()-Cates.time>1000*10){
    //重新发送请求
    this.getCates();
  }else{
    //可以使用旧数据
    this.Cates=Cates.data;
     //构造左侧的大菜单数据
     let leftMenuList=this.Cates.map(v=>v.cat_name);
     //构造右侧的商品数据
     let rightContent=this.Cates[0].children;
     this.setData({
       leftMenuList,
       rightContent
     })

  }
}
//没有旧数据,发送请求获取数据
this.getCates();

  },


  //获取分类数据

/*
request成功以后,触发then方法,里面返回值res
*/

  getCates(){
    request({
      url:"/categories"
    })
    .then(res=>{
     this.Cates=res.data.message;

   //把接口的数据存入到本地存储中
     wx.setStorageSync("cates",{time:Date.now(),data:this.Cates});


     /*
map是遍历数组的意思
*/

     //构造左侧的大菜单数据
     let leftMenuList=this.Cates.map(v=>v.cat_name);
     //构造右侧的商品数据
     let rightContent=this.Cates[0].children;
     this.setData({
       leftMenuList,
       rightContent
     })
    })
  }

 
})

优化 接口路径

您可以看到上图中的接口链接,代码非常长,您想简化代码

您还可以去看pages/index.js的链接,我们都具有公共相同的部分

您在pages/request/index.js中优化代码如下

export const request=(params)=>{
  //定义公共的URL
  const baseUrl="https://api-hmugo-web.itheima.net/api/public/v1"
  return new Promise((resolve,reject)=>{
    wx.request({
      ...params,
      url:baseUrl+params.url,
      success:(result)=>{
        resolve(result);
      },
      fail:(err)=>{
        reject(err);
      }
    });
  })
}

此时,举例子我们还是在分类页面,其URL只需要写成

request({

url:"/categories"

})

如果不这样简化写,还会报错。

例如此时首页的js代码没有简化书写,页面出不来。去掉那些公共部分即可。

代码优化:简化返回值和使用es7的async语法

es7的async号称是解决回调的最终方案

商品列表页面(goods_list)

 首先,先去category页面内的index.wxml右侧菜单的内容里添加一个URL进行一个页面跳转

然后这个跳转的key值是cat_id,根据该值的不同,所跳转的商品列表也不同。

这里URL中使用到的问号是传参的意思

<view class="goods_list">
<navigator
wx:for="{{item1.children}}"
wx:for-index="index2"
wx:for-item="item2"
wx:key="cat_id"
url="/pages/goods_list/index?cid={{item2.cat_id}}"

>
<image mode="widthFix" src="{{item2.cat_icon}}"></image>
<view class="goods_name">{{item2.cat_name}}</view>
</navigator>

您观察到搜索框下面有tab标签

我们首先我们在components文件夹中,添加文件夹Tabs,然后点击右键新建component

在index.js中

// pages/goods_list/index.js
Page({

  /**
   * 页面的初始数据
   */
  data: {
    tabs:[
      {
      id:0,
      value:"综合",
      isActive:true
      },
      {
        id:1,
        value:"销量",
        isActive:false
        },
        {
          id:2,
          value:"价格",
          isActive:false
          }
    ]

  },

  /**
   * 生命周期函数--监听页面加载
这里的console.log(options)是获取cat_id的意思
   */
  onLoad: function (options) {
console.log(options);
  },
  //标题点击事件
  handleTabsItemChange(e){
    // 获取被点击的标题索引
    const{index}=e.detail;
    // 修改源数据
    let{tabs}=this.data;
    tabs.forEach((v,i)=>i===index?v.isActive=true:v.isActive=false);
    // 赋值到data中
    this.setData({
      tabs
    })
  }
})

然后打开index.wxml中写 放一个组件名,属性值也是tabs

<SearchInput></SearchInput>
<Tabs tabs="{{tabs}}"><Tabs>

剩下的代码在tabs组件中去编写,回到组件Tabs文件夹中

Tabs.js

// components/Tabs/Tabs.js
Component({
  /**
   * 组件的属性列表
在properties中存放要接受副元素的数据
tabs属性名,这是一个对象,type数据类型
默认值是空数组value
   */
  properties: {
tabs:{
  type:Array,
  value:[]
}
  },

  /**
   * 组件的初始数据
   */
  data: {

  },

  /**
   * 组件的方法列表
在methods中定义点击事件
   */
  methods: {
//点击事件
handleItemTap(e){
//获取点击的索引 
const {index}=e.currentTarget.dataset;
//触发父组件中的事件 事件名是自己取的 属性名属性值都是index
this.triggerEvent("tabsItemChange",{index});


}
  }
})

Tabs.wxml

<view  class="tabs">
//分为标题部分
<view class="tabs_title">
//添加一个view容器,开始循环 还要给这个添加一个激活选中的代码
<view  
wx:for="{{tabs}}"
wx:key="id"
class="title_item {{item.isActive?'active':''}}"
//给每一个小标签都绑定一个点击事件
bindtap="handleItemTap"
//传递一个被点击的索引
data-index="{{index}}">
{{item.value}}
</view>


</view>
//还有一部分是内容部分
<view class="tabs_content">
<slot></slot>

</view>




</view>

然后在goods_list中的index.json 

{
  "usingComponents": { 
    "SearchInput":"../../components/SearchInput/SearchInput",
  "Tabs":"../../components/Tabs/Tabs"
},
  "navigationBarTitleText":"商品列表"
}

然后您再分析这个最终的效果图,左侧放的是图片,右侧放的是商品的名称,商品名称下面是商品价格

     商品列表的URL

      https://api-hmugo-web.itheima.net/api/public/v1/goods/search

这是获取到的数据

 

 

 goods_list的index.wxml代码


<SearchInput></SearchInput>
<Tabs tabs="{{tabs}}" bindtabsItemChange="handleTabsItemChange">
<block wx:if="{{tabs[0].isActive}}">
<view class="first_tab">
<!--点击图片跳转到商品详情页面 这里面分两个容器结构-->
<navigator class="goods_item"
wx:for="{{goodsList}}"
wx:key="goods_id"

>
<!--左侧 图片容器 wrap是容器的意思 -->
<view class="goods_img_wrap">
<!--因为有时候有的图片是空值,所以这里要加一个判断 ?前面,有值就使用值,无值就使用这个本地无图片样式的图片-->
  <image mode="widthFix" src="{{item.goods_small_logo?item.goods_small_logo:'https://ww1.sinaimg.cn/large/007rAy9hgy1g24by9t530j30i20i2glm.jpg'}}"></image>
</view>

<!--右侧 商品容器 右侧分为商品信息和价格两个容器 ps:看不懂goods_name样式代码的去百度css默认不换行样式-->
<view class="goods_info_wrap">

  <view class="goods_name">{{item.goods_name}}</view>
<view class="goods_price">¥{{item.goods_price}}</view>

</view>

</navigator>

</view>

</block>
<block wx:if="{{tabs[1].isActive}}">1</block>
<block wx:if="{{tabs[2].isActive}}">2</block>

</Tabs>

index.json

{
  "usingComponents": { 
    "SearchInput":"../../components/SearchInput/SearchInput",
  "Tabs":"../../components/Tabs/Tabs"
},
  "navigationBarTitleText":"商品列表",
  "enablePullDownRefresh":true,
  "backgroundTextStyle":"dark"
}

inde

// pages/goods_list/index.js
// pages/category/index.js


import{request}from "../../request/index.js";
Page({
  /**
   * 页面的初始数据
   */
  data: {
    tabs:[
      {
      id:0,
      value:"综合",
      isActive:true
      },
      {
        id:1,
        value:"销量",
        isActive:false
        },
        {
          id:2,
          value:"价格",
          isActive:false
          }
        
    ],
      //再来定义一个数据
      goodsList:[]

  },
//URL传参 前两个参数名给个空值 pagesize一次请求10条数据
QueryParams:{
query:"",
cid:"",
pagenum:1,
pagesize:10
},

//把总页数变成一个全局参数
totalPages:1,


  /**
   * 生命周期函数--监听页面加载,在onload里帮助上面的URL代码进行传参
   */
  onLoad: function (options) {
this.QueryParams.cid=options.cid;
this.getGoodsList();
/*
添加请求数据时候显示加载中的提示,这个在官方文档的交互中能看到相应的关键词
5000是五秒的意思,每一次有请求都手动写一次代码太繁琐了,把这个写到全局请求代码中去
*/
//wx.showLoading({
  //title:"加载中",
// })
// setTimeout(function(){
  //wx.hideLoading()
//},5000)
  },

  //发送异步请求 获取商品列表数据 发送请求的方式使用es7的,这里使用async是代表异步方法
 async getGoodsList(){
 const res=await request({url:"/goods/search",data:this.QueryParams});
 //获取总条数 因为这个总条数需要在别的方法使用,所以要把它变成一个全剧参数
 const total=res.data.message.total;
 //计算总页数
 this.totalPages=Math.ceil(total/this.QueryParams.pagesize);
 //console.log(this.totalPages);
 //goods_list属性名 res.goods属性值
 //首先先把旧数据用...解构出来,然后把新数据...解构出来,然后把getGoodsList()放到onReachBottom方法中进行重新调用
 this.setData({
   //拼接的数组 
   goodsList:[...this.data.goodsList,...res.data.message.goods]
  
 })

 //关闭下拉刷新的窗口 如果没有调用下拉刷新窗口,也不会报错
 wx.stopPullDownRefresh();
  },
  //标题点击事件 从子组件(tabs组件)传递过来的
  handleTabsItemChange(e){
    // 获取被点击的标题索引
    const{index}=e.detail;
    // 修改源数组 让它产生一个激活被选中的效果 v是循环项,i是索引等于大于
    let{tabs}=this.data;
    tabs.forEach((v,i)=>i===index?v.isActive=true:v.isActive=false);
    // 赋值到data中
    this.setData({
      tabs
    })
  },
  /*
现在我们来实现向下滑动实现加载下一页数据的动作
1、找到滚动条触底事件(去官方文档看)
2、判断是否还有下一页数据
step1 获取总页数(获取数据中的total就是总条数 根据总条数算一下总页数)
总页数的计算用一个数学方法天花板函数
总页数=Math.ceil(总条数/页容量pagesize)
     =Math.ceil(23/10)
这样算起来,第一页有10条数据,第二页有10条,第三页有三条
step2 获取到当前的页数(pagenum)
step3 判断一下 当前的页码是否大于或者等于总页数,如果为true,则不需要再加载下一页数据
3、如果没有下一页数据,弹出来一个提示
4、假如还有下一页数据,来加载下一页数据
1、当前页码加1
2、重新发送获取下一页请求
3、数据请求回来后,不要直接写this.setdata,而是要追加到原数组goodsList中
进行一个数据的拼接,而不是全部替换
*/
//页面上滑 滚动条触底事件
onReachBottom(){
// 判断还有没有下一页数据
//this.QueryParams.pagenum当前页码
//this.totalPages是总页数
if(this.QueryParams.pagenum>=this.totalPages){
  //没有下一页数据 
  //使用一个方法 这个方法是提示一会儿这个提示会自动隐藏
  wx.showToast({
    title: '没有下一页数据了'
  })
  
}else{
  //还有下一页数据 先把页码加一
  this.QueryParams.pagenum++;
  //调用获取数据方法
  this.getGoodsList();
}
},


/*需求二 下拉刷新页面
1、触发下拉刷新事件
需要在index.json文件中开启一个配置项目
添加enablePullRefresh属性,这是全局下拉刷新的意思,开启后上拉没有等待的小圆圈
还得添加“backgroundTextStyle":"dark"
找到触发下拉刷新的事件,并在里面添加逻辑
2、在这个方法中找到重置数据 数组 在触发事件后应该都变成空数组
3、 重置页码 设置为1
4、重新发送请求
5、数据刷新请求回来以后,需要手动的关闭等待效果
*/

//下拉刷新事件 这个事件存放在页面的生命周期函数中,在这里面重置数组,重置页码
onPullDownRefresh(){
//1、重置数组
this.setData(

  {
    goodsList:[]
  }
)
//2、重置页码
this.QueryParams.pagenum=1;
//3、发送请求
this.getGoodsList();
}

})

index.wxss


navigator{
display:flex;
border-bottom: 5rpx solid #ccc;
}
navigator .goods_img_wrap{
flex:2;
display:flex;
justify-content: center;
align-items: center;
}

navigator .goods_img_wrap image{
width:70%;
}
navigator .goods_info_wrap{
flex:3;
display: flex;
flex-direction:column;
justify-content:space-around;
}

navigator .goods_info_wrap .goods_name{
  display: -webkit-box;
  overflow: hidden;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
}
navigator .goods_info_wrap .goods_price{
color:rgb(238, 2, 34);
font-size: 32rpx;
}

这里用到es7新语法,详情里点击增强编译就好

request文件夹到index.js代码

  //同时发送异步代码的次数 ajax
  let ajaxTimes=0;

export const request=(params)=>{

  ajaxTimes++;
//显示加载中的效果 mask表示要不要显示一层蒙板
wx.showLoading({
  title: '加载中',
  mask:true
});


  
  //定义公共的URL
  const baseUrl="https://api-hmugo-web.itheima.net/api/public/v1"
  return new Promise((resolve,reject)=>{
    wx.request({
      ...params,
      url:baseUrl+params.url,
      success:(result)=>{
        resolve(result);
      },
      fail:(err)=>{
        reject(err);
      },
      //把第三个回调函数掉出来,这个函数不管成功或者失败都会触发都一个函数
      //关闭正在等待的图标
      complete:()=>{
        ajaxTimes--;
        if(ajaxTimes===0){
          //关闭正在等待的图标
          wx.hideLoading();
        }
  
 
      }
    });
  })
}

商品详情页面(goods_detail)

首先您得在goods_list页面写一个跳转代码,您看这个跳转需要有goods-id的key值

商品详情页面的效果图

首先我们先在onload函数中获取商品列表页面的点击参数

 

商品详情页面的URL是https://api-hmugo-web.itheima.net/api/public/v1/goods/detail

通过该地址直接通过文档访问是不行的,因为没有必填的参数,必须通过程序中的传入相关参数访问才可以的,必须要在后面加?goods_id=43986才有用

所以正确的URL是:https://api-hmugo-web.itheima.net/api/public/v1/goods/detail?goods_id=43986

这个goods_id的值是你点击不同的商品而成产生的id值,比如你点击三星电视,获取的id就是{goods_id: "25972"}

您再把数值换成26972,此时网页显示的详情列表就是三星电视的列表了。

它的返回值是一个对象模式

存储的时候不要写成数组

step开始发送请求获取数据

 

 您看效果图上面首页得来一个轮播图,轮播图数据就在pics里

step2 页面分析

 

 关于wxss的

padding:10rpx 0;

代表上下为10,左右为0

您想要一个容器离上下容器的距离和这中间的颜色

border-top:5rpx solid #dedede;

border-bottom:5rpx solid #dedede;

 您想要离左边容器有距离,用颜色表示距离

  border-left:1rpx solid  #000;

想要容器的框固定不被撑开,加上以下四行代码display...

.goods_name_row .goods_name{
flex:5;
 color:#000;
 font-size:35rpx;
 padding:0 10rpx;

 display:-webkit-box;
 overflow:hidden;
 -webkit-box-orient:vertical;
 -webkit-line-clamp:2;
}

 您拿到的这个数据非常巨大

 

 可是您使用到的属性值也不过仅仅五六个,如果全部就用的话加载会很慢,代码运行会很臃肿,所以去js文件的获取商品详情数据的方法的this.setData中手动添加需要用到的属性值

实现点击图片会放大的功能

 

<!--购物车在首页存在tabbar上,navigator不能直接跳,需要open设置-->

<navigator open-type="switchTab“ url="/pages/cart/index"

 goods_detail index.wxml代码

<!--首先先写轮播图数据 swiper-->
<view class="detail_swiper">
<!--给轮播图加上三个属性-->
<swiper
autoplay 
circular
indicator-dots

>
<!--对swiper-item进行循环,对这个循环要进行一个对象.方法的形式-->
<swiper-item 
wx:for="{{goodsObj.data.message.pics}}"
wx:key="pics_id"
bindtap="handlePreviewImage"
data-url="{{item.pics_mid}}"
>
 <!--这里选一个中间大小的图片URLpics_mid-->
 <image mode="widthFix" src="{{item.pics_mid}}">
 </image>
</swiper-item>


</swiper>

</view>

<!--价格-->
<view class="goods_price">
¥{{goodsObj.data.message.goods_price}}

</view>

<!--商品名称及收藏功能-->
<view class="goods_name_row">
<!--左边是商品名称-->
<view class="goods_name">{{goodsObj.data.message.goods_name}}</view>
<!--右边是收藏按钮-->
<view class="goods_collect">
<text class="iconfont icon-shoucang1"></text>
<view class="collect_text">收藏</view>

</view>
</view>


<!--图文详情-->
<view class="goods_info">
<!--标题-->
<view class="goods_info_title">图文详情</view>
<!--内容 要使用富文本,里面存在一个nodes属性-->
<view class="goods_info_content">
<rich-text nodes="{{goodsObj.data.message.goods_introduce}}"></rich-text>

</view>
</view>


<!--底部工具栏-->
<view class="btm_tool">

<view class="tool_item">
<view class="iconfont icon-kefu"></view>
<view>客服</view>
<button open-type="contact"></button>
</view>

<view class="tool_item">
<view class="iconfont icon-fenxiang"></view>
<view>分享</view>
<button open-type="share"></button>
</view>



<navigator open-type="switchTab" url="/pages/cart/index" class="tool_item">
    <view class="iconfont icon-qicheqianlian-"></view>
    <view>购物车</view>
    </navigator>


<!--使用小程序内置的本地存储来缓存购物车数据-->



<view class="tool_item btn_cart" bindtap="handleCartAdd">
加入购物车
</view>



<view class="tool_item btn_buy">
立即购买
</view>


</view>

index.js

// pages/goods_detail/index.js
import{request}from "../../request/index.js";
Page({
  /**
   * 页面的初始数据
   */
  data: {
    goodsObj:{}
  },
  //商品对象
  GoodsInfo:{},

  /**
   * 生命周期函数--监听页面加载
   * 首先我们先在onLoad里面添加商品列表页参数goods_id
   * 然后在onload中使用this调用getGoodsDetail方法就能在控制台看到获取的数据信息,然后我们把数据填充到data当中
   * 在data写一个goodObj对象,在数据拿到之后,给该对象赋值,写在getGoodsDetail中,this.setData
   * step2 放大预览功能
   * 思路;给轮播图绑定点击事件
   * 调用小程序对API previewImage
   给<swiper-item>标签添加bintap="handlePreviewImage"

   step3 点击加入购物车
   1、绑定点击事件
   2、获取缓存中的购物车数据 数组格式的数据,后期可以存储多个商品
   3、先判断 当前商品是否已经存在于购物车
    * 若已经存在商品,则添加其商品的数量(执行购物车数量++
   把购物车数组填充到缓存中
    *若这个商品是第一次添加,不存在于购物车的数组中,直接给购物车数组添加一个新元素即可
    带上一个购物数量属性num/nums都可以
   */
  onLoad: function (options) {
const{goods_id}=options;
this.getGoodsDetail(goods_id);
  },
  //获取商品详情数据 (goods_id是传递的参数),使用es7的方式获取
 async getGoodsDetail(goods_id){
//const res=await request({url:"/goods/detail",data:{goods_id}});
//干脆把这个变量名res页改成goodsObj
const goodsObj=await request({url:"/goods/detail",data:{goods_id}});

this.GoodsInfo=goodsObj;
//console.log(res);
this.setData({
  goodsObj
  
})


 },
//点击轮播图,放大预览,在data下建立商品对象GoodsInfo
 handlePreviewImage(e){
  //console.log("dayin");
  //先构造要预览的图片数组,可是在这里你无法拿到轮播图数据,所以需要去做全局声明
  //在data下声明一个全局对象GoodsInfo
  //等拿到商品详情数据后去给GoodsInfo赋值this.GoodsInfo=goodsObj;
  //然后在此方法内this.data中要使用goodsObj,而不是GoodsInfo,会报错
  const urls=this.data.goodsObj.data.message.pics.map(v=>v.pics_mid);
  //点击事件触发以后,需要接收传递过来的图片URL,回到wxml文件中的标签中自定义属性的方式来传参data-url
  const current=e.currentTarget.dataset.url;
  wx.previewImage({
    
    urls,
    current
   
  })
 },

 //点击 加入购物车 
 handleCartAdd(){
//1、获取缓存中的购物车 数组
//getStorageSync同步的方式 key的名称也叫cart
//let cart=wx.getStorageSync("key")
//因为第一次获取的时候是空字符串,所以需要用||转换一下数组[]格式
//经过转换以后可以确定该数组肯定是个数组格式
let cart=wx.getStorageSync("cart")||[];


//通过以下方式来判断商品对象是否存在于购物车数组中
//findIndex的作用是为真时返回-1
let index=cart.findIndex(v=>v.goods_id===this.GoodsInfo.goods_id);
if(index===-1){
//不存在 第一次添加
this.GoodsInfo.num=1;
cart.push(this.GoodsInfo);
}else{
  //已经存在于购物车,执行num++
cart[index].num++;
}

//5、把购物车重新添加回缓存中
wx.setStorageSync('cart', cart);

// 6、弹出提示
wx.showToast({
  title: '加入购物车成功',
icon:'success',
//把mask改成true,可以防止用户手抖疯狂点击加入
//该属性值打开后,只有过了五秒以后,用户才能继续点击
mask:true


});
 }


})

index.wxss

.detail_swiper swiper {
  height: 65vw;
  text-align: center;
}
.detail_swiper swiper image {
  width: 60%;
}

.goods_price{
padding:15rpx;
font-size:32rpx;
font-weight:600;
color:rgb(235, 13, 13)

}

.goods_name_row{
display:flex;
border-top:5rpx solid #dedede;
border-bottom:5rpx solid #dedede;
padding:10rpx 0;
}

.goods_name_row .goods_name{
flex:5;
 color:#000;
 font-size:35rpx;
 padding:0 10rpx;

 display:-webkit-box;
 overflow:hidden;
 -webkit-box-orient:vertical;
 -webkit-line-clamp:2;
}
.goods_name_row .goods_collect{
  flex:1;
  display:flex;
  flex-direction:column;
  justify-content: center;
  align-items:center;
  border-left:1rpx solid  #000;
}

.goods_name_row .goods_collect .iconfont{
 
}

.goods_name_row .goods_collect .collect_text{
 
}

.goods_info{

}

.goods_info .goods_info_title{
font-size:45rpx;
color:rgba(230, 9, 9, 0.89);
font-weight:600;
padding:20rpx;
}

.goods_info .goods_info_content{

}

.btm_tool{
border-top: 1px solid #ccc;
  position:fixed;
  left:0;
  
  bottom:0;
  width:100%;
  height:90rpx;
  background-color: #fff;
  display:flex;
}
.btm_tool .tool_item{
  flex:1;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  font-size:30rpx;
  position:relative;
}
  .btm_tool .tool_item button{
    position:absolute;
    top:0;
    left:0;
    width:100%;
    height:100%;
    opacity:0;
  }


.btm_tool .btn_cart{
  flex:2;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  background-color: #ffa500;
  color:#fff;
  font-size:30rpx;
  font-weight: 600;
}

.btm_tool .btn_buy{
  flex:2;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  background-color: #eb4450;
  color:#fff;
  font-size:30rpx;
  font-weight: 600;
}

index.json

{
  "usingComponents": {},
  "navigationBarTitleText":"商品详情"
}

购物车页面

 购物车页面index.js

/*
1、第一个功能:获取用户的收货地址
step1 绑定点击事件
step2 事件触发时候,调用小程序内置的API(获取用户的收货地址)
wx.chooseAddress
*/
Page({
//点击按钮 获取收获地址
handleChooseAddress(){
 //2、获取收获地址 无论是谁,调用这行代码时候都会调出这个张三的地址
 wx.chooseAddress({
   success: (result) => {
     console.log(result); 
   },
 })
}
})

 写到这里的时候编译运行,控制台有这个地址的信息

wx.getSetting(Object object)

查看获取用户当前的权限属性。

 wx.getSetting({
  success:(result)=>{
    console.log(result);//打印台输出authSetting里的scope值为true
    
  }

})

tips:undefined是未定义的意思

这个接口可以直接获取地址(系统默认地址张三),而不会有权限弹窗确定

//2、获取收获地址 调用这行代码时候都会调出这个张三的地址
wx.chooseAddress({
  success: (result) => {
     //console.log(result); 
   }
})

这个接口可以获取当前地址,会有权限弹窗提示,但仅有一次

wx.chooseLocation({
  success: (result) => {
     //console.log(result); 
   }
})

 此时此刻index.js代码

/*
1、第一个功能:获取用户的收货地址
step1 绑定点击事件
step2 事件触发时候,调用小程序内置的API(获取用户的收货地址)
wx.chooseAddress
获取用户对小程序对获取地址的「权限状态」(scope)
小程序现在更新了,已经不需要获取这个权限状态了
但还是照着教程写一遍,即使依旧没有弹窗提示要授予权限了

首先获取小程序授予获取地址对权限状态scope(使用const获取)
如果scope为true或者为undefined(未定义【从来没有调用过获取用户地址的API),则直接调用wx.chooseAddress方法
如果弹窗出现后,点击了取消,scope值为false,那需要引导用户再打开授权设置的页面
wx.openSetting,当用户重新获取地址权限后,再调用wx.chooseAddress方法




step3 把获取到的地址存入到本地存储中
页面加载完毕,放在onLoad事件中,由于购物车页面被频繁使用
希望购物车页面每一次打开的时候都能初始化,使用onShow生命周期事件
1、首先获取本地存储中到地址数据
2、把数据设置给data中到一个变量
*/
Page({
  data:{
address:{}
  },
  onShow:function(){
    //1、获取缓存中的收货地址信息
const address=wx.getStorageInfoSync('address');
 //2 给data赋值
 this.setData({
   address
 })

  },
//点击按钮 获取收获地址
handleChooseAddress(){
 //首先获取权限状态
 wx.getSetting({
  success:(result)=>{
   // console.log(result);//打印台输出authSetting里的scope值为true
    //authSetting后面本应该写.scope.address,但这里这样写就太奇怪了
    //如果发现一些属性名很怪异的时候,就要用[" "]的形式来获取属性值
   const scopeAddress=result.authSetting["scope.address"];

   if(scopeAddress===true||scopeAddress===undefined){
     wx.chooseAddress({
       //为了使这个api里的参数名和getSetting中的参数名不重复,所以这里加个1区分
      // success: (result) => {},
      success: (result1) => {
        console.log(result1);
      }
     });
   }else{
     //用户以前拒绝过授予权限,应该先又到用户打开权限
wx.openSetting({
  //获取到数据后,success表示用户重新授予权限
 success:(result2)=>{
//直接可以调用收获地址代码 
wx.chooseAddress({
  //为了使这个api里的参数名和openSetting中的参数名不重复,所以这里加个3区分
 // success: (result) => {},
 success: (result3) => {
   console.log(result3);
 }
});
 }
})

   }
  }

})




}
})

 如果写openSetting这个方法

 wx.getSetting({
  success:(result)=>{
   // console.log(result);//打印台输出authSetting里的scope值为true
    //authSetting后面本应该写.scope.address,但这里这样写就太奇怪了
    //如果发现一些属性名很怪异的时候,就要用[" "]的形式来获取属性值
   const scopeAddress=result.authSetting["scope.address"];

   if(scopeAddress===true||scopeAddress===undefined){
        //用户以前拒绝过授予权限,应该先又到用户打开权限
wx.openSetting({
  //获取到数据后,success表示用户重新授予权限
 success:(result2)=>{
//直接可以调用收获地址代码 
wx.chooseAddress({
  //为了使这个api里的参数名和openSetting中的参数名不重复,所以这里加个3区分
 // success: (result) => {},
 success: (result3) => {
   console.log(result3);
 }
});
 }
})
   }
  }

})

点击获取收货地址按钮后

       

如果您还想加一个弹窗提示

wx.showModal({
  title: '提示',
  content: '这是一个模态弹窗',
  success (res) {
    if (res.confirm) {
      console.log('用户点击确定')
    } else if (res.cancel) {
      console.log('用户点击取消')
    }
  }
})

我们在utils中的asyncWx.js中添加了一些优化封装代码

//对cart中的js代码进行一个优化,对getSetting进行封装
//export导出getSetting这个函数
export const getSetting=()=>{
  //resolve成功的回调函数 reject失败的
  return new Promise((resolve,reject)=>{
    wx.getSetting({
      withSubscriptions: true,
      success: (result) => {
        resolve(result);
      },
      fail: (err) => {
        reject(err);
      }
    })
  })
 
}

export const chooseAddress=()=>{
  //resolve成功的回调函数 reject失败的
  return new Promise((resolve,reject)=>{
    wx.chooseAddress({
      withSubscriptions: true,
      success: (result) => {
        resolve(result);
      },
      fail: (err) => {
        reject(err);
      }
    })
  })
 
}

export const openSetting=()=>{
  //resolve成功的回调函数 reject失败的
  return new Promise((resolve,reject)=>{
    wx.openSetting({
      withSubscriptions: true,
      success: (result) => {
        resolve(result);
      },
      fail: (err) => {
        reject(err);
      }
    })
  })
 
}

//对cart中的js代码进行一个优化,对showModel进行封装
//export导出showModel这个函数

export const showModel=(content)=>{
  //resolve成功的回调函数 reject失败的
  return new Promise((resolve,reject)=>{
    wx.showModal({
      title:'提示',
      content:'您确定要删除吗?',
      success:(res)=>{
      resolve(res);
      },
      fail:(err)=>{
        reject(err);
      }
    })
  })
 
}


export const showToast=(ctitle)=>{
  //resolve成功的回调函数 reject失败的
  return new Promise((resolve,reject)=>{
    wx.showToast({
      title:'title',
      icon:'none',
      success:(res)=>{
      resolve(res);
      },
      fail:(err)=>{
        reject(err);
      }
    })
  })
 
}

在detail的js文件中添加了


 //点击 加入购物车 
 handleCartAdd(){
//1、获取缓存中的购物车 数组
//getStorageSync同步的方式 key的名称也叫cart
//let cart=wx.getStorageSync("key")
//因为第一次获取的时候是空字符串,所以需要用||转换一下数组[]格式
//经过转换以后可以确定该数组肯定是个数组格式
let cart=wx.getStorageSync("cart")||[];


//通过以下方式来判断商品对象是否存在于购物车数组中
//findIndex的作用是为真时返回-1
let index=cart.findIndex(v=>v.data.message.goods_id===this.GoodsInfo.data.message.goods_id);
if(index===-1){
//不存在 第一次添加
this.GoodsInfo.num=1;
//商品的选中状态
this.GoodsInfo.checked=true;
cart.push(this.GoodsInfo);
}else{
  //已经存在于购物车,执行num++
cart[index].num++;
}

//5、把购物车重新添加回缓存中
wx.setStorageSync('cart', cart);

// 6、弹出提示
wx.showToast({
  title: '加入购物车成功',
icon:'success',
//把mask改成true,可以防止用户手抖疯狂点击加入
//该属性值打开后,只有过了五秒以后,用户才能继续点击
mask:true


});
 }


})

cart.wxml

<!--收获地址容器-->
<!--使用if判断是否存在收货地址,随便验证一个属性名,有值则存在,!感叹号是取反的意思-->
<view class="revice_address_row">
<view class="address_btn" wx:if="{{!address.userName}}">
<!--primary是绿色 背景为plain白色
给button添加一个点击事件,然后回js文件中写事件
-->
<button bindtap="handleChooseAddress" type="primary" plain>获取收货地址</button>

</view>
<!--当收获地址存在时候 显示详细信息 这里的else和上面的if相对于-->
<view wx:else class="user_info_row">
<!--左边-->
<view class="user_info">
<view>{{address.userName}}</view>
<view>{{address.all}}</view>
</view>
<!--右边-->
<view class="user_phone">
  <view>{{address.telNumber}}</view>
</view>
</view>
</view>


<!--购物车内容-->
<view class="cart_content">
<!--购物车部分名称-->
<view class="cart_title">
购物车
</view>
<!--内容部分-->
<!--当cart数组长度不为0 显示商品信息-->
<view class="cart_main">
<block wx:if="{{cart.length!==0}}">

</block>
<block wx:else><image mode="widthFix" src="https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fhbimg.b0.upaiyun.com%2Fe1b1467beea0a9c7d6a56b32bac6d7e5dcd914f7c3e6-YTwUd6_fw658&refer=http%3A%2F%2Fhbimg.b0.upaiyun.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1631121598&t=5ae46738ff4f8cf63de9f4b048d40905"></image></block>
<!--内容部分有以下元素-->
<view class="cart_item"
wx:for="{{cart}}"
wx:key="goods_id"

>
<!--复选框-->
<view class="cart_chk_wrap">
<checkbox-group data-id="{{item.data.message.goods_id}}" bindchange="handeItemChange">
<checkbox checked="{{item.checked}}"></checkbox>

</checkbox-group>
</view>
<!--商品图片-->
<navigator class="cart_img_wrap">
<image mode="widthFix" src="{{item.data.message.goods_small_logo}}"></image>

</navigator>
<!--商品信息-->
<view class="cart_info_wrap">
<view class="goods_name">{{item.data.message.goods_name}}</view>
<view class="goods_price_wrap">
<view class="goods_price">${{item.data.message.goods_price}}</view>
<view class="cart_num_tool">
<view  bindtap="handleItemNumEdit" data-id="{{item.data.message.goods_id}}" data-operation="{{-1}}" class="num_edit">-</view>
<view class="goods_num">{{item.num}}</view>
<view bindtap="handleItemNumEdit" data-id="{{item.data.message.goods_id}}" data-operation="{{1}}"class="num_edit">+</view>
</view>

</view>
</view>


</view>

</view>

</view>





<!--底部工具栏-->
<view class="footer_tool">
<!--全选-->
<view class="all_chk_wrap">
<checkbox-group bindchange="handleItemAllCheck">
<checkbox checked="{{allChecked}}">
全选
</checkbox>
</checkbox-group>

</view>
<!--总价格-->

<view class="total_price_wrap">
<view class="total_price">
合计:<text class="total_price_text">${{totalPrice}}</text>
</view>

<view>包含运费</view>
</view>
<!--结算-->
<view class="order_pay_wrap" bindtap="handlePay">
结算{{totalNum}}
</view>
<!---->

</view>

cart.js

/*
1、第一个功能:获取用户的收货地址
step1 绑定点击事件
step2 事件触发时候,调用小程序内置的API(获取用户的收货地址)
wx.chooseAddress
获取用户对小程序对获取地址的「权限状态」(scope)
小程序现在更新了,已经不需要获取这个权限状态了
但还是照着教程写一遍,即使依旧没有弹窗提示要授予权限了

首先获取小程序授予获取地址对权限状态scope(使用const获取)
如果scope为true或者为undefined(未定义【从来没有调用过获取用户地址的API),则直接调用wx.chooseAddress方法
如果弹窗出现后,点击了取消,scope值为false,那需要引导用户再打开授权设置的页面
wx.openSetting,当用户重新获取地址权限后,再调用wx.chooseAddress方法




 把获取到的地址存入到本地存储中
step3 页面加载完毕,放在onLoad事件中,由于购物车页面被频繁使用
希望购物车页面每一次打开的时候都能初始化,使用onShow生命周期事件
1、首先获取本地存储中到地址数据
2、把数据设置给data中到一个变量

step4 onShow
0、回到了商品详情页面,第一次添加商品时候,手动添加属性
num=1
checked=true
1、您在商品详情页面点击「加入购物车」之后会获取到一个商品数据
2、把购物车数据填充到data中

5 总价格totalPrice和总数量totalNum
1、商品被选中
2、获取购物车数组
3、遍历
4、判断商品是否被选中
5、总价格+=商品单价*商品数量
6、总数量+=商品的数量
7、把计算后的价格和数量,设置回data即可


6 点击勾选功能,取消该商品,同时结算价格改变,打开后端URL该功能属性名为checked
1、给wxml中的checkbox-group绑定一个change事件
2、获取到被修改的商品对象(可以通过索引或者goods_id)
在wxml中的checkbox-group富文本中添加data-id
3、商品对象的选中状态 取反
4、重新填充回data以及缓存中
5、重新计算全选的价格(总价格 总数量—)


7 全选和反选allChecked
1 给全选复选框绑定一个事件 
2 获取data中的全选变量 allChecked
3 遍历购物车数组,让里面的商品选中状态随着allChecked改变
4 把购物车数组和allChecked重新设置回data,把
购物车重新设置回缓存中

*/
//在js中添加在utils里写的优化代码,把三个函数放在花括号内
import{getSetting,chooseAddress,openSetting,showModel,showToast} from "../../utils/asyncWx.js";

//2、页面加载完毕(使用onload/onShow事件)
//1、获取本地存储中的地址数据
//2、把数据设置给data中的一个变量
Page({
  data:{
address:{},
cart:[],
allChecked:false,
totalPrice:0,
totalNum:0
  },
  onShow:function(){
    //1、获取缓存中的收货地址信息
const address=wx.getStorageSync('address');
//1、获取商品详情页面的加入购物车的数据 这还可能是空值 加一个数组[]
const cart=wx.getStorageSync("cart")||[];
/*
//计算全选 cart是数组 .every是个方法
//every是一个数组方法,会遍历每一个函数,最后返回true
//v的意思是每一个循环项目
//做个优化⬇️const allChecked=cart.length?cart.every(v=>v.checked):false;
let allChecked=true;
//计算 首先声明变量 然后遍历数组
let totalPrice=0;
let totalNum=0;
cart.forEach(v=>{
  if(v.checked){
    totalPrice+=v.num*v.data.message.goods_price;
    totalNum+=v.num;
  }else{
    allChecked:false;
  }
})
//判断一下数组是否为空
allChecked=cart.length!=0?allChecked:false;

//2 给data赋值
 this.setData({
   address,
   cart,
   allChecked,
   totalPrice,
   totalNum
 })

  },
*/
this.setData({address});
this.setCart(cart);
  },
  /*
//点击按钮 获取收获地址
async handleChooseAddress(){
 //首先获取权限状态
 wx.getSetting({
  success:(result)=>{
   // console.log(result);//打印台输出authSetting里的scope值为true
    //authSetting后面本应该写.scope.address,但这里这样写就太奇怪了
    //如果发现一些属性名很怪异的时候,就要用[" "]的形式来获取属性值
   const scopeAddress=result.authSetting["scope.address"];

   if(scopeAddress===true||scopeAddress===undefined){
     wx.chooseAddress({
       //为了使这个api里的参数名和getSetting中的参数名不重复,所以这里加个1区分
      // success: (result) => {},
      success: (result1) => {
        console.log(result1);
      }
     });
   }else{
     //用户以前拒绝过授予权限,应该先又到用户打开权限
wx.openSetting({
  //获取到数据后,success表示用户重新授予权限
 success:(result2)=>{
//直接可以调用收获地址代码 
wx.chooseAddress({
  //为了使这个api里的参数名和openSetting中的参数名不重复,所以这里加个3区分
 // success: (result) => {},
 success: (result3) => {
   console.log(result3);
 }
});
 }
})

   }
  }

});
//优化后用es7语法写
//1 获取 权限状态
const res1=await getSetting();
const scopeAddress=res1.authSetting["scope.address"];
//判断 权限状态
/*if(scopeAddress===true||scopeAddress===undefined){
//调用获取收货地址的API
const res2=await chooseAddress();
console.log(res2);
}else{
  //3 先诱导用户打开授权页面
  await openSetting();
  //4 调用获取收货地址的api
  const res2=await chooseAddress();
  console.log(res2);
}
*/
//判断权限状态,只要scope为false执行openSetting,其他全部chooseAddress
/*
if(scopeAddress===false){
  await openSetting();
}
// 调用获取收货地址的API
const res2=await chooseAddress();
console.log(res2);
}
*/
//点击收货
async handleChooseAddress(){
  try{
//1 获取 权限状态
const res1=await getSetting();
const scopeAddress=res1.authSetting["scope.address"];
// 2 判断权限状态
if(scopeAddress===false){
  await openSetting();
}
//3 调用获取收货地址的API
//const res2=await chooseAddress(); const是声明常量,所以这里改了let
let address=await chooseAddress();
console.log(address);
//4 把获取到的地址信息存入到缓存中
//wx.setStorageSync(key,data);这个key值可以随便取名儿,这个data本来是写res2,但是后期要做优化,改名了
//为了后期繁琐的写地址信息代码,在这里做优化,把address.all写到wxml页面去

address.all=address.provinceName+address.cityName+address.countyName+address.detailInfo;
wx.setStorageSync("address",address);
}catch(error){
console.log(error);
}
},


//商品的选中
handeItemChange(e){
//获取被修改的商品的ID
const goods_id=e.currentTarget.dataset.id;
//console.log(goods_id);
//获取购物车数组
let{cart}=this.data;
//找到被修改的商品对象 findIndex找到索引的意思
//v=>goods_id是指购物车数组中的goods_id,后面那个goods_id是指点击复选框被修改的商品的ID
let index=cart.findIndex(v=>goods_id===goods_id);

//选中状态取反
cart[index].checked=!cart[index].checked;

this.setCart(cart);

  

},

//封装 设置购物车状态同时,重新计算底部工具栏的数据全选总价格 购物的数量
setCart(cart){


let allChecked=true;
//计算 首先声明变量 然后遍历数组
let totalPrice=0;
let totalNum=0;
cart.forEach(v=>{
  if(v.checked){
    totalPrice+=v.num*v.data.message.goods_price;
    totalNum+=v.num;
  }else{
    allChecked:false;
  }
})
//判断一下数组是否为空
allChecked=cart.length!=0?allChecked:false;
this.setData({
  cart,
  totalPrice,
  totalNum,
  allChecked
});
//这就是es6中的解构
wx.setStorageSync('cart', cart)

},

//商品的全选功能
handleItemAllCheck(e){
//1 获取data中的数据
let {cart,allChecked}=this.data;
//2 修改值
allChecked=!allChecked;
//3 循环修改cart数组中的商品选中状态
cart.forEach(v=>v.checked=allChecked);
// 4 修改后的指填充回data或者缓存中
this.setCart({cart,allChecked});


},

//商品数量的编辑功能
 async handleItemNumEdit(e){
//1 获取传递过来的参数
const{operation,id}=e.currentTarget.dataset;
//console.log(operation,id);
// 2 获取购物车数组
let{cart}=this.data;
// 3 找到需要修改的商品的索引
const index=cart.findIndex(v=>v.goods_id===id.id);
//判断是否要执行删除
if(cart[index].num===1&&operation===-1){
//弹窗提示
const res=await showModel('您是否要删除?')
if(res.confirm){
  cart.splice(index,1);
 this.setCart(cart);
return
}

}else{
//4 进行修改数量
cart[index].num+=operation;
//5 设置回缓存和data中
this.setCart(cart);
}

},


//结算
async handlePay(e){

// 1 判断收货地址是否存在
const{address,totalNum}=this.data;
if(!address.userName){
  await showToast({title:"您还没有选择收货地址"});
  return;
}

// 2 判断用户有没有选购商品
if(totalNum===0){
await showToast({title:"您还没有选购商品"});
return;
}


// 3 跳转到支付页面
wx.navigateTo({
  url:'/pages/pay/index'
});
}





})

cart.wxss

page{
  padding-bottom: 90rpx;
}
.revice_address_row{


}

.revice_address_row .address_btn{
padding: 10rpx;

}
.revice_address_row .address_btn button{
width:70%;
}


.revice_address_row .user_info_row {
 display: flex;
 padding:20rpx;
  }
.revice_address_row .user_info_row .user_info{
 flex:5;
  }
  .revice_address_row .user_info_row .user_phone{
    flex:3;
    text-align: right;
    }

    .cart_content .cart_title{
     padding: 20rpx;
     font-size:36rpx;
     color:rgb(219, 22, 22);
     border-top: 1px solid rgb(230, 44, 44);
     border-bottom: 1px solid rgb(241, 9, 9);
      }
  
      .cart_content .cart_main{
        padding: 20rpx;
        font-size:36rpx;
        color:rgb(219, 22, 22);
        border-top: 1px solid rgb(230, 44, 44);
        border-bottom: 1px solid rgb(241, 9, 9);
         }
         .cart_content .cart_main .cart_item{
         display: flex;
         padding: 10rpx;
         border-bottom: 1px solid #ccc;
           }

           .cart_content .cart_main .cart_chk_wrap{
         flex:1;
         display: flex;
         justify-content: conter;
         align-items: center;
          }
          .cart_content .cart_main .cart_chk_wrap .checkbox-group{
            
             }
             .cart_content .cart_main .cart_chk_wrap .checkbox-group checkbox{
              
               }
          .cart_content .cart_main .cart_img_wrap{
         flex:2;
         display: flex;
         justify-content: conter;
         align-items: center;
          }
          .cart_content .cart_main .cart_img_wrap image{
            width:80%;
             }
          .cart_content .cart_main .cart_info_wrap{
         flex:4;
         display: flex;
         flex-direction: column;
         justify-content: space-around;
          }
          .cart_content .cart_main .cart_info_wrap .goods_name{
          display: -webkit-box;
          overflow: hidden;
          -webkit-box-orient: vertical;
          -webkit-line-clamp:2;  
          color:#666;    
               }
 .cart_content .cart_main .cart_info_wrap .goods_price_wrap{
                display: flex;
                justify-content: space-between;
                 }
.cart_content .cart_main .cart_info_wrap .goods_price_wrap .goods_price{
                color:rgb(235, 5, 5);
                font-size: 34rpx;
                   }
.cart_content .cart_main .cart_info_wrap .goods_price_wrap .cart_num_tool{
                    display: flex;
                    
                       }
.cart_content .cart_main .cart_info_wrap .goods_price_wrap .cart_num_tool .num_edit{
                       width: 55rpx;
                       height: 55rpx;
                       display: flex;
                       justify-content: center;
                       align-items: center;
                       border: 1rpx solid #ccc;
                       color:#000000;
                           }
.cart_content .cart_main .cart_info_wrap .goods_price_wrap .cart_num_tool .goods_num{
  width: 55rpx;
  height: 55rpx;
  display: flex;
  justify-content: center;
  align-items: center;
  color:#000;
  
                               }
.footer_tool{
  position: fixed;
  bottom:0;
  left:0;
  width:100%;
  height: 90rpx;
  background-color: #fff;
  display: flex;
  border-top: 1px solid #ccc;
  border-bottom: 4px solid #fff;
}

.footer_tool .all_chk_wrap{
flex:2;
display: flex;
justify-content: center;
align-items: center;
}



    .footer_tool .total_price_wrap{
      flex:5;
      padding-right: 15rpx;
      text-align: right;

      }

      .footer_tool .total_price_wrap .total_price{
        
        }
    .footer_tool .total_price_wrap .total_price .total_price_text{
       color: #ee0d0d;
       font-style: 28rpx;
       font-weight: 600;
          }
.footer_tool .order_pay_wrap{
            flex:3;
            background-color: #dd0303;
            color:#fff;
            font-style: 32rpx;
            font-weight: 600;
            display: flex;
            justify-content: center;
            align-items: center;
            }

cart.json

{
  "usingComponents": {},
  "navigationBarTitleText":"购物车"
}

支付页面

 

 

 

 

 

  

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值