仿网易严选微信小程序(一学就会👇)

2,989 阅读13分钟

image.png

前言

最近在学习微信小程序开发,在此正好把自己所学到的知识分享给大家。希望能为同是小白的前端代码人带来一点帮助吧。

1.gif

开发准备

总体架构

本项目开发,使用的模板是JavaScript基础模板,采用的wxml + wxss + js + json开发模式,基于组件化思想,把需要复用的组件放在外部components文件夹中;数据驱动式思想,通过fastmock存储JSON数据,实现前后端数据分离;模块化思想(使用es6的export import语法),例如使用export default将封装的request函数向外暴露,将每个需要请求的页面都在service文件夹下配置相应的js文件统一管理。

|-common  通用的常量
    |-const.js   配置化统一管理
|-components   可复用的组件
|-images (放些内存小的图标就行,图片尽量用远程的)
|-miniprogram_npm vant组件库
|-models  建模
    |-detail.js  详情页数据多复杂 建模筛选出自己需要的
|-pages
    |-home 首页
    |-category 商品分类页面
    |-cart 购物车页面
    |-person 个人账号页面
    |-detail 商品详情页
    |-search 搜索页
|-service
    |-config.js 将请求地址分为两部分 可变部分为相应接口相对地址 
    |-network.js  封装wx.request 统一管理
    各自的封装函数请求
    |-home.js 
    |-catagory.js
    |-cart.js
    |-person.js
    |-detail.js

项目规划

  • 在做小程序之前,我会先分析每个页面对应功能,了解这款小程序的交互细节,清楚各类数据项。如此可以分为分析页面,解构页面基本布局,梳理业务逻辑,创造后端数据四步来展开。

首先可以在app.json中配置一下tabbar,就能如下图所示一样。

Snipaste_2023-01-11_22-17-18.png

"tabBar": {
    "selectedColor": "#ff5777",
    "list": [
      {
        "pagePath": "pages/home/home",
        "text": "首页",
        "iconPath": "/images/icons/home.png",
        "selectedIconPath": "/images/icons/home_active.png"
      },
      {
        "pagePath": "pages/category/category",
        "text": "分类",
        "iconPath": "/images/icons/category.png",
        "selectedIconPath": "/images/icons/category_active.png"
      },
      {
        "pagePath": "pages/cart/cart",
        "text": "购物车",
        "iconPath": "/images/icons/cart.png",
        "selectedIconPath": "/images/icons/cart_active.png"
      },
      {
        "pagePath": "pages/person/person",
        "text": "个人",
        "iconPath": "/images/icons/profile.png",
        "selectedIconPath": "/images/icons/profile_active.png"
      }
    ]
  },

FastMock

解析页面的数据,比如一个商品信息可以把它分成一个对象,商品的名称,价格等等参数就作为对象属性名,相应的数据作为值,可以组成一个小的集合,然后不同的商品信息就可以作为一个个对象存储在一个数组中或者另一个对象。按照这样的思路把它们分好类,处理总结好,就能以JSON数据格式写在fastmock里。 按照fastmock新建接口的规则,创建好接口名称(任意字符,建议和文档接口名称一致方便查找),选择XHR的请求类型(支持四种类型,但是我们选择GET就好),设置接口的XHR访问相对地址(‘/’开头),接口描述就是注解一样也是任意的。

Snipaste_2023-01-11_23-50-48.png

Snipaste_2023-01-11_23-46-55.png

项目解构

以下是我主要实现的小程序页面展示

wx1.png wx2.png wx3.png wx4.png wx5.png Snipaste_2023-01-12_00-22-03.png

接下来对各个页面进行解构

首页页面

19b9ba0128d8c1880675 -original-original.gif

交互功能细节

Snipaste_2023-01-12_23-51-09.png

home.wxml由自定义组件解构成顶部导航的w-navigator组件,主体的大搜索栏为search-bar私有组件,图片和滑块功能区分为w-slide-bar组件,标签栏为w-tab-control组件,最后由w-goods组件显示商品,w-back-top组件实现回到顶部功能,通过scroll-view来绑定事件控制功能栏的变化。

