首页
小程序分层结构:
页面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 } 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即可
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,.1)inset; }
|
瀑布流布局和封装分页加载API
智能推荐
分页数据: 正在加载 loading 加载完成 没有更多数据了
分装请求分页的API
封装 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{ 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(){ 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 }
_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 }) }
|
间隔轮播设置
采坑:定义组件一定在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(.7) }
|
小程序原生picker
组件用法

<picker bin:change="binPicekerChange" range-key="nickName" value="{{index}}" range=""{{personList}}>
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=""
阻止小程序下拉出现白条
"window":{ "enablePullDownRefresh":false }
|
小程序图片上传
handleCancelPic() { let id = this.data.dbId; wx.chooseImage({ count: 3, sizeType: ['compressed'], sourceType: ['album', 'camera'], success: res => { 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(); } }) } 这个是多个图片上传的方法,单个图片上传的话,把循环去掉就好。主要是因为微信官方默认的就是一次上传一张图片这个很蛋疼。只能这么搞了。。。
|
reduce高级用法
传入参数上一次回调结果
,当前处理的元素
,当前处理的下标
,数组
计算总和
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
|
设置上一次回调的初始值
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
|
实战,计算订单总价格
getTotalPrice(){ return this.orderItems.reduce((pre, item)=>{ const price = accAdd(pre ,item.finalPrice) return price }, 0) }
|
加入判断条件
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, urls:ImagList }) },
|
SKU SPU
基本概念
SPU(Standard Product Unit) 标准化产品 -商品
SKU(Stock Keeping Unit) 库存量单位 -商品的规格,单品

这里的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){ 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对象并插入数组
initFence() { const matrix = this._createMatrix(this.skuList) let CurrentJ = -1 const fences = [] matrix.forEach((element, i, j) => { if (CurrentJ !== j) { CurrentJ = j const fence = new Fence() fences[CurrentJ] = fence } fences[CurrentJ].pushValuetitles(element.value) }) console.log(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 } }
|
这样就可以充分利用面向对象的作用 先实例化 将数组当做属性传入,最后返回一个这样的数组 属性充当数组作为对象元素

对cell去重
const existed = this.Cells.some(c=>{ return c.id === spec.value_id }) if(existed){ return }
|
SKU状态处理
核心思路
拿到的fences

sku算法的目的是为了体验性,
算法的核心:确定禁用状态
三种状态: 选中
未选
禁用

确认禁用状态的总体思路:
首先,所有规格值都是可选的,当用户选择一个规格值时,确认青芒色是否和七龙珠是否在一个sku路径
,如果在就是一个路径,如果不在就禁用。然后再选择灌篮高手,确定下一个小号s是否存在,确定它的禁用状态。
可能存在的问题:
点击青芒色 先确认青芒色 尺码 六个路径是否已存在 存在可选,不存在则禁用。然后当选择到灌篮高手时,再重新计算青芒色+灌篮高手+尺码的三条路径是否存在,不存在禁用 ,还有反向选择
。
已选规格的改变 都要计算所有的规格
- 已存在的sku路径
- 待确认的sku路径
处理已存在的sku路径
创建一个judger类 ,传入属性fence-group
然后对spu循环进行处理
这里我们主要针对code这个字段进行处理
this.spu.sku_list.forEach(s=>{ const Skucode = new SkuCode(s.code) })
|
code类
1.对code进行分割
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('#') }) }
|

然后再把所有的一维数组都连接到定义的空数组
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规格 对象

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

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

引用类型:
const a ={c:1} const b = a b.c =2 a.c =2
|
规律
- 当前的cell不需要判断潜在路径
- 对于某个cell,它的潜在路径是自己加上其他已选中的cell
- 不需要考虑当前行的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 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 }
|
遍历所有的节点,查找每个元素的潜在路径(待确定的路径)
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) } } } 回调函数中执行查找潜在路径方法 就是遍历循环
|
查找节点的所有待确认路径
首先遍历所有的行 拿出pending的已选中的元素 ,如果是当前行,cellCode就拼接出来,如果是当前行当前选中元素就不做任何处理。否则如果是其他行,选中状态就拼接已经选中的cellCode
举例: 选中的cell为 1-45 0,0
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) { 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) 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() }
|
打印出来的潜在路径

