小白前端项目经验总结(三)

1,460 阅读3分钟

写在前面

  因掘金文章篇幅有限,故另起一篇博文来总结记录前端项目的点滴经验,同时也希望能帮助到其他持有同样疑惑问题的小伙伴。

81.支付宝授权,绑定支付宝账号

  其实,核心步骤都是由后端进行处理,前端只需要请求接口,获得后端返回来的表单并提交,跳转到支付宝页面进行授权即可。

//处理后端返回表单的公共方法
export function handleWebPaymentForm(webPaymentForm) {
  if (!webPaymentForm) return
  //创建一个隐藏的div元素,将后端返回的表单内容追加到div中,并提交表单
  const div = document.createElement('div')
  div.style.visibility = 'hidden'
  div.style.width = '0'
  div.style.height = '0'
  div.innerHTML = webPaymentForm
  document.body.appendChild(div)
  // console.log(webPaymentForm, div.getElementsByTagName('form'))
  div.getElementsByTagName('form')[0].submit()
}
// 确认充值调用支付宝接口
confirmRecharge() {
  this.$refs['onlineForm'][0].validate(async (valid) => {
    if (valid) {
      try {
        this.loading = true
        const data = {
          totalAmount: this.onlineForm.recharge,
          payChannel: this.payChannel
        }
        const res = await createRechargeQrCode(data)
        handleWebPaymentForm(res.detail.webPaymentForm)
        // const div = document.createElement('div')
        // div.innerHTML = res.detail.webPaymentForm
        // document.body.appendChild(div)
        // console.log(res.detail.webPaymentForm)
        // document.forms['punchout_form'].submit()
      } catch (e) {
        this.$message.warning('充值异常,请重试')
        this.loading = false
      }
    } else {
      return false
    }
  })
}

82. 对传入的props数据进行分页构造

<el-pagination
  class="defaultClass"
  background
  @size-change="handleSizeChange"
  @current-change="handleCurrentChange"
  :current-page="page.pageIndex"
  :page-sizes="[10, 20, 50, 100, 200]"
  :page-size="page.pageSize"
  layout="total, sizes, prev, pager, next, jumper"
  :total="selectionListCopy.length"
>
</el-pagination>
<!-- 表格绑定的数据finalData是全部数据分割后的数据 -->
<el-table height="390px" :data="finalData" style="width: 100%">
  <el-table-column prop="name" label="产品名称"> </el-table-column>
  <el-table-column label="风格">
    <template slot-scope="{ row }">
      <el-tag class="mr10 mt10" type="primary" v-for="(item, index) in getStyle(row)" :key="index">
        {{ item }}
      </el-tag>
    </template>
  </el-table-column>
  <el-table-column label="操作">
    <template slot-scope="{ $index }">
      <el-button @click="onRemove($index)" type="text" size="small">移除</el-button>
    </template>
  </el-table-column>
</el-table>
<el-pagination
  class="defaultClass"
  background
  @size-change="handleSizeChange"
  @current-change="handleCurrentChange"
  :current-page="page.pageIndex"
  :page-sizes="[10, 20, 50, 100, 200]"
  :page-size="page.pageSize"
  layout="total, sizes, prev, pager, next, jumper"
  :total="selectionListCopy.length"
>
</el-pagination>
data() {
    return {
      selectedStyleList: [],
      selectedStyleListCopy: [],
      selectionListCopy: [],
      page: {
        pageIndex: 1,
        pageSize: 10
      }
    }
},
//selectionList是定义的props,selectionListCopy是对selectionList的深拷贝
computed: {
    finalData() {
      //记录当前页面数组的起始位置
      const start = (this.page.pageIndex - 1) * this.page.pageSize
      //使用slice方法切割数组
      return this.selectionListCopy.slice(start, start + this.page.pageSize)
    }
},
methods: {
    handleSizeChange(val) {
      this.page.pageSize = val
    },
    handleCurrentChange(val) {
      this.page.pageIndex = val
    }
}

83. 放在阿里云服务器上的图片的缩放处理

handleStyleClicked(index, curIndex) {
  const item = this.data[curIndex]
  item.$sizeIndex = index
  //使用数组中的方法实现响应式
  this.data.splice(curIndex, 1, item)
}

84. 使用watch监听自身,对自身数据进行改造

  假设有这么一种情景,需要对ab进行监听。如果a或者b发生了变化,那么c的值也需要跟随改变。但是ab被重新赋值后,要马上拿到c的数据并进行改造。但由于触发了watch方法,这个方法还在进行,c的值并未得到更新。这个时候就可以使用watch对自身进行监听,确保是拿到c最新的值,并在其中对数据进行一次改造。这也就意味着,只需要对自身监听一次,因此可以定义一个变量监听自身并调用该变量取消对自身的监听。

const unWatch = this.$watch('finalData', (newVal) => {
    const finalData2 = []
    newVal.forEach((v) => {
      myarr.forEach((v2) => {
        if (v2.combinedColorName === v.color && v2.combinedSizeName === v.size) {
          finalData2.push(v)
        }
      })
    })
    this.finalData = finalData2
    this.selectData.map((item, i) => {
      const combinedList = flat(
        detail.combinedProductStyleList.map(({ combinedProductSizeList }) => {
          return combinedProductSizeList.map((item) => item)
        })
      )
      this.finalData.map((finalData, index) => {
        finalData[`id_${i}_${index}`] = combinedList[index].combinedProductSizeAssociationList[i].id
        combinedList[index].combinedProductSizeAssociationList[i].combinedProductSizeId
        finalData[`combinedProductSizeId_${i}_${index}`] =
          combinedList[index].combinedProductSizeAssociationList[i].id
        finalData[`productQuantities_${i}`] = combinedList[index].combinedProductSizeAssociationList[i].productCount
        finalData[`select_${i}_${index}`] =
          'sizeId' +
          combinedList[index].combinedProductSizeAssociationList[i].productSizeId +
          '_colorId' +
          combinedList[index].combinedProductSizeAssociationList[i].productStyleId
      })
    })
    //使用深拷贝的方法来实现数据的响应式
    //相当于开辟了一个新的地址给finalData,然后重新双向绑定了
    this.finalData = deepClone(this.finalData)
    //调用自身取消监听
    unWatch()
  })

85. css相关

  • 修改el-checkbox的大小和样式
::v-deep {
    .el-checkbox__inner {
      width: 18px;
      height: 18px;
      border: 1px solid #e6e6e6;
      border-radius: 2px;
    }
    //重置√的样式
    .el-checkbox__inner::after {
      height: 10px;
      width: 6px;
    }
}
  • 隐藏元素同时保持页面的布局,visibility: hidden你值得拥有。 隐藏元素
  • 相邻选择器:
.row-line {
  margin: 0 -10px;
  height: 100px;
  display: flex;
  align-items: center;
  justify-content: center;
  & + .row-line {
    //相当于为除第一个row-line类以外的所有row-line类添加上边框样式
    border-top: 1px solid #ebeef5;
  }
}
  • 为图片添加背景颜色(正片叠底,通常给包裹图片的div父盒子增加背景颜色) 正片叠底的css使用
.image-warpper {
  background: #ccc;
  .el-image {
     mix-blend-mode: multiply;
  }
}

image.png

  • 合理使用flex: 1(在一个盒子中,给左半部分固定宽度,右半边可以使用这条css占据剩余空间)进行响应式布局

  • 元素水平垂直居中:

position: absolute;
top: 50%;
transform: translateY(-50%); //相当于向上偏移自身高度的50%
left: 0;
right: 0;
  • el-tabs下划线添加左右padding,给el-tabs父容器添加padding-tabs公共类名就好了。
//公共样式
.padding-tabs {
  .el-tabs__item{
     padding5px; //用于控制不同tab之前的间距
  }
  .el-tabs__item::before,
  .el-tabs__item::after {
    content: ' ';
    display: inline-block; //伪类相当于span标签,是行内元素,需要转换为行内块元素
    width: 15px; //相当于在每个tab文字前后增加了长度为15px的隐藏文字
  }
}

image.png

86. el-checkbox选中绑定值为对象数组,需要重置引用地址

//请求数据之后拿到数据之前的操作
initCallBack() {
  this.data.map((item) => {
    //获取选中数据的id
    const fIndex = this.checkList.findIndex((cItem) => {
      return cItem.id == item.id
    })
    //替换引用地址,将引用地址重置为之前的地址
    if (fIndex >= 0) {
      this.checkList.splice(fIndex, 1, item)
    }
  })
}

87. el-table伪分割表格样式以及表格合并

1. 使用css进行表格分割和合并:

  • el-table的表格塑造是以这一行高度的最高值进行构造的。 image.png
  • 表格合并:
<template #sizeSlot="{ scoped: row }">
  <div :key="index" v-for="(item, index) in getSize(row) || []" class="row-line">
    <el-tag type="primary">
      {{ item }}
    </el-tag>
  </div>
</template>
computed: {
   getSize() {
      return (row) => {
        return flat(
          row.customProductList[row.$sizeIndex].customSpecificProductList?.map(({ sizeName }) => sizeName)
        )?.filter(Boolean)
      }
   }
},
methods: {
    //表格cell-class-name触发的方法,用于清除表格padding
    cellClassName({ columnIndex }) {
      if ([4, 5, 8, 9, 10].includes(columnIndex)) {
        return 'clearTablePadding'
      }
    },
}
<style lang="scss">
// 全局样式,迭代类名增加样式的优先级
.clearTablePadding.clearTablePadding.clearTablePadding.clearTablePadding {
  padding: 0;
}
</style>

// 虚构表格分割的样式
// 扩大每一行表格的边框范围
<style lang="scss" scoped>
.row-line {
    border-bottom: 1px solid #ebeef5;
    margin: 0 -10px;
    height: 50px;
    display: flex;
    align-items: center;
    justify-content: center;
}
.row-line:last-child {
    border-bottom: none;
}
<style/>

2. 重构数据以及封装表格合并方法进行表格分割和合并:

computed: {
    //表格绑定的真正数据
    finalData({ tableData }) {
      const cloneData = cloneDeep(tableData)
      const tmpArr = []
      cloneData.map((item, index) => {
        //lodash中的get方法,当值为null时,给默认值[],避免map前面的值为null
        //同时也是为了后续计算表格合并的行数
        const combinedCustomProductList = $GET(item, 'combinedCustomProductList', [])
        combinedCustomProductList.map((product, productIndex) => {
          const { combinedColorName, id: combinedColorId } = product
          const combinedCustomSpecificProductList = $GET(product, 'combinedCustomSpecificProductList', [])
          combinedCustomSpecificProductList.map((specificInfo, specIndex) => {
            const { combinedSizeName, combinationCustomSku } = specificInfo
            tmpArr.push({
              ...item,
              //将多个产品尺码sku集数组进行拆分
              $combinedCustomSpecificProductItem: specificInfo,
              $combinedCustomProductItem: product,
              //这些单个数据是列表需要展示的数据
              combinedSizeName,
              combinationCustomSku,
              combinedColorName,
              combinedColorId,
              //用于判断这一等级合并多少行的数据
              $spanLevelList: {
                level1:
                  specIndex == 0 && productIndex == 0
                    ? combinedCustomProductList.length * combinedCustomSpecificProductList.length
                    : 0,
                level2: specIndex == 0 ? combinedCustomSpecificProductList.length : 0
              }
            })
          })
        })
      })
      return tmpArr
    }
},
methods: {
    //表格合并方法,spanLevelList为传入的props
    spanMethod({ row, column }) {
      return getSpanMethod({ row, column, spanLevelList: this.spanLevelList })
    }
}
//公共方法,用于获取合并的等级
export function getSpanLevel(col, spanLevelList) {
  //当col的property为空时, property = 'done'为最后一列操作列
  //因为操作列是没有prop的,不提供则为undefined,所以解构有默认值'done',用于和传入的spanLevelList中的'done'进行操作列匹配
  const { property = 'done' } = col
  //对键值进行循环遍历
  for (let level in spanLevelList) {
    const arr = spanLevelList[level]
    if (arr.includes(property)) {
      //返回当前的键名
      return level
    }
  }
  return null
}

export function getSpanMethod({ row, column, spanLevelList }) {
  //解构的值有对应层级需要合并的行数
  const { $spanLevelList = {} } = row
  const level = getSpanLevel(column, spanLevelList)
  let spanNum = 1
  if (level !== null) {
    spanNum = $spanLevelList[level]
  }
  return [spanNum, 1]
}
// 暴露的合并层级
// 相同层级为一类,合并相同的行数,这些除了done之外都是el-table对应列的prop名
export const spanLevelList = {
  level1: ['combinedInfo', 'createByName', 'createTime', 'done'],
  level2: ['combinedColorName']
}

image.png

88. 变相增大el-checkbox的选中区域

  其实el-checkbox的选中状态是由绑定的数组决定的,既然如此,何不给el-checkbox最外层的父级添加一个点击事件呢?点击拿到当前item项时,如果绑定的数组中存在该item项,说明现在的点击行为会触发取消勾选的状态;如果绑定的数组中不存在该item项,说明现在的点击行为会触发新增的状态。当然此种方法会有一种弊端,那就是会突破可勾选的el-checkbox数目,使用数组长度判断一下就可以解决这类问题。

handleClick(data) {
  const fIndex = this.checkList.indexOf(data)
  if (fIndex == -1) {
    if ([0, 1].includes(this.type) && this.checkList.length == 0) this.checkList.push(data)
    if (this.type == 2 && this.checkList.length >= 0 && this.checkList.length < 5) this.checkList.push(data)
  } else this.checkList.splice(fIndex, 1)
}

89.vue路由缓存以及路由动画(仅做备份,AppMain.vue)

<template>
  <section class="app-main">
    <transition :name="animationName" mode="out-in">
      <keep-alive :include="cachedViews">
        <router-view :key="key" />
      </keep-alive>
    </transition>
  </section>
</template>

<script>
export default {
  name: 'AppMain',
  computed: {
    cachedViews() {
      // 返回缓存的页面名称组成的字符串数组
      return ['prototypeList', 'productCombination', 'productCustomCombination']
    },
    key() {
      return this.$route.fullPath
    }
  },
  data() {
    return {
      animationName: 'fade-transform'
    }
  }
}
</script>

<style scoped>
.app-main {
  /*84 = navbar + tags-view = 50 +34 */
  min-height: calc(100vh - 84px);
  width: 100%;
  position: relative;
  overflow: hidden;
}
</style>
//transition.scss
//globl transition css

/*fade*/
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.28s;
}

.fade-enter,
.fade-leave-active {
  opacity: 0;
}

/*fade-transform*/
.fade-transform-leave-active,
.fade-transform-enter-active {
  transition: all .5s;
}
.fade-transform-enter {
  opacity: 0;
  transform: translateX(-30px);
}
.fade-transform-leave-to {
  opacity: 0;
  transform: translateX(30px);
}

/*fade*/
.breadcrumb-enter-active,
.breadcrumb-leave-active {
  transition: all .5s;
}

.breadcrumb-enter,
.breadcrumb-leave-active {
  opacity: 0;
  transform: translateX(20px);
}

.breadcrumb-move {
  transition: all .5s;
}

.breadcrumb-leave-active {
  position: absolute;
}

90.基于饿了么封装无限滚动组件以及组件的使用

<template>
  <div class="infinite-component-list-wrapper">
    <el-row
      class="list"
      :gutter="5"
      v-infinite-scroll="load"
      infinite-scroll-disabled="disabled"
      :infinite-scroll-distance="20"
    >
      <el-col :span="span" :key="item.id || index" v-for="(item, index) in data">
        <slot :data="item"></slot>
      </el-col>
      <el-col v-if="isShowLoading" :span="24">
        <p v-if="noMore && data.length">没有更多了</p>
        <p v-if="loading">加载中...</p>
      </el-col>
    </el-row>
  </div>
</template>

<script>
export default {
  props: {
    data: {
      type: Array,
      default: () => []
    },

    total: {
      type: Number,
      required: true
    },

    //每个col对应的部分
    span: {
      type: Number,
      default: 8
    },
    isShowLoading: {
      default: true
    }
  },
  data() {
    return {
      loading: false
    }
  },
  watch: {
    data() {
      this.loading = false
    }
  },
  computed: {
    noMore() {
      const bool = this.data.length >= this.total
      if (bool) {
        this.loading = false
      }
      return bool
    },
    disabled() {
      return this.loading || this.noMore
    }
  },
  methods: {
    load() {
      this.loading = true
      this.$emit('load')
    }
  }
}
</script>

<style lang="scss" scoped>
.infinite-component-list-wrapper {
  height: 100%;
  overflow-y: auto;
  overflow-x: hidden;
  .el-col {
    margin-bottom: 10px;
  }
  p {
    text-align: center;
    font-size: 14px;
    color: $color-gray;
    position: relative;
  }
}
</style>
  <InfiniteScroll :span="12" v-if="data.length" :data="data" :total="total" @load="load">
    <template v-slot="{ data }">
      <AutoImg @click="toggleDesign(data)" fit="contain" :active="active(data)" :src="cover(data)"></AutoImg>
    </template>
  </InfiniteScroll>
methods: {
    load() {
      this.page++
      this.init()
    }
}

91.虚拟dom以及diff算法

//假定为祖先组件
 <people
      :height="height"
      :weight="weight"
      :sex="sex"
      :age="age"
  />
//假定为子组件
//$attrs可以在该组件及其后代组件中使用,传给后代组件的值为除本组件未使用props接收(祖先组件和本组件props命名不一致)的值
// 需要绑定在组件上,而非标签上
//如果需要多级向下传递,则逐级绑定v-bind="$attrs"
 <male v-bind="$attrs" />
//假定为孙子组件
//则孙子组件可以使用祖先组件穿过来的值
 <zhangSan />
  • 饿了么表单校验及提交:
async onConfirm() {
  try {
    await this.$refs['ruleForm'].validate()
  } catch {
    return false
  }
  ...
}
  • 记录一下自己花二分钟解决的异步问题:
if (this.selectCountryId && this.ruleForm.provinceCode == '') {
  //子组件更新props的值是一个异步过程,后面调用接口需要使用更新后的值
  //必须得给一个延迟,以保证拿到的值是最新的
  this.$emit('update:selectCountryId', undefined)
  this.$nextTick(() => this.getCountryData())
}

92.翻页重新请求数据造成数据引用地址改变的问题

// 在获取页面数据之前,需要先拿到并修改选中的checkList数据
// 翻页时对象数组的地址发生了变化,例如: 第一页的数据,[{1(地址1)},{2(地址2)},{3(地址3)}]
// 翻到第二页再回到第一页的过程中,第一页的数据,[{1(地址4)},{2(地址5)},{3(地址6)}]
// 虽然数据内容没有发生变化,但保存在内存中的地址却发生了变化
// 这样导致的结果是checkList中的数据内容虽然是第一页数据的子集,
// 但是地址却和第一页的数据不同,这也就导致了之前选中的数据不会再展示的问题
// 因此需要找到原数据的内容和地址,并赋值给选中的数据
initCallBack() {
  this.data.map((item) => {
    //找到选中数据对应所有数据中的索引
    const fIndex = this.checkList.findIndex((cItem) => {
      return cItem.id == item.id
    })
    //将对应数据的内容和地址赋值给checkList,以保证能成功显示勾选的数据
    if (fIndex >= 0) {
      this.checkList.splice(fIndex, 1, item)
    }
  })
}

93.async函数一定会返回一个promise对象

94.给数组添加新字段一定要用$set才能达到响应式,否则后续只能用数组中的splice方法达到响应式的效果

//切换收藏
toggeleLove(data, index) {
  data.love = !data.love
  // this.data.splice(index, 1, data)
},
initCallBack() {
  this.data.map((item) => {
    this.$set(item, 'love', true)
    //item.love = true
  })
}

