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

1,650 阅读10分钟

写在前面

  今天更新项目经验时才发现,原来掘金文章也是有字数限制的...故不得不新建一篇博文来记录项目经验。

41.对象和数组的转换问题

  • 对象转换为数组

eg:

//该文件用于解释说明
//最终目的需要将original这一对象转换为transformed这一数组的数据格式
let original = {
    a_0: '值1',
    a_1: '值2',
    a_2: '值3',
    b_0: '值4',
    b_1: '值5',
    b_2: '值6'c_0: '值7'c_1: '值8',
    c_2:'值9'
}

//该对象数组的长度为3
let transformed = [{a: '值1'b: '值4'c: '值7'},
           {a: '值2'b: '值5'c: '值8'},
           {a: '值3'b: '值6'c: '值9'}]
//获取对象的值
let countryForm = this.$refs.previewTableRef.countryForm
//用于每次调用方法都清空临时数组
let tmp = []
//遍历对象
for (let key in countryForm) {
    //将对象_左边的值赋给fKey,将对象_右边的值赋给num
    const [fKey, num] = key.split('_')
    //用于每次遍历化空数组的第m位时,将其转换为对象数组
    if (!tmp[num]) {
      tmp[num] = {}
    }
    //根据num值的不同,为数组的第m个对象动态赋予key和value
    tmp[num][fKey] = countryForm[key]
}
  • 数组转换为对象

eg:

//该文件用于解释说明
//最终目的需要将original这一对象转换为transformed这一数组的数据格式
//该对象数组的长度为3
let transformed = [{a: '值1'b: '值4'c: '值7'},
           {a: '值2'b: '值5'c: '值8'},
           {a: '值3'b: '值6'c: '值9'}]

let original = {
    a_1: '值1',
    a_2: '值2',
    a_3: '值3',
    b_1: '值4',
    b_2: '值5',
    b_3: '值6'c_1: '值7'c_2: '值8',
    c_3:'值9'
}
this.fileList.map((item, index) => {
  for (let k in item) {
    form1[`${k}_${index}`] = item[k]
  }
})
  • vue中对象的增删操作
  const from = {
    a_0: '1',
    b_0: '2',
    a_1: '3',
    b_1: '4'
  }
  //使用vue的语法为form对象增加key和value
  this.$set(this.from, 'a_2', '输入a_2的值')
  this.$set(this.from, 'b_2', '输入b_2的值')
  //this.$delete,删除对象的某个key和value值
  delete this.from.a_0
  delete this.from.b_0

42.表单嵌套非动态表格

先上效果图:
image.png

  <el-form
    :model="countryForm"
    ref="countryRef"
    :rules="countryRules"
    :inline="true"
    label-width="108px"
  >
    <!--表格绑定的数据fileListData用于展示初始数据-->
    <!--form绑定的数据countryForm用于和输入框的值双向绑定-->
    <el-table :data="fileListData" border style="width: 100%">
      <el-table-column
        align="center"
        v-for="item in propList"
        :key="item.id"
        :prop="`${item.prop}`"
        :label="`${item.label}`"
        width="180"
      >
        <template slot-scope="scope">
          <el-form-item :prop="`${item.prop}_${scope.$index}`" :rules="countryRules[item.prop]">
            <el-input
              size="mini"
              v-model="countryForm[`${item.prop}_${scope.$index}`]"
              style="width: 120px"
            ></el-input>
          </el-form-item> </template
      ></el-table-column>
    </el-table>
  </el-form>
    //countryForm就是第41点中的form1
    return {
      currency: [],
      propList: [
          { id: 0, prop: 'countryCnName', label: '国家/地区中文名称' },
          { id: 1, prop: 'countryEnName', label: '国家/地区英文名称' },
          { id: 2, prop: 'twoCharCode', label: '国家/地区两位代码' },
          { id: 3, prop: 'continentDictCode', label: '所属洲' },
          { id: 4, prop: 'prepaymentType', label: '是否需要填写税号' },
          { id: 5, prop: 'currency', label: '报关金额币种' }
      ],
      countryRules: {
        countryCnName: [
          { required: true, message: '请填写中文名称', trigger: 'blur' },
          { validator: verifyCn, trigger: 'blur' }
        ],
        countryEnName: [
          { required: true, message: '请填写英文名称', trigger: 'blur' },
          { validator: verifyEn, trigger: 'blur' }
        ],
        twoCharCode: [
          { required: true, message: '请填写二位码', trigger: 'blur' },
          { validator: verifyCode, trigger: 'blur' }
        ],
        continentDictCode: [
          { required: true, message: '请填写所属洲名称', trigger: 'blur' },
          { validator: verifyState, trigger: 'blur' }
        ],
        currency: [
          { required: true, message: '请填写币种', trigger: 'blur' },
          { validator: verifyCurrency, trigger: 'blur' }
        ],
        prepaymentType: [
          { required: true, message: '请填写税号', trigger: 'blur' },
          { validator: verifyType, trigger: 'blur' }
        ]
      }
    }
  }

43.饿了么走马灯的使用

<el-carousel
  @change="slideChangeHandler"
  indicator-position="none"
  arrow="always"
  :autoplay="false"
  :loop="false"
  height="100px"
>
  <el-carousel-item v-for="item in originalPicture" :key="item.currentImageIndex">
    <el-image
      fit="contain"
      style="width: 98px; height: 98px; border: 1px solid #eeeeee"
      :src="item.path"
      :z-index="9999"
    />
  </el-carousel-item>
</el-carousel>
//computed计算属性
originalPicture() {
  try {
    if (!this.product) return
    let picList = []
    this.product.images.map((item) => {
      picList.push(item.thumbnail_path)
    })
    picList = picList.map((item, index) => ({
      path: item,
      currentImageIndex: index
    }))
    return picList
  } catch (error) {}
},
// 事件名称 | 说明            | 回调参数             
// change  | 幻灯片切换时触发 | 目前激活的幻灯片的索引,原幻灯片的索引
// 放在methods方法里
slideChangeHandler(activeIndex) {
  //activeIndex是当前激活的图片索引
  const item = this.originalPicture[activeIndex]
  this.currentImageIndex = activeIndex
  item.currentImageIndex = activeIndex
},
//重置走马灯的样式(根据当前图片前后是否存在图片,来动态地显示左右箭头)
<style lang="scss" scoped>
.dialog-warpper {
  ::v-deep {
    .el-carousel__arrow.el-carousel__arrow {
      border: none;
      outline: 0;
      padding: 0;
      margin: 0;
      height: 36px;
      width: 36px;
      cursor: pointer;
      transition: 0.3s;
      color: #fff;
      position: absolute;
      top: 50%;
      z-index: 10;
      transform: translateY(-50%);
      text-align: center;
      color: #cdcdcd;
      font-weight: 600;
      background: none;
      left: 0;
      font-size: 20px;
    }
    .el-carousel__arrow--left.el-carousel__arrow--left {
      left: 0;
    }
    .el-carousel__arrow--right.el-carousel__arrow--right {
      left: auto;
      right: 0;
    }
  }
}
</style>

43.对正则表达式的理解

[]表示匹配里面的任意字符,这些字符可以出现也可以不出现,但只要输入的字符在这些字符之内即正确。

//匹配中文和中英文括号
var verifyCn = (rule, value, callback) => {
  if (/^[\u4e00-\u9fa5()()]{0,}$/.test(value) == false) {
    callback(new Error('请输入中文'))
  } else {
    callback()
  }
  callback()
}

//匹配英文和中英文逗号和句号
var verifyEn = (rule, value, callback) => {
  if (/^[A-Za-z .,。, ]+$/.test(value) == false) {
    callback(new Error('请输入英文'))
  } else {
    callback()
  }
  callback()
}

44.项目首屏优化

首屏优化实现

45.v-model语法糖

v-model原理

46.eventBus的使用

eventBus的使用

47.el-popover跟随页面滚动条的滚动而滚动

  •  为el-popover组件添加:append-to-body="false"即可。

  •  学会定义常量来表示数字,方便后续维护。比如设置变量shelves的值为1,假设有多处地方使用到这个变量进行判断,后续如果要修改这个条件,只需要改变这个数值就好,而不需要重新多处改动,从而达到更好维护项目代码的目的。

  •  在main.js中引入echart的方式:

import * as echarts from 'echarts'
Vue.prototype.$echarts = echarts

48.表单嵌套表格对话框校验失败禁用按钮,校验成功激活按钮

 比较笨的方法是给按钮添加disabled属性。在表格每条校验规则中,如果校验成功就将按钮状态设置为false,校验失败就将按钮状态设置为true。每次点击确认按钮的时候进行表单校验,如果校验失败就将按钮状态设置为true,在对话框每次要销毁的时间中将按钮状态设置为false(如果表格字段较多,使用这种方法需要对表格进行不同类型的表单校验从而判断按钮的状态,会显得比较繁琐)在这里推荐使用一种简单的方法,同样是给按钮添加disabled属性,核心思想是通过饿了么未暴露出的字段validateState进行判断。一个表单可以看作是有很多个带有校验规则的输入框组成,如果其中某个输入框校验失败,那么这个输入框dom元素对应的validateState值就为false(这个变量可以通过使用vue-devtools调试工具get到)。因此可以在表格的失焦事件中,遍历所有输入框dom元素的validateState变量(校验成功为success,校验失败为error)。只要有一个值为error,那么就将按钮的状态设置为true。核心代码如下:

<template>
  <div class="previewTable">
    <slot name="center"></slot>
    <div class="secondPart">
      <el-form
        :validate-on-rule-change="true"
        :model="countryForm"
        ref="countryRef"
        :rules="countryRules"
        :inline="true"
        label-width="108px"
      >
        <el-table :data="fileListData" border style="width: 100%">
          <el-table-column
            align="center"
            v-for="item in propList"
            :key="item.id"
            :prop="`${item.prop}`"
            :label="`${item.label}`"
            width="180"
          >
            <template slot-scope="scope">
              <el-form-item ref="elFormItem" :prop="`${item.prop}_${scope.$index}`" :rules="countryRules[item.prop]">
                <el-input
                  @blur="onBlur"
                  size="mini"
                  v-model="countryForm[`${item.prop}_${scope.$index}`]"
                  style="width: 120px"
                ></el-input>
              </el-form-item> </template
          ></el-table-column>
        </el-table>
      </el-form>
    </div>
  </div>
</template>
  methods: {
    onBlur() {
      // 使用blur事件而不使用input事件的原因是为了避免一直调用方法。
      // 这里使用this.$nextTick的原因在于,失去焦点会先触发失焦事件,再触发饿了么的校验方法。
      // 而失焦方法中使用的变量validateState的值是由自定义的饿了么校验方法决定的,如果不延迟处理,可能会调用上次的校验状态。
      // 因此应该先调用校验方法,再使用失焦事件获取校验结果来改变按钮的状态,所以要使用this.$nextTick给个延迟调用。
      this.$nextTick(() => {
        // 获取所有输入框的dom元素,得到一个数组
        const { elFormItem } = this.$refs
        const validateStateArr = elFormItem.map((item) => {
          return item.validateState
        })
        // 遍历循环,查找校验失败的dom
        // 如果校验失败,就将按钮状态设置为true
        // 否则将按钮状态设置为false
        if (validateStateArr.some((state) => state === 'error')) {
          this.$emit('update:state', true)
          return
        }
        this.$emit('update:state', false)
      })
    }
  }