<!-- 顶部自定义导航 -->
<w-navigator  isHome="{{isHome}}" isHome_show="{{isHome_show}}"></w-navigator>
<!-- 页面主体 -->
<scroll-view scroll-y style="margin-top:120rpx;height:100vh" bindscroll="scrolltoupper" scroll-with-animation="true" scroll-top="{{topNum}}"
bindscrolltolower="loadMore">
<!-- 页面主体搜索栏 -->
<search-bar></search-bar>
<!-- 滑动栏 -->
<w-slide-bar slideList="{{slideList}}"></w-slide-bar>
<!-- 标签栏 -->
<w-tab-control class="tab-control" titles="{{titles}}" bind:tapclick="tapClick"></w-tab-control>
<!-- 商品栏 -->
<w-goods goodslist="{{goods[currentType].list}}"></w-goods>
<!-- 回到顶部按钮 -->
<w-back-top bind:tap="goTop" wx:if="{{up_show}}">sss</w-back-top>
</scroll-view>

导航栏的系统数据在app.js配置到全局globalData中方便所有页面使用。首先导航的布局采用了position: fixed;的固定定位,这样会脱离文档流,所以主体部分需要先设置margin-top来防止被部分覆盖。功能栏又分为图片部分和搜索部分,当scroll-view滚动触发的scrollTop属性值超过大搜索栏的高度时,功能栏从图片切换成搜索栏,但是这里只做了基础的搜索栏(其他页面需要复用)并没有直接做成大搜索栏的样式,所以在home.wxss上修改为特定样式。轮播功能使用了swiper来实现,但是自动轮播会有手动滑动图片的默认行为,采用绑定自定义事件catchtouchmove="stopTouchMove"来该行为。搜索框的布局就采用flex弹性布局就好了,搜索按钮和滚动内容的边框其实是两部分但是可以选择性拼接起来也就搞定了。

滑动块的部分,滑块的长度其实需要通过数据来驱动才是正确的做法,所以不能自己写死,根据数据列表的图片多少来按比例分配。

<!-- 自定义导航栏 -->
<view class="navigator-container" style="height: {{navHeight}};">
  <!-- 功能栏 -->
  <view class="func-container" style="height: {{navHeight - navTop}}px; margin-top: {{navTop}}px; width: {{navWidth - 10}}px">
    <!-- 显示品牌图片 -->
    <image class="{{isHome? 'func-img' : 'hide-func-img'}}" src="/images/icons/nav-img.png" style="height: {{funcBarHeight}}px;" />
    <!-- 显示搜索栏 -->
    <navigator class="{{isHome? 'hide-search' : 'search-container'}}" style="height: {{funcBarHeight}}px;" url="/pages/search/search">
      <view class="{{isHome? '' : 'navigator-search'}}">
        <!-- 图标 -->
        <image src="/images/icons/search-icon.png" style="height: 42rpx;width:42rpx;margin: 10rpx 20rpx" />
        <!-- 轮播功能 -->
        <swiper class="search-title" vertical="true" autoplay="true" circular="true" interval="4000">
          <block wx:for="{{msgList}}" wx:key="index">
            <swiper-item catchtouchmove="stopTouchMove">
              <text class="swiper_item"> {{isHome? '' : item.title}}</text>
            </swiper-item>
          </block>
        </swiper>
      </view>
      <!-- 搜索按钮 -->
      <view class="{{isHome_show? 'navigator-search-button':'clear-button'}}">
        <text>搜索</text>
      </view>
    </navigator>
  </view>
</view>

获取商品项