95.使用background:rgba()解决父级opacity属性带来的子代继承导致的文字也跟随背景一起有透明度的问题

96.客户端渲染、服务端渲染以及NuxtJS配置

97.使用live-server保持网站(html文件、dist打包文件)的热更新

98.nuxt静态化与seo渲染

  • 全局静态化: nuxt.config.js中使用mode默认属性universal,然后yarn generate
  • 局部静态化:nuxt.config.js中使用mode默认属性universal,然后在nuxt.config.js中进行配置需要局部静态化的页面路径最后再yarn build
serverMiddleware: [
    // 全局静态化在多级动态路由时会报错,一级的可以,二级的比如某个艺术家的某个艺术品详情就会出错
    // 局部静态化路径包括'/'、'/works'、 '/shipping'、'/create'和 '/faq'
    function(req, res, next) {
        const ssrPath = ['/works', '/shipping', '/create', '/faq']
        res.spa = true
        if (req.originalUrl === '/') {
            res.spa = false
        }
        ssrPath.forEach((item) => {
            if (req.originalUrl.indexOf(item) === 0) {
                res.spa = false
            }
        })
        next()
    }
]
  • SPA单页面模式打包: 只能使用yarn build进行打包,打包好的dist文件夹中的client文件夹(包含js、css以及字体文件)可以上传到CDN上进行加速
  • nuxt.config.jsplugins的配置,用来在nuxt页面初始化之前调用对应的js方法,可以用来做全局路由守卫
 plugins: [
        '~/plugins/vue-fb-customer-chat.js',
        '~~/4u2-common/plugins/consoledev.js',
        '~~/4u2-common/plugins/lodash.js',
        '~/plugins/sentry.js',
        // 数组扩展方法需放在最上面 避免使用过程出现数组扩展方法未定义报错
        '~~/4u2-common/plugins/array-extends.js',
        '~~/4u2-common/plugins/i18n-patch.js',
        '~~/4u2-common/plugins/price-converter.js',
        '~~/4u2-common/plugins/axios/index.js',
        '~~/4u2-common/plugins/error-handler.js',
        '~~/4u2-common/plugins/dispatch.js',
        '~/plugins/init.js',
        '~~/4u2-common/plugins/route.js',
        '~~/4u2-common/plugins/foru-components.js',
        '~~/4u2-common/plugins/vee-validate.js',
        '~~/4u2-common/plugins/mixin-methods.js',
        '~~/4u2-common/plugins/mixin-filters.js',
        '~/plugins/auth',
        '~~/4u2-common/plugins/element-ui',
        '~/plugins/init-echarts',
        '~~/4u2-common/plugins/process',
        '~~/4u2-common/plugins/directives/directives',
        {
            src: '~~/4u2-common/plugins/forudesigner.js',
            //配置这个属性的目的是由于这些js文件可能包含服务端无法识别的属性,比如windows和document,让这些js文件只在客户端生效,这样就可以解决报错
            mode: 'client'
        },
        {
            src: '~~/4u2-common/plugins/vue-waterfall',
            mode: 'client'
        },
        {
            src: '~~/4u2-common/plugins/swiper.js',
            mode: 'client'
        }
    ]
<head>
    <title>此处为网站标题</title>
    <meta name="viewport" content="width=device-width,initial-scale=1.0" />
    <meta
        name="description"
        content="此处为网站描述"
    />
</head>
  • 也可以在页面组件中使用head进行局部配置(局部优先级高于全局优先级)
export default {
    components: {
        Search
    },
    async fetch({ store, app, query }) {
        await app.$errorHandledDispatch('myProduct/getListAndCategoriesAndTagsData', query)
    },
    head () {
        return {
            //单独配置局部文件标题
            title: this.$t('head_title.design')
        }
    },
    middleware: ['is-not-agent', 'authenticated', 'is-dropshipper', 'has_product_permission']
}
  • vue项目中使用gzip插件对js文件压缩
const CompressionPlugin = require('compression-webpack-plugin')

configureWebpack: {
    plugins: [
        new CompressionPlugin({
            algorithm: 'gzip',
            test: /.(js|css|svg)$/, // 可配置正则
            threshold: 10240, //对超过10k的数据进行压缩
            deleteOriginalAssets: false //是否删除原文件,默认不删除
        })
    ]
}
  • nuxt项目中使用gzip插件对js文件压缩
const CompressionPlugin = require('compression-webpack-plugin')

build: {
  optimization:{
    minimizer:[
         new CompressionPlugin({
            algorithm: 'gzip',
            test: /\.(js|css|svg)$/, // 可配置正则
            threshold: 10240, //对超过10k的数据进行压缩
            deleteOriginalAssets: false //是否删除原文件,默认不删除
        })
    ]
  }
} 
  • css在最上面,html在中间,js在最后。网页渲染机制是:并行下载资源文件,串行执行。分享一个评价网站性格能的地址:gtmetrix

99. 封装方法,处理promise

// 在windows上挂载awaitWrap方法
// 这样写的好处是不用再在代码中使用try catch捕获错误信息
window.awaitWrap = (promise) => {
  // Node.js的Error-first回调模式
  // [null, data]中的第一个参数null为标识符,标识是否成功,data是后端返回的数据
  // [err, null]中的第一个参数err用于标识,表示是否成功,null是重置后的数据
  return promise.then((data) => [null, data]).catch((err) => [err, null])
}

// 成功状态码的判断
window.$SUC = (res) => {
  if (!res) return false
  const { code } = res
  return code == 0 || (code >= 200 && code < 300)
}

// awaitWrap方法的使用(获取用户子账号) 
async GetChildUser({ commit, state }, isFirst) {
  // if (!validatenull(state.childUser) && isFirst) return
  // eslint-disable-next-line no-undef
  // 返回结果对应标识符和处理的数据
  let [, res] = await awaitWrap(getChildUser())
  if ($SUC(res)) {
    commit('SET_CHILD_USER', res.detail)
  }
}
//添加操作成功/失败消息(基于封装的awaitWrap方法)
import { Message } from 'element-ui'

window.awaitResolve = async (promise, { isMsg, sucMsg = '操作成功', errMsg = '操作失败' } = {}) => {
  // eslint-disable-next-line no-undef
  let [, res] = await awaitWrap(promise)
  if (!$SUC(res)) res = false
  if (isMsg) {
    Message[res ? 'success' : 'warning'](res ? sucMsg : errMsg)
  }
  return res
}

100.vue2.x响应式的大致原理

typeof NaN  // 'number'
typeof null // 'object'
isNaN // 有隐式转换,先转换为Number类型,再判断是否为NaN,主要用于判断值是否为number
isNaN('a') //true
isNaN(1) //false
isNaN(NaN) //true
Number.isNaN //用来判断值是否为NaN
Number.isNaN('A')  //false
Number.isNaN(1)   // false
Number.isNaN(NaN) // true
Number.isNaN(Number('a')) // true

101.正则表达式的理解

// 获取请求前缀
// /这个区间里面的代表正则表达式/
// ^表示以什么开头
// \/表示转义字符,即/
// ?表示可有可无
// 连起来的意思就是如果 以/common或者common开头
export const GET_REQUEST_PREFIX = (url) => {
  return /^\/?common/.test(url) ? url : `/${REQUEST_PREFIX}/${url.replace(/^\//, '')}`
}

102.mes开发经验总结

// 原生html标签上增加crossorigin="true"属性,路径拼接上随机字符串
// 不过这种方法会导致无法拿图片缓存,导致每次打印时都要花费较长时间
// 图片最好使用缩略图,可以放到阿里云上进行压缩
<img class="pic" crossorigin="true" :src="getPath(item)" />

// getUUID的定义
import { v4 as uuidv4 } from 'uuid'
export function getUUID() {
  return uuidv4()
}

import { getUUID } from '@/utils'
getPath(item) {
 return `${item.style.displayImageUrl}?id=${getUUID()}`
}
  • 通过this.$refs.table.$el获取组件的dom, 且只有组件才有$el
  • 二维码的生成(根据给定属性)
 // 1. *安装* npm install *vue-qr* --save
 // 2. 页面中使用:
 import VueQr from 'vue-qr'
 
 components: {
   VueQr
 }
 
 // text props必须为字符串类型,根据给定的属性生成二维码
 // margin为二维码图像的外边距
 // 一般这种插件可以去github上查看
 <VueQr class="vue-qr" :text="String(item.factoryOrderCode)" :margin="20"></VueQr>
  • 条形码的生成(根据给定属性)
 // 1. *安装* npm install jsbarcode --save
 // 2.页面中引用:
 // html结构
 <img class="barcode" ref="productLabelBarcodeCanvas" :id="`productLabelBarcodeCanvas`" />
        
 const JsBarcode = require('jsbarcode') 
 
 watch: {
    data: {
      handler() {
        this.createBarcode()
      },
      immediate: true,
      deep: true
    }
 }
 
 createBarcode() {
  this.$nextTick(() => {
    //拿到条形码的dom元素
    const { productLabelBarcodeCanvas } = this.$refs
    if (productLabelBarcodeCanvas) {
      // 根据批次编码来生成条形码
      // 高度为60
      // 条形码上不展示批次编码
      JsBarcode(productLabelBarcodeCanvas, this.data.batchCode, {
        height: 60,
        displayValue: false
      })
    }
  })
 }
  • 生成txt,存放图片路径
import { saveAs } from 'file-saver'

async onExport(data) {
  const res = await awaitResolve(
    exportPrintPicture({
      id: data.id
    })
  )
  if (res) {
    // 对返回的字符串数组进行换行处理
    // 分行显示在txt上
    const str = new Blob(
      res.detail.map((item) => `${item}\n`),
      { type: 'text/plain;charset=utf-8' }
    )
    saveAs(str, `${data.id}.txt`)
  }
}
  • 导出excel文件
  1. 如果后端返回数据的格式为存放字符串的数组
export function getFileSuffix(path = '') {
  path = path || ''
  const chaLastIndex = path.lastIndexOf('.')
  const name = path.slice(chaLastIndex + 1)
  return name
}

