小白微信小程序&商户端&后台项目总结

写在前面

  这段时间参与了一个微信小程序项目,顺便做了小程序项目对应的商户端和后台管理。现在在网上对所做的项目做个总结,方便自己后续查找和翻阅。如有错误和不足之处,烦请在评论区中指出,不胜感激,谢谢。

微信小程序项目经验总结

1.轮播组件

  微信小程序自带的轮播组件很蛋疼,超出的地方会自动隐藏。正确的写法是:

 <view class="swiper-warp">
     <swiper
         autoplay
         circular
         interval="2000"
     >
         <swiper-item
             wx:for="{{swiperList}}"
             wx:key="index"
         >
         	<base-image
                     width="750rpx"
                     height="750rpx"
                     src="{{item.image}}"
                     fit="widthFix"
                 />
         </swiper-item>
     </swiper>
 </view>
复制代码
//需要对swiper指定宽度和高度,而不是直接对swiper-warp这个父盒子设定高度和宽度
.swiper-warp swiper{
    width: 750rpx;
    height: 750rpx;
}
复制代码

2.监听组件传参触发的方法

  在search-bar组件中:

//clear方法未写出
properties: {
  init: {
    type: Boolean,
    default: false,
    observer: function (val) {
      if(val) this.clear()
    }
  }
},
复制代码

  调用search-bar组件:

<search-bar bind:search="search" init="{{keyword === ''}}"/>
复制代码

  当然,这个案例也可以直接在父组件中调用子组件的方法。但是既然为组件,就需要扩展,最好将处理都写在组件内。

  • 在父组件中,先给调用的子组件加一个id:
 <search-bar bind:search="search" id="search"/>
复制代码
  • 然后在父组件的js文件中,调用子组件中存在的init方法:
 this.selectComponent("#search").clear()
复制代码

3.小程序阻止点击冒泡事件

  处理一个点击事件时,怎样都触发不了事件。最后发现是由于点击向上冒泡将事件冲突掉了,使用catchtap处理即可。

4.使用rich-text解析返回的html代码

 <rich-text nodes="{{product.description}}" class="product-description"></rich-text>
复制代码

5.传递函数参数

  小程序无法直接携带参数,只能通过自定义属性(data-xxx)间接传递参数。在函数中,使用e.target.dataset.xxx进行接收。同时,这些参数可以用来做判断,判断点击的高亮样式以及需要向接口传递的不同参数。传递参数时,可以定义orderParamMap对象来传递。

 <text
   class="{{selectedType === 'priceLowToHigh' || selectedType === 'priceHighToLow' ? 'activeClass' : ''}}"
   data-type="price"
   bindtap="handleClicked"
 >价格
 </text>
复制代码
handleClicked(e) {
  if (e.target.dataset.type === 'price') {
    this.setData({
      selectedType: this.data.lowToHigh ? 'priceLowToHigh' : 'priceHighToLow',
      lowToHigh: !this.data.lowToHigh
    })
  } else {
    this.setData({
      selectedType: e.target.dataset.type
    })
  }
  this.initRequestParams()
},
复制代码
/**
 * 获取产品数据列表
 */
getProductList(name) {
  const params = {
    merchant_id: getApp().globalData.merchant_info.id,
    limit: 6,
    page: this.data.page
  }
  //名称搜索
  if (name) {
    params.name = name
  }
  const orderParamMap = {
    new: {
      direction: "desc",
      order: "id"
    },
    priceLowToHigh: {
      direction: "asc",
      order: "lowest_price"
    },
    priceHighToLow: {
      direction: "desc",
      order: "lowest_price"
    }
  }
  this.requestProductsData(Object.assign({}, params, orderParamMap[this.data.selectedType]))
},
复制代码

