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

1,339 阅读11分钟

写在前面

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

微信小程序项目经验总结

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()

  应用场景扩展: 在编写小程序组件的过程中,子组件初始化后,父组件还未将产品信息传递给子组件,导致有些时候取不到product的值。这时候可以对传递的参数进行监听,如果能够取到产品的值,就可以在监听函数中做一些子组件需要的初始化操作。

properties: {
  product: {
    type: Object,
    observer: function (data) {
    //Object.keys()返回一个由一个给定对象的自身可枚举属性组成的数组。
    //监听父组件传递的product的值,如果有值,就进行一些初始化操作。
      if(Object.keys(data).length > 0) {
        this.setData({
          currentPrice: this.data.product.lowest_price
        })
      }
    }
  }
}

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]))
},

  应用场景扩展: 如果需要获取view标签或者button标签里面的文字,可以先将要获取的文字放入自定义属性中,再通过e.target.dataset.xxx接收需要获取的文字信息。

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 is not a function的错误
      try{
          this.getOpenerEventChannel().emit('acceptDataFromDetail', { loadData: false })
      } catch() {
      }
  }
  • 在产品列表父组件的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.小程序全局配置分享给好友和分享到朋友圈的功能

  • 未全局引入之前,小程序右上角的分享给好友和朋友圈的功能是灰显的;全局引入之后,才可以开启小程序右上角的分享功能。
  • 在CustomPage.js文件中,设置全局转发与分享:
import api from '../api/index'
  /**
   * 给 Page 增加自定义方法
   * @param {object} options 参数
   */
  export default function (options) {
    options.$api = api
    // 转发与分享
    options.onShareAppMessage = () => { },
    options.onShareTimeline = () => { }
    return Page(options)
}
  • 在app.js文件中引入CustomPage.js文件
import CustomPage from './utils/CustomPage'
import initSentry from './plugins/sentry'

import api from './api/index'
App({
  onLaunch: function (option) {
    
    initSentry()

    var obj = wx.getLaunchOptionsSync()
    api.merchant.getInfo({
        code: obj.query.code || 'HCFW'
      })
      .then(({
        data
      }) => {
        this.globalData.merchant_info = data
        //由于这里是网络请求,可能会在 Page.onLoad 之后才返回
        // 所以此处加入 callback 以防止这种情况
        if (this.employIdCallback) {
          this.employIdCallback(data);
        }
      })

    const token = wx.getStorageSync('token')
    const user_info = wx.getStorageSync('user_info')
    if (token && user_info) {
      this.globalData.token = token
      this.globalData.user_info = user_info
    }
  },
  onHide: function () {},
  onError: function (msg) {},
  onPageNotFound: function (options) {},
  $Page: CustomPage,
  globalData: {
    merchant_info: {},
    token: null,
    user_info: {},
  },
})

10.小程序iPhone X适配

 iphone X的齐刘海和胡子存在安全适配问题,需要给安全距离。如底部固定栏的布局:

.bottom-bar-fixed {
  position: fixed;
  bottom: 0;
  width: 100%;
  z-index: 99;
  background: var(--background-color--default);
  padding-bottom: constant(safe-area-inset-bottom);   /* 兼容 iOS < 11.2 */
  padding-bottom: env(safe-area-inset-bottom);  /* 兼容 iOS >= 11.2 */
}

          屏幕截图 2021-05-06 144846.png

 如果在css中,不加入padding-bottom做安全适配,则页面会存在兼容问题:

          屏幕截图 2021-05-06 145703.png

11.解决关闭回调和步进器之间的数量冲突

 在弹出层的关闭回调中,需要将步进器的数量重置为1。而在单击确定按钮把所选商品加入到购物车时,需要存储当前商品(即步进器)的数量(可能不为1)便于在购物车页面和结算页面进行展示,此时存储的商品数量将会以关闭回调中设置的步进器数量为准,因此导致冲突。解决方法是在弹出层的关闭回调中,使用wx.nextTick延迟重置的操作。

onClose() {
  wx.nextTick(() => {
    this.setData({
      value: 1
    })  
  });
}

12. Vant Weapp步进器异步变更

<van-stepper value="{{ value }}" async-change bind:change="onChange" />

13. 微信小程序登录流程

// components/login/index.js
import api from '../../api/index'
const app = getApp()
Component({
    data: {
        code: null
    },
    lifetimes: {
        created() {
            wx.login({
                success: (res) => {
                    // 发送 res.code 到后台换取 openId, sessionKey, unionId
                    this.code = res.code
                    console.log(res.code)
                },
            })
        }
    },
    methods: {
        getUserProfile: function (e) {
            wx.getUserProfile({
                desc: '正在获取', //不写不弹提示框
                success: (res) => {
                    if (res.errMsg !== 'getUserProfile:ok') return
                    this.wxLogin(res.userInfo)
                },
                fail: (err) => {
                    console.log('获取失败: ', err)
                },
            })
        },
        wxLogin(user_info) {
            const {
                nickName,
                avatarUrl,
                gender
            } = user_info
            const data = {
                code: this.code,
                nickname: nickName,
                avatar: avatarUrl,
                gender,
            }
            wx.showLoading({
                title: '加载中',
            })
            console.log(this)
            api.user.login(data).then(({
                data
            }) => {
                console.log(data)
                const {
                    plain_token,
                    grant_user
                } = data
                app.globalData.token = plain_token
                app.globalData.user_info = grant_user
                wx.setStorageSync('token', plain_token)
                wx.setStorageSync('user_info', grant_user)
                this.loginSuccess()
                wx.hideLoading()
            })
        },

        loginSuccess() {
            this.triggerEvent('myevent')
        }
    }
})