其实goods的这种写法不是很好(这里没去改正好拿来对比),比较复杂化了不过在解构分类页面时会提到另外一种更简便的写法。我觉得最好的做法还是goods改成数组的形式最简便,需要的type我们可以改成currentIndex的方式。这里还做了下拉刷新的功能,也就是把数据做了分页处理。

      titles: ["猜你喜欢", "居家生活", "美食酒水", "个护清洁", "数码家电", "服饰鞋包", "运动旅行", "母婴亲子", "全球特色" ],
      goods: {
        [TP1]: {
          page: 1, 
          list: []
        },
        [TP2]: {
          page: 1, 
          list: []
        },
        [TP3]: {
          page: 1, 
          list: []
        },
        [TP4]: {
          page: 1, 
          list: []
        },
        [TP5]: {
          page: 1, 
          list: []
        },
        [TP6]: {
          page: 1, 
          list: []
        },
        [TP7]: {
          page: 1, 
          list: []
        },
        [TP8]: {
          page: 1, 
          list: []
        },
        [TP9]: {
          page: 1, 
          list: []
        }
      },
      page: 1,
      currentType: 'TP1',
  // 尽量在onLoad中少放东西  做的事情很多
  onLoad(options) {
    this._getData();
  },
  _getData() {
    this._getSlideData();
    const list = [TP1,TP2,TP3,TP4,TP5,TP6,TP7,TP8,TP9]
    this._initData(list)
  },
  _getSlideData() {
    getSlideData() // promise 拿到接口返回的数据
      .then(res => {
        const slideList = res.slideList;
        this.setData({
          slideList
        });
        })
  },
  // 一次性请求完所有type的数据
  _initData(types) {
    types.forEach(type => {
     this._getProductData(type)
      })
  },
  _getProductData(type) {
    const page = this.data.goods[type].page;
    getProduct(type, page)
    .then(res => {
      const list = res.data[type].list;
      const goods = this.data.goods;
      goods[type].list.push(...list)
      goods[type].page += 1
      this.setData({
        goods
      })
    })
  },
  // 下拉加载数据
  loadMore() {
    this._getProductData(this.data.currentType);
  },

商品卡片跳转绑定详情页

利用id的唯一性,我们可以实现不同的商品点击跳转至对应的详情页。不过这里不是真正的后端,不能通过直接传参给后端来获取想要的相应数据。不过这里我添加了判断逻辑模拟了绑定相对应的详情页跳转。

商品的点击事件

methods: {
    itemClick(e) {
      const id = this.data.goodsitem.id;
      wx.navigateTo({
        url: `/pages/detail/detail?id=${id}`,
      })
    }
  }

详情页的数据请求

 onLoad(options) {
    const id = options.id;
    this.setData({
      id
    })
    this._getDetailData();
  },
  _getDetailData() {
    getDetailData(this.data.id)
      .then(res => {
        // 手动添加一个逻辑来判断 goods-item的id匹配detail一次性请求的所有数据中相关的那部分数据  用该数据来驱动页面更新
        // 模拟通过传参来向后端取得想要得到的数据
        const data1 = res.data;
        const index =  data1.findIndex(item => item.id == this.data.id);
        const data = res.data[index];
        const topImages = data.topImages;
        const imagesLength = topImages.length;
        const baseInfo = new GoodsBaseInfo(data.itemInfo, data.contents);
        const actInfo = data.actInfo;
        const shopInfo = data.shopInfo;
        const argumentInfo = data.argumentInfo;
        this.setData({
          topImages,
          baseInfo,
          imagesLength,
          actInfo,
          shopInfo,
          argumentInfo
        })
      })
  },

商品详情页

cf2e6aca561f87bd0 -original-original.gif

详情页功能交互

图片有个横向滑动的功能,通过swiper来实现,这里需要添加一个显示当前图片位置的下标框。也是需要通过数据驱动,根据数据的图片数决定。其他布局还是使用flex弹性布局来做,布局结构做出来仍然是通过数据来驱动显示。这里的商品邮费信息栏设置了一个动画样式,可以控制显示和隐藏。底部的菜单栏有一个添加购物车的功能,因为要把详情页的数据添加到购物车页面,跨页面操作,所以这里的业务逻辑是把数据直接添加到app.jsglobalData里这样就能让购物车页面拿到对应的数据,然后在app.js做逻辑判断第一次选购则直接添加这个商品,若存在则只改变数量值。

Snipaste_2023-01-13_13-24-59.pngSnipaste_2023-01-13_13-29-34.png

