微信小程序云开发实战:网上商城(四)
前言
本节我们来实现商品展示页面。代码下载
服务端代码实现
上节提到要生成上图所示的商品画面,那我们的商品比原来多需要两个属性:
- 类别
- 是否属于推荐商品
具体实现时,可以在具体商品属性页上添加这两个属性(recommend、type),也可以设置专门的属性管理页面。前者方便单个控制,后者方便统一配置。对于以前录入的商品,缺乏这两个属性,处理时默认类别为【未分类】,推荐属性为【false】
监控数据库变化
如果进入每个具体商品详细信息页面时都去云端获取商品的类别,就属于重复取数据。因此获取并保存商品类别信息,而且在管理端修改商品类别后更新,而这个商品类别信息以页面互动的方式传递出去。虽然我们也可以使用一个商品类别的全局变量,然后当商品类别有变化时更新它。但我不习惯使用全局变量,而且腾讯提供的云数据库的监视器更有趣。
对云数据库的监控可以参考官方文档。这里我为程序的配置项建立了一张表(虽然目前只有商品类别这一有意义的字段),然后注册一个监视器,当更新configs表的goodsTypes字段时,连带更新程序暂存的商品类比数据。
数据表权限设置:
this.data._db = wx.cloud.database()
this.data._watcher = this.data._db.collection('configs').where({
openid : this.data._openid
}).watch({
onChange : function(snapshot){
if(snapshot.docChanges[0].updatedFields != null
&&snapshot.docChanges[0].updatedFields.goodsTypes != null){
var types = []
snapshot.docChanges[0].updatedFields.goodsTypes.forEach(item =>{
types.push(item.title)
})
self.data._goodsTypes = types
}
},
onError : function(err){
console.error("db watcher:", err)
}
})
商品属性修改
这里复用add-item页面,当加载页面的时候,判断新增、修改这两种情况。修改时,通过页面的事件通道传递商品信息。
在页面的信息展示几乎是与数据表字段一一对应的情况下,使页面数据变量与数据表字段的名称保持一致,将会给批量操作带来便利。这里面也许会有个别字段不需要展示,属于冗余数据,但是缺省去了挨个给展示元素赋值的机械化代码,如果页面元素较多,这种便利将会很可观。特别是现在WXML仍然不支持变量的路径绑定。
if (options.op == "edit") {
const eventChannel = this.getOpenerEventChannel()
eventChannel.on('showItem', function (data) {
var keys = Object.keys(data)
for (var index = 0; index < keys.length; index++) {
console.debug(keys[index], data[keys[index]])
var key = keys[index]
this.setData({
[`${key}`]: data[keys[index]]
})
}
this.setData({
unitIndex: this.data._units.indexOf(data.unit),
previewImageUrls: data.imgs,
_editting: true,
_oldItem: data,
_deletedImgs: [],
title: '修改' + data.name + '信息'
})
})
} else {
this.setData({
previewImageUrls: [],//图片预览时需要的本地路径数组
})
}
给页面展示添加两个属性:
<view class="weui-cell weui-cell_switch">
<view class="weui-cell__bd">推荐</view>
<view class="weui-cell__ft">
<switch model:checked="{{recommend}}" disabled="!{{editable}}" />
</view>
</view>
<mp-cell ext-class="weui-cell_select weui-cell_select-after">
<view slot="title" class="weui-label">类别</view>
<picker bindchange="bindTypeChange" model:value="{{_goodsType}}" range="{{_goodsTypes}}">
<view class="weui-select"> {{type}}</view>
</picker>
</mp-cell>
判断商品信息是否发生了变化:
我最先设想的是追踪商品的所有属性变化,但用户可能修改到最后的值与初始值一致,而且实在太烦琐了。后来实施的做法是在页面加载时保存商品信息到某个变量,如_old_xxx,当发生了修改时,商品信息会更新到_new_xxx变量中对应的属性,在页面unload时,以_new_xxx这个对象的属性集合为基础对比这两个对象的相对应的属性值,如果不同,就确认某个属性最终发生了变化。从技术角度我认为这样处理较为妥当,但后来我认为从实用情况出发,将判断数据是否发生了变化以及是否提交的工作交给用户更为合适,即,只要用户点击“确认”(提交)按钮,程序就上传数据,不再去判断。
管理功能实现
在管理选项页添加两个功能入口:类别、推荐
本来想搞得跟win10的磁贴菜单似的,但本人没有美感,配色怎么弄都觉得难看,样式也看着别扭,先忍着吧。
商品类别管理
该功能采用离开管理页面时更新的方式(可以跟采用用户提交的方式对比一下),向左滑动选项会出现删除按钮。
虽然数据项看起来不多,但这个页面功能上增、删、改、查都涉及了。用户操作上没有限制,退出页面时,可能操作了多个数据项。这里采用的方法是:
- 删除 只是隐藏数据项,记录下索引
- 修改 原始数据保存在_old开头的变量中,供最终对比,也作为区别新增、修改的标识
在页面的unload函数中做的工作比较多,主要是图片要上传云存储,数据库更新成功、失败都要区别处理:
var modifiedTypes = []
var newTypes = []
var deletedTypes = []
console.debug("goodsTypes:", this.data.goodsTypes)
var deletedImgs = [] //如果更新成功,删除项的图片要从云存储删除
var uploadedImgs = [] //如果更新不成功,新上传的图片要删除
var oldImgs = [] //如果更新成功,原有图片要删除
for (var index = 0; index < this.data.goodsTypes.length; index++) {
let self = this
var item = this.data.goodsTypes[index]
if (item.deleted) {//删除的项只需要索引
deletedTypes.push(index)
deletedImgs.push(this.data.goodsTypes[index].img) //保存要删除的图像列表
continue
}
if (Object.keys(item).filter(key => key.startsWith("old_") ).length == 0
&& Object.keys(item).filter(key =>key.startsWith("new_") ).length == 0) {
continue//忽略没有更改过
}
if ( (item.old_title == item.title && item.old_desc == item.desc)//忽略虽然有更改过程但结果与最初数据一致的
|| item.title == ''
|| item.title == null) { //忽略没有标题的
continue
}
var type = {
title: item.title,
desc: item.desc,
img: item.img,
index : index
}
if (!item.img.startsWith("cloud://") || item.old_img != null) {
wx.showLoading({
title: '正在上传图片',
})
var ret = await wx.cloud.uploadFile({
cloudPath: item.imgCloud,
filePath: item.img,
})
if(ret.fileID != null){
uploadedImgs.push(ret.fileID)
}
type.img = ret.fileID
if (item.old_img != null) {
oldImgs.push(item.old_img)
}
wx.hideLoading({
success: (res) => {},
})
}
if (Object.keys(item).filter(key => key.startsWith("old_")).length != 0) {
modifiedTypes.push(type)
}
else {
delete type.index
newTypes.push(type)
}
}
if (modifiedTypes.length == 0
&& newTypes.length == 0
&& deletedTypes.length == 0) {
console.debug("没有更改信息")
return
}
wx.showLoading({
title: '正在变更信息',
})
console.debug(modifiedTypes)
var ret = await wx.cloud.callFunction({
name: 'config',
data: {
cmd: "goodsTypes-set",
data: {
modified: modifiedTypes,
added: newTypes,
deleted: deletedTypes
}
}
})
wx.hideLoading({
success: (res) => {},
})
console.debug(ret)
var imgsTodelete = []
if(ret.result.success ){
imgsTodelete = deletedImgs.concat(oldImgs)
}else{
imgsTodelete = uploadedImgs
}
if(imgsTodelete.length > 0){
var ret = await wx.cloud.deleteFile({
fileList: deletedImgs
})
console.debug("已删除:", ret.fileList)
}
config云函数:
case 'goodsTypes-set' :{
var data = {
openid : wxContext.OPENID,
modified: request.data.modified,
added : request.data.added,
deleted : request.data.deleted
}
console.log("data:", data)
var ret = await col.where({
openid : data.openid
}).count()
console.log("total:",ret.total)
if(ret.total == 0){
//新增配置
console.log("data:", data)
ret = await col.add({
data: {
openid : data.openid,
goodsTypes: data.added
}
})
console.log(ret)
return {
success :true,
data : ret._id
}
}else{
//更新类型列表
ret = await col.field({
goodsTypes : true
}).where({
openid : data.openid
}).get()
console.log(ret)
var goodsTypes = ret.data[0].goodsTypes
var finalTypes = []
if(data.modified.length !=0){
for(var index = 0; index < data.modified.length; index++){
goodsTypes[data.modified[index].index] = data.modified[index]
delete goodsTypes[data.modified[index].index].index
}
}
for(var index =0; index < goodsTypes.length; index++){
if(data.deleted.findIndex(o => o == index) != -1){
console.log("deleted:", index)
continue
}
finalTypes.push(goodsTypes[index])
}
finalTypes = finalTypes.concat(data.added)
console.log("finale types:", finalTypes)
ret = await col.where({
openid : data.openid
}).update({
data:{
goodsTypes : finalTypes
}
})
return {
success : ret.stats.updated == 1
}
}
break;
推荐商品管理
var onList = []
var offList = []
for (var index = 0; index < this.data.goodsInfos.length; index++) {
if (this.data.goodsInfos[index].recommend == this.data._old[index]) {
continue
}
if (!this.data._old[index]) {
onList.push(this.data.goodsInfos[index]._id)
}
else {
offList.push(this.data.goodsInfos[index]._id)
}
}
if(onList.length == 0 && offList.length == 0){
return
}
wx.cloud.callFunction({
name : 'goods-op',
data :{
cmd : 'update-recommend',
onList : onList,
offList : offList
}
}).then(res=>{
console.debug(res)
}).catch(err =>{
console.error(err)
})
case 'update-recommend': {
var onres = await collection.where({
_id: _.in(request.onList)
}).update({
data: {
recommend: true
}
})
var offres = await collection.where({
_id: _.in(request.offList)
}).update({
data: {
recommend: false
}
})
客户端代码实现
为了实现左边的类别tab与商品网格化显示,我们需要引入几个新控件:
实际的商品类别中并没有“推荐”,也没有“未分类”,这两个类别选项卡是页面加载时人为处理出来的:
this.data._goodsTypes.push({ title: "推荐" })
//获取商品分类
var ret = await wx.cloud.callFunction({
name: 'config',
data: {
cmd: 'goodsTypes-get'
}
})
var items = ret.result.data
if (items != null) {
self.data._goodsTypes = self.data._goodsTypes.concat(ret.result.data)
}
self.data._goodsTypes.push({ title: "未分类" })
self.setData({
goodsTypesForVtabs: self.data._goodsTypes.map(item => ({ title: item.title }))
})
this.data.total_price = 0
wx.cloud.callFunction({
name: 'goods-op',
data: {
cmd: 'get-recommend'
}
}).then(res => {
if (res.result.success) {
this.makeShowInfos(res.result.data)//生成展示需要的信息
}
})
展示商品详细信息
关于直接访问数据表的错误:
Unhandled promise rejection Error: document.get:fail document.get:fail cannot find document with _id xxxx, please make sure that the document exists and you have the corresponding access permission
如果出现类似上述的信息,通常是由于在客户端中直接访问云数据库时缺乏相关的权限造成的。只需要在云控制台中赋予非创建者访问权限即可。
商品详细信息展示页此处实现为组件,需要注意样式隔离设置:
参考下官方文档,styleIsolation 选项从基础库版本 2.6.5 开始支持。它支持以下取值:
- isolated 表示启用样式隔离,在自定义组件内外,使用 class 指定的样式将不会相互影响(一般情况下的默认值);
- apply-shared 表示页面 wxss 样式将影响到自定义组件,但自定义组件 wxss 中指定的样式不会影响页面;
- shared 表示页面 wxss 样式将影响到自定义组件,自定义组件 wxss 中指定的样式也会影响页面和其他设置了 apply-shared 或 shared 的自定义组件。(这个选项在插件中不可用。)
接下来
下一节我们将会处理购物车