6.重构接口返回的数据

  当公共组件中的字段和接口返回的字段不一样时,可以对接口返回的数据字段进行重构。

 this.$api.product.getList(params).then((data) => {
   const list = data.data.map((i) => ({
     url: i.image,
     title: i.name,
     price: i.lowest_price,
     path: `/pages/product-detail/index?product_id=${i.id}`
   }))

   this.setData({
     list: this.data.page === 1 ? list : this.data.list.concat(list),
     totalPage: data.meta.last_page,
     loading: false
   })
   wx.stopPullDownRefresh()
 })
复制代码

7.小程序数据存储

  • 存储数据:
//wx.setStorageSync('为存储的数据命名', 需要存储的数据)
wx.setStorageSync('searchList', list)
复制代码
  • 取数据:
//wx.getStorageSync('需要获取的数据名称')
//获取存储的搜索数组
lifetimes: {
  attached() {
    this.setData({
      searchList: wx.getStorageSync('searchList') || []
    })
  }
}
复制代码
  • 删除数据:
//wx.removeStorageSync('需要删除的数据名称')
//删除小程序中存储的搜索数组
 removeSearchList() {
   this.setData({
     searchList: []
   })
   wx.removeStorageSync('searchList')
 }
复制代码

8.小程序从产品详情页跳回到产品列表,保持原来的位置

  • 问题分析及解决思路: 由于使用onshow调用产品列表,每次打开产品列表页面,都会重新请求并显示第一页的数据。所以此时需要判断页面跳转来源,如果页面是从产品详情页跳转回来,就return掉,否则需要请求接口。因此这个问题就转换成了如何判断页面来源,可以使用wx.navigateTo中的events来监听原始页面和跳转页面之间的数据传递和数据流动。
  • 在产品列表的列表组件wxml中,使用wx.navigateTo代替navigator标签:
<view bindtap="goToRedirectPage" data-url="{{item.path}}">
  此处写图片的包裹信息
</view>
复制代码
  • 在产品列表的列表组件js文件中,定义goToRedirectPage方法,跳转到商品详情页,并使用wx.navigateTo中的events来监听原始页面和跳转页面之间的数据传递和数据流动。
goToRedirectPage(e) {
  //events里面的this并不指向windows,需要在开始强制转换指向
  var that = this
  const url = e.currentTarget.dataset.url
  wx.navigateTo({
    url,
    events: {
      // 为指定事件添加一个监听器,获取被打开页面传送到当前页面的数据,并传递loaddata事件
      acceptDataFromDetail(data) {
       that.triggerEvent('loadData',data)
      }
    }
  })
}
复制代码
  • 在产品详情的js文件中,向产品列表组件传递数据:
//   this.getOpenerEventChannel()也可以在除小程序生命周期以外的地方进行调用
  onLoad: function (options) {
    this.getOpenerEventChannel().emit('acceptDataFromDetail', { loadData: false })
  }
复制代码
  • 在产品列表父组件的wxml文件中,接收产品列表组件传递的事件:
  <base-list list="{{list}}" bind:loadData="loadData"/>
复制代码
  • 在产品列表父组件的js文件中,定义传递事件触发的函数:
data: {
     loadData: true
},
//小程序生命周期
onShow: function () {
 if(!this.data.loadData) {
   this.setData({
     loadData: true
   })
   return
 }
 this.setData({
   page: 1,
   list: []
 })
 this.initRequestParams()
 setCartBadge()
},
//详情页传递的事件
loadData(event) {
 this.setData({
   loadData: event.detail.loadData
 })
}
复制代码

9.小程序全局配置分享给好友和分享到朋友圈的功能(暂未实现)

  麻烦思路清晰的小伙伴评论区告知。

商户端项目经验总结

1.使用axios请求接口失败时,获取后端返回的信息

  使用res.response即可获取,示例如下:

inspectSpams() {
   this.isShowMessage = false
   if(this.productName.trim() !== '') {
     this.$store.dispatch('products/inspectSpams', this.productName).catch((err) => {
       this.errorMessage = err.response.data.message
       this.isShowMessage = true
     })
   }
 }
复制代码

2.修改env文件信息后,需要重新启动项目,否则会报错