<scroll-view scroll-y="true" style="height: 100vh;">
<!-- 图片滑动栏 -->
<w-slide-bar images="{{topImages}}" imagesLength="{{imagesLength}}"></w-slide-bar>
<!-- 价格区 -->
<w-price-bar baseInfo="{{baseInfo}}"></w-price-bar>
<!-- 分割线 -->
<w-dividing-line></w-dividing-line>
<!-- 邮费信息栏 -->
<w-info-bar actInfo="{{actInfo}}" showDialog="{{showDialog}}" bind:tap="showModals"></w-info-bar>
<!-- 分割线 -->
<w-dividing-line></w-dividing-line>
<!-- 其他信息栏 -->
<view>
  <block wx:for="{{shopInfo}}" wx:key="index">
    <w-info-bar actInfo="{{item}}" ></w-info-bar>
  </block>
</view>
<!-- 分割线 -->
<w-dividing-line></w-dividing-line>
<!-- 底部商品参数区 -->
<w-argument argumentInfo="{{argumentInfo}}"></w-argument>
</scroll-view>
<!-- 底部菜单栏 -->
<w-bottom-bar bind:addcart="onAddCart"></w-bottom-bar>

动画的wxml

<view class="postage-bar">
<view class="content-section">
  <text class="postage" style="color: gray;width: 140rpx;">{{actInfo.title}}</text>
  <text class="postage-method">{{actInfo.desc}}</text>
 <image style="margin-left: 500rpx;height: 40rpx;" src="/images/search-icons/right.png" mode="heightFix"/>  
</view>
<view class="mask {{showDialog? 'show' : ''}}" bind:tap="hideModals"></view>
<view class="dialog {{showDialog? 'show': ''}}" >
<view style="text-align: center;height: 60rpx;border-bottom: 2rpx solid gray;margin-top: 20rpx;">邮费</view>
<text style="color: gray;">{{actInfo.popContent}}</text>
</view>
</view>

绑定事件控制邮费栏动画显示和隐藏

  showModals() {
    const _showDialog = !this.data.showDialog
    this.setData({
      showDialog: _showDialog
    })
  },

添加购物车

detail.js

onAddCart() {
    const obj = {};
    obj.id = this.data.id;
    obj.imageURL = this.data.topImages[0];
    obj.title = this.data.baseInfo.title;
    obj.price = this.data.baseInfo.realPrice;
    app.addToCart(obj);
    wx.showToast({
      title: '加入购物车成功'
    })
  },

app.js

  addToCart(obj) {
    const oldInfo = this.globalData.cartList 
      .find(item => item.id === obj.id );
      // console.log(oldInfo);
    if (oldInfo) {
      oldInfo.count++;
    } else {
      obj.count = 1
      obj.checked = true // 默认勾选
      this.globalData.cartList.push(obj)
    }
    wx.setStorage({
      key: 'cart',
      data: this.globalData.cartList
    })
  },

商品分类页面

6944f -original-original.gif

交互功能

顶部导航复用w-navigator组件就好。侧边导航栏设置点击样式,初始默认第一项设置样式。内容区根据侧边栏的改变来更改数据项。

sxx-13-09.png

获取商品项

侧边导航栏

Component({
  /**
   * 组件的属性列表
   */
  properties: {
    sideList: {
      type: Array,
      value: []
    }
  },  
  /**
   * 组件的初始数据
   */
  data: {
    currentIndex: 0 // 私有属性
  },
  /**
   * 组件的方法列表
   */
  methods: {
    itemClick(e) {
      this.setData({
        currentIndex: e.currentTarget.dataset.index
      })
      const data = {index: this.data.currentIndex}
      // 向父组件传递数据
      this.triggerEvent("onclick", data, {})
    }
  }
})

这里采用的就是父子组件相互通信,父组件通过sideContents="{{goodsContents[currentIndex]}}"向子组件传递数据。子组件采用 this.triggerEvent("onclick", data, {})的方式将想要传递的数据添加在data中传递给父组件。this.triggerEvent的第一个参数是父组件绑定在子组件上的自定义事件,第二个参数则是想传递的数据data,第三个参数是事件的选项可以选择是否冒泡等,也可不写。