//将远程图片下载本地成为base64图片
export async function getURLBase64(url, config = {}) {
  let res = await getURLData(url, (config = {}))
  if (res) return res.target.result
  return res
}

export async function downloadImageByAixos(src, name) {
  if (name === undefined) {
    let tempArr = src.split('/')
    name = tempArr[tempArr.length - 1].split('.')[0]
  }
  const suffix = getFileSuffix(src)
  const url = await getURLBase64(src)
  if (!url) return Message.warning('下载失败')

  // 生成一个a元素
  var a = document.createElement('a')
  // 创建一个单击事件
  var event = new MouseEvent('click')
  // 将a的download属性设置为我们想要下载的图片名称,若name不存在则使用‘下载图片名称’作为默认名称
  a.download = name + '.' + suffix // one是默认的名称
  // 将生成的URL设置为a.href属性
  a.href = url
  // 触发a的单击事件
  a.dispatchEvent(event)
  return true
}

async onExport() {
  const { syncEndTime, syncStartTime, orderType, existExpressWaybill } = this.$refs.table.postData
  const res = await awaitResolve(
    orderApi.exportAll({
      syncEndTime,
      syncStartTime,
      orderType,
      existExpressWaybill
    })
  )
  if (!res) return
  if (!res.detail) return this.$message.error('暂无数据')
  const success = await downloadImageByAixos(res.detail)
  if (success) this.$message.success('下载成功')
}
  1. 如果后端返回的数据格式为二进制文件
//1. 安装file-saver npm install file-saver
//2. 接口定义必须要加responseType: 'blob',才能下载二进制文件
// export: { url: 'orderService/order/exportDefectiveOrder', responseType: 'blob' },

import saveAs from 'jszip/vendor/FileSaver.js'

export function parseTime(time) {
  if (time) {
    var date = new Date(time)
    var year = date.getFullYear()
    /* 在日期格式中,月份是从0开始的,因此要加0
     * 使用三元表达式在小于10的前面加0,以达到格式统一  如 09:11:05
     * */
    var month = date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1
    var day = date.getDate() < 10 ? '0' + date.getDate() : date.getDate()
    var hours = date.getHours() < 10 ? '0' + date.getHours() : date.getHours()
    var minutes = date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes()
    var seconds = date.getSeconds() < 10 ? '0' + date.getSeconds() : date.getSeconds()
    // 拼接
    return year + '年' + month + '月' + day + '日 ' + hours + ':' + minutes + ':' + seconds
  } else {
    return ''
  }
}

async onExport() {
  // 不能用await awaitResolve,这个会判断返回的code
  // 而返回二进制文件的接口是不会返回code值得
  const res = await awaitFormResolve(
    orderApi.export({
      ...this.$refs.table.postData,
      ...REQUEST_ALL_DATA
    })
  )
  if (res) {
    saveAs(res, `${parseTime(new Date())}不良品订单`)
    this.$message.success('导出成功')
  } else this.$message.error('导出失败')
},
  • 批量下载图片
// 1.安装jszip  npm install jszip
// 2.安装file-saver npm install file-saver

import JSZip from 'jszip'
import { Message } from 'element-ui'
import { saveAs } from 'file-saver'

window.awaitWrap = (promise) => {
  return promise.then((data) => [null, data]).catch((err) => [err, null])
}

window.awaitFormResolve = async (promise) => {
  return (await awaitWrap(promise))[1]
}

export function getURLData(url, config = {}) {
  return axios
    .get(url, {
      responseType: 'blob',
      ...config
    })
    .then((res) => {
      const { status, data } = res || {}
      if (status >= 200 && status < 300) {
        const fileReader = new FileReader()
        const p = new Promise((resolve, reject) => {
          fileReader.onloadend = function (e) {
            e.data = data
            e.size = data.size
            resolve(e)
          }
        })
        fileReader.readAsDataURL(data)
        return p
      }
    })
    .catch((err) => {
      const { message } = err
      if (message && message.cancelMessage) {
        Message.success('取消下载成功')
      }
      console.log(err)
      return false
    })
}

//创建随机id
export const createRandomNum = () => {
  return Date.now().toString(16) + Math.random().toString(16).slice(2, 8)
}

//产生随机图片名称
function createFolderPic() {
  var now = new Date()
  var year = now.getFullYear() //得到年份
  var month = now.getMonth() //得到月份
  var date = now.getDate() //得到日期
  var hour = now.getHours() //得到小时
  var minu = now.getMinutes() //得到分钟
  month = month + 1
  if (month < 10) month = '0' + month
  if (date < 10) date = '0' + date
  var number = now.getSeconds() % 43 //这将产生一个基于目前时间的0到42的整数。
  var time = year + month + date + hour + minu
  return time + '_' + number
}

export async function downloadByZip(paths, folderName) {
  if (!folderName) {
    folderName = createFolderPic()
  }
  var zip = new JSZip()
  var folder = zip.folder(folderName)
  const imgLoadedErr = []
  const p = paths.map(async (image, index) => {
    const imgUrl = image.url
    const name = image.name
    image.status = '1'
    return await getURLData(imgUrl + '?uid' + createRandomNum()).then((img) => {
      //下载失败
      if (!img) {
        imgLoadedErr.push(name || fileName)
        image.status = '4'
        return
      }

      let urlBase64 = img.target.result
      const file = imgUrl.split('/')
      const fileName = file[file.length - 1]
      const fileExtension = fileName.substring(fileName.lastIndexOf('.') + 1)
      // console.log(img)
      let props = ['loaded', 'total']
      props.forEach((prop) => {
        image[prop] = img[prop]
      })
      image.thumbnailPath = img.data
      image.status = '3'
      //以data:text/html开头的Base64下载的是pdf
      folder.file(
        `${name}.${fileExtension}`,
        urlBase64.replace(/^data:(image|text)\/(png|jpg|jpeg|html);base64,/, ''),
        {
          base64: true
        }
      )
    })
  })
  await Promise.all(p)
  Message.success('图片下载完成,正在压缩...')
  // eslint-disable-next-line no-undef
  let content = await awaitFormResolve(zip.generateAsync({ type: 'blob' }))
  if (imgLoadedErr.length == paths.length) {
    Message.error(`${imgLoadedErr.join('、')}文件下载失败`)
    return
  }
  if (content) {
    // see FileSaver.js
    saveAs(content, folderName)
    if (imgLoadedErr.length) {
      // Message.success(`下载完成, 其中${imgLoadedErr.join(',')}下载失败`)
      Message({
        type: 'success',
        dangerouslyUseHTMLString: true,
        message: `下载完成, <span style="color: red;">${imgLoadedErr.join('、')}等文件下载失败</span>`,
        duration: 10000,
        showClose: true
      })
    } else {
      Message.success('下载成功')
    }
  } else {
    Message.warning('下载失败')
  }
}

async batchDownload() {
  const selectData = await this.$refs.table.getSelectionDataAllArr()
  if (!selectData.length) {
    this.$message.warning('请先选择需要下载的数据')
    return
  }
  const validateData = selectData.filter((item) => item.filePath)
  const paths = []
  validateData.forEach(async (item) => {
    paths.push({
      url: item,
      name: item.substring(item.lastIndexOf('/') + 1).split('.')[0]
    })
  })
  downloadByZip(paths)
}
  • 使用饿了么自带的格式化插件自定义日期格式
import { formatDate } from 'element-ui/src/utils/date-util'

<span>{{ formatDate(item.orderDate'yyyy/MM/dd') }}</span>
  • 格式化后端返回的数据(分组)
 // 对后端返回的对象数组进行分组
 // 统计对象数组中具有相同sku和尺码的产品数量
 // 并返回一个新数组
 // 显示 sku 尺码 数量
tableData() {
  //this.printData为后端返回的对象数组
  this.printData.forEach((v) => {
    // 这两个字段必须得存在,不能为null
    v.remark = v.factorySize + ' ' + v.factorySku
  })
  const res = {}
  this.printData.forEach((v) => {
    //不是第一次,存在这个属性,就直接push
    if (res[v.remark]) {
      res[v.remark].push(v)
    } else {
      //是第一次,就创造一个数组,并push进去
      res[v.remark] = []
      res[v.remark].push(v)
    }
  })
  return Object.keys(res).map((v) => {
    const arr = v.split(' ')
    const factorySize = arr[0]
    const factorySku = arr[1]
    const count = res[v].length
    return {
      factorySize,
      factorySku,
      count
    }
  })
}
  • 解决google浏览器默认最小字体为12px的样式问题
.time {
  font-size: 12px;
  zoom: 0.9;
}
  • el-image图片路径配置
 <el-image fit="contain" :src="require(`./image/${item.path}`)" @click="linkToProduct"></el-image>
  • 打印问题
methods: {
    async onPrint(data) {
      if (!this.allowOneMore) {
        const { selectedData } = this
        if (!selectedData.length) return this.$message.warning('请选择数据再进行此操作')
        if (selectedData.length > 1) return this.$message.warning('请选择不超过一条数据')
      }
      const res = await awaitResolve(
        getPlanOrderDetail({
          planid: data.id
        })
      )
      if (res) {
        const { detail } = res
        if (!detail.length) {
          this.$message.error('暂无打印数据')
          return
        }
        this.printData = detail
        this.$nextTick(() => {
          // 打印纸张是pt单位,和px之间需要相互转换
          const dom = this.$refs.card.$el.getElementsByClassName('statistics-container')[0]
          // fixDom是调整的dom结构,用于动态设置高度,调整分页问题
          // 需求是先展示统计信息(动态),再展示表格(动态),最后再展示图片信息(动态)
          // 因此可以将统计信息和表格包裹在一起,计算他们的整体高度
          // 再计算当前的页数(向上取整)
          // 最后将高度差赋值给调整dom,使得表格和统计信息少于一页的自动成为一页
          // 纸张宽度是840pt,高度是595.28pt
          // 为了不被切割,需要对统计信息的每一行设置固定高度或者多行的行高
          // 而这个高度应该能被840整除
          // 同时需要清除el-table表头和表单元格的padding
          // 设置el-table表头和表单元格与统计信息行相同的高度
          // 防止表头被切割
          // 这个高度也是能被840整除的
          const fixDom = this.$refs.card.$el.getElementsByClassName('fix-warpper')[0]
          const height = accMul(dom.offsetHeight, 0.75)
          const page = Math.ceil(height / 840)
          const rate = accDiv(4, 3)
          fixDom.style.height = accMul(page * 840, rate) - dom.offsetHeight + 'px'
          this.$lodopPrintPdf({
            type: 'html',
            printable: this.$refs.card.$el
          })
        })
      }
    }
}
  • 格式化插件自动保存问题