3.页面刷新或者跳转,绝对定位的元素会抖动

  给赋予绝对定位的元素的父级添加相对定位,可以消除抖动。

4.页面刷新或者跳转,固定定位的元素会抖动

  • 为整行元素添加绝对定位,要赋予宽度:
.tabs {
   position: fixed;
   width: 100%;
   z-index: 9;
}
复制代码
  • 顶部固定之后,由于未占据空间,需要给下面的元素一个padding值:
.the-list {
  padding-top: 85px;
}
复制代码
  • 固定定位的时候只要不写top、bottom、left、right值,会默认基于父级进行定位,位置居于页面左上角。如果要定位在其它地方,加上margin值处理即可。如果使用top、bottom、left、right进行处理,会由于路由的过渡动画导致固定定位的元素抖动。
.fixed {
     padding: 15px 0;
     position: fixed;
     background: #F2F2F2;
     margin-top: 40px;
     width: 100%;
     z-index: 9;
}
复制代码

5.tab切换的列表渲染问题

  不用在每个<el-tab-pane>下嵌套一层List组件,否则组件创建的时候会渲染N(N为tab的个数)个List组件。直接在<el-tab-pane>外写一层List组件就好,当点击不同的tab,请求不同的接口时,将获得的数据存储在vueX中,供list组件调用即可。

<template>
  <div class='list-wrap'>
    <el-tabs v-model='status' @tab-click='handleClicked' class="tabs">
      <el-tab-pane label='在售商品' name='active'>
      </el-tab-pane>
      <el-tab-pane label='已下架' name='inactive' >
      </el-tab-pane>
    </el-tabs>
    <List :productStatus='status' ref="list"/>
  </div>
</template>
复制代码
<script>
import List from '@/components/Product/List'

export default {
  name: 'Product',
  components: {
    List
  },
  data() {
    return {
      status: 'active'
    }
  },
  created() {
    this.getProductListData('active')
  },
  methods: {
    getProductListData(status) {
      this.$store.dispatch('products/getList', {
        limit: 16,
        status,
        order: 'id',
        direction: 'desc'
      })
    },
    handleClicked(tab) {
      this.getProductListData(tab.name)
      this.$refs.list.initCheckAllButton()
    },
  }
}
</script>
复制代码
<style scoped>
    .tabs {
      position: fixed;
      width: 100%;
      z-index: 9;
    }
</style>
复制代码

6.消除后台返回效果图过长导致多出的灰色背景

  当后台返回的效果图过多时,可能会导致因为循环渲染的效果图过多导致页面多出大片的灰色背景。此时我们不想使用轮播来渲染后台返回的效果图,这时可以考虑使用css来调控。

//在外层父容器中使用flex布局,在子级中使用flex表明子组件应占据的份数
.product-detail {
     flex-direction: column;
     flex: 1;
     margin: 0;
     max-height: 850px;
     overflow-y: auto;
}
复制代码

  最后的效果如下所示:

2021-04-22 08-56-25 的屏幕截图.png

7.封装图片组件(用于响应式布局)

  在响应式布局中,为了适配不同大小的页面,所以不能写死图片的宽度和高度。当找不到图片时,图片的布局可能会因此受到影响。图片的宽度可以由父容器继承过来,但是如何计算图片的高度呢?此时可以在使用图片之前,就封装一个图片组件,用于响应式布局。

<script>
export default {
 name: 'BaseImage',
 props: {
   src: {
     required: true,
     type: String
   },
   fit: {
     required: false,
     type: String,
     default: 'contain'
   },
   previewSrcList: {
     required: false,
     type: Array,
     default() {
       return []
     }
   },
   isSquare: {
     required: false,
     type: Boolean,
     default: true
   }
 }
}
</script>

<template>
 <div
   v-if="src"
   :class="{ 'square-image-wrap': isSquare }"
 >
   <el-image
     :src="src"
     :fit="fit"
     :preview-src-list="previewSrcList"
   />
 </div>
