# 风袖项目笔记

# 开发文档

# 首页

appkey:

O7BMoq5Sm5MOZXuZ;
zmSPVc0UqzqlqeMM;
Uhzl0bGiikOrJrcr;
bhAAMYFnpUtUoa1X;

# 小程序分层结构:

页面 js -数据绑定
view 层 业务逻辑层 桥梁
Model 层 处理业务
寻找业务对象 重要

首页发送请求定义成model 处理业务,然后就是页面 js 接收数据

封装原生wx,request 使用 promisic 工具方法

class Http{
    static async request({
        url,
        data,
        method='GET'
    }){
        return await promisic (wx.request)P{
            url:`${config.basesURL}${url}`,
                data,
                method,
                header:{
                    appkey:config.appkey
                }
        }
    }
}

接下来每次请求只需要去 Http.request({url,data,method})即可

# 具体技巧学习

# 消除图片自带间距 display:flex
# 类不能保存数据的状态 ,只有类的对象才能保存数据和状态
Theme.a = 1;
Theme.a = 2;
// 改变了状态
const t = new Theme();
t.a = 1;
const t2 = new Theme();
t2.a = 2;
// 即保存了数据又保存了状态
# 封住请求的 theme 方法
import { Http } from "../utils/http";
class Theme {
  static ThemeA = "t-1";
  static ThemeB = "t-2";
  static ThemeC = "t-3";
  static ThemeD = "t-4";
  // 用类对象存储数据 改写方法  避免请求多次服务
  themes = [];
  async getHomeThemes() {
    const res = await Http.request({
      url: "/theme/by/names",
      data: {
        names: `${Theme.ThemeA},${Theme.ThemeB},${Theme.ThemeC},${Theme.ThemeD}`
      }
    });
    this.themes = res.data;
  }
  // 直接获取locationA LocationB 保证调用方的请求代码简洁
  async getThemeA() {
    return this.themes.find(t => t.name === Theme.ThemeA);
  }
  async getThemeB() {
    return this.themes.find(t => t.name === Theme.ThemeB);
  }
  async getThemeC() {
    return this.themes.find(t => t.name === Theme.ThemeC);
  }
  async getThemeD() {
    return this.themes.find(t => t.name === Theme.ThemeD);
  }
  static getHomeLocationESpu() {
    return Theme.getThemeSpuByName(Theme.ThemeB);
  }

  static getHomeThemeCSpu() {
    return Theme.getThemeSpuByName(Theme.ThemeC);
  }
  static async getThemeSpuByName(name) {
    const res = await Http.request({
      url: `/theme/name/${name}/with_spu`
    });
    return res.data;
  }
}

export { Theme };
# 页底提示其实只有两种状态

加载中(当滑动到底部就显示 常驻状态) 所以再最外层 直接设置show="true"

没有更多数据了 此时没有更多数据了,设置 loading 的 type 和 end-text 即可

# scroll-view 组件使用
spu 一件商品
sku 一件商品有多种颜色 库存 种类 等等

将某些数据抽象为不同的模型:例如热卖榜单的数据就可以视为轮播图

scroll-view 组件 定义为这样的结构较好

<scroll-view scroll-x="{{true}}">
  <view class="inner">
    <block wx:for="{{spuList}}">
      <view class="item">
        <image></image>
        <view class="desc">
          <view>title</view>
          <view>price</view>
        </view>
      </view>
    </block>
  </view>
</scroll-view>

控制 inner 的样式

.inner {
  display: flex;
  flex-direction: row;
}
# 点击动画- view 组件的hover-class,hover-stay-time设置动画时间(ms)
.react-hover {
  position: relative;
  top: 3rpx;
  left: 3rpx;
  box-shadow: 0px 0px rgba(0, 0, 0, 0.1) inset;
}
# 瀑布流布局和封装分页加载 API

智能推荐

点击过后 用标签记录数量 然后根据标签随机分配

分页数据: 正在加载 loading 加载完成 没有更多数据了

分装请求分页的 API

// 定义属性 保持状态要用类对象属性
// 获取更多数据状态  1.getLocker 当别的请求正在执行 不能发送请求
2. request 3. 释放locker

封装 req

// 考虑两种情况
1. url = '/v1/spu/latest?start=0&count=10'
2. url已经有了query '/v1/spu/latest?other=1'

拼接方法
url = this.url
if(url.indexOf('?')!==-1){
    url += '&'+params
}else{
    url += '?'+params
}

正式封装

class Paging{
  // 1. 初始化属性 url-原始请求地址 locker是否上锁 req请求的对象 accumlator历史请求数组
    url
    locker= false
    start
    count
    req
    moreData = true
    accumlator=[]
// 初始化
	constructor(req,count=10,start=0){
        this.req = req
        this.url = req.url
        this.count = count
        this.start =start
    }
// 定义业务过程
getMoreData(){
    if(!this.getLocker()){
        return
    }
    this.getData()
    this._releaseLocker()
}

// 封装 getData()

getData(){
    const req = this._getCurrentUrl()
    const Paging = Http.request(req)
    if(!paging){
        return null
    }
    if(paging.data.total === 0){
        return {
            empty:true,
            moreData:false,
            items:[],
            accumulator:[]
        }
    }
    this.moreData = this._getMoreData(paging.data.total_page,paging.data.page)
    if(this.moreData){
        this.start += this.count
    }
    this.accumulator = this._getaccumlator()
    return {
        empty:false,
        moreData:this.moreData,
        items:paging.data.items,
        accumulator:this.accumulator
    }

}
// 获取拼接后的地址
_getCurrentUrl()// 上面提到
// 是否有更多数据
_getMoreData(totalPage,page){
    return page<totalPage-1
}
// 拼接items
_getaccumlator(items){
    this.accumulator = this.accumulatro.conctat(items)
}
}

调用数据就需要封装一个单独的模型 来实例化 paging 对象 在通过对象调用方法 1

瀑布流传递数据

1. 定义抽象节点 接收的属性名一定要定义为data
2. 传递数据
 wx.lin.renderWaterFlow(data.items)  data为数组
3. 编写节点

# 小程序开发技巧

# 使用 wxs

处理价格是打折还未打折 打折的原价划线 未打折不划线

function mainPrice(price, discount_price) {
  if (!discount_price) {
    return price;
  } else {
    discount_prce;
  }
}
function slashPrice(price, discountpirce) {
  if (discountpirce) {
    return price;
  } else {
    return;
  }
}

# 动态计算宽高 是图片自适应高度或宽度

<image bind:Load="onImgLoad" style="width:{{w}}rpx;heiht:{{h}}rpx">
onImgLoad(event){
    const {width,height} = event.detail
    this.setData({
        w:340rpx;
        h:340rpx*height/width
    })
}
// 或者直接使用mode

# 间隔轮播设置

采坑:定义组件一定在 index.json 加上:components:true

小程序 wxs 导出 function 不能再 module.exports={}简写

轮播图设置间距 previous-margin``next-margin

样式代码

1. swiper居中 2. image设置为块状 3. 设置swiper属性 .swiper {
  height: 360rpx;
  width: 100%;
  background: #ffffff;
}
swiper-item {
  text-align: center;
}

.swiper-img {
  display: inline-block;
  width: 610rpx;
  height: 360rpx;
  border-radius: 10rpx;
}

# 小程序路由传参(绑定属性值,传递属性值)

1. 设置传递参数属性 dom上绑定data-属性名
拿到属性 `e.currentTarget.dataset.id`

变量设置只能为小写 大写会报错
2. 绑定点击事件跳转路由
wx.navigateTo({
    url:`/pages/detail/index?pid=${pid}`
})
拿到参数: 子路由通过
onLoad:function `options.pid`拿到传递的参数


业务组件可以这样写 但考虑到通用性 组件里面不应该包含路由跳转事件
所以可以向外触发事件 有父组件进行路由跳转
this.triggleEvent('changeNavigate',{params})

如果传递的参数为对象

先转为字符串

let obj = JSON.stringify(e.currentTarget.dataset.item);
wx.navigateTo({
  url: `/pages/detail/index?detail=${obj}`
});

// 接收再转回来
let item = JSON.parse(options.obj);

# 改变原生组件大小

绑定class 进行缩放 .radio {
  transform: scale(0.7);
}

# 小程序原生picker组件用法

<picker bin:change="binPicekerChange" range-key="nickName" value="{{index}}" range=""{{personList}}>