14. onLaunch异步,首页onLoad先执行

问题详情传送门

 app.js:

import CustomPage from './utils/CustomPage'
import initSentry from './plugins/sentry'

import api from './api/index'
App({
 onLaunch: function (option) {
   
   initSentry()

   var obj = wx.getLaunchOptionsSync()
   api.merchant.getInfo({
       code: obj.query.code || 'HCFW'
     })
     .then(({
       data
     }) => {
       this.globalData.merchant_info = data
       //由于这里是网络请求,可能会在 Page.onLoad 之后才返回
       // 所以此处加入 callback 以防止这种情况
       if (this.employIdCallback) {
         this.employIdCallback(data);
       }
     })

   const token = wx.getStorageSync('token')
   const user_info = wx.getStorageSync('user_info')
   if (token && user_info) {
     this.globalData.token = token
     this.globalData.user_info = user_info
   }
 },
 onHide: function () {},
 onError: function (msg) {},
 onPageNotFound: function (options) {},
 $Page: CustomPage,
 globalData: {
   merchant_info: {},
   token: null,
   user_info: {},
 },
})

首页js:

import { setCartBadge } from '../../utils/helper'

const app = getApp()
app.$Page({
 data: {
   swiperList: []
 },
 /**
  * 生命周期函数--监听页面加载
  */
 onLoad: function (options) {
   const {
     merchant_info
   } = app.globalData
   if (Object.keys(merchant_info).length === 0) {
     app.employIdCallback = data => {
       this.getSwiperList(data)
     }
   } else {
     this.getSwiperList(merchant_info)
   }
 },
 getSwiperList(merchant_info) {
   const data = {
     merchant_id: Number(merchant_info.id)
   }
   wx.setNavigationBarTitle({
     title: merchant_info.name
   })
   this.$api.home.getBanners(data).then(({
     data
   }) => {
     this.setData({
       swiperList: data
     })
   })
 },
 onShow: function () {
   setCartBadge()
 },
 goToCreate: function () {
   this.$api.basic.getList({
     merchant_id: getApp().globalData.merchant_info.id
   })
     .then(({
       data
     }) => {
       if (data.length === 1) {
         wx.navigateTo({
           url: `/pages/design-detail/index?id=${data[0].id}`
         })
         return
       }
       wx.switchTab({
         url: '/pages/design/index'
       })
     })
 },
});

15. 加入购物车底部固定栏布局问题

需求:

         image.png

正确的布局:

  • 整个底部栏看成一个父盒子,将这个父盒子分为左右两侧,左侧放单选框和全部,右侧放合计、价格和结算按钮。为这个父盒子添加flex布局,并使用justify-content: space-between;进行布局;
  • 左侧盒子使用flex布局,给全部文字添加一个margin-left值;
  • 右侧盒子使用flex布局,给价格一个左右的margin值;
  • 因为价格不固定,所以切记不能给右侧盒子一个固定的左侧margin值;
  • 结算按钮可以直接使用button或者view标签,结算文字后面需要添加一个text标签判断是否显示结算数量。在点击事件中,需要判断当前结算数量的值。如果为0,就直接rertun,相当于触发了按钮的禁用事件,再根据不同条件赋予禁用或者启用的样式即可。
<button
  class="{{ productCount > 0 ? 'btn-01 pay-button' : 'btn-01 disable-pay-button' }}"
  bind:tap="redirectToPayPage"
>
    结算<text wx:if="{{productCount > 0}}">({{productCount}})</text>
</button>
  • 价格可以使用toFixed(2)保留2位小数。

商户端项目经验总结

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文件中,=左右最好空一格。)在调用mixins的组件中,先挂载mixins中存在的数据变量,再挂载调用mixins的组件中的数据变量,这也就表明在调用mixins的组件中定义的数据变量可以覆盖mixins中的数据变量。

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表单验证

//自定义校验规则可以使用多个{validator: xxx, trigger: 'blur'}
//表单的校验顺序以自定义校验规则的书写顺序为准,从上往下校验

 表单结构:

   <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()
   }
   
  return {
   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' }
        //自定义校验规则可以使用多个{validator: xxx, 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;
}

3.使用return promise减少代码冗余

场景: 两个函数只有catch方法不同,可以考虑使用return一个promise重新封装函数减少代码冗余,调用时分别对返回的promise使用不同的catch方法进行捕获即可。

方法的封装

    changeStatus(status, data) {
      return new Promise((resolve, reject) => {
        if(status === 'disable') {
          this.$confirm(
            '禁用后,该商户将无法登录商户后台,同时关闭商户旗下店铺。',
            '禁用商户状态',
            {
              confirmButtonText: '确定',
              cancelButtonText: '取消',
            }
          )
            .then(() => {
              this.onActiveStatus(data, '禁用商户状态成功!')
            })
            .catch(() => {
              reject()
            }) 
        } else {
          this.onActiveStatus(data, '成功激活商户状态!')
        }
      })
    }

方法的调用

    changeListStatus(merchantInfo) {
      const data = {
        id: merchantInfo.id,
        status: merchantInfo.status,
      };
      const status = merchantInfo.status
      this.changeStatus(status, data).catch(() => {
        merchantInfo.status = !merchantInfo.status;
        this.initMerchantStatus();
      })
    },
    changeDetailStatus(status, id) {
      const data = {
        id,
        status,
      };
      this.changeStatus(status, data).catch(async() => {
        await this.$store.dispatch('merchant/getDetail', id);
        this.initMerchantStatus();
      })
    }

写在最后

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


                            学习&总结