49.防抖按钮组件的封装

<template>
  //使用v-bind="$attrs"(获取父组件传递过来的props除去自身组件带有的props的值)获取父组件传递过来的props
  //v-on必须绑定"new$listeners",因为这有这样才能触发本组件的点击方法,从而进行防抖
  <el-button :loading="loading" v-bind="$attrs" v-on="new$listeners">
    <slot></slot>
  </el-button>
</template>

<script>
export default {
  data() {
    return {
      loading: false
    }
  },
  computed: {
    new$listeners() {
      //重构事件,使用方法中定义的点击事件覆盖...this.$listeners中的点击事件
      return Object.assign(
        {
          ...this.$listeners
        },
        {
          click: this.onclick
        }
      )
    }
  },
  methods: {
    async onclick(e) {
      // 防抖核心代码,如果请求未结束,无法再次点击按钮
      if (this.loading) return
      // 设置按钮为loading状态
      this.loading = true
      try {
        // 获取父组件中定义的点击事件
        await this.$listeners.click.fns(e)
      } catch (err) {
        console.log(err)
      }
      //相当于finally,最后都会将按钮的loading状态重置为false
      this.loading = false
    }
  }
}
</script>

<style></style>

 组件的使用(防抖按钮触发的方法必须使用async和await):

<template #footer>
  <LoadingBtn type="primary" @click="doSubmit" :isDisabled="isDisabled"> 确认 </LoadingBtn>
  <LoadingBtn @click="cancel"> 取消 </LoadingBtn>
</template>

50.echart组件封装

<template>
  <div class="chart-wrapper">
    <div ref="chart" style="width: 100%; height: 100%"></div>
  </div>
</template>

<script>
    const echarts = require('echarts')
    //引入echar配置文件
    import { pieTotalOption, zhuOption, lineOption } from './option'
    export default {
      props: {
        //fncName用于指定初始化函数名称
        fncName: {
          type: String,
          default: 'initPieTotalOption'
        },
        //写死的静态数据或者后端返回的数据(需要对数据进行重构)
        data: {
          type: Array | Object,
          default: () => []
        }
      },
      mounted() {
        this.$nextTick(() => {
          this.myChart = echarts.init(this.$refs.chart)
          this[this.fncName]()
        })
        window.addEventListener('resize', this.refresh)
      },
      destroyed() {
        window.removeEventListener('resize', this.refresh)
      },
      watch: {
        data: {
          handler() {
            this[this.fncName]()
          },
          deep: true
        },
        siderBarStatus() {
          this.refresh()
        }
      },
      methods: {
        refresh() {
          this.myChart.dispose()
          this.$nextTick(() => {
            this.myChart = echarts.init(this.$refs.chart)
            this[this.fncName]()
          })
        },
        //初始化销售数量种类占比饼状图(使用引入的对应echarts配置项)
        initPieTotalOption() {
          this.myChart.setOption(pieTotalOption(this.data))
        },
        //初始化柱状图(使用引入的对应echarts配置项)
        initZhuOption() {
          this.myChart.setOption(zhuOption(this.data))
        },
        //初始化订单折线图(使用引入的对应echarts配置项)
        initOrderLineOption() {
          this.myChart.setOption(lineOption(this.data))
        }
      }
    }
</script>

<style lang="scss" scoped>
    .chart-wrapper {
      width: 100%;
      height: 100%;
      div {
        position: relative;
        z-index: 1;
        width: 100%;
        height: 100%;
      }
    }
</style>
//option.js
//所有需要配置的数据都放置在这里
//按需引入即可
//销售数量种类占比饼状图配置项
export const pieTotalOption = (data) => {
  var colorList = ['#73DDFF', '#73ACFF', '#FDD56A', '#FDB36A', '#FD866A', '#9E87FF', '#58D5FF']
  const option = {
    tooltip: {
      trigger: 'item'
    },
    series: [
      {
        type: 'pie',
        center: ['48%', '50%'],
        radius: ['60%', '45%'],
        clockwise: true,
        avoidLabelOverlap: true,
        hoverOffset: 15,
        itemStyle: {
          normal: {
            color: function (params) {
              return colorList[params.dataIndex]
            }
          }
        },
        label: {
          show: true,
          position: 'outside',
          formatter: '{a|{b}:{d}%}\n{hr|}',
          rich: {
            hr: {
              backgroundColor: 't',
              borderRadius: 3,
              width: 3,
              height: 3,
              padding: [3, 3, 0, -12]
            },
            a: {
              padding: [-30, 15, -20, 15]
            }
          }
        },
        labelLine: {
          normal: {
            length: 20,
            length2: 30,
            lineStyle: {
              width: 1
            }
          }
        },
        data
      }
    ]
  }
  return option
}

//柱状图配置项
export const zhuOption = (data) => {
  //对data进行拆分
  const { orderData, timeData, saleData } = data
  const option = {
    echartsOption: {
      title: {
        text: ''
      },
      tooltip: {
        trigger: 'axis',
        axisPointer: {
          type: 'shadow'
        }
      },
      toolbox: {
        show: true,
        orient: 'vertical',
        left: 'right',
        top: 'center',
        feature: {
          mark: { show: true },
          dataView: { show: false, readOnly: false },
          magicType: { show: true, type: ['line', 'bar', 'stack', 'tiled'] },
          restore: { show: true },
          saveAsImage: { show: true }
        }
      },
      legend: {
        data: ['销售数量', '订单数量']
      },
      xAxis: {
        type: 'category',
        name: '时间',
        data: timeData || ['01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12']
      },
      yAxis: {
        type: 'value',
        name: '数量'
      },
      series: [
        {
          name: '销售数量',
          type: 'bar',
          data: saleData || [5, 20, 36, 10, 10, 20, 10, 25, 36, 14, 24, 23],
          barGap: '0%',
          emphasis: {
            focus: 'series'
          }
        },
        {
          name: '订单数量',
          type: 'bar',
          data: orderData || [7, 10, 26, 10, 20, 10, 15, 24, 35, 25, 14, 36],
          emphasis: {
            focus: 'series'
          }
        }
      ]
    }
  }
  return option
}

//折线图配置项
export const lineOption = (data) => {
  //需要对数据进行重构
  const { timeData, orderData, productData } = data
  const option = {
    backgroundColor: '#fff',
    color: ['#73A0FA', '#73DEB3', '#FFB761'],
    tooltip: {
      trigger: 'axis',
      axisPointer: {
        type: 'cross',
        crossStyle: {
          color: '#999'
        },
        lineStyle: {
          type: 'dashed'
        }
      }
    },
    grid: {
      left: '25',
      right: '25',
      bottom: '24',
      top: '75',
      containLabel: true
    },
    legend: {
      data: ['订单数量', '销售产品件数'],
      orient: 'horizontal',
      icon: 'circle',
      show: true,
      left: 800,
      top: 25
    },
    xAxis: {
      type: 'category',
      data: timeData || ['01-01', '01-02', '01-03', '01-04', '01-05', '01-06', '01-07'],
      splitLine: {
        show: false
      },
      axisTick: {
        show: false
      },
      axisLine: {
        show: false
      }
    },
    yAxis: {
      type: 'value',
      // max: max_value>=100? max_value + 100: max_value+10,
      // max: max_value > 100 ? max_value * 2 : max_value + 10,
      // interval: 10,
      // nameLocation: "center",
      axisLabel: {
        color: '#999',
        textStyle: {
          fontSize: 12
        }
      },
      splitLine: {
        show: true,
        lineStyle: {
          color: '#F3F4F4'
        }
      },
      axisTick: {
        show: false
      },
      axisLine: {
        show: false
      }
    },
    series: [
      {
        name: '订单数量',
        type: 'line',
        smooth: true,
        data: orderData || [13, 1, 4, 44, 45, 322, 76]
      },
      {
        name: '销售产品件数',
        type: 'line',
        smooth: true,
        data: productData || [13, 54, 34, 344, 35, 53]
      }
    ]
  }
  return option
}

 引入:

 //可以使用calc计算组件的高度
 <show style="height: calc(100vh - 572px)" :data="statisticsData" fncName="initOrderLineOption" v-else></show>

51.css相关

 1.遮罩的制作:

<div class="mask">
    <el-image :src="item.productShowCover" :z-index="9999" />
</div>

.mask {
  position: relative;
  background-color: white;
  opacity: 0.5;
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: center;
}

 2.悬浮动画:

.image-border {
    width: 277px;
    height: 150px;
    background: #ffffff;
    box-shadow: 0px 5px 16px 0px rgba(134, 149, 224, 0.2);
    border-radius: 4px;
    margin-right: 30px;
    display: flex;
    align-items: center;
    justify-content: center;
    &:hover {
      padding-top: 10px;
      box-shadow: 0px 5px 16px 0px rgba(134, 149, 224, 0.4);
    }
}

52.利用v-bind绑定当前标签的dom,使用dom元素的is属性强制改变标签。

 有这么一种需求,是在有权限访问产品详情页面的情况下,支持在谷歌浏览器中右键打开新窗口,因此需要使用router-link或者a标签,同时标签需要带有to或者href属性。但是使用router-link标签跳转的不好之处在于无法触发点击事件,也无法根据是否满足条件才跳转到指定页面。因此,可以可以将router-link标签绑定到一个dom元素上,在页面初始化时,判断用户是否拥有访问权限。如果用户有权限,就默认设置为router-link标签,并跳转到详情页面;如果用户不具备权限,就将这个标签更改为div标签。

 其实如果遇到问题,可以尝试在代码中添加debugger语句进行一步又一步的调试。

<template>
  <router-link
    v-bind="domObj"
    :to="`/design/detail?id=${this.data.id}`"
    :class="['protoCardComponent', 'hover']"
    v-loading="loading"
  >
    ...
  </router-link>
</template>
 data() {
    return {
      domObj: {}
    }
 },
 created() {
   if (!checkPermission(['business:design:detail'])) {
     //如果将is属性改为router-link中的tag属性
     //其实是将router-link标签以div的形式进行渲染,不会触发div的点击事件,
     //而且还是会跳转到to属性所在的路由
     this.$set(this.domObj, 'is', 'div')
   }
 }

53.数组中使用?.进行判空处理,如果存在则继续,不存在则不继续

//特别注意?.[0]这个操作
let productShowCover = row?.customShowImageList?.[0]?.showImagePath

53.对scss中使用scoped的理解

scss中使用socped属性后,页面样式只对本组件自己编写的类有效(element组件中改变的样式不会生效)。如果需要改变element组件中的样式,需要使用::v-deep {.需要穿透的第三方类名}进行穿透;或者将本组件自己编写的类放在使用scoped属性的scss中,防止相同的类名相互影响造成样式污染,将element组件中需要修改的类写在不使用scoped属性的scss中。