// js
data:{
    personList:[
        {"nickName:"小名","sex":"0"},
          {"nickName:"小民","sex":"1"}
    ]
}

# 去除滚动条

::webKit-scrollbar{
    width:0;
    height:0;
    color:transparent;
}

# 设置页面高度百分之百

.container{
    position:fixed;
    height:100%;
    width:100%;
    display:flex;
}

page { height: 100vh;  // 或者height: 100% }

# 阻止事件冒泡

catchtap=""

# 阻止小程序下拉出现白条

// app.json中配置
"window":{
    "enablePullDownRefresh":false
}

# 小程序图片上传

handleCancelPic() {
        let id = this.data.dbId;
        wx.chooseImage({
          count: 3, // 默认9
          sizeType: ['compressed'], // 可以指定是原图还是压缩图,默认二者都有
          sourceType: ['album', 'camera'], // 可以指定来源是相册还是相机,默认二者都有
          success: res => {
            // 返回选定照片的本地文件路径列表,tempFilePath可以作为img标签的src属性显示图片
            var tempFilePaths = res.tempFilePaths;

            this.setData({
                src: tempFilePaths
            })
            upload(this,tempFilePaths,'','');
          }
        })
    }
然后一个封装好的方法
function upload(page, path,way,id) {
    console.log(path)
  wx.showToast({
    icon: "loading",
    title: "正在上传"
  });
  var test = [],
    that = this;
  for (var i = 0; i<path.length; i++) {
        wx.uploadFile({
          url: api.CancelImg,
          filePath: path[i],
          name: 'file',
          header: { "Content-Type": "multipart/form-data" },
          success: res => {
            test.push(res);
            wx.setStorageSync('cancelImg',test)
            console.log(test)
            if (res.statusCode != 200) {
              wx.showModal({
                title: '提示',
                content: '上传失败',
                showCancel: false
              })
              return;
            }else {
                wx.showModal({
                    title: '提示',
                    content: '上传成功',
                    showCancel: false
                })
            }
          },
          fail: function (e) {
            console.log(e);
            wx.showModal({
              title: '提示',
              content: '上传失败',
              showCancel: false
            })
          },
          complete: function () {
            wx.hideToast();  //隐藏Toast
          }
        })
    }
这个是多个图片上传的方法,单个图片上传的话,把循环去掉就好。主要是因为微信官方默认的就是一次上传一张图片这个很蛋疼。只能这么搞了。。。

# reduce 高级用法

传入参数上一次回调结果当前处理的元素,当前处理的下标,数组

  1. 计算总和

    var arr = [1, 2, 3, 4];
    var sum = arr.reduce(function(prev, cur, index, arr) {
        console.log(prev, cur, index);
        return prev + cur;
    })
    console.log(arr, sum);
    
    打印结果:
    1 2 1
    3 3 2
    6 4 3
    [1, 2, 3, 4] 10
    
  1. 设置上一次回调的初始值

    var  arr = [1, 2, 3, 4];
    var sum = arr.reduce(function(prev, cur, index, arr) {
        console.log(prev, cur, index);
        return prev + cur;
    }0) //注意这里设置了初始值
    console.log(arr, sum);
    
    打印结果:
    0 1 0
    1 2 1
    3 3 2
    6 4 3
    [1, 2, 3, 4] 10
    
  2. 实战,计算订单总价格

       getTotalPrice(){
            return this.orderItems.reduce((pre, item)=>{
                // 返回 回调+item.finalPrice 即为总价格
                const price = accAdd(pre ,item.finalPrice)
                return price
            }, 0)
        }
    
  3. 加入判断条件

        getSatisfactionCount(coupons) {
                return coupons.reduce((pre, coupon) => {
                    if (coupon.satisfaction === true) {
                        return pre + 1
                    }
                    return pre
                }, 0)
            },
    

# 粘贴板

 onCopyGit(event){
    const index = event.currentTarget.dataset.index
    wx.setClipboardData({
      data: this.data.clipborardData[index]
  })
  },

# 音乐播放

# 预览图片

previewImage(event) {
    const cursrc=event.currentTarget.dataset.cursrc
    const ImagList = this.data.spu.spu_img_list.map(item=>item.img)
    wx.previewImage({
      current: cursrc, // 当前显示图片的http链接
      urls:ImagList // 需要预览的图片http链接列表
    })
  },

# SKU SPU

# 基本概念

SPU(Standard Product Unit) 标准化产品 -商品

SKU(Stock Keeping Unit) 库存量单位 -商品的规格,单品

1586505050343

这里的 SPU-这台电脑的信息,SKU-下方可选择配置颜色等

规格名 规格值

规格:
颜色: 暗夜绿 黑色
运存: 64GB 256GB
版本 : 全网通 电信

规格名:颜色
规格值 : 暗夜绿 黑色

变量命名技巧: 尽量不要加前缀领域进行命名 抛开 sku 寻找特定名字

单个规格为一个对象 一个规格组合是一个对象

sku 状态判断

sku 状态判断就像是字典里面查字典,如果查不到组合就是不可选状态,如果有就是可选状态

将 skuList 抽离成一个矩阵 然后进行旋转

金属灰 七龙珠 小号 s
青芒色 灌篮高手 中号 M
青芒色 圣斗士 大号 L
橘黄色 七龙珠 大号 L

# 处理规格数据

实现矩阵转置

  • 遍历二维数组

首先拿到二维数组

_createMatrix(){
    const m = []
    this.sku_list.forEach((sku)=>{
        m.push(sku.specs)
    })
}

创建matrix对象,在对象中定义遍历方法

class Matrix {
  m;
  constructor(m) {
    this.m = m;
  }
  // 获取行列数
  get row() {
    return this.m.length;
  }
  get col() {
    return this.m[0].length;
  }
  // 回调传递参数
  forEach(callback) {
    // 先遍历列 在遍历行可以拿到旋转90度的数组
    for (let j = 0; j < this.col; j++) {
      for (let i = 0; i < this.row; i++) {
        const element = this.m[i][j];
        callback(element, i, j);
      }
    }
  }
}

遍历过程中判断当前列 然后创建 fence 对象并插入数组

// 遍历拿到所有的element 然后初始化fence
  initFence() {
    const matrix = this._createMatrix(this.skuList)
    let CurrentJ = -1
    const fences = []
    matrix.forEach((element, i, j) => {
      // 首先判断当前列是否与currentJ相等
      if (CurrentJ !== j) {
        // 零列开始  然后将每列元素赋给fences 列变为行
        CurrentJ = j
        // 创建fence对象
        const fence = new Fence()
        fences[CurrentJ] = fence
      }
      // 将fence对象的title传入数组
      fences[CurrentJ].pushValuetitles(element.value)
    })
    console.log(fences);
      // [['金属灰',...],['七龙珠',..],[‘小号s’..]]

  }
  • 定义矩阵转置方法实现

    // 矩阵转置
      transpose() {
        let desArray = []
        for (let j = 0; j < this.col(); j++) {
          // 定义行数组
          desArray[j] = []
          for (let i = 0; i < this.row(); i++) {
            // 行数组元素 和原来的i,j位置互换
            desArray[j][i] = this.m[i][j]
          }
        }
        return desArray
      }
    

    然后 fence-group 拿到转置后的矩阵 遍历转置矩阵

    initFence() {
        const matrix = this._createMatrix(this.skuList)
        const fences = []
        // 拿到转置矩阵
        const AT = matrix.transpose()
        // 将矩阵的元素赋给对象属性
        AT.forEach((specs) => {
          const fence = new Fence(specs)
          // 实例化Cell对象 插入到Cells属性中
        fence.init()
          if (this._hasSktech_id() && this._IsSktech_id(fence.title_id)) {
          fence.setFenchSktech(this.skuList)
          }
          // 置于fences中
          fences.push(fence)
        })
        this.fences = fences
      }
    

对应模型业务

fence 则遍历每一个 specs 数组

  init(){
      this.specs.forEach(spec=>{
          const cell = new Cell(spec)
          this.cells.push(cell)
      })
  }

cell()

class Cell {
  title;
  id;
  status = CellStatus.WAITING;
  spec;
  constructor(spec) {
    // 设置属性
    this.title = spec.value;
    this.id = spec.value_id;
    this.spec = spec;
  }

  _getCellCode() {
    return this.spec.key_id + "-" + this.spec.value_id;
  }
}

这样就可以充分利用面向对象的作用 先实例化 将数组当做属性传入,最后返回一个这样的数组 属性充当数组作为对象元素

1586610274779

对 cell 去重

// 去重cell  some,every 区别是some只要有一个条件满足表达式返回true every需要全部的元素
const existed = this.Cells.some(c => {
  return c.id === spec.value_id;
});
if (existed) {
  return;
}

# SKU 状态处理

核心思路

拿到的 fences

1586615412711

sku 算法的目的是为了体验性,

算法的核心:确定禁用状态

三种状态: 选中 未选 禁用

1586616422555

确认禁用状态的总体思路:

首先,所有规格值都是可选的,当用户选择一个规格值时,确认青芒色是否和七龙珠是否在一个sku路径,如果在就是一个路径,如果不在就禁用。然后再选择灌篮高手,确定下一个小号 s 是否存在,确定它的禁用状态。

可能存在的问题:

点击青芒色 先确认青芒色 尺码 六个路径是否已存在 存在可选,不存在则禁用。然后当选择到灌篮高手时,再重新计算青芒色+灌篮高手+尺码的三条路径是否存在,不存在禁用 ,还有反向选择

已选规格的改变 都要计算所有的规格

  1. 已存在的 sku 路径
  2. 待确认的 sku 路径
# 处理已存在的 sku 路径

创建一个 judger 类 ,传入属性fence-group 然后对 spu 循环进行处理

这里我们主要针对 code 这个字段进行处理

this.spu.sku_list.forEach(s => {
  const Skucode = new SkuCode(s.code);
});

code 类

1.对code进行分割
// 2$1-45#3-9#4-14
const SpuIdAndSpec = this.code.split('$')
this.SpuId=SpuIdAndSpec[0]
// 对剩下的规格进行所有可能的组合
const SpecArray = SpuIdAndSpec[1].split('#')
for(let i =1;i<SpecArray.length;i++){
    // 组合所有可能
  const result = combine(SpecArray,i)// 得到的结果是二维数组
  result.map(r=>{
      return r.join('#')
  })
    // 用# 连接所有的二维数组元素
}

1586677237138

然后再把所有的一维数组都连接到定义的空数组

this.seqments = this.seqments.concat(joinedResult);
this.pathDirt = this.pathDirt.concat(SkuCode.seqments);

最终就会得到一个数组包含所有的路径

["1-45", "3-9", "4-14", "1-45#3-9", "1-45#4-14", "3-9#4-14", "1-45#3-9#4-14", "1-42", "3-10", "4-15", "1-42#3-10", "1-42#4-15", "3-10#4-15", "1-42#3-10#4-15", "1-42", "3-11", "4-16", "1-42#3-11", "1-42#4-16", "3-11#4-16", "1-42#3-11#4-16", "1-44", "3-9", "4-14", "1-44#3-9", "1-44#4-14", "3-9#4-14", "1-44#3-9#4-14"]

这样就获取到了所有的已存在的sku路径

# 处理待确认路径(重要)

先定义 cell 的状态:可选 待选 选中

踩坑:wxs 文件不能引入别的 js 文件

小程序开启跨越组件冒泡 :bubbles:true,composed:true

this.triggerEvent("cellTap", { cell: this.propertes.cell }, { bubbles: true, composed: true });

重要的对象

cell;
fences;
Cells;
FenceGroup;

cell: 一个 sku 规格 对象

1586760453456

fences: 一组 Fence 对象 ,一个 Fence 对象(Cells 数组,specs 转置后的原数组,title,titleid)

1586760590859

Cells : 一个规格名下的 一组规格值

1586760649321

FenceGroup : 包含 fences,skuList,spu 的对象

1586760696975

引用类型:

const a = { c: 1 };
const b = a;
b.c = 2;
a.c = 2;

规律

  1. 当前的 cell 不需要判断潜在路径
  2. 对于某个 cell,它的潜在路径是自己加上其他已选中的 cell
  3. 不需要考虑当前行的 cell 是否已选

主体逻辑

主要分为两个部分: 改变点击后的状态 ,改变其他元素的状态

首先 judge 类里面传递 cell 对象,cell 对象是子 cell 组件传递过来的参数的 spec 对象,

然后加入 status 属性.

编写 judge 方法 传递 cell,x,y

改变元素当前的状态,来控制样式的改变

  changeCurrentCellStatus(cell, x, y) {
    if (cell.status === CellStatus.WAITING) {
      this.fenceGroup.fences[x].Cells[y].status = CellStatus.SELECTED
      // pending数组添加一个规格
      this.SkuPending.insertCell(cell, x)
    } else {
      if (cell.status === CellStatus.SELECTED) {
        this.fenceGroup.fences[x].Cells[y].status = CellStatus.WAITING
        this.SkuPending.removeCell(x)
      }
    }
  }

SkuPending 对象:保存从 waiting 变为选中状态的元素的对象,每一行必须只能有一个选中元素,后续选中状态需要用 isSelected 判断

insertCell(cell, x) {
    this.pending[x] = cell
  }
  removeCell(x) {
    this.pending[x] = null
  }
  findSelectedCell(x){
    return this.pending[x]
  }
  isSelected(cell,x){
    const selectCell = this.pending[x]
    if(!selectCell){
      return
    }
    return cell.id === selectCell.id
  }

遍历所有的节点,查找每个元素的潜在路径(待确定的路径)

  1. fence-group类添加遍历cell方法
eachCell(cb){
    for(let i =0;i<this.fences.length;i++){
      for(let j = 0; j<this.fences[i].Cells.length;j++){
        const cell = this.fences[i].Cells[j]
        cb(cell,i,j)
      }
    }
  }
回调函数中执行查找潜在路径方法 就是遍历循环
  1. 查找节点的所有待确认路径

    首先遍历所有的行 拿出 pending 的已选中的元素 ,如果是当前行,cellCode 就拼接出来,如果是当前行当前选中元素就不做任何处理。否则如果是其他行,选中状态就拼接已经选中的 cellCode

    举例: 选中的cell为 1-45 00
    
    i = 0 ,selected = pending[0]:1-45, cellCode = 1-45 return
    
    i = 1,selected =pending[1]null, return null
    
    i = 2,selected =pending[1]null, return null
    
    i = 3,selected =pending[1]null, return null
    
    上面是 x=0,y=0时的遍历 直接是return了,就是没有执行
    
    然后  x=0,y=1
    
    i=0 selected = 1-45,cellCode = 1-44  return
    
    i=1 selected =null return null
    
    ....    这里就是大致的循环思路
    
_findPotentialPath(cell, x, y) {
    // 查找潜在路径
    //* 1.获取当前行已选元素的潜在路径
    const joiner = new Joiner('#')
    for (let i = 0; i < this.fenceGroup.fences.length; i++) {
      const selected = this.SkuPending.findSelectedCell(i)
      // 如果是当前行
      if (x === i) {
        const cellCode = this._getCellCode(cell.spec)
        // 如果当前行的元素已经被选中 就不做任何处理  该判断存在bug,可能同行会存在几个已选元素
        if (this.SkuPending.isSelected(cell,x)) {
          return
        }
        // 拼接器
        joiner.join(cellCode)
      } else {
        // 其他行
        if (selected) {
          // 如果其他行有选中的,就拼接当前行的和其他行已选中的路径
          const selectedCode = this._getCellCode(selected.spec)
          joiner.join(selectedCode)
        }
      }
    }
    return joiner.getStr()
  }

打印出来的潜在路径

1586858352010

  1. 通过查找是否已存在的路径中存在潜在路径,更改元素状态

    this.fenceGroup.eachCell((cell, x, y) => {
      // 在遍历所有节点的回调中拿到潜在路径 九个节点遍历循环九次 每次都去寻找该节点的潜在路径
      const path = this._findPotentialPath(cell, x, y);
      if (!path) {
        // 不去更改当前行已选元素的状态
        return;
      }
      const isIn = this.pathDirt.includes(path);
      // 如果存在 就置为waiting 否则禁用 如果是当前行的已选元素就不用去查找
      if (isIn) {
        this.fenceGroup.fences[x].Cells[y].status = CellStatus.WAITING;
      } else {
        this.fenceGroup.fences[x].Cells[y].status = CellStatus.FORBIDDEN;
      }
    });
    
# 默认 sku 状态

首先获取到 sku,然后将 specs 数组初始化 cell 对象 push 进 pending 数组,

设置默认的规格的状态,首先定义一个方法(传入 cellId 就能改变 cell 的状态)

changeCellStatus(cellID,status){
    this.eachCell((cell)=>{
        if(cell.id === cellID){
            cell.status = status
        }
    })
}

然后在初始化默认 sku 之后,遍历 skuPending 改变 fences 里面的 cells 的状态.

_setdefaultSkuStatus(){
    this.SkuPending.pending.forEach(cell=>{
      this.fenceGroup.eachCell(((c)=>{
        if(c.id === cell.id){
          c.status = 'selected'
        }
      }))
    })
  }

这里不需要调用改变当前行的状态的方法changeCurrentCellStatus()

# 选择联动(重要)

  1. 判断是否是完整路径

    skupending中进行判断,在初始化这个对象时传入默认的规格个数 this.fenceGroup.fences.length

    isIntact(){
        if(this.size !==this.pending.length){
            return false
        }
        // 可能存在数组元素是undefined 这里需要判断
         for (let i = 0; i < this.pending.length; i++) {
          if (this._isEmptyPart(i)) {
            return false
          }
        }
        return true
    }
    
  2. 如果是完整路径的话,在judger中编写直接获取这个完整路径的sku的方法,这里一定要保证pending数组全部都是实例化的cell

    // 首先在skupending中定义方法 拼接每一个元素的code,也就之前在cell模型中定义好的拼接规格名id和规格值id
    
    getSkuCode(){
        // 借助拼接器函数
    const joiner = new Joiner()
    this.pending.forEach(cell=>{
        const cellCode = this._getCellCode()
        joiner.join(cellCode)
    })
        return joiner.getStr()
    }
    
    // 然后在fencegroup中定义方法 参数是全sku路径
    getSkuBySkuCode(code){
        const SkuCode = `${this.spu.id}$${code}`
        this.spu.sku_list.find(s=>{
            return s.code === SkuCode
        })
    }
    
    
  3. 如果是不完整的路径,需要在选择完成完整路径前,给用户提示下一个选择的规格是什么,然后在控制器上方显示少了哪些规格

    skupending模型中定义方法,返回当前的存储的 cell 的规格值

    // 拿到当前选择的元素 规格值数组
    getCurrentSpecValue(){
        const value = this.pending.map(cell=>{
            // 可能有undefined
            return cell? cell.spec.value:null
        })
        return value
    }
    
    // 获取到缺失的规格值数组
    getMisssingSpecKeyIndex(){
        keyindex = []
        // 遍历次数为所有规格数量
         for (let i = 0; i < this.size; i++) {
             // 如果pending数组没有 后面下标的元素
         if(!this.pending[i]){
          keyIndex.push(i)
         }
        }
        return keyIndex
    }
    

// 获取到了缺失规格数组下标,在 judge 中拿到 这些元素的 title getMissingKeys(){ const MissingKeysIndex = this.SkuPending.getMissingSpecKeyIndex() // 返回 title 数组 return MissingKeysIndex.map(index=>{ return this.fenceGroup.fences[index].title }) }


4. 现在我们拿到了 缺失的规格名数组,当前选择的规格值数组,接下来就是`前端渲染`

监听到点击`cell`元素点击事件,逻辑还是蛮复杂的

> 里面的逻辑应该有顺序
>
> 1. 拿到点击的`cell`的对象,`x`,`y`值
> 2. 初始化`Cell对象`,传入拿到的`cell`的规格对象`spec`,这里一定要传入实例化的对象,然后设置状态为拿到的`cell`的`status`
> 3. 拿到初始化的`judger`进行判断路径然后改变`cell`的状态
> 4. 判断是否是全路径,全路径获取`sku`然后绑定数据,渲染前端,判断是否库存充足
> 5. 绑定气泡提示数据 ,重新绑定`fences`数据为`judger.fenceGroup.fences`
> 6. 触发选择联动方法,改变面板上的值

初始化数据,在接收到`detail`页面传过来的`spu`

监听`spu`

> 判断是否是无规格
>
> 无规格 进入无规格的绑定数据
>
> 有规格 进如有规格的绑定数据

```js
observers:{
 spu:function(spu){
     if(!spu){
         return
     }
   if (Spu.isNoSpec(spu)) {
     this.processNoSpec(spu)
   } else {
     this.prcessHasSpec(spu)
   }
   //选择联动绑定初始化数据
   this.triggerSpec()
 }
}

提前写好绑定数据的方法,分为绑定sku,和绑定spu,响应前端

  • 无规格情况: 无规格只有一个sku,所以绑定sku
 processNoSpec(spu) {
      this.setData({
        NoSpec: true,
      })
// 无规格情况下只有一个sku
this.bindSku(spu.sku_list[0])

// 初始化判断库存
     this.setOutOfStock(spu.sku_list[0].stock,this.data.count)
      return
    },
  • 有规格情况:

    获取到FenceGroupjudger,初始化FenceGroup数据,绑定judger方便使用,判断是否有默认的sku,如果有默认规格,就绑定默认规格的sku,如果不是就绑定spu,最后渲染数据

 // 有规格
    prcessHasSpec(spu) {
      const fenceGroup = new FenceGroup(spu)
      fenceGroup.initFence()
      const Judger = new judger(fenceGroup)
      this.setData({ judger: Judger })
      const defaultSku = fenceGroup.getDefaultSku()
      // 如果有默认的sku
      if (defaultSku) {
        this.bindSku(defaultSku)
        this.bindTipData()
        this.setOutOfStock(defaultSku.stock,this.data.count)
      } else {
        this.bindSpu()
        this.bindTipData()
      }
      this.getFences(fenceGroup)
    }
  1. 前端在响应状态变化时的状态改变

    通过wxscelldom绑定不同的样式名

    function statusStyle(status) {
      if (status === "forbidden") {
        return {
          outer: "forbidden",
          inner: ""
        };
      }
      if (status === "selected") {
        return {
          outer: "selected",
          inner: "s-inner"
        };
      }
    }
    module.exports = {
      statusStyle: statusStyle
    };
    
    <view wx:if="{{!NoSpec}}" class="select">
      <text class="left" wx:if="{{isSkuIntact}}">已选择</text>
      <text class="left" wx:else>请选择</text>
      <text class="right" wx:if="{{isSkuIntact}}">{{CurrentValues}}</text>
      <text class="right" wx:else>{{MissingKeys}}</text>
    </view>
    
  2. 判断库存量

    onTapcount(e){
        const count =e.detail.count
        this.setData({
            count
        })
           const sku = this.data.judger.SkuPending.getDetermineSku()
          this.setOutOfStock(sku.stock,count)
        },
    
        isoutOfStock(stock, count) {
          return stock < count
        },
        setOutOfStock(stock,count){
          this.setData({
            outStock:this.isoutOfStock(stock,count)
          })
    }
    
  3. 处理可视规格

    fence-group判断是否spu有可视规格,并且传入一个fenceId是否有可视规格 在初始化时对fence处理

    fence.init();
    if (this._hasSktech_id() && this._IsSktech_id(fence.title_id)) {
      fence.setFenchSktech(this.skuList);
    }
    

    fence模型中定义向cell中插入img属性

    setFenceSktech(skulist){
        // cells 中查找出包含cell的code码的sku
        this.Cells.forEach(c=>{
            this.setFenceImg(c,skulist)
        })
    }
    
    setFenceImg(cell,skulist){
        const SkuCode = cell._getCellCode()
        const Sku = this.skulist.find(s=>s.code.includes(SkuCode))
        if(Sku){
            cell.img = Sku.img
        }
    }
    

    这样就可以拿到可视的cell 解决页面滚动条高度覆盖底部tabbar问题

    计算出页面的可视高度 ,将外层 view 替换成scroll-view,然后给一个高度

# 分类

# 解决滚动条问题

左侧segment滚动条处理,不处理滚动条的话页面会出现滚动条,体验效果差

计算高度;
关闭均分模式;
赋值给l - segment一个高度;

原理就是设置scroll-view的高度,滚动条可以控制,防止盖住自定义tabbar或者解决隐藏页面的滚动条

动态换算 在不同机型下将px转换为rpx

  1. 计算rate 小程序的宽度都是750rpx,高度通过wx.getSystemInfo回调可以获取到,机型的宽度res.screenWidth,rate=750/res.screenWidth
  2. px数值乘以rate即可计算出rpx数值
const getSystemHeight = async function() {
  const res = await promisic(wx.getSystemInfo)();
  return {
    // 可视区域 除去tabbar和导航条
    windowHeight: res.windowHeight,
    windowWidth: res.windowWidth,
    // 设备屏幕高宽
    screenHeight: res.screenHeight,
    screenWidth: res.screenWidth
  };
};
// 获取可用高度 并转换为rpx
const getWindowHeight = async function() {
  const res = await getSystemHeight();
  const windowHeight = px2rpx(res.windowHeight);
  return windowHeight;
};

计算segement高度,然后赋值给组件即可

// 设置segement的高度 高度为可用高度减去搜索框 再减去padding
   async setSegementHeight(){
    const segementHeight =  await getWindowHeight()-82
    this.setData({
      segementHeight:segementHeight
    })
  },

# 一级分类

首先一般电商获取分类数据都是一次性加载全部的数据的,只有那些较大的电商需要点击一级分类然后分别加载不同的二级分类数据。

确定好一次性加载后,在model下定义categories模型,该模型下需要定义三个方法,同样应该是保存状态的方法

  roots = []
  subs = []
  async getHomeCategory(){
   const res = await Http.request({
      url:'/category/all'
    })
    this.roots = res.data.roots
    this.subs = res.data.subs
  }
  // 获取roots
  getCategoryRoots(){
    return this.roots
  }
  // 根据rootID获取subs
  getSubsByRootID(rootID){
    // 切换时传递的ID是字符型 需要转换一下类型
    return this.subs.filter(sub=>sub.parent_id == rootID)
  }
  // 根据rootID获取root
  getRootByRootID(rooID){
    return this.roots.find(root=>root.id == rooID)
  }class Categories {
  roots = []
  subs = []
  async getHomeCategory(){
   const res = await Http.request({
      url:'/category/all'
    })
    this.roots = res.data.roots
    this.subs = res.data.subs
  }
  // 获取roots
  getCategoryRoots(){
    return this.roots
  }
  // 根据rootID获取subs
  getSubsByRootID(rootID){
    // 切换时传递的ID是字符型 需要转换一下类型
    return this.subs.filter(sub=>sub.parent_id == rootID)
  }
  // 根据rootID获取root
  getRootByRootID(rooID){
    return this.roots.find(root=>root.id == rooID)
  }
}

然后再categoryjs 文件中初始化一级分类和二级分类,在这之前需要定义一个模拟的默认的一级分类 ID,这里使用2

async initCategoryData(){
   const categories = new Categories()
   this.data.categories = categories
    await categories.getHomeCategory()
    const roots = categories.getCategoryRoots()
    // 获取默认的一级分类 和二级分类
    const defaultRoot = this.getDefaultRoot(roots)
    const defaultSubs = categories.getSubsByRootID(defaultRoot.id)
    this.setData({
      roots,
      currentSubs:defaultSubs,
      currentBannerImd:defaultRoot.img
    })
    this.setSegementHeight()
  },
  getDefaultRoot(roots){
    let defaultRoot = roots.find(r => r.id === this.data.defaultID)
    if (!defaultRoot) {
      defaultRoot = roots[0]
    }
    return defaultRoot
  },

# 二级分类

利用宫格组件快速搭建二级分类组件,然后接受的数据由分类页面传递

要传递的数据有两个 sunbs bannerImg

当选项卡进行 进行切换时,监听linchange事件拿到activeKey,然后通过不同的 key 来设置当前的值

 // 切换选项卡
  changeTabs(event){
    // 监听事件拿到key
    const key = event.detail.activeKey
    const currentRoot = this.data.categories.getRootByRootID(key)
    const currentSubs = this.data.categories.getSubsByRootID(currentRoot.id)
    this.setData({
      currentSubs,
      currentBannerImd:currentRoot.img
    })
  },

这样就完成了分类页面的

# 搜索

# 通用历史搜索类

使用单例模式
1. 设置最大值
2. 要去重
3. 提供三个方法 保存 获取 清除
class HistoryCkeyWord{
    static MAX_ITEM_COUNT = 20
	keywords = []
constructor(){
    // 初始化keywords
    this.keywords = this._getLocal()
}
    save(keyword){
        // 去重
        const items = this.keywods.filter(k=>k === keyword)
        if(items.length > 0){
            return
        }
        // 长度限制 超长踢旧push进去新的
        if(this.keywords.length >= MAX_ITEM_COUNT){
            this.keywords.pop()
        }
        this.keywords.unshift()
        this.refreshLocal()
    }
    get(){
        return this.keywords
    }
    clear(){
        this.keywords = []
        this.refreshLocal()
    }
	refreshLocal(){
        wx.setLocalStorage(key,this.keywords)
    }
getLocal(){
    const keywords = wx.getLocalStorage(key)
    if(!keywords){
         wx.setLocalStorage(key,[])
        return []
    }
    return kewwords
}
}

js单例模式

构造函数中加入下面代码

constructor(){
    if(typeof 类名.instance === 'object'){
        return 类名.instacne
    }
    // 保存类对象
    类名.instance = this
    return this
    // 永远返回this
}

# 搜索结果

两个地方要显示结果:1.输入搜索词 2.点击标签

# 考虑的情况

需要考虑的点;
空数据;
空格很多;
空搜索结果;
加载中;
点击标签搜索;

输入搜索词回车后和点击标签都要进行搜索

async onConfirm(event){
    // 初始化search 显示结果页
    this.setData({
      search:true
    })
    // 获取输入框值 如果是点击标签输入框的值更换
    const keyword = event.detail.value || event.detail.name
    if(keyword === event.detail.name){
      this.setData({
        value:keyword
      })
    }
    // 如果没有值 或者有空格
    if(!keyword){
      wx.showToast({
        title: '请输入关键词',
        icon: 'none',
        duration:2000
      })
      return
    }
    if(!keyword.trim()){
      console.log('全是空格')
      wx.showToast({
        title: '请输入正确的关键词',
        icon: 'none',
        duration:2000
      })
      return
    }
    // 保存历史
    history.save(keyword)
    this.setData({
      historytags:history.get()
    })
    wx.lin.showLoading({
      type:'flip',
      color:'#157658',
      fullScreen:true
    })
    // 搜索
    const SearchPaging = Search.searchKeywords(keyword)
    const data = await SearchPaging.getMoreData()
    if(!data){
      return
    }
    // 如果是空结果就显示空商品状态页
    wx.lin.renderWaterFlow(data.items)
    if(!data.items.length){
      this.setData({
        status:true
      })
    }
    wx.lin.hideLoading()
  },

标签的dom上要设置name属性为标签的文字,然后监听点击事件再进行搜索关键词,搜索API也是分页的,所以创建一个paging对象实例,然后调用方法。

hottagshistorytags记录标签,当historytags长度不为零才显示. 显示结果复用瀑布流组件

这样就完成了搜索页

# 专题详情页

# 购物车

# 详情页面点击购物按钮事件编写

区分无规格和有规格

第一步 判断是否是无规格商品

无规格商品逻辑: 返回 skulist[0]的 sku,然后编写抛出事件

携带参数为 : orderway,spuId,sku,skuCount

有规格商品:先确认是否是 sku 的满路径 ,如果不是 拿到之前编写的misskeys,然后弹出器跑提示,return

然后直接抛出满路径的 sku 事件

 // 点击加入购物车按钮或者立即购买
    shopping() {
      if(Spu.isNoSpec(this.properties.spu)){
        const sku = this.properties.spu.sku_list[0]
        this.triggerSpuEvent(sku)
        return
      }
        this.shopingHasSpec()
    },

 // 辅助函数 抛出添加购物车的商品信息
triggerSpuEvent (sku) {
  this.triggerEvent("shopping",{
    orderWay:this.properties.orderWay,
    spuID: this.properties.spu.id,
    sku,
    skuCount:this.data.count
  })
},

// 购买与规格商品
shopingHasSpec(){
  if(!this.data.isSkuIntact){
    const missKeys = this.data.judger.getMissingKeys()
    wx.showToast({
      title: `请选择${missKeys.join(',')}`,
      icon: 'none',
      duration:3000
    })
    return
  }
  this.triggerSpuEvent(this.data.judger.getDetermineSku())
}

# 购物车详情和逻辑

购物车模型构建

>1. 添加商品 判断是否超过库存量,然后添加商品(首先获取缓存对象,然后判断是否历史记录中存在该商品,进行处理) 计算选中价格 刷新缓存 >2. 移除商品 传入`skuId` 删除缓存中的`index`下标的商品 计算价格 刷新缓存 >3. 添加辅助方法

添加商品

 addItem(newItem) {
      console.log(newItem)
        if (this._beyondMaxCartItemCount()) {
            throw new Error('超过购物车最大数量限制')
        }
        this._pushItem(newItem)
        this._calCheckedPrice()
        this._refreshStorage()
    }
 // 如果历史缓存存在 就把数量加上
_pushItem(newItem) {
        const cartData = this._getCartData()
        const oldItem = this._findEqualItem(newItem.skuId)
        if (!oldItem) {
            cartData.items.unshift(newItem)
        } else {
            this._combineItem(oldItem, newItem)
        }
    }
// By SkuId 找到历史缓存元素
 _findEqualItem(newSkuId) {
        const cartData = this._getCartData()
        const olditems = cartData.items.filter(item => item.skuId == newSkuId)
        return olditems.length == 0 ? null : olditems[0]
    }
// 刷新缓存
 _refreshStorage() {
        wx.setStorageSync(Cart.STORAGE_KEY, this._cartData)
    }
// 数量渲染
 _combineItem(oldItem, newItem) {
        this._plusCount(oldItem, newItem.count)
    }

    _plusCount(item, count) {
        item.count += count
        if (item.count >= Cart.SKU_MAX_COUNT) {
            item.count = Cart.SKU_MAX_COUNT
        }
    }

// 获取购物车对象
_getCartData() {
        if (this._cartData !== null) {
            return this._cartData
        }
        let cartData = wx.getStorageSync(Cart.STORAGE_KEY)
        if (!cartData) {
            cartData = this._initCartDataStorage()
        }
        this._cartData = cartData
        return cartData
    }

// 初始化购物车对象
    _initCartDataStorage() {
        const cartData = {
            items: []
        }
        wx.setStorageSync(Cart.STORAGE_KEY, cartData)
        return cartData
    }

    _beyondMaxCartItemCount() {
        const cartData = this._getCartData()
        return cartData.items.length >= Cart.CART_ITEM_MAX_COUNT
    }

// 计算选中价格
 _calCheckedPrice() {
        const cartItems = this.getCheckedItems()
        if (cartItems.length == 0) {
           this.checkedPrice = 0
           this.checkedCount = 0
           return
        }
        this.checkedPrice = 0
        this.checkedCount = 0
        let partTotalPrice = 0
        for (let cartItem of cartItems) {
            if (cartItem.sku.discount_price) {
                partTotalPrice = accMultiply(cartItem.count, cartItem.sku.discount_price)
            } else {
                partTotalPrice = accMultiply(cartItem.count, cartItem.sku.price)
            }
            this.checkedPrice = accAdd(this.checkedPrice, partTotalPrice)
            this.checkedCount += cartItem.count
        }
    }
// 获取选中元素数组
    getCheckedItems() {
        const cartItems = this._getCartData().items
        const checkedCartItems = []
        cartItems.forEach(item=>{
            if(item.checked){
                checkedCartItems.push(item)
            }
        })
        return checkedCartItems
    }

移除商品

通过skuid 删除元素

removeItem(skuId) {
        const oldItemIndex = this._findEqualItemIndex(skuId)
        const cartData = this._getCartData()
        cartData.items.splice(oldItemIndex, 1)
        this._calCheckedPrice()
        this._refreshStorage()
    }

辅助方法

// 是否处于全选状态
isAllChecked() {
        let allChecked = true
        const cartItems = this._getCartData().items
        for (let item of cartItems) {
            if (!item.checked) {
                allChecked = false
                break
            }
        }
        return allChecked
    }

// 全选
    checkAll(checked) {
        const cartData = this._getCartData()
        cartData.items.forEach(item => {
            item.checked = checked
        })
        this._calCheckedPrice()
        this._refreshStorage()
    }
// 获取选中的元素数组
  getCheckedItems() {
        const cartItems = this._getCartData().items
        const checkedCartItems = []
        cartItems.forEach(item=>{
            if(item.checked){
                checkedCartItems.push(item)
            }
        })
        return checkedCartItems
    }

// 选中元素
 checkItem(skuId) {
        const oldItem = this._findEqualItem(skuId)
        oldItem.checked = !oldItem.checked
        this._calCheckedPrice()
        this._refreshStorage()
    }

// 更新商品的数量 传入skuId count
    replaceItemCount(skuId, newCount) {
        const oldItem = this._findEqualItem(skuId)
        if (!oldItem) {
            console.error('异常情况,更新CartItem中的数量不应当找不到相应数据')
            return
        }
        if (newCount < 1) {
            console.error('异常情况,CartItem的Count不可能小于1')
            return
        }
        oldItem.count = newCount
        if (oldItem.count >= Cart.SKU_MAX_COUNT) {
            oldItem.count = Cart.SKU_MAX_COUNT
        }
        this._calCheckedPrice()
        this._refreshStorage()
    }

// 判断是否缓存为空数组
    isEmpty() {
        const cartData = this._getCartData()
        return cartData.items.length === 0;
    }
// 判断是否售空
   static isSoldOut(item) {
        return item.sku.stock === 0
    }
// 判断上架
    static isOnline(item) {
        return item.sku.online
    }
// 从本地数据获取购物车数据
    getAllCartItemFromLocal() {
      console.log(this._getCartData())
        return this._getCartData()
    }
// 从服务端获取购物车对象数组
    async getAllSkuFromServer() {
        const cartData = this._getCartData();
        if (cartData.items.length === 0) {
            return null
        }
        const skuIds = this.getSkuIds()
        const serverData = await Sku.getSkusByIds(skuIds)
        this._refreshByServerData(serverData)
        this._calCheckedPrice()
        this._refreshStorage()
        return this._getCartData()
    }
// 获取购物车商品数量
    getCartItemCount() {
        return this._getCartData().items.length
    }
// 获取skuids数组
    getSkuIds() {
        const cartData = this._getCartData()
        if (cartData.items.length === 0) {
            return []
        }
        return cartData.items.map(item => item.skuId)
    }
// 获取选中的商品的skuIds
    getCheckedSkuIds() {
        const cartData = this._getCartData()
        if (cartData.items.length == 0) {
            return []
        }
        const skuId = []
        cartData.items.forEach(item => {
            if (item.checked) {
                skuId.push(item.sku.id)
            }
        })
        return skuId
    }
// 通过skuId获取购物车该商品数量
    getSkuCountBySkuId(skuId) {
        const cartData = this._getCartData()
        const item = cartData.items.find(item => item.skuId === skuId)
        if (!item) {
            console.error('在订单里寻找CartItem时不应当出现找不到的情况')
        }
        return item.count
    }
//配合服务端数据 刷新服务端数据
    _refreshByServerData(serverData) {
        const cartData = this._getCartData()
        cartData.items.forEach(item=>{
            this._setLatestCartItem(item, serverData)
        })
    }
//配合服务端数据  刷新单个sku数据
    _setLatestCartItem(item, serverData) {
        let removed = true
        for (let sku of serverData) {
            if (item.skuId === sku.id) {
                removed = false
                item.sku = sku
                break
            }
        }
        if(removed){
            item.sku.online = false
        }
    }


# 购物车页面编写

  1. detail页面进行判断

    如果按钮是加入购物车,实例化购物车和商品模型 然后添加商品,更新detail页面购物车商品数量,刷新角标

  2. cart页面

    cart_item:

    sku,skuId,count,checked

    cart-item组件 加上 空页面组件 加上底部价格计算跳转结算页面 和全选框

    设置 是否全选 是否显示红点 设置总数量 总价格 购物车数据

    1. 页面加载时 重新加载服务端购物车数据

    2. 本地获取购物车数据 如果为空就隐藏角标 不为空进行的操作是:绑定数据 判断是否全选绑定数据 重新计算价格数量并绑定

    3. 监听商品组件传来的删除(判断是否全选 刷新缓存),选中(判断是否全选 刷新缓存),数量改变事件(刷新缓存)

    4. 订单跳转按钮逻辑

    5. 全选按钮事件 获取到checkbox传来的 checked 然后调用模型全选方法,绑定数据(购物车商品 总价 总数)

  1. cart-item

    需要使用到l-slide-view组件

    : `checkbox`组件 商品图片(下架 售空 仅剩库存判断) 打折标签 标题 规格处理 价格 数量选择器
    右: 删除商品按钮
    
    

    首先初始化数据 监听到传过来的cartItem对象

    1. 初始化并绑定数据 :规格值,是否打折 ,是否售完 是否下架 库存量 选择的 sku 数量

    2. 监听删除事件 调用移除方法 向外抛出删除事件 获取skuId调用删除方法 然后绑定数据 再抛储事件

      onDelete(event) {
            const skuId = this.properties.cartItem.skuId
            cart.removeItem(skuId)
            this.setData({
              cartItem: null
            })
            this.triggerEvent('itemdelete', {
              skuId
            })
          },
      
    1. 监听选中事件 调用选中方法 向外抛出选中事件

    2. 监听数量选择器事件 更新购物车数量方法 向外抛出数量变化事件

# 通用购物车页面大总结:

第一.构建购物车模型:

提供以下接口
-- 初始化购物车数据
主要逻辑: 创建一`cartData`对象,包含`items`数组,写入缓存,返回对象

-- 私有获取购物车数据(用于模型内部调用)
主要逻辑: 首先获取初始化的`_cartData`属性,判断不为空返回属性值,判断为空再判断缓存对象是否为空,为空就初始化购物车数据,最后绑定`_carData`等于缓存对象,返回`carData`

-- `添加商品进入购物车`
主要逻辑: 判断是否当前购物车数组超过`库存量`,然后书写`添加商品方法`(首先获取缓存对象,然后判断是否历史记录中存在该商品,进行处理(传入`skuId`,获取缓存数组已存在的第一个`Item`,然后增加(加入购物车的商品的数量)数量) 计算选中商品价格 刷新缓存

-- `移除某个商品出购物车`
主要逻辑:移除商品 传入`skuId`  删除缓存中的`index`下标的商品 计算选中商品价格 刷新缓存

-- `计算选中的商品的价格和数量`
主要逻辑: 拿到选中的商品数组,如果不存在把初始值归零, 遍历前赋给变量的值为零,遍历数组,算出`折扣价格`和或者价格和,里面要使用到`浮点运算辅助方法`-- 获取选中的商品的数组
主要逻辑: 获取缓存对象 ,遍历数组,如果存在`checked`,往数组push元素

-- 判断是否处于全选状态
主要逻辑: 遍历缓存数组 判断如果有一个元素的`checked``false`就返回false,默认为true

-- 判断购物车是否为空
主要逻辑: cartData.items.length === 0

-- `更新购物车缓存中商品数量`
主要逻辑: 传入skuId和count,获取已存在的商品,判断极端情况:1.不存在oldItem,2.传入的count < 1  3.先把oldItem的count设为newCount 判断count是否大于购物车最大值 超过置同

-- 判断是否售空
主要逻辑: 传入item item.sku.stock < 0

-- 判断是否下架
主要逻辑: 传入item item.sku.online

-- `从服务端获取购物车对象`
主要逻辑: 获取缓存数组,不存在数组就返回null,再获取购物车的skuId数组,从服务端获取sku数组,刷新数据,计算价格 最后返回对象

// 服务端获取购物车数据 先获取数据,再更新数据,再返回
  async getServerCartData() {
    let Ids = this.getSkuIds()
    const serverData = await Sku.getSkubySKuIds(Ids)
    const cartData = this._getCartData()
    if(!cartData.items.length){
      return null
    }
    this._refreshByServerData(serverData)
    this._calCheckedPrice()
    return this._getCartData()
  }
  // 更新原有的购物车数据
  _refreshByServerData(serverData){
    const cartData = this._getCartData()
    cartData.items.forEach(item=>{
      this._setLatestCartItem(item,serverData)
    })
  }
  _setLatestCartItem(item,serverData){
    // 更新单个数据的sku
    serverData.forEach(sku=>{
      if(sku.id === item.skuId){
        item.sku = sku
      }
    })
  }

-- 从本地缓存中获取购物车对象
主要逻辑:调用方法

-- 获取到当前购物车的skuId数组
主要逻辑: 遍历缓存数组,`映射`skuId数组

-- 获取选中的商品的skuId数组
主要逻辑: 加一层判断 item.checked

-- 通过skuId获取该商品选择的数量
主要逻辑: 传入skuId 数组中查找

-- `全选`
主要逻辑: 获取缓存 遍历数组把所有元素checked设为传入的checked 重新计算价格数量 刷新缓存

-- `单选`
主要逻辑: 获取缓存中该对象 计算价格 刷新缓存

第二.搭建购物车页面逻辑

搭建基本骨架
<!--购物车主体-->
<view wx:if="{{!isEmpty}}" class="container">
  <block wx:for="{{cartItems}}" wx:key="{{index}}">
    <s-cart-item
      bind:deleteItem="deleteCartItem"
      bind:checkItem="checkCartItem"
      bind:changeCount="changeCartItemCount"
      cart-item="{{item}}"
    ></s-cart-item>
  </block>
</view>
<!-- 购物车为空页面 -->
<view class="empty-container" wx:if="{{isEmpty}}">
  <s-empty show-btn show text="购物车空空的,去逛逛吧" btn-text="去逛逛"></s-empty>
</view>
<!-- 底部结算框 -->
<view wx:if="{{!isEmpty}}" class="total-container">
  <view class="data-container">
    <view class="checkbox-container" wx:if="{{!isEmpty}}">
      <l-checkbox-group bind:linchange="checkAll">
        <l-checkbox key="1" checked="{{allChecked}}" size="40rpx" select-color="#157658" color="#DCEBE6"></l-checkbox>
      </l-checkbox-group>
      <text>全选</text>
    </view>
    <view class="price-container">
      <text>合计</text>
      <l-price value="{{totalPrice}}" color="#157658" value-size="38" unit-size="38"></l-price>
    </view>
  </view>
  <view bind:tap="onSettle" class="settlement-btn {{totalCount===0?'disabled':''}}">
    <text>结算 ( {{totalCount}} )</text>
  </view>
</view>
<view style="height: 100rpx"></view>

创建单个商品组件s-cart-item

基本上都有复选框,商品描述,商品数量选择器,侧滑菜单

<!-- 处理价格和处理库存量判断wxs -->
<wxs src="../../wxs/price.wxs" module="p"></wxs>
<wxs src="../../wxs/stock.wxs" module="s"></wxs>

<l-slide-view wx:if="{{cartItem}}" height="220" width="750" slide-width="200">
  <view slot="left" class="container">
    <!-- 复选框 -->
    <view class="checkbox">
      <l-checkbox-group bind:linchange="selectCheckBox">
        <l-checkbox key="1" checked="{{isChecked}}" size="40rpx" select-color="#157658" color="#DCEBE6"></l-checkbox>
      </l-checkbox-group>
    </view>
    <!-- 商品描述 -->
    <view class="skuContainer" catch:tap="todetail">
      <view class="image-container">
        <view wx:if="{{!online}}" class="image-sold-out">
          <text>下 架</text>
        </view>
        <view wx:elif="{{soldOut}}" class="image-sold-out">
          <text>售 罄</text>
        </view>
        <view wx:elif="{{s.shortage(cartItem.sku.stock)}}" class="image-stock-pinch">
          <text>仅剩{{cartItem.sku.stock}}件</text>
        </view>
        <image mode="aspectFit" class="image" src="{{cartItem.sku.img}}" />
      </view>
      <view class="info {{soldOut?'disabled':''}}">
        <view class="desc">
          <view class="tag-container">
            <l-tag
              wx:if="{{discount}}"
              l-class="discount-tag"
              size="mini"
              bg-color="#c93756"
              shape="circle"
              type="reading"
              height="24"
            >
              打折
            </l-tag>
          </view>
          <text class="title">{{cartItem.sku.title}}</text>
        </view>
        <view class="spec">
          <text class="spec-text" wx:if="{{specStr.length}}">{{specStr}}</text>
        </view>
        <view class="bottom">
          <!--商品价格-->
          <view class="price-container">
            <l-price
              unit-size="32"
              value-size="32"
              l-class="price"
              color="#157658"
              value="{{p.mainPrice(cartItem.sku.price,cartItem.sku.discount_price)}}"
            ></l-price>
          </view>
          <!--商品数量选择器-->
          <view class="counter">
            <s-counter catch:lintap="onChangeCount" max="{{stock}}" count="{{skuCount}}"></s-counter>
          </view>
        </view>
      </view>
    </view>
  </view>
  <view slot="right" catch:tap="onDelete" class="slide">
    <text>删除</text>
  </view>
</l-slide-view>

单个商品组件需要做的事情:

  1. 初始化数据 ,通常监听cartItem数据,接收到specs将规格转化为字符串,确定是否打折,确定售完状态,确定上架状态,来控制左边图片状态显示区域的样式,初始化绑定数据有 specStrdiscount``soldout,online,stock确定是库存不足状态,skuCount确定数量选择器初始数量。

  2. 监听滑块选择器删除事件,获取skuId,调用remove方法,将 cartItem 值为空,向外抛出删除事件

  3. 监听数量选择器事件,调用更新数量方法,向外抛出更新数量选择器

  4. 监听选中事件 ,调用模型的选中单个元素方法,设置当前的checked为监听到的detal.checked更改复选框样式,向外抛抛出事件

然后页面监听到这三个事件后需要做的事情

  1. 首先在app.js文件中初始化购物车模型,如果不为空就设置红色角标 tabBar

  2. 购物车页面首次加载获取服务端购物车数据

  3. 购物车onShow钩子获取缓存中的购物车对象数组,判断是否为空进行绑定,然后不为空,设置角标显示,*绑定不为空数据,重新计算价格个数量,然后判断是否处于全选状态,重新渲染数量和价格

  4. 监听商品组件传来的删除(判断是否全选 刷新缓存),选中(判断是否全选 刷新缓存),数量改变事件(刷新缓存)

  5. 全选事件,获取当前checkbox点击后的状态,调用全选方法

checkAll(event) {
   const checked = event.detail.checked
   // 设置当前复选框状态
   this.setData({
     allChecked: checked,
   })
   cart.checkALl(checked)
    // 重新计算价格数量 购物车数组
   this.setData({
     cartItems: this.data.cartItems,
     totalPrice: cart.checkedPrice,
     totalCount: cart.checkedCount,
   })
 },


  1. 部分方法需要调用的是否是全选方法

    // 判断是否全选
      isAllChecked(){
        if (cart.isAllChecked()) {
          this.setData({
            allChecked: true,
          })
        } else {
          this.setData({
            allChecked: false,
          })
        }
      },
    

这样一个完整的购物车就构建完成了,之后设置商品的点击事件跳转购物车详情。

# 优惠券页面

优惠券分为两种类型活动优惠券,分类优惠券,活动优惠券在活动页面用于展示,可以领取优惠券等,分类优惠券用于订单页面核算价格使用。

首先创建coupon页面和coupon组件

# Coupon 页面

首先在入口的页面设置跳转的的页面优惠券类型和跳转时活动的名称,然后获取优惠券数据

onLoad:async function(options){
     // 判断传递的type和活动名
    const type = options.type
    const activityName = options.aName
    if(type === CouponType.ACITYITY){
      const coupons = await Activity.getCounponsByActivityName(activityName)
      this.setData({
        coupons:coupons.coupons
      })
}

然后传给子组件数据: coupon优惠券子项,is_collected 是否领取

子组件接受status将 boolean 类型转为Number类型,因为定义的优惠券状态为Number类型的,好判断

# Coupon 组件

首先监听到页面传来的couponstatus,然后绑定初始化的值,这里我们需要提前定义好枚举来代表四种状态:

  • 可领取-0
  • 可用-1
  • 已用-2
  • 已过期-3

监听优惠券领取事件

首先判断用户是否领取过优惠券,如果已经领取了跳转到分类页面,如果没有领取,调用领取优惠券接口,然后绑定状态

async onGetCoupon(event){
      if(this.data.HasCoupon || this.data._status === CouponStatus.AVALIABLE){
        wx.switchTab({
          url:"/pages/category/index"
        })
        return
      }
      // 领取优惠券
      const couponId = event.currentTarget.dataset.id
      // 防止重复领取
      let msg
      try{
      msg = await Coupon.CollectCoupon(couponId)
      }catch(e){
        // 已经领取 获取异常错误码
        if(e.errCode === 40006){
          this.seUserCollected()
          wx.showToast({
            title: '已经领取过这张优惠券了',
            icon: 'none',
            duration:2000
          })
        }
        return
      }
      // 领取成功
      if(msg.code === 0){
        this.seUserCollected()
        wx.showToast({
          title: '领取成功,在"我的优惠券"中查看',
          icon: 'none',
          duration:2000
        })
      }
    },
    seUserCollected(){
      this.setData({
        HasCoupon:true,
        _status:CouponStatus.AVALIABLE
      })
    }

  }

优惠券组件的布局比较难,可见css是一门玄学,需要多加练习

# 订单结算

# 收货地址模块(通用微信授权处理)

创建address模块

  1. 获取缓存内容(如果为空 返回 null)
  2. 设置缓存对象
# 收货地址组件

主要逻辑

一般获取到微信授权提供的信息都是这样的开发流程:

  • 创建一个模型,提供设置缓存和获取的方法
  • 事件获取信息时,判断是否已经获得权限,如果没有获取一般都需要弹出获取权限的按钮
  • 调用 wx 的方法,绑定数据,然后写入缓存,最后渲染到页面上
  • 每次组件或页面加载时 ,判断缓存数据即可,然后绑定数据
  1. 组件被创建时( lifetimes:{ attached(){)获取内存中地址数据,如果已经有数据了就绑定地址数据和 抛出地址存在事件
  2. 监听点击事件,用户点击时首先判断该用户的授权状态,如果授权状态是成功的,获取地址的详情信息,然后绑定数据,如果是未授权就吊起面板授权
  3. 编写这三个接口: 1.点击事件 2.获取用户的地址信息绑定 3. 判断用户是否授权

点击事件方法

async onChooseAddress(event) {
          // 判断用户授权状态
            const authStatus = await this.hasAuthorizedAddress()
            if(authStatus === AuthAddress.DENY){
                this.setData({
                    showDialog:true
                })
                return
            }
            this.getUserAddress()
        },

拉起选择地址面板,获取用户的地址信息,并绑定

// 这一步前提条件是已经授权地址信息 但是还是要捕获异常
async getUserAddress(){
    let res
    try{
        res = await promisic(wx.chooseAddress)()
    }(e){
        console.log(e)
    }
    // 绑定数据
    if(res){
        this.setData({
            hasChosen:true,
            address:res
        })
        // 写入缓存
        Address.setLocal(res)
        this.triggerEvent("address",{
            address:res
        })
    }

}

判断用户授权

async hasAuthorizedAddress(){
    // wx.getSetting()转为promise  获取用户设置对象
    const setting = await promisic(wx.getSetting)()
    // 获取是否授权地址信息 返回真假
    const addressSetting = setting.authSetting['scope.address']

    // 考虑三种情况 授权 拒绝授权 未知权限
      if(addressSetting === undefined){
                return AuthAddress.NOT_AUTH
            }
            if(addressSetting === false){
                // 未授权 吊起面板授权
                return AuthAddress.DENY
            }
            if (addressSetting === true) {
                return AuthAddress.AUTHORIZED
            }
}

getSetting 获取用户设置信息,openSetting() 获取权限 ,chooseAddress() 拉起地址选择面板

authSetting 用户授权结果  都是些boolean值
 res.authSetting = {
       "scope.userInfo": true,
       "scope.userLocation": true,
	   "scpre.address":true
    }

1589383628658

1589383391036

# 订单模型

订单子项模型构建

订单子项包含属性:

- title
- img
- skuId
- stock
- online
- specs 规格
- count
- finalPrice 总价格
- singalFinalPrice 单品价格
- rootCategoryId 根分类 id
- categoryId 子分类 id
- isTest
- cart 购物车实例
  1. 设置order-item属性
constructor(sku, count) {
        this.title = sku.title
        this.img = sku.img
        this.skuId = sku.id
        this.stock = sku.stock
        this.online = sku.online
        this.categoryId = sku.category_id
        this.rootCategoryId = sku.root_catgory_id
        this.specs = sku.specs

        this.count = count

        this.singleFinalPrice = this.ensureSingleFinalPrice(sku)
        this.finalPrice = accMultiply(this.count, this.singleFinalPrice)
    }
  1. 提供这几个接口
- 检查订单是否正常(库存量检测,购物车数量检测)
- 计算单品sku的最后的价格
  1. 订单异常捕获

订单模型

订单模型传入订单子项数组对象和订单的sku的数量,然后进行订单异常检测

class Order {
  orderItems;
  localOrdernum;
  constructor(orderItems, localOrdernum) {
    this.orderItems = orderItems;
    this.localOrdernum = localOrdernum;
  }
  checkOrderIsOk() {
    // 子项检查异常
    this.orderItems.forEach(item => {
      item.checkOrderItemIsOK();
    });
    // 订单数组检查异常
    this._orderIsOk();
  }

  //* 需要检查 空订单 购物车订单数与服务器订单数不同处理
  _orderIsOk() {
    this._isEmptyOrder();
    this._containNotSaleItem();
  }

  _isEmptyOrder() {
    if (this.orderItems.length === 0) {
      throw new OrderException("订单中没有商品", OrderExceptionType.EMPTY);
    }
  }
  _containNotSaleItem() {
    if (this.localOrdernum !== this.orderItems.length) {
      throw new OrderException("商品数量核验不正确,可能商品下架", OrderExceptionType.NOT_ON_SALE);
    }
  }
}
# 订单页面

订单页面需要时刻 获取服务器选中的商品数组,然后实例化订单模型,一个选中的购物车对象对应一个订单子模型,最后实例化订单数组

  • 获取购物车选中商品skuids数组,拿到本地订单数量
  • 获取服务端商品数组,实例化orderItems模型(遍历数组,传入countsku,实例化order子项)
  • 实例化订单模型,绑定数据,检测订单异常
onLoad(){
    let orderItems
    let localItemCount
    // 获取本地购物车的选中的sku数组
    const skuIds  = cart.getCheckedSkuIds()
    // 获取服务器sku数组,然后实例化订单数组
    orderItems = this.getServerOrderItems(skuids)
    // 获取本地订单数量
    localItemCount = skuIds.length
    // 实例化订单模型
    const order = new Order(orderItems, localItemCount)
    try {
     // 检查订单是否正常
      order.checkOrderIsOk()
    } catch (e) {
      console.error(e)
      return
    }
}
   async getServerOrderItems(skuIds){
    const skus = await Sku.getSkubySKuIds(skuIds)
    const orderItems = skus.map((sku)=>{
      // 实例化order子项 需要传入选中的sku数量
      const count = cart.getSkuCountBySkuId(sku.id)
      return new OrderItem(sku,count)
    })
    return orderItems
  },
# 订单结算逻辑(最复杂)
# Order 模型方法

进入订单计算页面 添加order模型接口方法

  • 计算订单的总价格
  • 计算订单在使用分类优惠券后的总价格
getTotalPrice(){
    return this.orderItems.reduce((pre,order)=>{
        // 引入计算浮动方法
        cosnt price = accAdd(pre,order.finalPrice)
        return price
    },0)
}

首先将分类列表数组传入,然后计算每个分类元素的订单总价,将订单子项的categoryId或者rootCategoryId匹配,然后计算出这一分类下的订单总价格,最后将所有分类的订单总价格累加


getTotalPriceByCategoryIdList(categoryIdList) {
        if (categoryIdList.length === 0) {
            return 0
        }
        // 衣服、鞋子、书籍  累加所有分类下的订单总价
        const price = categoryIdList.reduce((pre, cur) => {
            const eachPrice = this.getTotalPriceEachCategory(cur)
            return accAdd(pre, eachPrice)
        }, 0);
        return price
    }

// 计算单个分类下订单总价格
    getTotalPriceEachCategory(categoryId) {
        const price = this.orderItems.reduce((pre, orderItem) => {
            // 匹配分类id
            const itemCategoryId = this._isItemInCategories(orderItem, categoryId)
            if (itemCategoryId) {
                return accAdd(pre, orderItem.finalPrice)
            }
            return pre
        }, 0)
        return price
    }

// 传入categoryId 匹配订单的分类id
    _isItemInCategories(orderItem, categoryId) {
        if (orderItem.categoryId === categoryId) {
            return true
        }
        if (orderItem.rootCategoryId === categoryId) {
            return true
        }
        return false
    }
}
# 优惠券计算业务层

然后构建couponBo层,提供计算使用优惠券后的价格接口。这里也是最复杂的地方

class CouponBO {
  constructor(coupon) {
    this.type = coupon.type;
    this.fullMoney = coupon.full_money;
    this.rate = coupon.rate;
    this.minus = coupon.minus;
    this.id = coupon.id;
    this.startTime = coupon.start_time;
    this.endTime = coupon.end_time;
    this.wholeStore = coupon.whole_store;
    this.title = coupon.title;
    this.satisfaction = false; // 该优惠券是否对订单可以使用

    this.categoryIds = coupon.categories.map(category => {
      return category.id;
    });
  }

  // 参数为订单 判断优惠券是否可用
  meetCondition(order) {
    let categoryTotalPrice;
    if (this.wholeStore) {
      // 全场券无视适用分类
      categoryTotalPrice = order.getTotalPrice();
    } else {
      categoryTotalPrice = order.getTotalPriceByCategoryIdList(this.categoryIds);
    }

    let satisfaction = false;

    switch (this.type) {
      case CouponType.FULL_MINUS:
      case CouponType.FULL_OFF:
        satisfaction = this._fullTypeCouponIsOK(categoryTotalPrice);
        break;
      case CouponType.NO_THRESHOLD_MINUS:
        satisfaction = true;
        break;
      default:
        break;
    }
    this.satisfaction = satisfaction;
  }

  // 传入订单价格,优惠券对象 计算出最后打折的价格
  static getFinalPrice(orderPrice, couponObj) {
    if (couponObj.satisfaction === false) {
      throw new Error("优惠券不满足使用条件");
    }

    let finalPrice;

    switch (couponObj.type) {
      case CouponType.FULL_MINUS:
        return {
          finalPrice: accSubtract(orderPrice, couponObj.minus),
          discountMoney: couponObj.minus
        };
      case CouponType.FULL_OFF:
        const actualPrice = accMultiply(orderPrice, couponObj.rate);
        finalPrice = CouponBO.roundMoney(actualPrice);

        // 元、角、分、厘
        // 1.111 = 1.12
        // 银行家舍入
        // toFixed

        // 98.57 * 0.85
        // accMultiply(orderPrice, 1-couponObj.rate)

        // discountMoney = orderPrice - finalPrice

        const discountMoney = accSubtract(orderPrice, finalPrice);

        // finalPrice = orderPrice - discountMoney
        // orderPrice = discountMoney + finalPrice

        return {
          finalPrice,
          discountMoney
        };

      case CouponType.NO_THRESHOLD_MINUS:
        finalPrice = accSubtract(orderPrice, couponObj.minus);
        finalPrice = finalPrice < 0 ? 0 : finalPrice;
        return {
          finalPrice,
          discountMoney: couponObj.minus
        };
    }
  }
  // 四舍五入
  static roundMoney(money) {
    // 对于小数的约束可能模式有4种:向上/向下取整、四舍五入、银行家模式
    // 前端算法模式必须同服务端保持一致,否则对于浮点数金额的运算将导致订单无法通过验证

    const final = Math.ceil(money * 100) / 100;
    return final;
  }

  _fullTypeCouponIsOK(categoryTotalPrice) {
    if (categoryTotalPrice >= this.fullMoney) {
      return true;
    }
    return false;
  }
}
# 订单结算步骤

初始化数据

  1. 两种情况初始化订单数据:

    • 点击了立即购买直接结算
    • 在购物车选中后结算,初始化订单数组后,实例化订单对象
  2. 调用接口获取我的优惠券数组,然后初始化优惠券业务层

    初始化数据的步骤:

    获取本地购物车商品数量

    获取服务端的选中的商品数组

    实例化订单对象

 onLoad: async function (options) {

    // 分为 立即购买 购物车计算两种方式初始化
    const orderWay = options.orderWay
    const skuId = options.skuId
    const count = options.count

    let orderItems
    let localOrdernum
    if(orderWay === 'buy'){
      localOrdernum = 1
      orderItems = await this.getSingalOrderItem(skuId,count)
    }else{


  // 本地获取选中商品数组 服务器获取数据并实例化订单数组 检测异常
    const skuIds = cart.getCheckedSkuIds()
    localOrdernum = skuIds.length
    orderItems =await this.getServerOrderItems(skuIds)
    }
    const order = new Order(orderItems,localOrdernum)
    try{
      order.checkOrderIsOk()
    }catch(e){
      console.log(e)
      return
    }

    // 获取优惠券业务对象数组
    const coupons = await Coupon.getMyAviableCoupons()
    const counBoList = this.packageCouponBoList(coupons,order)
    console.log(counBoList)
    this.initData(order,counBoList)
  },


  // 初始化优惠券的业务对象 传入当前的订单数组实例化对象
  packageCouponBoList(coupons,order){
    return coupons.map((coupon)=>{
      const couponbo = new CouponBO(coupon)
      couponbo.meetCondition(order)
      return couponbo
    })
  }

优惠券选择器组件

三种状态,禁用,正常,选中

监听传递过来的数据,计算出可以使用的优惠券的数量,将传递过来的coupon数组转换格式,转为格式化时间值并且排好序的数组,绑定数据

监听单选框点击事件,拿到当前点击的currentKey,还有key(反选的时候有用),绑定currentKey用于控制选中行的样式,然后在数组中找到改id的优惠券,向外抛事件,传递当前优惠券对象和操作(选中或未选中)

onCheckRadio(event){
    const currentKey = event.detail.currentKey
    // 获取绑定的key 也就是优惠券id 反选使用
    const key = event.detail.currentKey
    const currentCoupon = this._findCurrentCoupon(currentKey,key)
    // 判断正选还是反选
    const option = this.pickOptionType(currentKey)
    this.setData({
      currentKey
    })
    // 向外抛出选中事件
    this.triggerEvent('checked',{
      currentCoupon,
      option
    })


  },

初始化数据里面格式化事件并且排序置顶元素

CouponView(coupons){
      // 格式化数组
      const couponsView = coupons.map((coupon)=>{
        return {
          id: coupon.id,
          title: coupon.title,
          // 格式化时间
          startTime: getSlashYMD(coupon.startTime),
          endTime: getSlashYMD(coupon.endTime),
          satisfaction: coupon.satisfaction
        }
      })
      // 排序
      couponsView.sort((a,b)=>{
        // 如果满足使用就置顶该元素 返回排序的index
        if(a.satisfaction){
          return -1
        }
      })
      return couponsView
    },

监听优惠券选择器选中事件

获取优惠券选择器传递的优惠券对象,当前是否选择了优惠券

  • 选择了优惠券:计算打折后的价格,获取优惠价格
  • 没有选择你和优惠券:直接获取总价格
 //* 监听优惠券选择事件
  onChecked(event){
    const currentCoupon = event.detail.currentCoupon
    const option = event.detail.option
    if(option === PickType.PICK){
      // 获取优惠券业务对象处理后的价格对象
      const Priceobj = CouponBO.getFinalPrice(this.data.order.getOrderTotalPrice(),currentCoupon)
      // 绑定数据
      this.setData({
        finalTotalPrice:Priceobj.finalPrice,
        discountMoney:Priceobj.discountMoney
      })
    }else{
      this.setData({
        finalTotalPrice:this.data.order.getOrderTotalPrice(),
        discountMoney:0
      })
    }
    console.log(this.data.finalTotalPrice,this.data.discountMoney)

  },

# 目前封装过后使用到的重要对象

# 订单对象

# 优惠券业务对象

# 订单优惠券视图对象

1589767781958

# 购物车对象

# 小程序通用请求异常处理和 token 校验方法

# 创建异常处理类

首先定义好前端的错误信息

// 用于异常处理的code码
const HttpExceptionCode = {
  "-1": "网络中断、超时或其他异常",
  9999: "抱歉,server_error",
  777: "抱歉,no_codes",
  30001: "优惠券没找到",
  10001: "参数异常",
  // 30002
  40006: "您已经领取过该优惠券"
};

然后创建HttpException处理类

class HttpEception extends Error {
  errorCode = 9999;
  statusCode = 500;
  message = "";
  constructor(errorCode, message, statusCode) {
    this.errorCode = errorCode;
    this.message = message;
    this.statusCode = statusCode;
  }
}

# 封装请求处理异常

  • 封装wx.request方法,转为promise,参数包括:url,data,header,refetch,throwError
  • 处理网络请求异常,给出提示
  • 处理 Http 状态码为401,触发二次重刷token
  • 处理404和后端传递的错误码异常
static async request({
    url,
    data,
    method="GET",
    refetch = true,
    throwError = false
  }){
    let res
    try{
        // 转为promise请求
      res = await promisic(wx.request)({
        url:`${config.prodURL}${url}`,
        data,
        method,
        header:{
          "content-type":"application/json",
          // 头部携带token令牌
          'authorization':`Bearer ${wx.getStorageSync('token')}`
        }
      })
    }
    catch(e){
      // 检测网络异常
      if(throwError){
        throw new HttpException(-1,HttpExceptionCode[-1],null)
      }
      Http.showError(-1)
      return null
    }
    // 检测HTTP code码的异常 通常有401 403 404
    // 获取回调返回的状态码
    const HttpCode = res.statusCode.toString()
    if(HttpCode.startsWith("2")){
      return res.data
    }else{
      if(HttpCode === '401'){
        // 触发二次重刷token
          if (refetch) {
            return Http._refetch({
              url,
              data,
              method,
            })
          }
      }else{
        if(throwError){
          // 后端抛出异常返回数据 赋给HttpException
          throw new HttpException(res.data.code,res.data.message,HttpCode)
        }
        if(HttpCode === '404'){
          // 通常404 不会抛出异常
          if(res.data.code !==undefined){
            return null
          }
          return res.data
        }

        //*  异常抛出时(这里是前端的error_code码 并不是后端的code码) 加入错误提示
        const error_code = res.data.errorCode
        Http.showError(error_code,res.data)
      }
    }

  }
  // 二次重刷
  async _refetch(data){
    const token = new Token()
    await token.getTokenFromServer()
    data.refetch = false
    return await Http.request(data)
  }

  // 提示气泡  : 默认气泡 config定义有气泡提示 没有的气泡提示
  // error_code 错误码 error_data 抛出异常返回的data
 static showError(error_code,error_data){
    let tip
    if(!error_code){
      tip = HttpExceptionCode[9999]
    }else{
      if(HttpExceptionCode[error_code]){
        tip = HttpExceptionCode[error_code]
      }else{
        tip = error_data.message
      }
    }
    wx.showToast({
      title: tip,
      icon: 'none',
      duration:3000
    })
  }
}

# 通用的 token 校验方法

  • 获取token,存入内存
  • 校验token
  • 统一获取和校验的方法
class Token {
  constructor() {}
  // 获取`token`
  async getTokenFromServer() {
    // 获取wx的code
    const Loginres = await promisic(wx.login)();
    const code = Loginres.code;
    // 获取token 保存本地
    const res = await promisic(wx.request)({
      url: config.prodURL + "/token",
      data: {
        account: code,
        type: 0
      },
      method: "POST"
    });
    const token = res.data.token;
    wx.setStorageSync("token", token);
    return token;
  }

  // 统一获取和校验的方法 校验内存token
  async verify() {
    const token = wx.getStorageSync("token");
    if (!token) {
      this.getTokenFromServer();
    } else {
      this._verifyToken(token);
    }
  }

  // 校验`token`
  async _verifyToken(token) {
    const res = await promisic(wx.request)({
      url: config.prodURL + "/token/verify",
      data: {
        token
      },
      method: "POST"
    });
    const isValid = res.data.is_valid;
    if (!isValid) {
      this.getTokenFromServer();
    }
  }
}

# 我的页面相对简单

Last update: 7/20/2021, 8:53:54 AM