通过查找是否已存在的路径中存在潜在路径,更改元素状态
this.fenceGroup.eachCell((cell, x, y) => { const path = this._findPotentialPath(cell, x, y) if(!path){ return } const isIn = this.pathDirt.includes(path) 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()
选择联动(重要)
判断是否是完整路径
在skupending
中进行判断,在初始化这个对象时传入默认的规格个数 this.fenceGroup.fences.length
isIntact(){ if(this.size !==this.pending.length){ return false } for (let i = 0; i < this.pending.length; i++) { if (this._isEmptyPart(i)) { return false } } return true }
|
如果是完整路径的话,在judger
中编写直接获取这个完整路径的sku
的方法,这里一定要保证pending
数组全部都是实例化的cell
getSkuCode(){ const joiner = new Joiner() this.pending.forEach(cell=>{ const cellCode = this._getCellCode() joiner.join(cellCode) }) return joiner.getStr() }
getSkuBySkuCode(code){ const SkuCode = `${this.spu.id}$${code}` this.spu.sku_list.find(s=>{ return s.code === SkuCode }) }
|
如果是不完整的路径,需要在选择完成完整路径前,给用户提示下一个选择的规格
是什么,然后在控制器上方显示少了哪些规格
在skupending
模型中定义方法,返回当前的存储的cell的规格值
getCurrentSpecValue(){ const value = this.pending.map(cell=>{ return cell? cell.spec.value:null }) return value }
getMisssingSpecKeyIndex(){ keyindex = [] for (let i = 0; i < this.size; i++) { if(!this.pending[i]){ keyIndex.push(i) } } return keyIndex }
getMissingKeys(){ const MissingKeysIndex = this.SkuPending.getMissingSpecKeyIndex() return MissingKeysIndex.map(index=>{ return this.fenceGroup.fences[index].title }) }
|
现在我们拿到了 缺失的规格名数组,当前选择的规格值数组,接下来就是前端渲染
监听到点击cell
元素点击事件,逻辑还是蛮复杂的
里面的逻辑应该有顺序
- 拿到点击的
cell
的对象,x
,y
值
- 初始化
Cell对象
,传入拿到的cell
的规格对象spec
,这里一定要传入实例化的对象,然后设置状态为拿到的cell
的status
- 拿到初始化的
judger
进行判断路径然后改变cell
的状态
- 判断是否是全路径,全路径获取
sku
然后绑定数据,渲染前端,判断是否库存充足
- 绑定气泡提示数据 ,重新绑定
fences
数据为judger.fenceGroup.fences
- 触发选择联动方法,改变面板上的值
初始化数据,在接收到detail
页面传过来的spu
监听spu
判断是否是无规格
无规格 进入无规格的绑定数据
有规格 进如有规格的绑定数据
observers:{ spu:function(spu){ if(!spu){ return } if (Spu.isNoSpec(spu)) { this.processNoSpec(spu) } else { this.prcessHasSpec(spu) } this.triggerSpec() } }
|
提前写好绑定数据的方法,分为绑定sku
,和绑定spu
,响应前端
前端在响应状态变化时的状态改变
通过wxs
给cell
的dom
绑定不同的样式名
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>
|
判断库存量
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) }) }
|
处理可视规格
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){ 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
- 计算
rate
小程序的宽度都是750rpx
,高度通过wx.getSystemInfo
回调可以获取到,机型的宽度res.screenWidth
,rate=750/res.screenWidth
- 将
px
数值乘以rate
即可计算出rpx
数值
const getSystemHeight =async function(){ const res = await promisic(wx.getSystemInfo )() return { windowHeight: res.windowHeight, windowWidth: res.windowWidth, screenHeight: res.screenHeight, screenWidth: res.screenWidth } }
const getWindowHeight = async function(){ const res = await getSystemHeight() const windowHeight = px2rpx(res.windowHeight) return windowHeight }
|
计算segement
高度,然后赋值给组件即可
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 } getCategoryRoots(){ return this.roots } getSubsByRootID(rootID){ return this.subs.filter(sub=>sub.parent_id == rootID) } 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 } getCategoryRoots(){ return this.roots } getSubsByRootID(rootID){ return this.subs.filter(sub=>sub.parent_id == rootID) } getRootByRootID(rooID){ return this.roots.find(root=>root.id == rooID) } }
|
然后再category
js文件中初始化一级分类和二级分类,在这之前需要定义一个模拟的默认的一级分类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){ 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(){ this.keywords = this._getLocal() } save(keyword){ const items = this.keywods.filter(k=>k === keyword) if(items.length > 0){ return } 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 }
|
搜索结果
两个地方要显示结果:1.输入搜索词 2.点击标签
考虑的情况
需要考虑的点 空数据 空格很多 空搜索结果 加载中 点击标签搜索
|
输入搜索词回车后和点击标签都要进行搜索
async onConfirm(event){ 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
对象实例,然后调用方法。
用hottags
和historytags
记录标签,当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) } }
_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() }
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 }
getSkuIds() { const cartData = this._getCartData() if (cartData.items.length === 0) { return [] } return cartData.items.map(item => item.skuId) }
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 }
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) }) }
_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 } }
|
购物车页面编写
detail
页面进行判断
如果按钮是加入购物车,实例化购物车和商品模型 然后添加商品,更新detail
页面购物车商品数量,刷新角标
cart
页面
cart_item
:
sku,skuId,count,checked
cart-item
组件 加上 空页面组件 加上底部价格计算跳转结算页面 和全选框
设置 是否全选 是否显示红点 设置总数量 总价格 购物车数据
页面加载时 重新加载服务端购物车数据
本地获取购物车数据 如果为空就隐藏角标 不为空进行的操作是:绑定数据 判断是否全选绑定数据 重新计算价格数量并绑定
监听商品组件传来的删除(判断是否全选 刷新缓存),选中(判断是否全选 刷新缓存),数量改变事件(刷新缓存)
订单跳转按钮逻辑
全选按钮事件 获取到checkbox
传来的checked 然后调用模型全选方法,绑定数据(购物车商品 总价 总数)
cart-item
需要使用到l-slide-view
组件
左: `checkbox`组件 商品图片(下架 售空 仅剩库存判断) 打折标签 标题 规格处理 价格 数量选择器 右: 删除商品按钮
|
首先初始化数据 监听到传过来的cartItem
对象
初始化并绑定数据 :规格值
,是否打折
,是否售完 是否下架 库存量 选择的sku数量
监听删除事件 调用移除方法 向外抛出删除事件 获取skuId调用删除方法 然后绑定数据 再抛储事件
onDelete(event) { const skuId = this.properties.cartItem.skuId cart.removeItem(skuId) this.setData({ cartItem: null }) this.triggerEvent('itemdelete', { skuId }) },
|
监听选中事件 调用选中方法 向外抛出选中事件
监听数量选择器事件 更新购物车数量方法 向外抛出数量变化事件
通用购物车页面大总结:
第一.构建购物车模型:
提供以下接口
-- 初始化购物车数据 主要逻辑: 创建一`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){ 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 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>
|
单个商品组件需要做的事情:
初始化数据 ,通常监听cartItem
数据,接收到specs
将规格转化为字符串,确定是否打折,确定售完状态,确定上架状态,来控制左边图片状态显示区域的样式,初始化绑定数据有 specStr
,discount``soldout
,online
,stock
确定是库存不足状态,skuCount
确定数量选择器初始数量。
监听滑块选择器删除事件,获取skuId
,调用remove
方法,将cartItem值为空,向外抛出删除事件
监听数量选择器事件,调用更新数量方法,向外抛出更新数量选择器
监听选中事件 ,调用模型的选中单个元素方法,设置当前的checked
为监听到的detal.checked
更改复选框样式,向外抛抛出事件
然后页面监听到这三个事件后需要做的事情
首先在app.js
文件中初始化购物车模型,如果不为空就设置红色角标tabBar
购物车页面首次加载获取服务端购物车数据
购物车onShow
钩子获取缓存中的购物车对象数组,判断是否为空进行绑定,然后不为空,设置角标显示,*绑定不为空数据,重新计算价格个数量,然后判断是否处于全选状态,重新渲染数量和价格
监听商品组件传来的删除(判断是否全选 刷新缓存),选中(判断是否全选 刷新缓存),数量改变事件(刷新缓存)
全选事件
,获取当前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, }) },
|
部分方法需要调用的是否是全选方法
isAllChecked(){ if (cart.isAllChecked()) { this.setData({ allChecked: true, }) } else { this.setData({ allChecked: false, }) } },
|
这样一个完整的购物车就构建完成了,之后设置商品的点击事件跳转购物车详情。
优惠券页面
优惠券分为两种类型活动优惠券
,分类优惠券
,活动优惠券在活动页面用于展示,可以领取优惠券等,分类优惠券用于订单页面核算价格使用。
首先创建coupon
页面和coupon
组件
Coupon页面
首先在入口的页面设置跳转的的页面优惠券类型
和跳转时活动的名称,然后获取优惠券数据
onLoad:async function(options){ 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 组件
首先监听到页面传来的coupon
和status
,然后绑定初始化的值,这里我们需要提前定义好枚举来代表四种状态:
监听优惠券领取事件
首先判断用户是否领取过优惠券,如果已经领取了跳转到分类页面,如果没有领取,调用领取优惠券接口,然后绑定状态
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
模块
- 获取缓存内容(如果为空 返回null)
- 设置缓存对象
收货地址组件
主要逻辑
一般获取到微信授权提供的信息都是这样的开发流程:
- 创建一个模型,提供设置缓存和获取的方法
- 事件获取信息时,判断是否已经获得权限,如果没有获取一般都需要弹出获取权限的按钮
- 调用wx的方法,绑定数据,然后写入缓存,最后渲染到页面上
- 每次组件或页面加载时 ,判断缓存数据即可,然后绑定数据
- 组件被创建时(
lifetimes:{ attached(){
)获取内存中地址数据,如果已经有数据了就绑定地址数据和 抛出地址存在事件
- 监听点击事件,用户点击时首先
判断该用户的授权状态
,如果授权状态是成功的,获取地址的详情信息,然后绑定数据,如果是未授权就吊起面板授权
- 编写这三个接口: 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(){ 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 }
|


订单模型
订单子项模型构建
订单子项包含属性:
- title - img - skuId - stock - online - specs 规格 - count - finalPrice 总价格 - singalFinalPrice 单品价格 - rootCategoryId 根分类id - categoryId 子分类id - isTest - cart 购物车实例
|
设置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) }
|
提供这几个接口
- 检查订单是否正常(库存量检测,购物车数量检测) - 计算单品sku的最后的价格
|
订单异常捕获
订单模型
订单模型传入订单子项数组对象和订单的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
模型(遍历数组,传入count
,sku
,实例化order子项
)
- 实例化订单模型,绑定数据,检测订单异常
onLoad(){ let orderItems let localItemCount const skuIds = cart.getCheckedSkuIds() 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)=>{ 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) => { const itemCategoryId = this._isItemInCategories(orderItem, categoryId) if (itemCategoryId) { return accAdd(pre, orderItem.finalPrice) } return pre }, 0) return price }
_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)
const discountMoney = accSubtract(orderPrice, 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) {
const final = Math.ceil(money * 100) / 100 return final }
_fullTypeCouponIsOK(categoryTotalPrice) { if (categoryTotalPrice >= this.fullMoney) { return true } return false } }
|
订单结算步骤
初始化数据
分两种情况初始化订单数据:
- 点击了
立即购买
直接结算
- 在购物车
选中后结算
,初始化订单数组后,实例化订单对象
调用接口获取我的优惠券数组,然后初始化优惠券业务层
初始化数据的步骤:
获取本地购物车商品数量
获取服务端的选中的商品数组
实例化订单对象
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 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)=>{ 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)
},
|
目前封装过后使用到的重要对象
订单对象

优惠券业务对象

订单优惠券视图对象

购物车对象

小程序通用请求异常处理和token校验方法
创建异常处理类
首先定义好前端的错误信息
const HttpExceptionCode ={ "-1": "网络中断、超时或其他异常", 9999: '抱歉,server_error', 777: '抱歉,no_codes', 30001: '优惠券没找到', 10001: '参数异常', 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{ res = await promisic(wx.request)({ url:`${config.prodURL}${url}`, data, method, header:{ "content-type":"application/json", 'authorization':`Bearer ${wx.getStorageSync('token')}` } }) } catch(e){ if(throwError){ throw new HttpException(-1,HttpExceptionCode[-1],null) } Http.showError(-1) return null } const HttpCode = res.statusCode.toString() if(HttpCode.startsWith("2")){ return res.data }else{ if(HttpCode === '401'){ if (refetch) { return Http._refetch({ url, data, method, }) } }else{ if(throwError){ throw new HttpException(res.data.code,res.data.message,HttpCode) } if(HttpCode === '404'){ if(res.data.code !==undefined){ return null } return res.data }
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) }
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(){} async getTokenFromServer(){ const Loginres = await promisic(wx.login)() const code = Loginres.code 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 }
async verify(){ const token = wx.getStorageSync('token') if(!token){ this.getTokenFromServer() }else{ this._verifyToken(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() } } }
|
我的页面相对简单