<!-- 顶部导航栏 -->
<w-navigator></w-navigator>
<!-- 侧边导航栏 -->
<w-side-bar sideList="{{sideList}}" bind:onclick="onClick"></w-side-bar>
<!-- 侧边栏对应的内容区 -->
<w-sideContents  class="w-sideContents" sideContents="{{goodsContents[currentIndex]}}"></w-sideContents>

这里通过w-side-bar的列表下标来选择对应的内容区数据项是比较合适且简便的写法。所以category.jsdata也是选择通过数组的方式来存储数据,这种写法就比首页页面采用对象的写法简便很多。

  data: {
    search: '',
    activeCategory: 0,
    sideList: ["猜你喜欢", "居家生活", "美食酒水", "个护清洁", "数码家电", "服饰鞋包", "运动旅行", "母婴亲子", "全球特色"],
    goodsContents: [],
    currentIndex: 0
  },
  // 自定义父组件的绑定事件 接收子组件传递的数据
  onClick(e) {
    this.setData({
      currentIndex: e.detail.index
    })
  },
  /**
   * 生命周期函数--监听页面加载
   */
  onLoad(options) {
    this._getContents();  // onLoad尽量少放代码 需要写的逻辑单独封装成函数
  },
  _getContents() {
    // 向外请求数据
    getContents()
      .then(res => {
        this.setData({
          goodsContents: res.data.goodsContent
        })
      })
  },

购物车页面

WeChat_2023011316591 -original-original.gif

交互功能

Snipaste_2023-01-13_17-12-06.png

点击勾选的图标框我们作为全局的复用组件来使用。微信小程序提供了icon的标签来实现这种勾选的图标,官方文档中会有说明,不同的type属性有不同的样式,根据自己的需要来选择。用isChecked实现勾选样式变化,单选就可以在使用icon组件时绑定点击事件来操作,全选则可以通过底部菜单栏的绑定事件改变商品区的勾选事件的属性值。

<icon
  class="check-icon icon"
  type="success"
  size="18"
  style="border-color: {{isChecked?'#fafafa':'#f9363a'}}"
  color="{{isChecked?'#ff5777':'#fff'}}"
/>
<!-- 购物区 -->
<view class="cart">
  <scroll-view class="cart-list" scroll-y style="background-color: #f4f4f4;">
    <block wx:for="{{cartList}}" wx:key="index">
      <lists-item goods="{{item}}" index="{{index}}" checked="{{isSelectAll}}" />
    </block>
  </scroll-view>
</view>
<!-- 推荐区 -->
<view style="text-align: center;width: 750rpx;">猜你喜欢</view>
<!-- 推荐商品列表 -->
<w-goods goodslist="{{cartGoods}}"></w-goods>
<!-- 底部功能栏 -->
<bottom-bar selected="{{isSelectAll}}" price="{{totalPrice}}" counter="{{totalCounter}}" bind:tap="onSelectAll"></bottom-bar>