54.增删改关联接口的传参

 如果使用四个数组去存放订单增删改关联的内容,会产生很多意想不到的问题。最简单的方法是,直接比对初始数据和最终数据就好:关联订单数据时,为订单数据增添是否关联字段。因此,与原数据相比,关联数据多了关联字段,某些在原数据中存在但在最终数据中消失的订单子项id即为删除的数据,新增数据是那些没有id的数据。(1. 当数据较为复杂时,多学习使用vue devtools工具点开组件查看数据的变化和流动;2. 遇到bug时,可以在js代码中使用debugger调试bug;3. 查找某个组件中的数据,先打开路由对应的组件文件,再一级一级往下找; 4.查找表格数据时,先查看表格绑定的数据源,这个数据源可能是经过重构的计算属性或者是经过改造的数据;

//找到在A数组中存在而在B数组中不存在的id的数据
//相当于A数组是原数据,B数据是最终数据,需要找到删除的数据
方法1let allIdList = b.map(item => item.id)
let delList = a.filter(item => !allIdList.includes(item.id))

55.$emit传递函数

//刷新订单页面
//子组件传递事件
refreshOrderDetail() {
  // 这个方法返回的是一个promise
  // 因此使用() => 的形式返回函数
  // 正常函数直接调用this.$emit('refreshOrderDetail', this.function())即可
  this.$emit('refreshOrderDetail', () => this.refreshData('special'))
},

//父组件接收事件和函数,并调用方法
async refreshOrderDetail(func) {
  await this.list()
  func()
},

56.对象和对象之间不能使用===或者==判断是否相等

 两个Object类型对象,即使拥有相同属性、相同值,当使用 == 或 === 进行比较时,也不认为他们相等。这就是因为他们是通过引用(内存里的位置)比较的,不像基本类型是通过值比较的。

var obj1 = { name: "xiaoming", sex : "male" } 
var obj2 = { name: "xiaoming", sex : "male" } 
console.log(obj1 === obj2); // false

 但是如果浅拷贝指向同一内存的时候,此时两个对象相等。

var obj1 = { name: "xiaoming", sex : "male" }
var obj2 = { name: "xiaoming", sex : "male" }
var obj3 = obj1 
console.log(obj1 === obj3) // true 
console.log(obj2 === obj3); // false

 判断两个对象是否相等的方法:使用getDiffData(objectA,objectB)进行判断,如果返回值为undefined,则表明两个对象相等;如果返回有值,则表明两个对象不同。

export const getObjType = function getObjType(obj) {
  var toString = Object.prototype.toString
  var map = {
    '[object Boolean]': 'boolean',
    '[object Number]': 'number',
    '[object String]': 'string',
    '[object Function]': 'function',
    '[object Array]': 'array',
    '[object Date]': 'date',
    '[object RegExp]': 'regExp',
    '[object Undefined]': 'undefined',
    '[object Null]': 'null',
    '[object Object]': 'object',
    '[object Promise]': 'promise'
  }
  if (obj instanceof Element) {
    return 'element'
  }
  return map[toString.call(obj)]
}

/**
 * 判断是否为空
 */
export function validatenull(val) {
  if (typeof val == 'boolean') {
    return false
  }
  if (val instanceof Array) {
    if (val.length == 0) return true
  } else if (val instanceof Object) {
    if (JSON.stringify(val) === '{}') return true
  } else {
    if (val == 'null' || val == null || val == 'undefined' || val == undefined || val === '') return true
    return false
  }
  return false
}

/**
 * 获取两个数据的差异性
 * @param {any} nData
 * @param {any} oData
 * @returns {Object}
 * @example see
 */
export function getDiffData(nData, oData, { isStrict } = {}) {
  let nType = getObjType(nData)
  let oType = getObjType(oData)
  let obj
  
  if (nType === 'array' && oType === 'array') {
    let tempArr = []
    for (var i = 0, len = nData.length; i < len; i++) {
      delete nData[i].$parent
      let nVal = nData[i]
      let oVal = oData[i]
      let diffData = getDiffData(nVal, oVal)
      if (diffData !== undefined) tempArr.push(diffData)
    }
    if (!validatenull(tempArr)) obj = tempArr
  } else if (nType === 'object' && oType === 'object') {
    let tempObj = {}
    for (var key in nData) {
      delete nData.$parent
      let nVal = nData[key]
      let oVal = oData[key]
      let diffData = getDiffData(nVal, oVal)
      if (diffData !== undefined) tempObj[key] = diffData
    }
    if (!validatenull(tempObj)) obj = tempObj
  } else {
    if (!isStrict) {
      if (nData == oData) {
        if (isNumber(nData) && isNumber(oData)) {
          return
        }
      }
    }
    if (nData !== oData) {
      obj = nData
    }
  }
  return obj
}

56.引用类型修改

 引用类型:函数、数组、日期、正则、错误注意:所有的引用类型都是对象,也就是Object对象下的一个类。

  • 以数字基本类型值为例,将数字赋给变量a,此时a持有的是该值的一个复制本。再将a赋给变量b,此时b持有的是该值得另一个复制本,不论b怎么变化,都不会影响a的值。 1127212-20170411150052766-604991618.jpg

 注意:所有的基本类型值都是不会变的。比如一个字符串"abcd",它的值永远是"abcd",不可能发生改变。如果把它赋给一个变量,var x="abcd",然后给x赋其他的值,那么x的值可以改变,但是abcd"这个字符串本身的值没有发生任何变化。包括使用某些自带的函数,比如x.toUpperCase();这个函数返回的是x字符串的大写形式"ABCD"。注意,是“返回”一个值,而不是改变原有的值。此时,变量x的值仍然是"adcd",除非你使用了x=x.toUpperCase()。(即重新对变量赋值了)

    对于基本类型,将其值赋给一个变量时,就是将这个值赋值给了变量,值本身不会发生任何变化。在给变量重新赋值后,变量的值就变化了。变量之间是可以比较的,比较的就是他们本身的值。

  • 定义一个对象(数组[1,2,3]),此时这个对象在内存中建立。当给把这个对象赋值给一个变量时,变量a仅仅是对这个对象的引用,而不是将该对象复制到了该变量中。即变量a中存储的是指向对象的地址。将a的值赋给b,也即将a中的地址赋给了变量b。这是变量a和b都指向同一个对象。所以b值得改变就会直接引起对象本身的改变,因为变量a也指向这个数组,所以a的值肯定也会发生变化。 1127212-20170411151611719-1584284575.jpg

 注意:对象的比较与基本类型值不同。即使两个对象完全相同,比如两个完全相同的数组,它们也是不相等的。只有两个变量指向同一个对象时,它们才是相等的。如:

var a = [1,2,3],b = [1,2,3];
console.log(a===b);//false
var c=a;
console.log(c===a);//true
  • 例3与例2的区别在于,对b进行了重新赋值操作,b就不再是引用a的指向,并与a的指向没有任何关系,而是指向了一个新的数组[1,2,3,4],所以b的操作也不再影响到a指向的值。 1127212-20170411155449954-2099114629.jpg

57.无法读取父组件传入的异步data的值,导致代码报错

watch: {
   //由于异步操作,此时传入的data无值
   //给个监听,如果data有值了或者data发生变化了就给当前尺码名称和颜色名称赋值
   data: {
     handler(n) {
       this.currentSizeName = this?.data?.sizeList?.[0]?.sizeName
       this.currentColorName = this.data.styleList?.[0]?.shortName
     },
     immediate: true
   }
}

58.知识盲区碎碎念(first blood)

  • 如果需要修改el-table的样式,可以使用官方提供的自定义类来修改表格样式。
<el-table
    :data="tableData"
    @expand-change="expandOpen"
    style="width: 980"
    :cell-class-name="cellClassName"
    :header-cell-class-name="headerClassName"
    :row-class-name="firstRowClassName"
    v-loading="loading"
    height="455"
>
...
</el-table>
//父组件(用在这里只是举例子,但是会有bug,因为不是响应式)
export default {
  data() {
    return {
      activeName: 'subAccount',
      tabsList: [
        {
          label: '设计师',
          name: 'subAccount',
          valid: checkPermission(['admin:picture:statistics:merchant']),
          permission: {
            exportExcel: checkPermission(['admin:picture:statistics:merchant:export'])
          }
        },
        {
          label: '图片',
          name: 'picture',
          valid: checkPermission(['admin:picture:statistics:gallery']),
          permission: {
            exportExcel: checkPermission(['admin:picture:statistics:gallery:export'])
          }
        }
      ]
    }
  },
  computed: {
    curTab({ tabsList, activeName }) {
      return tabsList.find(({ name }) => name === activeName)
    }
  },
  provide() {
    return {
      permission: this.curTab.permission
    }
  }
}
//子组件 接收permission的值
export default {
  inject: {
    permission: {
      default: () => ({})
    }
  }
}
  • 将一个引用变量的值赋值给另一个引用变量时,需要使用深拷贝(例如deepClone的方式),否则两个变量指向同一个地址,相当于双向绑定了。
  • 严格模式下,this指向undefined,非严格模式下,this指向window对象。
function Foo(){
     var i=0;
     return function(){
         document.write(i++);
     }
}
var f1=Foo();
//此时为非严格模式,虽然f2没有声明,但还是被挂载到window上
f2=Foo();
f1();
f1();
//这句话相当于window.f2(),如果Foo()中存在this,也是指向window的this
f2();
  • 一道用于比较let和var区别的经典的for循环问题
//使用var声明,得到3个3
var a = [];
for (var i = 0; i < 3; i++) {
  //注意此时是声明了一个函数,但此时函数还未被调用
  a[i] = function () {
    console.log(i);
  };
}
//每次for循环时,都会重新声明变量i,由于是全局变量,所以后面的i会覆盖掉前面的i
//调用的时候,i已经循环到3,所以打印出来是3个3
a[0](); //3
a[1](); //3
a[2](); //3

//使用let声明,得到 0,1,2
//每次for循环时,都会重新使用let声明变量i,由于块级作用域的问题,变量i互不影响
//因此打印出来为 0,1,2
var a = [];
for (let i = 0; i < 3; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[0](); //0
a[1](); //1
a[2](); //2
  • let和var举一反三 image.png

核心思想: 声明时未调用函数,调用时i已经循环到10。使用var定义的i的值为全局变量是10,使用let定义的值为局部变量是当前作用域6。

  • 可以在vsCode中使用node test执行test.js文件,查看打印结果。
  • 监听页面的滚动事件
methods:{
    onScroll(e) {
      //判断滚动条距离当前页面顶部的距离
      if (e.target.scrollTop >= 2850) {
        if (!this.$refs['designedBoxRef'].style || !this.$refs['slideBoxRef'].style) return
        this.$refs['designedBoxRef'].style.width = '520px'
        this.$refs['slideBoxRef'].style.left = '504px'
      }
},
}
mounted() {
    window.addEventListener('scroll', this.onScroll, true)
},
  • svg图标在后台项目的引入 在vue-element-admin中引入svg图标
  • 在表格中自定义校验样式(仿饿了么表单验证) 先上效果(后端未提供接口,自己使用假数据模拟重构后端返回的数据):

image.png 再上实现代码:

<template>
  <div class="app-container">
    <div class="header-warpper">
      <div>晋级及会员权益设置</div>
      <color-text-btn style="font-size: 16px; margin-left: 20px" @click="showDiscountHistory"
        >查看折扣历史记录</color-text-btn
      >
    </div>
    <div class="table-warpper">
      <el-table
        :data="tableData"
        border
        style="width: 47%; border-left: 1px solid #ebeef5; border-top: 1px solid #ebeef5"
        :header-cell-style="{ borderRight: '1px solid #ebeef5' }"
      >
        <el-table-column prop="level" label="会员等级" width="180"> </el-table-column>
        <el-table-column label="销售额范围" width="300">
          <template slot-scope="{ row, $index }">
            <div class="flex">
              <div class="input-disabled">
                <el-input style="width: 80px" size="small" readonly v-model="row.minRange"></el-input>
              </div>
              <div class="rangeLine"></div>
              <el-input
                :class="{ 'table-disabled': row.rangeError }"
                @blur="maxBlured(row.maxRange, $index)"
                style="width: 80px"
                size="small"
                v-model="row.maxRange"
              >
              </el-input>
            </div>
            <div v-if="row.rangeError" class="valid-warpper">
              <span class="warning-message">{{ row.rangeError }}</span>
            </div>
          </template>
        </el-table-column>
        <el-table-column label="商品价格折扣">
          <template slot-scope="{ row, $index }">
            <div v-if="$index === 0">{{ row.discount }}%</div>
            <div class="flex" v-else>
              <div>普通用户价 *</div>
              <el-input
                size="small"
                style="width: 80px; margin-left: 3px"
                v-model="row.discount"
                :class="{ 'table-disabled': row.discountError }"
                @blur="priceBlured(row.discount, $index)"
              ></el-input>
              <div>%</div>
            </div>
            <div v-if="row.discountError" class="valid-warpper">
              <span class="message">{{ row.discountError }}</span>
            </div>
          </template>
        </el-table-column>
      </el-table>
      <div class="button-group">
        <el-button type="default" @click="onCancel">取消</el-button>
        <el-button type="primary" @click="handleSave">保存</el-button>
      </div>
    </div>
    <historyDialog :visible="showDialog" />
  </div>
</template>

<script>
import historyDialog from './module/discountHistoryDialog'
import { deepClone, getDiffData } from '@/components/avue/utils/util'

export default {
  components: { historyDialog },
  data() {
    return {
      showDialog: false,
      tableData: [
        { level: '普通用户', minRange: 0, maxRange: 20, discount: 100 },
        { level: '青铜用户', minRange: 21, maxRange: 30, discount: 90 },
        { level: '白银用户', minRange: 31, maxRange: 40, discount: 80 },
        { level: '黄金用户', minRange: 41, maxRange: 72, discount: 70 },
        { level: '铂金用户', minRange: 73, maxRange: 96, discount: 60 },
        { level: '钻石用户', minRange: 97, maxRange: 105, discount: 50 },
        { level: '星耀用户', minRange: 106, maxRange: 150, discount: 40 },
        { level: '王者用户', minRange: 151, maxRange: '无穷大', discount: 30 }
      ],
      tableDataCopy: []
    }
  },
  methods: {
    onCancel() {
      this.$router.push('/system/vip')
    },
    handleSave() {
      this.tableData.map((item, index) => {
        if (index === this.tableData.length - 1) {
          this.$set(item, 'maxRange', '无穷大')
        }
        if (Number(item.minRange) >= Number(item.maxRange)) {
          this.$set(item, 'rangeError', '销售额终止范围必须大于销售额初始范围')
          return
        }
      })
      const valid = this.tableData.find((item, index) => {
        if (!item.rangeError && !item.discountError) return
        return item.rangeError || item.discountError
      })
      console.log(valid, this.tableData)
      if (valid && (valid.rangeError || valid.discountError)) return
      //调用保存接口
      this.$router.push('/system/vip')
      this.$message.success('保存成功')
    },
    showDiscountHistory() {
      this.showDialog = true
    },
    priceBlured(val, index) {
      let msg = ''
      if (!Number.isInteger(Number(val)) || val <= 0 || val >= 100) {
        msg = '请输入一个介于1到99之间的整数'
      }
      this.$set(this.tableData[index], 'discountError', msg)
    },
    maxBlured(val, index) {
      let msg = ''
      if (!Number.isInteger(Number(val)) || val < 0) {
        msg = '请输入一个大于0的整数'
      }
      if (this.tableData[index].maxRange <= this.tableData[index].minRange) {
        msg = '销售额终止范围必须大于销售额初始范围'
      }
      this.$set(this.tableData[index], 'rangeError', msg)
      if (index + 1 < this.tableData.length) {
        this.tableData[index + 1].minRange = Number(val) + 1
      }
    }
  },
  created() {
    this.tableDataCopy = deepClone(this.tableData)
  }
}
</script>

<style lang="scss" scoped>
.app-container {
  padding: 30px 0 0 40px;
  .header-warpper {
    display: flex;
    align-items: center;
  }
  .table-warpper {
    font-size: 14px;
    margin-top: 20px;
    .table-disabled {
      border: 1px solid red;
    }
    ::v-deep {
      .el-table th,
      .el-table td {
        //25px像素用于存放错误提示信息
        padding-bottom: 25px;
      }
    }
    .valid-warpper {
      .message,
      .warning-message {
        font-size: 12px;
        color: red;
        position: absolute;
        right: 20px;
      }
      .message {
        right: 0px;
        left: 95px;
      }
    }
    .flex {
      display: flex;
      align-items: center;
      .input-disabled {
        ::v-deep {
          .el-input__inner {
            background: #f0f0f0;
          }
        }
      }
    }
    .button-group {
      margin-top: 40px;
    }
    .rangeLine {
      height: 1px;
      width: 10px;
      border: 1px solid #dcdee0;
      margin: 0 13px 0 13px;
    }
  }
}
</style>
  • 包括多种指定字符的正则校验
var verifyEn = (rule, value, callback) => {
  if (!/^[A-Za-z .,。,()()/ ]+$/.test(value)) {
    callback(new Error('请输入合法的英文名称'))
  } else {
    callback()
  }
}
  • 引用类型的props定义
  props: {
    form: {
      type: Object,
      default: () => ({})
      // https://blog.csdn.net/qq_45279574/article/details/118937652
      // 必须使用函数返回引用类型,避免指向同一地址的引用变量相互冲突干扰
      // 函数每调用一次就会产生一个新的作用域,些返回值不在一个作用域里,不会相互影响
    }
  }

59.el-table展开行的应用

el-table展开行实现手风琴效果

el-table展开行的介绍

 自定义展开行的图标和文字 (核心思想是隐藏了expand项)

<el-dialog title="英雄介绍" :visible.sync="show" width="30%" :before-close="onclose">
  <el-table
    border
    style="width: 100%; border-left: 1px solid #ebeef5; border-top: 1px solid #ebeef5"
    :header-cell-style="formatHeadClassStyle"
    :cell-style="firstColumnClassStyle"
    row-key="id"
    :data="tableData"
    ref="tableRef"
  >
    <el-table-column type="expand" width="0">
      <template slot-scope="{ row }">
        <el-form label-position="left" inline class="demo-table-expand">
          <el-form-item label="英雄特点:">
            <div>{{ row.feature }}</div>
          </el-form-item>
        </el-form>
      </template>
    </el-table-column>
    <el-table-column width="50px">
      <template slot-scope="{ row, $index }">
        <color-text-btn v-if="!row.$expand" @click="handleExpanded(row, $index)">展开</color-text-btn>
        <div v-if="row.$expand">
          <color-text-btn @click="handleExpanded(row, $index)">收起</color-text-btn>
        </div>
      </template>
    </el-table-column>
    <el-table-column label="英雄名称" prop="name"> </el-table-column>
    <el-table-column label="英雄定位" prop="category"> </el-table-column>
  </el-table>
</el-dialog>
show: false,
tableData: [
    {
      id: 0,
      name: '宫本武藏',
      category: '战士',
      feature: '锁定、落地秒'
    },
    {
      id: 1,
      name: '李白',
      category: '刺客',
      feature: '刮痧'
    },
    {
      id: 2,
      name: '马可',
      category: '射手',
      feature: '滑板鞋'
    },
    {
      id: 3,
      name: '狄仁杰',
      category: '射手',
      feature: '自带净化'
    }
],
watch: {
    show: {
      //在对话框显示或隐藏时,重置$expand属性值
      handler(val) {
        this.$set(this.tableData, '$expand', false)
      },
      immediate: true,
      deep: true
    }
},
onclose() {
  this.show = false
},
//格式化表头样式
formatHeadClassStyle({ row, column, rowIndex, columnIndex }) {
  if (columnIndex === 0) {
    return 'border-right: none;'
  } else {
    return 'border-right: 1px solid #ebeef5'
  }
},
//格式化表列样式
firstColumnClassStyle({ row, column, rowIndex, columnIndex }) {
  if (columnIndex === 0) {
    return 'border-right: none;'
  }
},
//点击切换,为$expand属性重新赋值
handleExpanded(data, index) {
  const expand = !this.tableData[index].$expand
  this.$set(this.tableData[index], '$expand', expand)
  this.$refs.tableRef.toggleRowExpansion(data, expand)
},
<style lang="scss" scoped>
.app-container {
  display: flex;
  flex-direction: column;
  font-size: 14px;
  ::v-deep {
    //隐藏展开icon
    .el-table__expand-icon {
      display: none;
    }
    .el-dialog__title {
      font-size: 14px;
      color: #1a1a1a;
    }
    .el-dialog__header {
      height: 52px;
      line-height: 52px;
      padding: 0 0 0 24px;
      border: 1px solid #dcdee0;
    }
  }
}
</style>

 效果图:

image.png

说明: 也可以通过设置class类名修改表格样式

<el-table
    border
    style="width: 100%; border-left: 1px solid #ebeef5; border-top: 1px solid #ebeef5"
    :header-cell-class-name="formatHeadClassStyle"
    :cell-class-name="firstColumnClassStyle"
    row-key="id"
    :data="tableData"
    ref="tableRef"
>
</el-table>
formatHeadClassStyle({ row, column, rowIndex, columnIndex }) {
  if (columnIndex === 0) {
    return 'brn'
  } else {
    return 'brc'
  }
},
firstColumnClassStyle({ row, column, rowIndex, columnIndex }) {
  if (columnIndex === 0) {
    return 'brn'
  }
}
<style lang="scss" scoped>
.app-container {
  display: flex;
  flex-direction: column;
  font-size: 14px;
  ::v-deep {
    .el-table__expand-icon {
      display: none;
    }
    //修改el-table的样式需要进行穿透
    .brn {
      border-right: none;
    }
    //修改el-table的样式需要进行穿透
    .brc {
      border-right: 1px solid #ebeef5;
    }
    .el-dialog__title {
      font-size: 14px;
      color: #1a1a1a;
    }
    .el-dialog__header {
      height: 52px;
      line-height: 52px;
      padding: 0 0 0 24px;
      border: 1px solid #dcdee0;
    }
  }
}
</style>

60.$set的响应式应用(再次声明)

 if (Object.keys(this.settingData).length === 0) {
    // 这种方式错误
    // this.form = {}
    // this.form.fee = 0
    // 以下两种方式都可以(优先第二种)
    // this.form = { minMoney: null, maxMoney: null, fee: null }
    // this.form.fee = 0
    this.$set(this.form, 'fee', 0)
    this.visible = true
    return
  }

61.el-table动态表头渲染

  • 老规矩,先上效果图image.png
  • 需求分析: 如果不存在VIP用户,显示暂无数据;如果存在VIP用户,则编辑时间这一列是固定的。等级ID最多有八个(只能添加和删除,且无法更改等级ID名称),但目前添加的等级明细无法得知,只能由后端返回。
  • 思路重点: 首先得有两个数组,一个数组为所有用户等级列表(包括禁用和非禁用的),一个数组为当前的折扣历史记录列表;其次,需要对折扣历史记录列表返回的数据进行格式化操作,对每一列添加一个等级ID名称字段,用于展示这个等级对应的折扣信息,然后使用el-table进行遍历。
//格式化日期的方法
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 ''
  }
}
<template>
  <div class="dialog-warpper">
    <el-dialog
      append-to-body
      title="历史记录"
      :visible.sync="visible"
      :before-close="handleClose"
      class="dialog-warpper"
    >
      <!--为对话框附加纵向的滚动条-->
      <div style="max-height: 600px; overflow-y: auto">
        <div style="text-align: center" v-if="tableHead.length === 0">暂无数据</div>
        <!--el-table中虽然有一列是对tableHead进行循环,但其实它真正绑定的数据是finalData-->
        <!--这一列也是对finalData中的每一个levelName字段进行循环,有则显示,无则不显示-->
        <el-table
          v-else
          :data="finalData"
          border
          style="border-left: 1px solid #ebeef5; border-top: 1px solid #ebeef5"
          :header-cell-style="{ borderRight: '1px solid #ebeef5' }"
        >
          <!--用于显示编辑时间这一列的宽度-->
          <el-table-column prop="importTime" label="编辑时间" width="200">
            <template slot-scope="{ row }">
              {{ parseTime(row.importTime) }}
            </template>
          </el-table-column>
            <!--其余每一列都不加宽度,让其自适应-->
          <el-table-column v-for="item in tableHead" :key="item.levelName" :label="item.levelName" :prop="item.levelName">
          </el-table-column>
        </el-table>
      </div>
    </el-dialog>
  </div>
