微信小程序云开发实战:网上商城(四)
前言
本节我们来实现商品展示页面。代码下载
服务端代码实现
上节提到要生成上图所示的商品画面,那我们的商品比原来多需要两个属性:
- 类别
- 是否属于推荐商品
具体实现时,可以在具体商品属性页上添加这两个属性(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 = thisvar 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.fileIDif (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.indexnewTypes.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].goodsTypesvar 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.dataif (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 = 0wx.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 的自定义组件。(这个选项在插件中不可用。)
接下来
下一节我们将会处理购物车