底部菜单栏的绑定事件

  onSelectAll() {
    this.setData({
      isSelectAll: !this.data.isSelectAll
    })

组件上的绑定事件

  methods: {
    onCheckClick() {
      this.setData({
        checked: !this.properties.checked
      })
    }
  }

购物商品添加

cart.js

import {
  getCartData
} from '../../service/cart.js'
const app = getApp();
Page({
  /**
   * 页面的初始数据
   */
  data: {
    cartList: [],
    totalPrice: 0,
    totalCounter: 0,
    isSelectAll: true,
    cartGoods: []
  },

  /**
   * 生命周期函数--监听页面加载
   */
  onLoad(options) {
   this._getCartData()
    this.setData({
      cartList: app.globalData.cartList
    })
  },
  _getCartData() {
    getCartData() 
      .then(res => {
        this.setData({
          cartGoods: res.data
        });
        })
  },
  onSelectAll() {
    this.setData({
      isSelectAll: !this.data.isSelectAll
    })
  },
  /**
   * 生命周期函数--监听页面初次渲染完成
   */
  onReady() {

  },
  changeData() {
    let cartList = app.globalData.cartList;
    let totalCounter = 0;
    let totalPrice = cartList.reduce((pre, cur) => {
      totalCounter++;
      return  pre + cur.price * cur.count;
    }, 0)
    const selectAll = !cartList.find(item => !item.checked);

    this.setData({
      cartList,
      totalPrice,
      totalCounter,
      isSelectAll: selectAll
    })
  },
})

app.js

 wx.getStorage({
      key: 'cart',
      success: (res) => {
        this.globalData.cartList = res.data || []
      }
    })
addToCart(obj) {
    const oldInfo = this.globalData.cartList 
      .find(item => item.id === obj.id );
      // console.log(oldInfo);
    if (oldInfo) {
      oldInfo.count++;
    } else {
      obj.count = 1
      obj.checked = true // 默认勾选
      this.globalData.cartList.push(obj)
    }
    wx.setStorage({
      key: 'cart',
      data: this.globalData.cartList
    })
  },

detail.js

 onAddCart() {
    const obj = {};
    obj.id = this.data.id;
    obj.imageURL = this.data.topImages[0];
    obj.title = this.data.baseInfo.title;
    obj.price = this.data.baseInfo.realPrice;
    app.addToCart(obj);
    wx.showToast({
      title: '加入购物车成功'
    })

搜索页页面 和 个人账号页面

Snipaste_2023-01-12_00-22-03.pngwx4.png

搜索页面

搜索页面主要尝试了vant提供的组件库,搜索栏使用了van-search组件,布局采用了van-row,van-col,van-grid组件。

Snipaste_2023-01-13_17-49-01.png
<van-search value="{{value}}"
placeholder="请输入搜索关键词"
show-action
bind:search = "onSearch"
bind:cancel = "onCancel"
>
</van-search>
<view style="margin: 20rpx;">热门搜索</view>
<view style="margin-left: 30rpx;">
<van-row gutter="10">
    <van-col style="color: red;">🔥耳机</van-col>
    <van-col offset="1">枕头</van-col>
    <van-col offset="1">行李箱</van-col>
    <van-col offset="1">鞋</van-col>
    <van-col offset="1">咖啡</van-col>
</van-row>
<van-row gutter="10">
    <van-col>毛巾</van-col>
    <van-col offset="1">湿巾</van-col>
    <van-col offset="1">床</van-col>
    <van-col offset="1">纸巾</van-col>
</van-row>
</view>
<view style="margin: 20rpx;">
热门分类
</view>
<van-grid :column-num="5"  :border="false" gutter="10">
<van-grid-item text="国产酒" icon="/images/search-icons/Snipaste_2023-01-04_19-06-32.png/">
</van-grid-item>
<van-grid-item text="国产酒" icon="/images/search-icons/Snipaste_2023-01-04_19-06-32.png/">
</van-grid-item>
<van-grid-item text="国产酒" icon="/images/search-icons/Snipaste_2023-01-04_19-06-32.png/">
</van-grid-item>
<van-grid-item text="国产酒" icon="/images/search-icons/Snipaste_2023-01-04_19-06-32.png/">
</van-grid-item>
</van-grid>
<van-grid gutter="10">
<van-grid-item icon="/images/search-icons/Snipaste_2023-01-04_19-06-32.png/" text="国产酒"></van-grid-item>
<van-grid-item icon="/images/search-icons/Snipaste_2023-01-04_19-06-32.png/" text="国产酒"></van-grid-item>
</van-grid>

个人账号页面

这个页面直接用图片 + 购物车的推荐区内容拼接了一下,并没做其他的交互功能。

难点重点

因为考虑到并没有使用真正的后端接口来开发这个小程序。所以我在做的时候,就决定还是要模拟一下和有后端接口的大致效果。所以从首页商品卡片点击跳转至详情页,要模拟商品id的唯一性来正确跳转到对应的详情页并用数据驱动。这里的业务逻辑是要考虑到的。

w-goods-item

 methods: {
    itemClick(e) {
      const id = this.data.goodsitem.id;
      wx.navigateTo({
        url: `/pages/detail/detail?id=${id}`,
      })
    }
  }

detail.js

 onLoad(options) {
    const id = options.id;
    this.setData({
      id
    })
    this._getDetailData();
  },
  _getDetailData() {
    getDetailData(this.data.id)
      .then(res => {
        // 手动添加一个逻辑来判断 goods-item的id匹配detail一次性请求的所有数据中相关的那部分数据  用该数据来驱动页面更新
        // 模拟通过传参来向后端取得想要得到的数据
        const data1 = res.data;
        const index =  data1.findIndex(item => item.id == this.data.id);
        const data = res.data[index];
        const topImages = data.topImages;
        const imagesLength = topImages.length;
        const baseInfo = new GoodsBaseInfo(data.itemInfo, data.contents);
        const actInfo = data.actInfo;
        const shopInfo = data.shopInfo;
        const argumentInfo = data.argumentInfo;
        this.setData({
          topImages,
          baseInfo,
          imagesLength,
          actInfo,
          shopInfo,
          argumentInfo
        })
      })
  },

添加购物车的业务逻辑,也是需要考虑清楚的。首先要把详情页将购物车的需要的数据提取保存添加到app.js在本地,在app.js中还需要判断商品是否首次添加。

import {
  getCartData
} from '../../service/cart.js'
const app = getApp();
Page({
  /**
   * 页面的初始数据
   */
  data: {
    cartList: [],
    totalPrice: 0,
    totalCounter: 0,
    isSelectAll: true,
    cartGoods: []
  },

  /**
   * 生命周期函数--监听页面加载
   */
  onLoad(options) {
   this._getCartData()
    this.setData({
      cartList: app.globalData.cartList
    })
  },
  _getCartData() {
    getCartData() 
      .then(res => {
        this.setData({
          cartGoods: res.data
        });
        })
  },
  onSelectAll() {
    this.setData({
      isSelectAll: !this.data.isSelectAll
    })
  },
  /**
   * 生命周期函数--监听页面初次渲染完成
   */
  onReady() {

  },
  changeData() {
    let cartList = app.globalData.cartList;
    let totalCounter = 0;
    let totalPrice = cartList.reduce((pre, cur) => {
      totalCounter++;
      return  pre + cur.price * cur.count;
    }, 0)
    const selectAll = !cartList.find(item => !item.checked);

    this.setData({
      cartList,
      totalPrice,
      totalCounter,
      isSelectAll: selectAll
    })
  },
  /**
   * 生命周期函数--监听页面显示
   */
  onShow() {
    this.changeData();
  },
})

小建议

在写项目的过程不管是js的逻辑错误,还是数据的获取请求错误等等,建议在这个过程中有不理解的地方多使用console.log()去打印,这样能清楚知道是不是自己想要的东西和数据,还需要学会从bug提示中获取信息解决错误。实际上很多地方,本人都通过console.log()打印验证过是否为我需要的数据,但是源码中是删除了这些痕迹的。所以再次强调不懂的地方,迷惑的地方一定多多尝试。

功能和交互细节总结

- 解构本项目实现功能
  - 首页功能介绍
  1. 自定义顶部导航 实现了滚动控制导航栏的功能栏变化
      交互细节1: 自定义导航数据和微信系统完全一致 数据精准   可以通过wx自身提供的api获取
      交互细节2: 功能栏显示为品牌图片时不提供跳转到搜索页面的功能   显示为搜索栏时提供跳转功能
      交互细节3: 功能栏的搜索栏并没有直接做成和主页主体的搜索栏的样式 而是做成其他页面需要的顶部搜索栏基础样式 
                目的是方便其他页面复用  主页页面可以通过对基础样式进一步修改达到和主页搜索栏一致
  2. 搜索栏轮播功能  自动轮播+跳转  跳转可以用 navgator标签来实现(小程序用该标签代替了a标签) 或者可以绑定点击事件 通过wx.navigateTo实现
      交互细节1:自动轮播时默认允许手动滑动轮播内容显然是不好的行为 所以这里使用 catchtouchmove定义一个事件来禁止该行为
  3. 图片滚动栏  在滚动栏下方 创造了一个滑块框  通过滚动图片来同步显示移动变化效果
      交互细节1: 数据驱动 跟随图片数据自动同步滑块长度  通过计算比例得到滑块滑动距离  不能把数据写死
  4. 标签栏 会有一个点击更新样式的功能 可以根据点击选中的标签的下标与当前下标是否一致来判断是否更新样式
      交互细节1: 未点击时需要显示第一个为默认选择的样式
  5. 内容区 需要实现同步标签栏的更新来用当前标签的数据驱动显示 
      交互细节1: 内容区的商品卡片点击跳转到详情页的数据,需要利用id的唯一性判断,这样才可以使得不同的商品卡片跳转至详情页的数据也是唯一的
                  实现的逻辑在w-goods-item和detail中注解了
      交互细节2: 有下拉加载更新数据的功能,采用分页的功能来实现
  6. 回到顶部按钮  通过改变scrollTop属性值来实现功能
      交互细节1: 通过设置scroll-with-animation="true"为其添加动画效果,这样回到顶部会有从当前位置逐渐到底顶部的视觉效果 提高用户体验感

  - 详情页页面功能介绍
  1. 图片横向滚动 
      交互细节1: 需要设置一个显示当前图片数的标识框  但是数字不能写死 是根据获取的数据来动态更新的
  2. 邮费信息栏
      交互细节1: 设置点击事件来显示和隐藏动画,  动画的实现可以通过设置一个遮罩层和一个显示层, 用延时透明度的变化来制造视觉效果
      交互细节2: 因为这里只简单设置了显示层的固定内容,并没有需要设置显示层的滚动,
                  所以这里的点击事件是冒泡事件,也就意味着无论是点击遮罩层还是显示层都能够实现显示和隐藏功能
                  如果需要实现显示层滚动显示内容,那么隐藏的功能则不能设置为冒泡事件
  3. 底部菜单栏  仅实现了一个添加购物车功能
      交互细节1: 当通过加入购物车的点击事件,这里需要做个反馈让用户知道成功添加了,使用wx.showToast()添加提示框 增强用户体验

  - 分类页面功能介绍
  1. 侧边导航栏  类似于首页页面的标签栏 也是通过点击更新样式 但是竖向需要考虑position:fixed 因为不能因为滚动导致侧边栏改变位置
      交互细节1: 同样默认未点击操作时选择第一项为更新样式  整个侧边栏需要固定定位,不随着页面内容滚动而改变
  2. 内容区  采用flex弹性布局  
      交互细节1: 数据驱动页面显示,所以我们需要将弹性布局的每个商品卡片的宽高固定写死,这样卡片才会自动换行
                  图片下方的文字实现水平垂直居中的方式其实很简单  
                  可以采用view标签包裹文字内容text-align: center;实现水平居中 设置line-height来实现垂直居中

  - 购物车页面功能介绍
  1. 购物车的商品卡片 可以单选 选中或取消
      交互细节1: 当商品被添加至购物车时,默认选中该商品,但是提供通过点击事件来单选选中或取消
  2. 底部菜单栏  可以实现全选或者全部取消勾选
      交互细节1: 当商品被添加至购物车时,默认选中该商品,但是提供通过点击事件统一改变商品项的单选功能
  3. 推荐商品区
      交互细节1: 该商品区的不同之处在于推荐的业务逻辑,本质上推荐区的数据实际上需要通过大数据及算法获取相应数据。
                  (此处我并没有实现该功能,仅仅通过普通数据驱动罢了)

  - 其他页面介绍
    1. 搜索页面本人是采用了Vant组件库的一些基础组件,写了一些布局及功能,总的来说并没有细致研究,就在此不多提及。
        需要的同学其实可以通过官方文档去使用,还是很方便的对某些项目开发来说。
    2. 个人账号和另一个搜索页面其实也仅仅是挂了张照片 + 一部分数据驱动的商品项,也没什么好说的。

源码

本项目源码

结语

源码中会有很多注解,欢迎需要的同学自行获取,也写了一些总结之类的在README.md中。

写项目的过程对我来说是一个挑战,毕竟是我第一次专注于做项目,项目中遇到的bug会烦人但是坚持写完看见成果后是非常有成就感的。如果你喜欢我的这篇文章或者看到这里对你有些许帮助,不妨点个赞吧❤️!同时也非常希望看到文章的你能给我一些建议,期待与你一起讨论学习微信小程序

dogecoin-meme-sd3wtbxwa7cvn72f.gif