</template>

<script>
import { parseTime } from '@/utils'
export default {
  props: {
    visible: {
      type: Boolean,
      default: false
    }
  },
  data() {
    return {
      finalData: [],
      //折扣历史记录列表(用于模拟后端返回的接口数据)
      tableData: [
        {
          id: 0,
          importTime: '2021-07-22 15:41:15',
          discountList: [
            { id: 0, levelID: 6, levelName: 'V6', level: '星耀用户', discount: 0.9 },
            { id: 2, levelID: 7, levelName: 'V7', level: '王者用户', discount: 0.85 }
          ]
        },
        {
          id: 1,
          importTime: '2021-11-03 08:34:46',
          discountList: [
            { id: 3, levelID: 6, levelName: 'V6', level: '星耀用户', discount: 0.95 },
            { id: 4, levelID: 5, levelName: 'V5', level: '钻石用户', discount: 0.98 }
          ]
        },
        {
          id: 2,
          importTime: '2021-10-08 19:27:27',
          discountList: [
            { id: 6, levelID: 6, levelName: 'V6', level: '星耀用户', discount: 0.93 },
            { id: 7, levelID: 5, levelName: 'V5', level: '钻石用户', discount: 0.96 },
            { id: 8, levelID: 7, levelName: 'V7', level: '王者用户', discount: 0.9 }
          ]
        }
      ],
      //所有用户等级列表(包括禁用和非禁用,用于模拟后端返回的接口数据)
      levelList: [
        { levelID: 1, level: '普通用户', levelName: 'V0', minRange: 0, maxRange: 20, discount: 100 },
        { levelID: 2, level: '青铜用户', levelName: 'V1', minRange: 21, maxRange: 30, discount: 90 },
        { levelID: 3, level: '白银用户', levelName: 'V2', minRange: 31, maxRange: 40, discount: 80 },
        { levelID: 4, level: '黄金用户', levelName: 'V3', minRange: 41, maxRange: 72, discount: 70 },
        { levelID: 5, level: '铂金用户', levelName: 'V4', minRange: 73, maxRange: 96, discount: 60 },
        { levelID: 6, level: '钻石用户', levelName: 'V5', minRange: 97, maxRange: 105, discount: 50 },
        { levelID: 7, level: '星耀用户', levelName: 'V6', minRange: 106, maxRange: 150, discount: 40 },
        { levelID: 8, level: '王者用户', levelName: 'V7', minRange: 151, maxRange: '无穷大', discount: 30 }
      ]
    }
  },
  created() {
    //重构后端返回的最终数据,用于表格绑定
    this.finalData = this.getFinalData(this.tableData)
  },
  methods: {
    parseTime,
    handleClose() {
      this.$emit('update:visible', false)
    },
    getFinalData(data) {
      return data.map((row) => {
        //新增对象,用于存放不同等级对应的折扣信息
        let tempObj = {}
        row.discountList.forEach((item) => {
          tempObj[item.levelName] = item.discount
        })
        //将后端返回的数据和不同等级对应的折扣信息糅合在一起
        return Object.assign(row, tempObj)
      })
    }
  },
  computed: {
    //用于展示表头信息
    tableHead() {
      const temArr = this.levelList.map(({ levelName, level }) => ({ levelName, level }))
      return temArr
    }
  }
}
</script>