// 格式化插件自动化保存可能会受到其他插件影响,因此可以删除
// vue3 support all in one 这类插件
{
  "[javascript]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode",
    "editor.formatOnSave": false
  },
  "terminal.integrated.shell.windows": "C:\\Windows\\System32\\cmd.exe",
  "[vue]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[json]": {
    "editor.defaultFormatter": "vscode.json-language-features"
  },
  "[html]": {
    "editor.defaultFormatter": "vscode.html-language-features"
  },
  "[xml]": {
    "editor.defaultFormatter": "redhat.vscode-xml"
  },
  "eslint.codeAction.showDocumentation": {
    "enable": true
  },
  "prettier.semi": false,
  "prettier.singleQuote": true,
  "prettier.trailingComma": "none",
  "files.associations": {},
  "diffEditor.ignoreTrimWhitespace": false,
  "workbench.editorAssociations": {
    "*.mdwn": "default"
  },
  "editor.codeActionsOnSave": null,
  "editor.rulers": [],
  "editor.tabSize": 2,
  "editor.fontFamily": " 'Ubuntu Mono derivative Powerline',Consolas,'Courier New', monospace",
  "editor.fontSize": 16,
  "[markdown]": {
    "editor.defaultFormatter": "yzhang.markdown-all-in-one"
  },
  "[jsonc]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  }
}
async mounted() {
    const data = {
      level: 1,
      ...REQUEST_ALL_DATA
    }
    this.loading = true
    const { code, detail } = await getProvinceList(data)
    if (!$SUC(code)) {
      // 为省市打上首字母字段标签
      this.provinceList = detail.map((item) => {
        if (item.areaEnName) {
          item.letters = item.threeCharCode?.substr(0, 1)
        }
        return item
      })
    }
    this.loading = false
},
computed: {
    // 格式化后端返回的数据
    formatData({ provinceList }) {
      const obj = {}
      const arr = []
      // 遍历所有存在的首字母
      provinceList.forEach((v, i) => {
        obj[v.letters] = []
      })
      // 遍历完成后开始push
      provinceList.forEach((v, i) => {
        obj[v.letters].push(v)
      })
      // 遍历对象构造的key和value
      // 组成新的省份数据
      Object.keys(obj).forEach((v, i) => {
        arr.push({
          label: v,
          children: obj[v]
        })
      })
      arr.sort((a, b) => {
        return a.label.charCodeAt(0) - b.label.charCodeAt(0)
      })
      return arr
    }
}
  • 字符串长度超过指定宽度自动换行
  word-break: break-all;
  • js代码的简便写法
// 繁琐的写法是:
if (!res.detail)  {
    this.$message.warning('暂无清单数据')
    return 
}

// 简便的写法是:
if (!res.detail) return this.$message.warning('暂无清单数据')
// 繁琐的写法是:
if (res)  {
   // ...
}

// 简便的写法(避免if的多层嵌套)是:
if (!res) return
// ...
  • git branch -a 查看git远程分支的名称
  • 全局挂载vue组件
import Vue from 'vue'
import store from '@/store'
import PrintBtn from './printBtn'
import merge from 'element-ui/src/utils/merge'

// 以下为merge方法
export default function(target) {
  for (let i = 1, j = arguments.length; i < j; i++) {
    let source = arguments[i] || {};
    for (let prop in source) {
      if (source.hasOwnProperty(prop)) {
        let value = source[prop];
        if (value !== undefined) {
          target[prop] = value;
        }
      }
    }
  }
  return target;
};

export default function lodopPrintPdf(option) {
  const ExtendPrintBtn = Vue.extend(PrintBtn)
  const vm = new ExtendPrintBtn({
    computed: merge({}, ExtendPrintBtn.computed, {
      webPrinterList: () => store.getters.webPrinterList,
      selectedMapPrinterForm: () => store.getters.selectedMapPrinterForm
    })
  })
  //合并option 相当于遍历添加vm的prop
  merge(vm, option)
  //js动态加载完成
  vm.printHandler()
}
// 全局注册$lodopPrintPdf方法
export default {
  install(Vue) {
    Vue.prototype.$lodopPrintPdf = lodopPrintPdf //lodop打印pdf
  }
}
// 使用
this.$lodopPrintPdf({
    type: 'html',
    printable: this.$refs.list.$el,
    onSuccess: this.resetLoading,
    onError: this.resetLoading
})
  • $message提示框被dialog遮罩层挡住问题解决 解决方案
  • 元素水平垂直居中(使用transform属性相对自身进行移动)
.guide-warpper {
  width: 850px;
  height: 800px;
  border-radius: 10px;
  z-index: 3;
  position: absolute;
  top: 50%;
  left:50%;
  transform: translate(-50%, -50%);
  background-color: $subMenuBg;
  border: 1px solid $border-logo;
}
  • 去除饿了吗表单必填校验前面的星号
.el-form-item.is-required .el-form-item__label:before {
  content: '';
}
  • 关于是否加scoped(局部和全局)的描述 image.png
  • 遮罩层的制作
.mask {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0, 0, 0, 0.5);
  z-index: 3;
}
// 等待父组件的方法执行完毕后再关闭 loading
// 这样也不需要向父组件传递一个自定义事件了
try {
  await this.$parent.handleCopied()
} finally {
  this.loading = false
  loading.close()
}
  • 在组件上绑定不同的key值来渲染和刷新组件,用于标识不同的组件,杜绝相同组件不渲染的问题出现 image.png
  • 修改el-tooltip的宽度和行间距:
<el-tooltip
    popper-class="user-behavior-tips"
    placement="top"
    effect="light"
    :open-delay="300"
    :disabled="!row.remark"
    :content="row.remark"
>
    <div class="cut">
      {{ row.remark || '未标记' }}
    </div>
</el-tooltip>
<style lang="scss">
.user-behavior-tips {
  max-width: 180px;
  line-height: 20px;
}
</style>

<style lang="scss" scoped>
.cut {
  max-width: 160px;
  overflow: hidden;
  text-overflow: ellipsis;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
}
</style>
  • 计算属性中的get方法和set方法:
computed: {
    colorCheckAll: {
      get() {
        return this.selectColorList?.length == this.allColors?.length
      },
      set(bool) {
        if (bool) {
          this.selectColorList = this.allColors
        }
        if (!bool) {
          this.selectColorList = []
        }
      }
    }
}

-VSCode点击出现多tab的解决方案

  • 解决排序问题(分组问题使用groupBy)
import { orderBy } from 'lodash'

computed: {
    finalData({ fileListData }) {
      // 按照countryName字段进行分组
      return orderBy(fileListData, 'countryName')
    }
}
import { orderBy } from 'lodash'

props: {
    orderByKeys: {
      type: Array,
      default: () => ['countryName', 'startWeight']
    }
},

if (this.orderByKeys) this.fileList = orderBy(this.fileList, this.orderByKeys)
  • vue中设置动态class
第一种,使用数组形式添加动态class,里面的类名需要加单引号(外双内单)
:class="['group', index == curIndex && 'active']"
第二种,使用对象形式添加动态class
:class="{ ac: true, 'flex-between': isPutOn }"
  • 全局安装live-server插件
安装完成之后,就可以进入打包好的dist项目文件目录,直接使用liver-server运行,查看项目运行效果
npm install -g live-server
  • Sentry的配置
1.main.js中引入sentry文件夹的index文件
import '@/sentry'
2.index.js配置(参照官网)
import Vue from 'vue'
import router from '../router'
import * as Sentry from '@sentry/vue'
import { BrowserTracing } from '@sentry/tracing'
const needSentry = ['pro'].includes(process.env.VUE_APP_ENV_STAGE)
// Sentry会捕获用户操作系统过程中在控制台上产生的错误信息
// 判断是否为生产环境,生产环境才配置
if (needSentry) {
  Sentry.init({
    Vue,
    dsn: '请填写Sentry对应的dsn地址',
    integrations: [
      new BrowserTracing({
        routingInstrumentation: Sentry.vueRouterInstrumentation(router),
        // VUE_APP_BASE_URL对应后端api地址
        tracingOrigins: [process.env.VUE_APP_BASE_URL]
      })
    ],
    tracesSampleRate: 1.0
  })
}
  • vue2.x响应式原理

  在datareturn的变量都是响应式变量。一个组件a在html模板中(在js代码中使用到的data中return的变量,也是响应式变量,只是变化后不会更新dom)使用到这个定义好的响应式变量(简而言之,就是data中return的变量如果有在html模板中使用,就会收集依赖),就会收集依赖。那什么是依赖呢?打印data中定义的变量,会展示subssubs下对应的watcher,这个watcher就是依赖。一个vue组件a只有一个watcher,用来监听这个组件a中所有定义好的响应式变量。而在a组件的子组件及孙子组件中,如果有n个组件在html模板中,使用到a组件定义好的响应式变量,就会收集到nwatcher对象。

  • 当a中定义的某个变量发生变化,会触发set方法,刷新a组件。同时通知a组件的后代组件的html模板中使用到这个变量的watcher对象,包含html、computed以及watch里面的watcher,执行update方法来更新组件的dom
  • 当a组件的后代组件中使用到a中定义的响应式变量发生改变时,就会通过emit方法逐级通知a组件更新对应的变量,同时触发set方法,刷新a组件。同时通知a组件的后代组件的html模板中使用到这个变量的watcher对象,包含html、computed以及watch里面的watcher,执行update方法来更新组件的dom