</template>
复制代码
/* 自适应正方形图片
-------------------------- */
.base-square {
     position: relative;
     width: 100%;
     padding-bottom: 100%;
}

.base-square-item {
     position: absolute;
     top: 0;
     bottom: 0;
}

.square-image-wrap {
   @extend .base-square;
    .el-image {
        width: 100%;
        @extend .base-square-item;
    }
}
复制代码

  图片组件的使用(使用<el-row>和<el-col>进行响应式布局):

  <div class="the-list">
      <el-row
        :gutter="20"
      >
        <el-col
          v-for="(item, index) in products"
          :key="index"
          :xs="12"
          :sm="12"
          :md="8"
          :lg="6"
          :xl="6"
          class="list-item"
        >
          <el-checkbox-group v-model="selectedImgIds" class="checkbox-group">
            <div class="image-warp">
              <router-link
                class="link-to"
                :to="{ name: 'detail', params: { id: item.id } }"
              >
                <BaseImage
                  class="product-image"
                  :src="item.image"
                  fit="cover"
                />
              </router-link>
              <el-checkbox
                :key="item.id"
                class="selected-warp"
                :label="item.id"
              />
            </div>
            <div class="product-info">
              <div class="product-name">{{ item.name }}</div>
              <span class="product-price">¥ {{ item.lowest_price }}</span>
              <BaseIcon icon="edit" @click.native="showDialog(item.id)" />
            </div>
            <div
              v-if="item.basic_status === 'inactive'"
              class="invalid-product-cover"
            >
              <BaseIcon
                class="invalid-icon"
                icon="prompt"
              />
              <div class="invalid-product">
                失效产品
              </div>
            </div>
          </el-checkbox-group>
        </el-col>
      </el-row>
      <el-pagination
        background
        layout="prev, pager, next"
        class="product-pagination"
        :current-page="page.current_page"
        :page-size="Number(page.per_page)"
        :total="page.total"
        @current-change="handlePageChange"
      />
    </div>
复制代码

8.使用字符串模板封装函数,减少代码冗余

  在商户端产品模块,由于上架和下架的样式和逻辑基本一致,因此可以在字符串模板使用${}控制变量,封装函数,减少代码的冗余。

  changeProductStatus(status) {
     if (this.selectedImgIds.length === 0) {
       this.$message({
         type: 'warning',
         message: '请先选择产品'
       })
       return
     }
     this.$confirm(
       `您确定要将所选产品${status === 'inactive' ? '下架' : '上架'}吗?`,
       `产品${status === 'inactive' ? '下架' : '上架'}`,
       {
         distinguishCancelAndClose: true,
         confirmButtonText: '确认',
         cancelButtonText: '取消'
       })
       .then(() => {
         this.allChecked = false
         this.changeStatus(status)
       })
   },
  changeStatus(status) {
     this.$store.dispatch('products/changeStatus', {
       status,
       ids: this.selectedImgIds
     })
       .then(() => {
         const message = this.productStatus === 'active' ? '下架成功' : '上架成功'
         this.$message({
           type: 'success',
           message
         })
         this.selectedImgIds = []
         this.getProductListData(this.productStatus, 1)
       })
       .catch(message => {
         this.$message({
           type: 'error',
           message
         })
       })
   }
复制代码

9.mixins

  在mixins中,如果需要引入mixins的组件中存在某个变量,则可以直接在mixins中使用this.变量名进行调用(html访问data中的变量不需要使用this指向,js访问data中的变量需要使用this进行指向)。同时,在mixins中定义的data变量,也能被调用mixins的组件访问到。这也就意味着,mixins和组件之间是不存在父子关系的,是融为一体的。(在html中,=左右不需要空格;在js文件中,=左右最好空一格)

export default {
  methods: {
    inspectSpams( name) {
      this.isShowMessage = false
      if(name !== '') {
        this.$store.dispatch('products/inspectSpams', name).catch((err) => {
          this.errorMessage = err.response.data.message
          this.isShowMessage = true
        })
      }
    }
  }
}
复制代码