<style lang="scss" scoped>
.dialog-warpper {
  ::v-deep {
    .el-dialog__title {
      font-size: 14px;
      color: #1a1a1a;
    }
    .el-dialog__header {
      height: 52px;
      line-height: 52px;
      padding: 0 0 0 24px;
      border: 1px solid #dcdee0;
    }
  }
}
</style>

62.使用el-popover组件处理多条件搜索

 老规矩,先上效果图: image.png  效果描述:初始化默认展示款式的搜索,点击其它搜索项,清空上个搜索项绑定的值,并展示其它搜索项对应的搜索框。

<template #multiplySearchSlot="{ scoped: { prop } }">
    <div class="ml20">
      <el-popover
        placement="bottom"
        width="200"
        trigger="manual"
        v-for="(item, index) in searchList"
        :key="index"
        :value="curSearchType === index"
      >
        <el-input @change="to" clearable :placeholder="`请输入${item.name}`" v-model="query[item.prop]"></el-input>
        <el-button type="text" @click="onClicked(item.prop, index)" class="label" slot="reference">{{
          item.name
        }}</el-button>
      </el-popover>
    </div>
</template>
 mounted() {
    this.curSearchType = 0
  },
 data() {
    return {
      curSearchType: -1,
      searchList: [
        { name: '款式', prop: 'styleName' },
        { name: '内部SKU', prop: 'sku' },
        { name: '自定义SKU', prop: 'customSku' }
      ],
    }
 },
methods: {
  to() {
    this.$nextTick(() => {
      this.toQuery()
    })
  },
  onClicked(prop, index) {
      this.curSearchType = index
      const queryList = ['styleName', 'sku', 'customSku']
      queryList.map((item) => {
        if (prop !== item) {
          this.query[prop] = ''
          delete this.query[prop]
        }
      })
   }
}
<style lang="scss" scoped>
.label {
  font-size: 14px;
  color: #606266;
  font-weight: bold;
  margin-right: 20px;
}
</style>

<style lang="scss">
.el-popper[x-placement^='bottom'] {
  padding: 0;
  margin-top: 6px;
}
</style>

63.点击保持树状结构的高亮状态

 使用el-tree构造的树状结构,点击节点或者悬浮在节点上会触发背景的切换效果,但是这个效果在点击树状结构以外的地方或者移出树状节点就会消失。如果要保持树状结构的高亮状态,可以使用css解决。

/* 点击后的当前节点的样式 */
.el-tree-node.is-current > .el-tree-node__content {
    background-color#f5f7fa;
}

64.el-form只有一行表单,敲击回车会触发表单提交的get方法

 遇到这个问题,可以为表单多添加一行隐藏的输入框

<el-form ref="form" :model="form" label-width="120px" :rules="rules">
    <el-form-item label="文件夹名称:" prop="folderName">
      <el-input
        size="small"
        style="width: 240px"
        v-model="form.folderName"
        placeholder="请输入文件夹名称"
        clearable
      ></el-input>
      <el-input
        size="small"
        style="position: absolute; display: none"
        v-model="form.folderName"
        placeholder="请输入文件夹名称"
        clearable
      ></el-input>
    </el-form-item>
</el-form>

65.将输入字符串中的任意空格和逗号删除并以字符串数组返回

searchChange() {
  const tempObj = this.query
  for (let key in tempObj) {
    if (['orderCodeList', 'expressWaybillCodeList'].includes(key)) {
      //去掉空格,并使用带有正则的split函数进行分割
      const strArr = tempObj[key].trim().split(/\s+|,|,/)
      tempObj[key] = strArr
    }
  }
  this.page = 1
  this.init()
}

66.分享一个模拟后端返回数据的网站

import axios from 'axios'

created() {
    const res = axios({
      url: 'https://getman.cn/mock/api'
    }).then((success) => {
      //解构返回promise的data字段,这里就是我们模拟的后端数据
      const {data} =success
    })
},

67.deep选择器穿透原理解析

68.this的指向问题

68.$watch和watch的用法

69.全选状态下,翻页仍能保证当页全选,表格的取消状态在切换页面的过程中也会保留

YOHZ97VQWC~4O%Y81DDBTGI.png

<template>
  <div class="app-container">
    <el-checkbox v-model="checkedAll" :indeterminate="indeterminate" @change="changeCheckAll">全选</el-checkbox>
    <CommonTable
      height="auto"
      :cols="cols"
      :infoData="data"
      ref="tableRef"
      @selection-change="handleSelectionChange"
      @select="selectHandler"
      @select-all="selectAllHandler"
      v-loading="tableLoading"
    ></CommonTable>
    <PaginationBar :page="page" :size="size" :total="total" @refreshTableEventFun="refreshTableEventFun" />
  </div>
</template>