103.vue新官方地址

104.格式化el-table的数据

<el-table-column
    v-if="orderStatus === DELIVER"
    label="包裹重量"
    prop="sendOutWeight"
    :formatter="sendOutWeightFormatter"
></el-table-column>

sendOutWeightFormatter(row, column, value, index) {
  return `${value}kg`
}

105.lodash中获取对象的属性(如没有提供默认值)的方法get

import get from 'lodash/get'

computed: {
    orderList({ initData }) {
      // 此处的第一个参数为变量,第二个参数可以使用[]或者.嵌套多层
      // 第三个参数是变量.给定属性是undefined时才返回默认值
      return get(initData, 'orderList', [])
    }
}

106.vue全局注册组件及挂载方法(install)

传送门

// globalConst.js
/* eslint-disable */
import store from '@/store'
import { Message, Loading } from 'element-ui'
import { OSS_DIR_MAP_WITH_TYPE, uploadOSSPics } from '@/commons/oss'
import { checkPermission, getDiscountPrice, parseImgSrc } from '@/utils'
import { isNumber } from '@/components/avue/utils/validate'
import get from 'lodash/get'
import { COMPANY_NAME, KEEP_ON_RECORD_CODE  } from '@/utils/constant/systemConst'
import {
  accAdd,
  accSub,
  accDiv,
  accMul
} from '@/utils/calculate'
import options from './options'

export default {
  install(Vue) {
    Vue.prototype.$formOptions = options
    Vue.prototype.COMPANY_NAME = COMPANY_NAME
    Vue.prototype.KEEP_ON_RECORD_CODE = KEEP_ON_RECORD_CODE
    Vue.prototype.$console = console.log
    Vue.prototype.parseImgSrc = parseImgSrc
    Vue.prototype.checkPermission = checkPermission
    Vue.prototype.goback = function (defaultRoute) {
      let timer
      this.$router.beforeEach((to, from, next) => {
        clearTimeout(timer)
        // console.log('beforeEach', to, from)
        next()
      })
      timer = setTimeout(() => {
        this.$router.push(defaultRoute)
      }, 200)
      
      this.$router.back()
    }
    Vue.prototype.$math = {
      // 浮点数求和
      add: accAdd,
      // 浮点数相减
      subtract: accSub,
      // 浮点数相除
      divide: accDiv,
      // 浮点数相乘
      multiply: accMul
    }
    Vue.prototype.getDiscountPrice = getDiscountPrice
    
    window.$SUC = (res) => {
      if (!res) return false
      const { code } = res
      return code == 0 || (code >= 200 && code < 300)
    }
    /*
     * 处理promise,方便await使用
     * @param {promise}
     * @return {array}
     * */
    window.awaitWrap = (promise) => {
      return promise.then((data) => [null, data]).catch((err) => [err, null])
    }
    window.awaitLoading = async (promise, option = '请稍候') => {
      typeof option === 'string' && (option = { lock: true, text: option })
      option.customClass = (option.customClass || '') + 'zIndexMax'
      const loading = Loading.service(option)
      await awaitWrap(promise)
      loading.close()
      return promise
    }
    window.awaitFormResolve = async (promise) => {
      return (await awaitWrap(promise))[1]
    }
    window.awaitResolve = async (promise, option = false, callbacks = {}) => {
      typeof option === 'boolean' && (option = { isMsg: option })
      typeof option === 'string' && (option = { isMsg: true, sucMsg: option })
      let { isMsg, sucMsg = '操作成功', errMsg } = option
    
      callbacks = callbacks || option.callbacks
      typeof callbacks === 'function' && (callbacks = { success: callbacks })
      let { success, error } = callbacks
    
      let [err, res] = await awaitWrap(promise)
      if (!$SUC(res)) {
        if (err && /1001000/.test(err.code) && err.message) {
          Message.warning(err.message)
        }
        res = false
      }
    
      if (res) {
        isMsg && sucMsg && Message.success(sucMsg)
        typeof success === 'function' && success()
      } else {
        isMsg && errMsg && Message.warning(errMsg)
        typeof error === 'function' && error()
      }
    
      return res
    }
    window.awaitResolveLoading = async (promise, option, callbacks) => {
      return awaitLoading(awaitResolve(promise, option, callbacks), option?.loadingOption)
    }
    window.awaitResolveDetail = async (promise) => {
      let res = await awaitResolve(promise)
      return res ? res?.detail : undefined
    }
    window.awaitResolveDetailLoading = async (promise, option) => {
      return awaitLoading(awaitResolveDetail(promise, option))
    }
    window.promiseAll = (promises) => {
      return new Promise((resolve) => {
        let res = []
        if (!Array.isArray(promises) || promises.length === 0) return resolve(res)
        let resLen = 0
        promises.map((promise, index) => {
          if (!(promise instanceof Promise)) {
            res[index] = promise
            if (++resLen === promises.length) resolve(res)
            return
          }
          return promise
            .then((data) => {
              res[index] = data
            })
            .catch(() => {
              res[index] = false
            })
            .finally(() => {
              if (++resLen === promises.length) resolve(res)
            })
        })
      })
    }
    window.refreshDic = async (promise, dicName) => {
      await awaitWrap(promise)
      store.dispatch('RefreshDic', dicName)
      return promise
    }
    window.doSomethingAfterRequest = async (promise, fn) => {
      // eslint-disable-next-line no-undef
      await awaitWrap(promise)
      fn()
      return promise
    }
    
    //oss图片上传公共方法
    window.$uploadOSSPics = uploadOSSPics
    //oss图片上传对应的目录映射
    window.$ossDirMapWithType = OSS_DIR_MAP_WITH_TYPE
    
    window.$GET = (object, path, defaultValue) => {
      return get(object, path, defaultValue) || defaultValue
    }
  
    /**
     * 计算文本在页面所占px宽度 -- 扩展String原型方法pxWidth
     * 获取文本px宽度
     * @param font{String}: 字体样式
     **/
    String.prototype.pxWidth = function(font) {
      // re-use canvas object for better performance
      var canvas = String.prototype.pxWidth.canvas || (String.prototype.pxWidth.canvas = document.createElement('canvas')),
        context = canvas.getContext('2d')
    
      font && (context.font = font)
      var metrics = context.measureText(this)
    
      return metrics.width
    }
  
    /**
     * 格式化数字
     * @param num <String|Number>
     * @param fixed
     **/
    Vue.prototype.parseNumber = window.parseNumber = function (num = this, fixed = 2) {
      if (!isNumber(num)) return '0.00'
      return parseFloat(num || 0).toFixed(fixed)
    }
    Number.prototype.parseNumber = String.prototype.parseNumber = function (fixed) {
      return parseNumber(this, fixed)
    }
  }
}
// main.js
import globalConst from '@/commons/globalConst'
Vue.use(globalConst)

107.vue集成i18n语言配置

传送门

108.优雅地使用vue-treeselect组件的插槽

vue-treeselect组件官方文档传送门

  • $scopedSlots是记录一个组件中自带的各类插槽名称及其细节的对象
  • 使用v-for可以循环对象,支持三个传参,value,keyindex
// 父组件:
<treeselect
  :class="[size == 'mini' && 'mini']"
  v-model="value"
  :options="options"
  :normalizer="normalizer"
  :defaultExpandLevel="$attrs.defaultExpandLevel || Infinity"
  :disabled="isDisabled"
  v-bind="$attrs"
  v-on="$listeners"
  @select="selectHandler"
>
    <template v-for="(val, key) in $scopedSlots" #[key]="scope">
      <slot :name="key" v-bind="scope"></slot>
    </template>
</treeselect>
// 子组件:
<TreeSelect
    style="width: 340px"
    :selectedValue.sync="folderId"
    :options="foldersList"
    :defaultExpandLevel="2"
    placeholder='Please select folder'
>
   <template #option-label="{ node }">
      <div class="flex-between">
        <div class="text-cut">
          <svg-icon icon-class="folder" />
          {{ node.label }}
        </div>
        <div v-if="node.raw.isDefault == 1">
          <el-button size="mini">Default</el-button>
        </div>
      </div>
   </template>
</TreeSelect>

109.修改谷歌浏览器样式(需要穿透)

<style lang="scss" scoped>
::v-deep {
  ::-webkit-scrollbar {
    width: 6px; //滚动条宽度
  }
  ::-webkit-scrollbar-thumb {
    border-radius: 2.5px;
    // 设置滚动条滑块样式
    background: #d25565 !important;
  }
  ::-webkit-scrollbar-track {
    // 设置滚动轨道的样式
    background: #2e94b9 !important;
    border-radius: 2.5px;
  }
}
</style>

110.i18n多语言集成及配置

// src/i18n/index.js
/* eslint-disable */
import Vue from 'vue'
import VueI18n from 'vue-i18n'

Vue.use(VueI18n)

function loadLocaleMessages() {
  const locales = require.context(
    './locales',
    true,
    /[A-Za-z\d-_,\s]+\.js$/i
  )
  const messages = {}
  locales.keys().forEach(key => {
    const matched = key.match(/([A-Za-z\d-_]+)\./i)
    if (matched && matched.length > 1) {
      const locale = matched[1]
      messages[locale] = locales(key).default
    }
  })
  return messages
}


const i18n = new VueI18n({
  locale: process.env.VUE_APP_I18N_LOCALE || 'en',
  fallbackLocale: process.env.VUE_APP_I18N_FALLBACK_LOCALE || 'en',
  messages: loadLocaleMessages()
})
//添加全局变量
window.$i18n = i18n
window.$t = i18n.t.bind(i18n)
export default i18n
// .env
VUE_APP_I18N_LOCALE=en
VUE_APP_I18N_FALLBACK_LOCALE=en
// main.js
import i18n from './i18n' //多语言配置