后台项目经验总结

1.el-form表单验证

   表单结构:

   <el-dialog
    title="修改密码"
    class="change-password"
    :visible.sync="showEditDialog"
    width="60%"
    :close-on-click-modal="false"
    :before-close="close"
  >
    <el-form
      :model="form"
      :rules="passwordRules"
      ref="changePasswordRef"
      label-width="100px"
    >
      <el-form-item
        label="密码:"
        prop="password"
        class="el-form-item"
      >
        <el-input
          v-model="form.password"
          placeholder="请输入密码"
          type="password"
        ></el-input>
      </el-form-item>
      <el-form-item
        label="确认密码:"
        prop="confirm"
        class="el-form-item"
      >
        <el-input
          v-model="form.confirm"
          placeholder="确认密码"
          type="password"
        ></el-input>
      </el-form-item>
      <div class="button-group">
        <el-button @click="cancel">取消</el-button>
        <el-button type="primary" @click="confirmed">确认</el-button>
      </div>
    </el-form>
  </el-dialog>
复制代码

  表单验证:

data() {
 //多加入一个手机号/邮箱验证
  const checkPhoneAndEmail = (rule, value, callback) => {
    const mailReg = /^([a-zA-Z]|[0-9])(\w|\-)+@[a-zA-Z0-9]+\.([a-zA-Z]{2,4})$/ 
    const phoneReg =  /^((0\d{2,3}-\d{7,8})|(1[3584]\d{9}))$/
    if (mailReg.test(value) || phoneReg.test(value)) {
      return callback()
    } 
    return callback(new Error('请输入合法的手机号/邮箱'))
  }

  const comfirmPassword = (rule, value, callback) => {
    if (value !== this.form.password) {
      return callback(new Error('两次输入密码不一致'))
    }
    callback()
   }
   
   form: {
      password: '',
      confirm: ''
    },
    passwordRules: {
      password: [
        { required: true, message: '请输入商户密码', trigger: 'blur' },
        {
          message: '为保证账号的安全性,商户密码须在6位及6位以上',
          min: 6,
          trigger: 'blur',
        },
      ],
      confirm: [
        { required: true, trigger: 'change', message: '请再一次输入密码' },
        { validator: comfirmPassword, trigger: 'blur' }
      ]
    }
 },
methods: {
  confirmed() {
         this.$refs.changePasswordRef.validate( valid => {
          if (!valid) return;
          this.$store
            .dispatch('merchant/changeMerchantStatus', {
              id: this.merchantId,
              status: this.detail.status,
              password: this.form.password
            })
            .then(() => {
              this.showEditDialog= false;
              this.$refs.changePasswordRef.resetFields();
              this.$message({
                message: '密码修改成功!',
                type: 'success',
              });
            });
        });
  }
}


复制代码

2.el-pagination需要指定每页显示的数据条数

  如果不给el-pagination指定每页应该显示的条数,它默认一页显示的是10条数据。假设后端返回的结果是一页显示16条产品数据,虽然每页能正常显示16条产品数据,但是分页器的页码会存在问题。如果数据只有20条,那么分页器会显示2页;但如果数据有21条,分页器就会显示3页了,但是实际上展示给我们的只有前两页有数据,第3页为空白页面。所以需要为el-pagination指定每页应该显示的条数。
  分页器的使用:

    <el-pagination
        background
        :current-page="meta.current_page"
        layout="prev, pager, next"
        :page-size="Number(meta.per_page)"
        :total="meta.total"
        @current-change="(page) => fetchMerchantData({ page })"
  />
复制代码
async fetchMerchantData(params = {}) {
    this.listLoading = true;
    await this.$store.dispatch(
      'merchant/getList',
      Object.assign({}, this.queryParam, params)
    );
    this.listLoading = false;
}
复制代码
.el-pagination {
     text-align: right;
     margin-top: 20px;
}
复制代码

写在最后

  还是很小白,还有很多知识需要学习和总结,专心学习技术。


                              学习&总结


© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享