<script>
import { getMyProduct } from '@/api/product/index'
import { initDataMixin } from '@/mixins'
import { deepClone } from '@/components/avue/utils/util'
export default {
  mixins: [initDataMixin],
  data() {
    return {
      url: '/business/productService/productPrototypeCollection/productList',
      cols: [
        {
          prop: 'id',
          label: 'id'
        },
        {
          prop: 'categoryName',
          label: '分类名称'
        },
        {
          prop: 'createTime',
          label: '创建时间'
        }
      ],
      // 全选按钮的值
      checkedAll: false,
      // tableRef 引用
      tableRef: {},
      // 选择的列表
      multipleSelection: [],
      //删除的列表
      clearArr: []
    }
  },

  watch: {
    // 监听全选按钮的变化
    checkedAll() {
      //1.只要全选按钮有勾选,就为全选状态
      //2.只要未勾选全选按钮,就为非全选状态
      //3.只要勾选上全选按钮,就算取消列表中的某一行或者多行数据,状态仍为全选(除非列表中所有勾选状态都被取消);
      //4.选中某一行或者多行数据,只要数量不为全部表格数据的数量,状态就不为全选,反之为全选;
      //5.只要列表中有选上,就是半选(半选层次高于全选,会覆盖掉全选)
      //6.只要列表中没有一行被选中,就无半选状态(半选层次高于全选,会覆盖掉全选)
      //7.当状态为全选时,我们使用clearArr这个数组进行判断
      //8.当状态不为全选时,我们使用multipleSelection这个数组进行判断
      this.clearArr = [] // 当全选为false状态时,里面还有东西,但我们用不上
      this.multipleSelection = [] // 当全选为true状态时,里面还有东西,但我们用不上
    },
    // 监听选中的数据的变化
    multipleSelection: {
      handler(newVal) {
        // 如果选中的数据长度等于 所有数据总长度,就把全选按钮改为true
        if (this.multipleSelection.length == this.total) {
          this.checkedAll = true
        }
        console.log('multipleSelection', newVal)
      },
      deep: true
    },
    clearArr: {
      handler(newVal) {
        console.log('clearArr', newVal)
      },
      deep: true
    }
  },
  computed: {
    // indeterminate,控制全选按钮上的 一杠是否显示
    indeterminate() {
      if (this.checkedAll) {
        // 当全选按钮选中时,看看this.clearArr里有没有东西,有的话标识有东西没有选中,则返回true 没有就返回false
        return !!this.clearArr.length
      }
      // 当全选按钮没有选中时,this.multipleSelection 里有无我们选中的东西,如果有的话还需要选中东西的长度不能等于所有数据的
      // 长度,如果满足就返回true,不满足就返回false
      return !!this.multipleSelection.length && this.multipleSelection.length != this.total
    }
  },

  methods: {
    // 获取所有数据
    async getMyProduct() {
      const { detail } = await getMyProduct({
        page: { pageIndex: 1, pageSize: 0 }
      })
      this.allData = detail
    },
    // 点击当前页全选的事件
    selectAllHandler(selection) {
      console.log('我被调用了', selection)
      // 拿到选中的数组以及要删除的数组
      const { clearArr, multipleSelection } = this
      if (!this.checkedAll) {
        // 如果全选按钮为false
        // 通过selection的长度来判断当页全选是选上还是取消
        const isDel = !selection.length
        if (isDel) {
          // 如果当页全选是false的话意思是取消了当页的所有选项
          // 先拿到当页数据的 id 组成的数组(取消的id数组)
          const ids = this.data.map(({ id }) => id)
          // 查看每个删除的id是否在选中的数组里
          ids.map((id) => {
            const fIndex = multipleSelection.findIndex((cId) => cId == id)
            //如果存在的话说明我们删除了一个在“选中”数组里的东西
            if (fIndex > -1) {
              // fIndex > -1 说明存在删除的东西在“选中”的数组里,并且fIndex为下标,把它从选中数组中去掉
              multipleSelection.splice(fIndex, 1)
            }
          })
        } else {
          // 如果当页是全选'是true'
          const ids = this.data.map(({ id }) => id)
          // 就把原来的“选中数组”与现在的选中数组拼接并且去重赋值给已“选中”的数组
          this.multipleSelection = [...new Set(this.multipleSelection.concat(ids))]
        }
        return
      }
      // 当全选按钮为true的情况下
      // 查看现在的selection的长度
      const isClear = !!selection.length
      if (isClear) {
        // 有长度说明当前页全选选择了true
        const ids = selection.map(({ id }) => id)
        // 拿到当前的selection数组的id组成的数组
        ids.map((id) => {
          // 去要取消的数组例离遍历看看有没有我们选中的数组,如果有就从我们要删除的数组中去掉那个id
          const fIndex = clearArr.findIndex((cId) => cId == id)
          if (fIndex > -1) {
            clearArr.splice(fIndex, 1)
          }
        })
      } else {
        // 如果是当页取消的话
        const ids = this.data.map(({ id }) => id)
        // 直接把他与删除的数组拼接并且去重后重新赋值给删除的数组
        this.clearArr = [...new Set(this.clearArr.concat(ids))]
      }
    },
    // 点击表格中的单选框触发事件
    selectHandler(selection, row) {
      // 如果全选按钮为false的情况下
      if (!this.checkedAll) {
        // 判断当前选择的id 是否在我们已选择数组的中
        const isExist = this.multipleSelection.includes(row.id)
        if (isExist) {
          // 如果在 说明是取消操作,就在已选中把这个id去掉
          const delIndex = this.multipleSelection.findIndex((id) => id == row.id)
          this.multipleSelection.splice(delIndex, 1)
        } else {
          // 如果不在就是选中操作,就在选中数组中添加此id
          this.multipleSelection.push(row.id)
        }
        return
      }
      // 如果全选按钮为true 的情况下
      // 判断选中的id 是否在要删除的数组中
      const isExist = this.clearArr.includes(row.id)
      if (isExist) {
        // 如果在那就是选中操作,直接把它从删除数组中去除
        const delIndex = this.clearArr.findIndex((id) => id == row.id)
        this.clearArr.splice(delIndex, 1)
      } else {
        // 如果不在那就是删除操作,直接把此id添加到删除数组中
        this.clearArr.push(row.id)
      }
    },
    // init之后会调用的函数
    // 每次翻页之后都会调用,切换全选状态
    // 因为表格数据可能还没拿到,所以需要在这个方法中拿到数据
    initCallBack() {
      this.$nextTick(() => {
        // 拿到选中的数组,要删除的数组,全选框的值
        const { multipleSelection, clearArr, checkedAll } = this
        // 如果 全选框为true 就拿删除的数组,否则就拿选中的数组
        const data = checkedAll ? clearArr : multipleSelection
        if (checkedAll) {
          // 如果全选为true,在当前页面数据中筛选 id 不在 删除数组中的id,返回一个数组,遍历这些数组,如果row存在就选中
          this.data
            .filter(({ id }) => !data.includes(id))
            .forEach((row) => {
              if (row) {
                this.tableRef.toggleRowSelection(row, true)
              }
            })
        } else {
          // 如果全选为false 在当前页面数据中筛选包含选中数组的中的id,返回一个数组,遍历,如果有row就选中
          this.data
            .filter(({ id }) => data.includes(id))
            .forEach((row) => {
              if (row) {
                this.tableRef.toggleRowSelection(row, true)
              }
            })
        }
      })
    },
    // 改变全选所有复选框,传入布尔值(其实就是checkAll)
    // 当前页面切换全选状态
    changeCheckAll(val) {
      if (val) {
        // 如果全选按钮checkAll的值为true,把当页的表格中的复选框全部勾上
        this.data.forEach((row) => {
          if (row) {
            this.tableRef.toggleRowSelection(row, true)
          }
        })
      } else {
        // 如果全选按钮checkAll的值为false,就取消当页选中的所有复选框
        this.tableRef.clearSelection()
      }
    }
  },
  created() {
    // 拿到所有数据,用于后续导出操作
    this.getMyProduct()
  },
  mounted() {
    this.tableRef = this.$refs.tableRef.$refs.table //拿到table的ref
  }
}
</script>

<style lang="scss" scoped>
.app-container {
  flex-direction: column;
  display: flex;
  height: calc(100vh - 100px);
}
</style>
//1.页面全选有勾上时,选择的数组没有使用,是通过删除的数组和所有数组进行判断。
const selectedList = []
    this.allData.map(({ id }) => {
    this.clearArr.map((item) => {
      if (item !== id) {
        selectedList.push(id)
      }
    })
})

//2.页面全选没有勾上时,直接使用选择的数组multipleSelection进行判断就好
//3.这样就可以使用选择的数组进行操作。

70.js中的相等操作符

71.箭头函数中的this指向

   当前定义的箭头函数处于什么作用域,那么this就指向什么。

72.keydown、keypress和keyup的区别及注意事项

  • keydownkeypress都是鼠标按下事件,如果按住不松手就会一直触发,调用顺序是先执行keydown事件,再执行keypress事件。keypress事件中的e.keyCode区分大小写,但是不识别按下的功能键,而keydown事件中的e.keyCode不区分大小写,且默认为大写,能够识别按下的功能键。
  • keyup是鼠标弹起事件,只会在松开鼠标的时候触发一次。
  • 监听鼠标按下事件和鼠标事件时,有一点需要特别注意: 如果监听输入框的鼠标事件,鼠标按下事件是先调用鼠标按下事件中的方法,再进行输入,而鼠标弹起事件是先输入,再调用鼠标弹起事件中的方法。

73.CSS画三角

全部效果.png

74.回到顶部组件编写

  • App.vue
<template>
  <div id="app">
    <transition :name="animationName" mode="out-in">
      <router-view />
    </transition>
    <Back />
    <Mission v-if="showMission" />
  </div>
</template>

<script>
import { MENUS_NAME } from '@/utils/constant'
import Mission from '@/components/mission'
import Back from '@/components/back'
import FONT_LIST from '@/utils/fontConfig'
import { mapMutations } from 'vuex'

export default {
  name: 'App',
  components: {
    Mission,
    Back
  },
  data() {
    return {
      animationName: 'fade-transform'
    }
  },
  mounted() {
    //设置window信息
    this.setWindowData()
  },
  created() {
    FONT_LIST.map(({ fontFamily, url }) => {
      faker
      this.loadFont(fontFamily, url)
    })
  },
  computed: {
    key() {
      return this.$route.fullPath
    },
    showMission() {
      const hiddenArr = [MENUS_NAME]
      const { name } = this.$route
      if (!name) return true
      return !hiddenArr.includes(name)
    }
  },
  methods: {
    ...mapMutations(['SET_WINDOW_DATA']),

    setWindowData() {
      this.SET_WINDOW_DATA({
        devicePixelRatio: window.devicePixelRatio
      })
    },
    loadFont(name, url) {
      //name 字体的名称
      //url  字体链接
      let style = document.createElement('style')
      style.type = 'text/css'
      style.innerText = '@font-face {font-family:' + name + ';src:url(' + url + ')};font-display: swap'
      document.getElementsByTagName('head')[0].appendChild(style)
    }
  }
}
</script>
  • 方法一的思路一(使用饿了么原生回到顶部组件):
<template>
  <el-backtop
    v-if="isShow"
    :target="target"
    :style="{
      position: 'fixed',
      right: '50px',
      bottom: '100px',
      zIndex: 99999999999999
    }"
  >
    <el-button size="mini" type="primary">回到顶部</el-button>
  </el-backtop>
</template>

<script>
export default {
  data() {
    return {
      isShow: false,
      target: ''
    }
  },
  mounted() {
    //1.需要在mounted之后才能拿到topic-page这个类
    //2.必须使用isShow来销毁和重新启用el-backtop这个组件(组件里target的值不是响应式的,不会动态更新,即重新启用组件以更新target的值)
    //3.必须使用setTimeout,因为回到顶部组件先渲染,再渲染route-view占位的路由页面,需要等route-view占位的路由页面渲染完毕之后再拿到topic-page这个类
    //4.target的值可以用计算属性定义,使用vueX进行传递,因为每个页面包裹滚动条的父容器的类、id或标签可能是不尽一致的
    //5.也可以使用watch对target立即进行监听
    //6.el组件回到顶部的样式可以自定义编写,在此只是写了一个简单的案例,并没用到icon
    this.isShow = false
    setTimeout(() => {
      this.isShow = true
      this.target = '.topic-page'
    }, 2000)
  }
}
</script>

<style lang="scss" scoped></style>
  • 方法一的思路二(使用vueX):
<template>
  <el-backtop
    v-if="isShow"
    :target="target"
    :style="{
      position: 'fixed',
      right: '50px',
      bottom: '100px',
      zIndex: 99999999999999
    }"
  >
    <el-button size="mini" type="primary">回到顶部</el-button>
  </el-backtop>
</template>

<script>
import { mapState } from 'vuex'
export default {
  data() {
    return {
      isShow: false
    }
  },
  computed: {
    ...mapState({
      target(state) {
        return state.app.backtopTarget
      }
    })
  },
  watch: {
    target() {
      this.isShow = false
      this.$nextTick(function () {
        this.isShow = true
      })
    }
  }
}
</script>

<style lang="scss" scoped></style>
//在每个index页面分别使用vueX传递el-target的值
mounted() {
    this.$store.commit('SET_BACKTOP_TARGET', '.topic-page')
}
  • 方法二(自定义回到顶部组件):
<template>
  <el-button
    v-if="visible"
    style="position: absolute; right: 19px; bottom: 30%; z-index: 2021"
    type="primary"
    @click="backTop"
  >
    回到顶部</el-button
  >
</template>