new Vue({
  el: '#app',
  i18n,
  router,
  store,
  render: (h) => h(App)
})
// i18文件夹有index.js,和locales文件夹,用于存放各类语言的js文件
// @/locales/en.js
import locale from 'element-ui/lib/locale/lang/en'
export default {
  ...locale,
  //公共组件
  base: {
    upload: {
      describe: 'Add your order data file'
    },
    folder: {
      placeholder: 'Please select folder'
    }
  },
  //自定义组件
  comp: {

  },
  //页面
  page: {

  }
}
// 使用
<div class="describe">{{$t('base.upload.describe')}}</div>

111.打印清单中,img标签添加请求失败时的默认图片

<img class="pic" crossorigin="true" :src="getPath(item)" @error="errorHandler(item)"/>
 // 错误回调,如果请求失败,则为当前数据添加errUrl默认图片字段
 errorHandler(item) {
   this.$set(item, 'errUrl', require('@/assets/images/default.png'))
 },
 getPath(item) {
   return item.errUrl || `${item.style.displayImageUrl}?id=${getUUID()}`
 }

112.解决el-tree树状结构高亮错误显示的问题

  el-tree会在某些情况下出现祖先级和后代级同时高亮的问题。为避免这种情况发生,点开vue调试工具,发现树状的高亮样式是由节点的isCurrent控制的。如果isCurrenttrue,则树状结构高亮,否则不高亮。因此,可以通过插槽中的node来判断。如果!node.isCurrent,则说明当前节点不高亮,给个正常的文字样式就好。

<TreeSelect
  style="width: 340px"
  v-model="folderId"
  :defaultText="defaultText"
  :options="foldersList"
  @handleNodeClick="handleNodeClick"
>
  <template #prefix>
    <svg-icon class="folder-icon" icon-class="folder" />
  </template>
  <template v-slot="{ node, data }">
    <div class="flex-between">
      <div class="text-cut">
        <svg-icon class="folder-icon" icon-class="folder" />
        <span :class="[!node.isCurrent && 'normal-name']"> {{ data.name }} </span>
      </div>
      <div class="flex-center" v-if="data.isDefault == 1">
        <div class="default-button">{{ $t('base.folder.defaultText') }}</div>
      </div>
    </div>
  </template>
</TreeSelect>

113.善用lodash中的方法

// 深拷贝 和 判断两个对象/数组是否相等
import { cloneDeep, isEqual } from 'lodash'

watch: {
    visible(val) {
      if (val) this.selectAssginCheckListCopy = cloneDeep(this.form.selectAssginCheckList)
    }
},

if (isEqual(this.selectAssginCheckListCopy, styleIds)) return this.$message.warning('当前分配的款式未做任何改变')

114.全局封装loading

import { Loading } from 'element-ui'

window.awaitLoading = async (promise, option = '请稍候') => {
    typeof option === 'string' && (option = { lock: true, text: option })
    option.customClass = (option.customClass || '') + 'zIndexMax'
    const loading = Loading.service(option)
    await awaitWrap(promise)
    loading.close()
    return promise
}

.zIndexMax {
    z-index: 99999999 !important;
}

this.$Confirm({
    type: 'warning',
    title: this.$t('page.folder.del'),
    message: this.$t('page.folder.conDel'),
    beforeClose: (action, instance, done) => {
      this.onDelete(action, done)
    }
})

async onDelete(action, done) {
  if (action == 'confirm') {
    const res = await awaitLoading(
      awaitResolve(
        imageApi.deletePicture({
          idList: this.selectData.map(({ id }) => id)
        })
      )
    )
    if (!res) return
    done(true)
    this.init()
    this.$message.success(this.$t('page.folder.success'))
  } else done(true)
}

115. 使用deactivated钩子函数处理页面缓存问题(早期问题记录)

  某些详情页面可能因为缓存问题导致再次进入时,出现第n(n不为1)页的列表数据,需要使用 deactivated钩子函数去处理和解决。被缓存过的页面组件只在第一次加载时触发created生命周期函数,并且不会在离开页面时销毁组件。

  被keep-alive缓存的组件的生命周期:第一次进入,created -> mounted -> activated,退出时触发deactivated。当再次进入时,只触发activated

deactivated() {
    this.page = 1
}

116. 使用插件解决el-table中的表单验证

  1. 在package.json中添加"table-form-item": "^1.0.4",重新npm install启动项目;

  2. 在需要验证的表格index文件中,

// row绑定的是后端返回的当前行的表格数据
// prop绑定的是表单的绑定值
// required表示的是是否必填
// label表示的是必填的前缀文本提示信息
// rules表示的是自定义表单校验的值

<TableFormItem ref="tableFormItem" :row="row" :prop="`productQuantities_${index}`" :required="true" label="数量" :rules="rules">
  <el-input clearable size="small" v-model.number="row[`productQuantities_${index}`]"></el-input>
</TableFormItem>

import { tableFormItem as TableFormItem } from 'table-form-item'

components: { TableFormItem }

rules: [{ validator: isInteger }]

async validHandler() {
  const tableFormItemList = this.$refs.tableFormItem
  tableFormItemList.map((node) => node.onFieldChange())
  await this.$nextTick()
  // 此处不为组件,则不用$el
  // 如果此处为组件,则用$el, this.$refs.table.$el表示组件的dom元素
  // 此处的table ref是包裹整个表格的div容器
  const errList = this.$refs.table.querySelectorAll('.el-form-item__error')
  return !!errList.length
},

async handleSave() {
  if (await this.validHandler()) return
  ...
}

117. el-table多字段关联校验

  多字段关联校验是本人自定义的名称,代表一个字段可能需要关联表格上的其他字段的值,然后进行判断校验

// 校验规则
var verifyTaxRate = ({ field }, value, callback) => {
  const continentDictCodeProp = `continentDictCode_${field.split('_')[1]}`
  const continentDictCodeValue = this.countryForm[continentDictCodeProp]
  if (continentDictCodeValue !== '欧洲') return callback()
  if (!value.trim().length) return callback(new Error('税率必填'))
  if (!Number(value) || Number(value) <= 0) return callback(new Error('税率必须为正数'))
  callback()
}

118. el-select显隐是通过visible属性判断的

<el-select
  ref="select"
  v-bind="selectProps"
  :class="[defaultText && 'has-default-text']"
  :value="valueTitle"
  :popperClass="concatPopperClass"
>
 ...
</el-select>

const selectTreeEl = this.$refs.select
// 关闭下拉列表
selectTreeEl.visible = false
// 开启下拉列表
selectTreeEl.visible = true

119. js中导出异步函数(export async function)

120. 一个获取嵌套多层数组的数据的好方法

import { flatMapDeep, map } from 'lodash'

export function flatMapDeepByArray(data, mapArr = [], mapKeyArr = []) {
  let flatMapArr = []
  if (!mapArr.length) return []
  if (isPlainObject(data)) {
    const shiftData = data[mapArr.shift()]
    //传递进来的data是一个空对象,返回[]
    if(!shiftData) return []
    flatMapArr = Array.isArray(shiftData) ? shiftData : [shiftData]
  } else {
    flatMapArr = data
  }
  //重置mapKeyArr
  mapKeyArr = mapKeyArr.slice(0, mapArr.length)
  for (let i = 0; i < mapArr.length; i++) {
    flatMapArr = flatMapDeep(flatMapArr, (n) => {
      const arr = $GET(n, `${[mapArr[i]]}`, [])
      const sliceKeyArr = mapKeyArr.slice(0, i + 1)
      const sliceMapArr = mapArr.slice(0, i + 1)
      sliceKeyArr.map((key, k) => {
        arr.map((nItem) => {
          if (k == sliceMapArr.length - 1) {
            return (nItem[`$${key}`] = n)
          }
          nItem[`$${key}`] = n[`$${key}`]
        })
      })
      return arr
    })
  }
  return flatMapArr
}


// 先扁平化数组,再获取数组里面的某个属性,形成数组
const pictureList = map(
        flatMapDeepByArray(selectedData, ['customProductList', 'customShowImageList']),
        'showImagePath'
      )

121. 将一个函数拆成表达多个业务的函数

// 函数过长时,需要将我们有条理地将其拆分成若干个函数,处理不同的业务逻辑,条理清晰

async toQuery() {
    if (!this.validate()) return
    ...  // dosomething
}

validate() {
  if (this.searchForm.expressCompanyId && this.text) return true
  this.$message.warning('请先选择物流方式并扫描')
}

122.js是解释型语言还是编译型语言

123.分享一个vue拖拽插件

`首先需要npm install vuedraggable, 然后引入draggable组件并注册`
`使用draggable组件包裹并绑定需要拖拽的数据,即可实现实时拖拽并改变数据的功能`
`它本质是基于draggable.js进行开发的`

<template>
  <div class="app-container">
    <draggable v-model="JayList" class="drag-class">
      <div class="item" v-for="item in JayList" :key="item">
        {{ item }}
      </div>
    </draggable>
    <div class="mt20">{{ JayList }}</div>
  </div>
</template>

<script>
import draggable from 'vuedraggable'

export default {
  components: {
    draggable
  },

  data() {
    return {
      JayList: [
        '蒲公英的约定',
        '说好的幸福呢',
        '搁浅',
        '等你下课',
        '彩虹',
        '安静',
        '爱情废材',
        '说好不哭',
        '枫',
        '反方向的钟',
        '一路向北'
      ]
    }
  }
}
</script>

<style lang="scss" scoped>
.item {
  cursor: pointer;
  margin-bottom: 10px;
  &:hover {
    color: blue;
  }
}
</style>

  效果浏览:

image.png

124. 通过加锁实现分情况动态提示

  假定有这么一种需求:需要在模态框关闭的时候提示信息,但是在模态框里面点击跳转到其他页面时,不需要提示信息。这个时候就可以通过使用加锁的方式实现分情况动态提示。

// 模态框里面的跳转方法

linkTo() {
  this.lock = true
  
  setTimeout(() => {
    this.lock = false
  }, 1000)
  
  switch (this.currentIndex) {
    case 0:
      this.$router.push({ name: '创建组合产品' })
      this.changeDialogStatus()
      break

    case 1:
      this.$router.push({ name: '编辑自定义底板定制' })
      this.changeDialogStatus()
      break

    case 3:
      this.$router.push({ name: '组合定制产品' })
      this.changeDialogStatus()
      break
  }
},

// 模态框关闭的回调方法

closeHandler() {
  this.changeDialogStatus()
  if (this.lock) return
  this.$message({
    message: '引导窗口已隐藏到右上方操作引导里,需要时可再次进行点击',
    type: 'success',
    customClass: 'message-index'
  })
}
.message-index {
  z-index: 3;
}

125. 字体翻译的自定义动态文案

  可以考虑定义$变量,使用replace方法,将静态文案使用动态变量替换,然后再用v-html解析需要展示的样式,比如:

`配置字体翻译:`

`1. 在en.js中:`

helpCenter: {
  results: '$total results for <span style="color: #384edb">"$keyWord"</span> in All Categories'
}

`2. 在zh-CN.js中:`

helpCenter: {
  results: '在所有分类中搜索到关于<span style="color: #384edb">“$keyWord”</span>的$total个结果'
}
<template>
  <div class="result" v-html="results"></div>
</template>

<script>
export default {
  props: {
    totalTitle: Number,
    keyWord: String
  },

  computed: {
    results({ totalTitle, keyWord }) {
      return this.$t('page.helpCenter.results').replace('$total', totalTitle).replace('$keyWord', keyWord)
    }
  }
}
</script>

<style lang="scss" scoped>
.result {
  color: $color-light-gray;
  font-size: 14px;
  margin-bottom: 40px;
}
</style>

效果浏览:

image.png

126. provideinject的用法

`祖先组件:`
import commonApi from '@/api/common'

provide() {
    return {
      `materialDic需要为响应式数据,因为初始值为空数组`
      materialDic: this.materialDic,
      technologyDic: this.technologyDic
    }
},

data() {
    return {
      materialDic: [],
      technologyDic: []
    }
},

methdos: {
     async getMaterialDic() {
      try {
        const { detail } = await commonApi.getDic({ dictName: '原型材质' })
        `不能重新赋值,重新赋值是新的引用地址,无法达到响应式的效果`
        `直接使用vue重写的push方法来达到响应式的效果`
        this.materialDic.push(...detail)
      } catch {}
    },

    async getTechnologyDic() {
      try {
        const { detail } = await commonApi.getDic({ dictName: '生产工艺' })
         this.technologyDic.push(...detail)
      } catch {}
    }
}

`后代组件:`

inject: ['materialDic', 'technologyDic'],

127. 使用vue指令封装暂无数据组件

`子组件,定义组件暂无数据的三种不同情况:`

<template>
  <div class="empty-component">
    <i :class="['iconfont', 'icon', emptyObj.icon]"></i>
    <div class="txt">
      <slot>
        <span v-html="emptyObj.txt"></span>
      </slot>
    </div>
  </div>
</template>

<script>
export default {
  props: {
    type: {
      type: String,
      default: 'data'
    }
  },

  computed: {
    emptyObj({ type }) {
      return ({
        data: {
          icon: 'icon-wushuju',
          txt: '暂无数据'
        },
        picture: {
          icon: 'icon-wutu',
          txt: '暂无图片'
        },
        error: {
          icon: 'icon-yemiandiushi',
          txt: '404<br/>抱歉,页面没找到'
        }
      })[type]
    }
  }
}
</script>

<style lang="scss" scoped>
.empty-component {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-direction: column;
  padding: 30px;
  font-size: $text-medium;
  color: $color-sub;
}

.icon {
  font-size: 50px;
  max-width: 50vw;
}

.txt {
  text-align: center;
  margin-top: 8px;
}
</style>
`暂无数据指令封装:`

import Vue from 'vue'
import empty from '@/components/base/empty'
import { validatenull } from '@/components/avue/utils/validate'
import { isArray } from 'lodash'
import { validData } from '@/components/avue/utils/util';

const DEFAULT_ARG = 'data'

export default {
  `当指令第一次绑定到元素时调用,只调用一次`
  bind(el, binding) {
    `这个arg为指令上绑定的arg。例如: v-empty:picture和v-empty:product,
    对应的arg为picture和product`
    let { arg } = binding
    `如果不指定arg,则默认传data`
    arg = validData(arg, DEFAULT_ARG)
    `创建empty组件的子类构造器,并实例化`
    const EmptyConstructor = Vue.extend(empty)
    const emptyInstance = new EmptyConstructor({
      `需要新建一个div来包裹组件,不能直接用el`
      el: document.createElement('div'),
      propsData: {
        type: arg
      }
    })
    `在el上挂载empty组件的子类实例`
    `在el上挂载empty组件的子类dom`
    el.emptyInstance = emptyInstance
    el.emptyInstanceEl = emptyInstance.$el
  },
  
  `当绑定的元素插入到 DOM 中时调用,可以发生多次`
  inserted(...args) {
    renderEmptyDom(...args)
  },
  
  `当绑定元素的所有子节点都被更新后调用`
  componentUpdated(...args) {
    renderEmptyDom(...args)
  },
  
  `当指令从元素上解除绑定时调用,只调用一次`
  `为了避免内存泄漏或者其他问题, 需要在unbind钩子函数中清理一些引用和状态`
  unbind(el) {
    if (el.emptyInstanceEl) el.emptyInstanceEl.parentNode.removeChild(el.emptyInstanceEl)
    el.emptyInstance.$destroy()
    el.emptyInstance = null
    el.emptyInstanceEl = null
  }
}

/**
 * 渲染空白占位
 * @param args
 * @returns {void|*|ActiveX.IXMLDOMNode}
 */
function renderEmptyDom(...args) {
  const [el, binding] = args
  let { value } = binding
  `对后端返回的数据做校验,即v-empty="data"`
  if (!isArray(value)) return
  `在el上遍历查找挂载暂无数据组件的dom`
  const emptyEl = [...el.childNodes].find(node => node === el.emptyInstanceEl)
  `如果数组为空数组,且找得到暂无数据组件,则说明已经挂载,直接return就好`
  `如果找不到暂无数据组件,则在el上挂载暂无数据组件的dom元素`
  if (validatenull(value)) {
    if (emptyEl) return
    return el.appendChild(el.emptyInstanceEl)
  }
  `如果数组不为空数组,则说明后端返回了数据,则清除对应的dom`
  if (emptyEl) emptyEl.parentNode.removeChild(emptyEl)
}
<el-button class="mt10" size="mini" type="primary" @click="clickHandler">
    click
</el-button>
<el-button class="mt10" size="mini" type="primary" @click="clearHandler">
    clear
</el-button>
<div v-empty="list">
    <div class="mt10" v-for="{ name, id } in list" :key="id">
        我是{{ name }}
    </div>
</div>

data() {
    return {
        list: []
    }
},

methods: {
    clickHandler() {
      this.list = [
        { id: 1, name: 'cxk1' },
        { id: 2, name: 'cxk2' },
        { id: 3, name: 'cxk3' },
        { id: 4, name: 'cxk4' }
      ]
    },
    
    clearHandler() {
      this.list = []
    }
}

128. git小技巧

  • 暂存:
`当我们需要拉取代码,又不想提交代码时,可以使用git的暂存功能:`
`传送门: https://blog.csdn.net/weixin_38629529/article/details/120240362`

git add .
git stash // 暂存
git stash pop // 需要时,使用这行代码取出最新一次暂存的代码

129. 关于.native修饰符

1.png

130. 如何理解vue的单页面模式

  Vue的单页面模式是指在一个页面内使用前端路由来实现多个视图的切换,而不需要每次切换视图都重新加载整个页面,从而提高了页面的加载速度和用户体验。

  在Vue的单页面模式中,页面会被拆分成多个组件,每个组件对应一个视图,通过前端路由来切换不同的视图。当用户点击导航链接或执行其他操作时,前端路由会根据当前的URL路径来匹配对应的组件,并将其渲染到页面上。

  Vue使用了自己的前端路由库Vue Router来实现单页面模式。Vue Router提供了一个Router 实例来管理前端路由,可以在应用中定义多个路由和对应的组件,通过 router-link 组件和 router-view 组件来实现导航链接和视图的切换。

  单页面模式可以提高页面的加载速度和用户体验,因为它避免了每次切换视图都需要重新加载整个页面的问题,同时也方便了前端开发人员对页面的管理和维护。然而,由于单页面应用在前端路由和组件的管理方面较为复杂,因此需要开发人员具备一定的前端技术和经验才能正确地使用和维护单页面应用。这也是为什么刷新(本页面)或者新开页面(新开页面)中,vueX数据会丢失的原因。

131. this.$el获取当前vue组件实例的dom

132. 带有表头的表格合并问题处理

  何为带有表头的表格呢?其实数据是由表头和数据一起构成的。

image.png

handleTableData(data) {
  let tmpArr = []
  data.forEach((item) => {
    item.isHeader = true
    tmpArr.push(item)
    `记录合并的行数distributionLen`
    const distributionLen = item.factoryDistributionOrderVOList?.length
    item.factoryDistributionOrderVOList?.forEach((distributionOrder, dIndex) => {
      const productCount = flatMapDeepByArray(distributionOrder, ['factoryOrderVOList', 'orderItem', 'productCount'])
      distributionOrder.$createTime = item.createTime
      distributionOrder.$index = dIndex
      distributionOrder.$len = distributionLen
      distributionOrder.$order = item
      distributionOrder.productCount = productCount.reduce((prev, cur) => cur + prev, 0)
      tmpArr.push(distributionOrder)
    })
  })
  return tmpArr
},
    
spanMethod({ row, columnIndex }) {
  if (row.isHeader) {
    return columnIndex === 0 ? [1, 4] : [0, 0]
  } else if ([2, 3].includes(columnIndex)) {
    return row.$index === 0 ? [row.$len, 1] : [0, 0]
  }
}