<script>
export default {
  props: {
    visibility_height: {
      required: false,
      type: Number,
      default: 1000
    },
    el_target: {
      required: false,
      type: String,
      default: '.topic-page'
    }
  },
  data() {
    return {
      visible: false,
      container: null
    }
  },
  mounted() {
    setTimeout(() => {
      let scroll_element = document.documentElement
      if (this.el_target) {
        scroll_element = document.querySelector(this.el_target)
        if (!scroll_element) {
          throw new Error('target is not existed: ' + this.el_target)
        }
      }
      this.container = scroll_element
      this.$nextTick(() => {
        this.container.addEventListener('scroll', this.scrollToTop)
      })
    }, 2000)
  },
  destroyed() {
    this.container.removeEventListener('scroll', this.scrollToTop)
  },
  methods: {
    backTop() {
      // 使用scrollTo方法平滑地滚动到顶部
      // 可以使用计算属性,在App.vue中根据不同的路由传递不同的el_target
      this.container.scrollTo({
        left: 0,
        top: 0,
        behavior: 'smooth'
      })
    },
    scrollToTop() {
      this.visible = this.container.scrollTop > this.visibility_height ? true : false
    }
  }
}
</script>

<style lang="scss" scoped>
.back-to-top {
  position: absolute;
  right: 19px;
  bottom: 30%;
  z-index: 2021;
}
</style>
  • 方法三(编写指令实现回到顶部效果):
   // 滚动条指令
    Vue.directive('scrollTop', {
      inserted(el, binding) {
        if (binding.value >= 1 || binding.value <= 0) return new Error('指令 v-scrollTop 绑定的值要在 0 到 1 之间')

        // 获取元素的高度
        const elH = el.offsetHeight
     
        // 使用binding.value,根据滚动条的总高度所占比例,间接判断回到顶部图标出现的位置
        let visibilityHeight = 0
        if (binding.value) visibilityHeight = binding.value * elH
        else visibilityHeight = 0.2 * elH

        // 为滚动条返回顶部添加平滑的过渡效果
        el.style.scrollBehavior = 'smooth'

        const backEl = document.createElement('div')
        backEl.className = 'scroll-top-class el-icon-top'
        el.appendChild(backEl)
        backEl.addEventListener('click', () => (el.scrollTop = 0))

        el.addEventListener('scroll', () => {
          if (el.scrollTop >= visibilityHeight) backEl.style.opacity = 1
          else backEl.style.opacity = 0
        })
      }
    })

App.vue

<template>
  <div id="app">
    <transition :name="animationName" mode="out-in">
      <router-view />
    </transition>
    <Back />
    <Mission v-if="showMission" />
  </div>
</template>

<script>
import { MENUS_NAME } from '@/utils/constant'
import Mission from '@/components/mission'
import Back from '@/components/back'
import FONT_LIST from '@/utils/fontConfig'
import { mapMutations } from 'vuex'

export default {
  name: 'App',
  components: {
    Mission,
    Back
  },

  data() {
    return {
      animationName: 'fade-transform'
    }
  },

  mounted() {
    //设置window信息
    this.setWindowData()
  },

  created() {
    FONT_LIST.map(({ fontFamily, url }) => {
      faker
      this.loadFont(fontFamily, url)
    })
  },

  computed: {
    key() {
      return this.$route.fullPath
    },
    showMission() {
      const hiddenArr = [MENUS_NAME]
      const { name } = this.$route
      if (!name) return true
      return !hiddenArr.includes(name)
    }
  },
  methods: {
    ...mapMutations(['SET_WINDOW_DATA']),

    setWindowData() {
      this.SET_WINDOW_DATA({
        devicePixelRatio: window.devicePixelRatio
      })
    },

    loadFont(name, url) {
      //name 字体的名称
      //url  字体链接
      let style = document.createElement('style')
      style.type = 'text/css'
      style.innerText = '@font-face {font-family:' + name + ';src:url(' + url + ')};font-display: swap'
      document.getElementsByTagName('head')[0].appendChild(style)
    }
  }
}
</script>

<style lang="scss">
.scroll-top-class {
  position: fixed;
  bottom: 120px;
  right: 30px;
  opacity: 0;
  height: 40px;
  width: 40px;
  line-height: 40px;
  font-size: 30px;
  text-align: center;
  color: #ddd;
  opacity: 0;
  z-index: 3000;
  cursor: pointer;
  border-radius: 50%;
  box-shadow: 0px 0px 8px 1px #ccc;
  background-color: rgba($color: #666, $alpha: 0.5);
  transition: all 1s;
}
</style>

指令的使用:为滚动条容器添加v-scrollTop指令即可。可以通过为其赋值,来设置需要滚动的距离。例如:

<template>
  <div class="topic-page" v-scrollTop="0.1">
    <div>
      <top />
      <banner />
      <tab />
      <step />
      <product />
      <design />
      <store />
      <support />
      <bottom />
    </div>
  </div>
</template>

75. 为el-select中的change方法绑定对象

// change方法绑定的值是el-option中的value值,可以使用对象进行绑定
// 但如果使用对象进行绑定,则必须为el-select添加字符串类型的value-key属性
// value-key绑定的值是接口返回列表中的字段
<el-select
  @change="onChange"
  @clear="onClear(item.type)"
  :class="customClass"
  :style="customStyle"
  value-key="id"
  clearable
  v-model="ruleForm[item.prop]"
  :placeholder="item.placeholder"
>
  <el-option
    v-for="(curData, index) in item.data"
    :key="index"
    :label="curData.areaCnName"
    :value="{ id: curData.id, type: item.type }"
  >
  </el-option>
</el-select>

onChange({ id, type }) {
  switch (type) {
    case 'province':
      //如果省份ID没有变化,则不需要重置
      if (this.previousProvinceId == id) return
      this.getList(id, 1)
      this.initCityAndCounty()
      this.previousProvinceId = id
      break
    case 'city':
      this.getList(id, 2)
      break
  }
}

77. ECharts图标配置效果浏览

78. pink js笔记总结

79. 用数据驱动视图(选择国家和已选国家的双向绑定以及地址选择器组件封装)

  • 国家选择: image.png
<template>
  <div class="container add-country-button-component">
    <el-dialog
      title="指定国家/地区"
      append-to-body
      :visible.sync="visible"
      width="824px"
      :before-close="close"
      class="add-country-button-add-dialog dd-country-button-dialog-warpper"
    >
      <div class="whole-warpper" :loading="loading">
        <div class="left">
          <div class="search-warpper">
            <el-input
              size="mini"
              v-model="content"
              placeholder="输入国家/地区名称、二字码"
              style="width: 300px"
              clearable
            >
              <i class="el-icon-search" style="line-height: 30px" slot="suffix"> </i
            ></el-input>
          </div>
          <div class="continent-warpper">
            <el-radio-group class="ml15" v-model="seletcedId" border>
              <el-radio-button label="all"> 全部 </el-radio-button>
              <el-radio-button :label="id" :key="id" v-for="{ id, itemName } in continentDictCode">
                {{ itemName }}
              </el-radio-button>
            </el-radio-group>
          </div>
          <div class="continent-select-warpper">
            <el-checkbox
              :indeterminate="isIndeterminate"
              class="all"
              v-model="checkAll"
              v-if="filterCountryList && filterCountryList.length"
              >全部</el-checkbox
            >
            <span v-if="!(filterCountryList && filterCountryList.length)">暂无数据</span>

            <el-checkbox-group v-model="checkedCountry">
              <el-checkbox v-for="(country, index) in filterCountryList" :label="country.id" :key="index">{{
                country && country.countryCnName
              }}</el-checkbox>
            </el-checkbox-group>
          </div>
        </div>

        <div class="right">
          <span class="search-warpper"
            >已选国家/地区:<span class="cancel-all" @click="cancelAllButton">(取消全部)</span></span
          >
          <div class="select">
            <el-checkbox-group v-model="checkedCountry">
              <el-checkbox v-for="(country, index) in checkedCountryData" :label="country && country.id" :key="index">{{
                country && country.countryCnName
              }}</el-checkbox>
            </el-checkbox-group>
          </div>
        </div>
      </div>

      <span slot="footer" class="dialog-footer">
        <el-button @click="cancel">取 消</el-button>
        <el-button :loading="loading" type="primary" @click="confirm">确 定</el-button>
      </span>
    </el-dialog>
  </div>
</template>

<script>
import { mapGetters } from 'vuex'
import { debounce } from '@/utils'
import { getList } from '@/api/country'
import cloneDeep from 'lodash/cloneDeep'
export default {
  props: {
    visible: {
      type: Boolean,
      default: false
    },
    form: {
      type: Object,
      default: () => ({})
    }
  },
  watch: {
    visible: {
      handler(val) {
        if (val) {
          this.formCopy = cloneDeep(this.form)
          this.checkedCountryCopy = cloneDeep(this.checkedCountry)
          this.checkedCountry = this.form.checkedCountry
        }
      },
      immediate: true,
      deep: true
    }
  },
  data() {
    return {
      seletcedId: 'all',
      content: '',
      allCountry: [],
      checkedCountry: [],
      loading: false,
      formCopy: {}
    }
  },

  created() {
    this.getCountryList()
  },

  computed: {
    ...mapGetters(['continentDictCode']),

    checkAll: {
      get({ checkedCountry, countryList }) {
        const curSelectData = checkedCountry.filter((checkeId) => {
          return this.countryList.find(({ id }) => checkeId == id)
        })
        return curSelectData.length == countryList.length
      },

      set(bool) {
        if (bool) {
          this.checkedCountry.push(...this.countryList.map(({ id }) => id))
          this.checkedCountry = [...new Set(this.checkedCountry)]
        }
        if (!bool) {
          this.checkedCountry = this.checkedCountry.filter((checkedId) => {
            return !this.countryList.find(({ id }) => id == checkedId)
          })
        }
      }
    },

    countryList() {
      if (this.seletcedId == 'all') return this.allCountry
      return this.allCountry.filter((item) => {
        return item.continentDictCode == this.seletcedId
      })
    },

    filterCountryList({ countryList, content }) {
      if (!content) return countryList
      return countryList.filter(({ countryCnName, twoCharCode }) => {
        return countryCnName.indexOf(content) >= 0 || twoCharCode.toUpperCase().indexOf(content.toUpperCase()) >= 0
      })
    },

    //所有选中数据
    checkedCountryData({ allCountry, checkedCountry }) {
      return allCountry.filter(({ id }) => {
        return checkedCountry.includes(id)
      })
    },

    isIndeterminate({ countryList, checkedCountry }) {
      const curSelectData = checkedCountry.filter((checkeId) => {
        return this.countryList.find(({ id }) => checkeId == id)
      })
      if (curSelectData.length == 0)  return false
      if (curSelectData.length < countryList.length)  return true
      return false
    }
  },
  
  methods: {
    close() {
      this.$emit('update:visible', false)
    },
    
    cancel() {
      this.$emit('update:visible', false)
      this.checkedCountry = this.checkedCountryCopy
    },

    confirm() {
      this.loading = true
      if (this.checkedCountry.length === 0) {
        this.$message.warning('请先选择国家/地区')
        this.loading = false
        return
      }
      this.loading = false
      this.formCopy.checkedCountry = this.checkedCountry
      this.$emit('update:form', this.formCopy)
      this.$emit('update:visible', false)
    },

    //获取所有信息
    async getCountryList() {
      const res = await getList({
        isDeleted: 0,
        page: {
          pageIndex: 1,
          pageSize: 0
        }
      })
      this.allCountry = res.detail || []
    },

    //取消全部
    cancelAllButton() {
      this.checkedCountry = []
    }
  }
}
</script>

<style lang="scss" scoped>
.ml15 {
  margin-left: 15px;
}
.addButton {
  padding: 0;
  width: 128px;
  height: 32px;
}
.dd-country-button-dialog-warpper {
  ::v-deep {
    .el-dialog__body {
      padding: 0;
    }
  }
}
.add-country-button-add-dialog {
  ::v-deep {
    .el-dialog__title {
      font-size: 14px;
      color: #1a1a1a;
    }
    .el-dialog__header {
      height: 52px;
      line-height: 52px;
      padding: 0 0 0 24px;
      border: 1px solid #dcdee0;
    }
  }
  .select-warpper {
    margin-top: 14px;
    font-size: 14px;
    font-weight: 500;
    color: #1a1a1a;
  }
  .whole-warpper {
    display: flex;
    height: 500px;
    .left {
      width: 527px;
      border-right: 1px solid #dcdee0;
      border-bottom: 1px solid #dcdee0;
      .continent-warpper {
        height: 48px;
        align-items: center;
        border-bottom: 1px solid #dcdee0;
        display: flex;
      }
      .continent-select-warpper {
        padding: 16px 0 0 16px;
        height: 390px;
        overflow: auto;
        .all {
          ::v-deep {
            .el-checkbox__label {
              padding-right: 14px;
            }
          }
        }
        ::v-deep {
          .el-checkbox__inner {
            width: 16px;
            height: 16px;
            border: 1px solid #c8c9cc;
            border-radius: 2px;
          }
          .el-checkbox-group {
            display: inline;
          }
          .el-checkbox__label {
            padding-left: 5px;
            margin-right: 18px;
          }
          .el-checkbox {
            color: #595961;
            font-weight: normal;
            margin-bottom: 13px;
          }
        }
      }
      .search-warpper {
        height: 56px;
        line-height: 56px;
        border-bottom: 1px solid #dcdee0;
        padding-left: 16px;
      }
    }
    .right {
      width: 296px;
      border-bottom: 1px solid #dcdee0;
      .cancel-all {
        color: #3841db;
        cursor: pointer;
      }
      .select {
        padding: 16px 0 0 16px;
        height: 420px;
        overflow: auto;
        .el-checkbox {
          color: #595961;
          font-weight: normal;
          margin-bottom: 13px;
        }
        ::v-deep {
          .el-checkbox__label {
            margin-right: 13px;
          }
        }
      }
      .search-warpper {
        display: inline-block;
        height: 56px;
        width: 100%;
        line-height: 56px;
        border-bottom: 1px solid #dcdee0;
        padding-left: 16px;
        color: #1a1a1a;
      }
    }
  }
}
</style>
  • 地址选择器组件的封装:
<template>
  <el-row>
    <el-col :span="colSpan" v-for="(item, index) in addressList" :key="index">
      <el-form-item :label="item.label" :prop="item.prop">
        <el-select
          :class="[customClass]"
          :style="customStyle"
          size="small"
          clearable
          v-model="ruleForm[item.prop]"
          :placeholder="item.placeholder"
        >
          <el-option
            v-for="(curData, index) in addressListData[index] || []"
            :key="index"
            :label="curData.areaCnName"
            :value="curData.id"
          >
          </el-option>
        </el-select>
      </el-form-item>
    </el-col>
  </el-row>
</template>

<script>
import { list } from '@/api/addressApi'

export default {
  props: {
    colSpan: {
      type: Number,
      default: 3
    },
    customClass: {
      type: String,
      default: 'mr20'
    },
    customStyle: {
      type: String,
      default: 'width: 200px'
    },

    ruleForm: {
      type: Object,
      default: () => ({})
    }
  },

  data() {
    return {
      //记录上次选择的省ID
      addressListData: [],

      addressList: [
        {
          label: '所在地区',
          placeholder: '请选择省/自治区/直辖市',
          type: 'province',
          prop: 'provinceCode'
        },
        {
          label: undefined,
          placeholder: '请选择市/区',
          type: 'city',
          prop: 'cityCode'
        },
        {
          label: undefined,
          placeholder: '请选择区/县',
          type: 'county',
          prop: 'countyCode'
        }
      ]
    }
  },
  watch: {
    'ruleForm.provinceCode': {
      handler(newVal) {
        this.addressListData = [this.addressListData[0]]
        this.ruleForm.cityCode = ''
        this.ruleForm.countyCode = ''
        if (newVal) {
          this.getNextData(newVal, 1)
        }
      },
      immediate: true
    },
    'ruleForm.cityCode'(newVal) {
      this.addressListData = [this.addressListData[0], this.addressListData[1]]
      this.ruleForm.countyCode = ''
      if (newVal) {
        this.getNextData(newVal, 2)
      }
    }
  },
  methods: {
    //根据当前id获取市或县的列表
    async getList(id) {
      const { detail } = await list({ id })
      return detail
    },
    //获取下一级数据并显示
    async getNextData(newVal, index) {
      const data = await this.getList(newVal)
      this.addressListData.splice(index, 1, data)
    }
  },

  mounted() {
    list().then(({ detail }) => {
      this.addressListData.splice(0, 1, detail || [])
    })
  }
}
</script>
  • 地址选择器组件的使用:
<template>
  <div class="app-container personalInfoPage">
    <div class="header-warpper">{{ message }}</div>
    <div style="margin-top: 47px">
      <el-form :model="ruleForm" :rules="rules" ref="ruleForm" label-width="100px" class="demo-ruleForm">
        <el-form-item label="收件人姓名:" prop="name">
          <el-input size="small" v-model="ruleForm.name" style="width: 488px" placeholder="请输入收件人姓名"></el-input>
        </el-form-item>
        <addressPicker :ruleForm.sync="ruleForm" />
        <el-form-item label="手机号码:" prop="phoneNumber">
          <el-input
            size="small"
            v-model="ruleForm.phoneNumber"
            style="width: 488px"
            placeholder="请输入手机号码"
          ></el-input>
        </el-form-item>
        <el-form-item label="详细地址:" prop="address">
          <el-input
            type="textarea"
            maxlength="60"
            show-word-limit
            :rows="2"
            style="width: 488px"
            placeholder="请输入有效地址"
            v-model="ruleForm.address"
          >
          </el-input>
        </el-form-item>
        <el-form-item label="邮政编码:" prop="postCode">
          <el-input
            size="small"
            v-model="ruleForm.postCode"
            style="width: 488px"
            placeholder="请输入邮政编码"
          ></el-input>
        </el-form-item>
        <el-form-item v-if="$route.params.type == 'add'">
          <el-checkbox v-model="checked">设为默认地址</el-checkbox>
        </el-form-item>
        <el-form-item style="margin-top: 40px">
          <el-button @click="onCancel">取消</el-button>
          <el-button :loading="loading" v-if="$route.params.type === 'add'" type="primary" @click="onConfirm('add')"
            >添加</el-button
          >
          <el-button :loading="loading" v-else type="primary" @click="onConfirm('edit')">修改</el-button>
        </el-form-item>
      </el-form>
    </div>
  </div>
</template>

<script>
import addressPicker from '@/components/addressPicker'
import { addAddress, getAddressById, editAddress } from '@/api/addressApi'

export default {
  components: { addressPicker },
  data() {
    return {
      ruleForm: {
        name: '',
        phoneNumber: '',
        address: '',
        provinceCode: '',
        cityCode: '',
        countyCode: '',
        postCode: ''
      },
      addressData: {},
      loading: false,
      checked: true,
      rules: {
        name: [{ required: true, message: '收件人姓名必填', trigger: 'blur' }],
        phoneNumber: [{ required: true, message: '手机号码必填', trigger: 'blur' }],
        provinceCode: [{ required: true, message: '省/区/市必填', trigger: 'blur' }],
        cityCode: [{ required: true, message: '市/区必填', trigger: 'blur' }],
        countyCode: [{ required: true, message: '区/县必填', trigger: 'blur' }],
        address: [{ required: true, message: '详细地址必填', trigger: 'blur' }],
        postCode: [{ required: true, message: '邮政编码必填', trigger: 'blur' }]
      }
    }
  },
  methods: {
    onCancel() {
      this.$refs.ruleForm.resetFields()
      this.$refs.ruleForm.clearValidate()
      this.$router.push('/personalCenter/account/myAddress')
    },
    onSuccessMessage(type) {
      this.loading = false
      this.$router.push('/personalCenter/account/myAddress')
      const message = type == 'add' ? '添加' : '修改'
      this.$message.success(`${message}成功`)
    },
    onConfirm(type) {
      const { name, phoneNumber, provinceCode, postCode, address, cityCode, countyCode } = this.ruleForm
      const data = {
        cityCode,
        districtCode: countyCode,
        provinceCode,
        consigneeName: name,
        contactMobilePhone: phoneNumber,
        detailAddress: address,
        postcode: postCode,
        isDefault: this.checked ? 1 : 0
      }
      this.$refs['ruleForm'].validate(async (valid) => {
        if (!valid) return
        this.loading = true
        if (type === 'add') {
          try {
            const { code } = await addAddress(data)
            if (code == 0) {
              this.onSuccessMessage(type)
            }
          } catch (error) {}
        } else {
          Object.assign(data, { id: this.$route.query.id })
          try {
            const { code } = await editAddress(data)
            if (code == 0) {
              this.onSuccessMessage(type)
            }
          } catch (error) {}
        }
      })
    }
  },
  computed: {
    message() {
      return this.$route.params.type === 'add' ? '新增收货地址' : '修改收货地址'
    }
  },
  async created() {
    //路由存在id则表明是修改,请求接口,并将值赋值给addressData
    if (this.$route.query.id) {
      const { id } = this.$route.query
      try {
        const { code, detail } = await getAddressById({ id })
        if (code == 0) {
          this.addressData = detail
          const { consigneeName, contactMobilePhone, detailAddress, postcode, provinceCode, cityCode, districtCode } =
            detail
          this.ruleForm.name = consigneeName
          this.ruleForm.phoneNumber = contactMobilePhone
          this.ruleForm.address = detailAddress
          this.ruleForm.postCode = postcode
          this.ruleForm.provinceCode = provinceCode
          //在数据清除之后再赋值
          this.$nextTick(() => {
            this.ruleForm.cityCode = cityCode
            this.$nextTick(() => {
              this.ruleForm.countyCode = districtCode
            })
          })
        }
      } catch (error) {}
    }
  }
}
</script>

<style lang="scss" scoped>
.app-container {
  padding-left: 30px;
  display: flex;
  flex-direction: column;
  .header-warpper {
    padding: 10px 0 10px 0;
    font-size: 16px;
    color: #495060;
    border-bottom: 1px solid #f0f4f8;
  }
}
</style>

80. 在can I use网站上查看兼容性问题

  • 兼容性判断网站

  • 扁平化数组flat方法兼容性不是很好,可以封装一个类似flat的方法。

    传送门之flat函数封装

  • 关于主页面模态框组件的生命周期问题: 如果在主页面不使用v-if="visible"打开或者强制销毁模态框组件,则模态框的生命周期只在主页面初始化的时候跟随dom走一次;如果在模态框子组件中使用v-if="visible",则主页面渲染后,模态框还是会跟随dom进行渲染,生命周期同样只会触发一次。