Element-ui select 远程搜索 + 下拉加载更多

452 阅读1分钟
<!-- 选择成员 - 远程搜索 -->
<template>
  <el-select
    id="resign-select-member"
    v-selectLoadmore="loadMore"
    :value="value"
    v-bind="$attrs"
    filterable
    remote
    reserve-keyword
    placeholder="请输入姓名/工号/电话号码"
    :remote-method="key => debounce(fetchData, key)"
    :loading="isLoading"
    class="resign-select-member"
    popper-class="resign-member-select-dropdown"
    size="medium"
    clearable
    @change="changeHandler"
    @visible-change="visibleChange">
    <el-option
      v-for="(item, index) in selectData"
      :key="index + item.mchUserNo"
      :label="item.mchUserName + '/' + item.mchUserNo + '/' + item.entUserId"
      :value="item[valueKey]">
      <show-tooltip
        :text="item.mchUserName + '/' + item.mchUserNo + '/' + item.entUserId"
        title-class="content-title"
        :width="427"
        tooltip-class="content-record"></show-tooltip>
    </el-option>
    <p v-show="isMoreLoading" class="more-loading"><i class="el-icon-loading"></i>奋力加载中...</p>
  </el-select>
</template>

<script>
import { debounce, regPhone, regNo } from './tools'
import ShowTooltip from 'components/show-tooltip'
import resignSuccessionService from '@/api/resign-succession'

export default {
  components: { ShowTooltip },
  directives: {
    selectLoadmore: {
      inserted(el, binding, vnode) {
        const selectDom = el.querySelector('.el-select-dropdown .el-select-dropdown__wrap')
        el.selectDom = selectDom
        // 将选择的 panel 挂在到,当前组件实例
        vnode.context.selectPanel = selectDom
        el.selectDom.loadMoreFun = () => {
          if (selectDom.scrollHeight - selectDom.scrollTop <= selectDom.clientHeight) binding.value()
        }
        el.selectDom.addEventListener('scroll', el.selectDom.loadMoreFun)
      },
      unbind(el) {
        el.selectDom.removeEventListener('scroll', el.selectDom.loadMoreFun)
        el.selectDom = null
      },
    },
  },
  model: { prop: 'value', event: 'change' },
  props: {
    value: { type: String, default: '' }, // 双向绑定 v-model value 配置
    corpId: { type: [Number, String], default: '' }, // 所属微信主体 ID
    valueKey: { type: String, default: 'entUserId' }, // 设置当前绑定 value 的 key
    isTack: { type: Boolean, default: false }, // 是否为接替列表
    isWatch: { type: Boolean, default: true }, // 是否执行监听
  },
  data() {
    this.cache = { loading: false, data: [], total: 0 } // 用户缓存第一次加载的 options 数据
    this.selectItem = {} // 选中的项-对象类型

    return {
      excludeIds: [], // 需要排除的数据
      params: {
        corpId: '', // 主体id
        limit: 10,
        start: 1,
      },
      selectData: [],
      isLoading: false,
      total: 0,
      isMoreLoading: false, // 下拉加载更多 loading
    }
  },
  watch: {
    corpId(val) {
      if (!this.isWatch) return
      // 监听到 主题id 有变动 清除已选中的数据
      this.$emit('change', '')
      // 清除缓存数据
      this.cache = { loading: false, data: [], total: 0 }
      // 重新获取数据
      this.params.corpId = val
      this.fetchData()
    },
  },
  mounted() {
    // 限制搜索关键字不超过 200 个字符
    const input = document.querySelector('#resign-select-member')
    input && input.setAttribute('maxLength', 200)
  },
  methods: {
    /**
     * @description 防抖
     */
    debounce: debounce(function (fn, ...args) {
      fn.call(this, ...args)
    }),
    /**
     * @description 提供给外部初始化调用
     */
    init(corpId, excludeIds = []) {
      this.params.corpId = corpId
      this.excludeIds = excludeIds
      // 清除缓存数据
      this.cache = { loading: false, data: [], total: 0 }
      this.fetchData()
    },
    /**
     * @description 远程搜索方法
     * @param {String} key 搜索关键字
     * @param {Boolean} isRest 重置页码
     * @param {Boolean} isMore 是否是远程加载loading
     */
    async fetchData(key = '', isRest = true, isMore = false) {
      // 重置数据
      if (isRest) {
        this.params.start = 1
        this.selectData = []
      }

      // 根据输入字符设置 key
      const params = this.setParamsKey(key)
      try {
        isMore ? (this.isMoreLoading = true) : (this.isLoading = true)
        const { code, data, message } = this.isTack
          ? await resignSuccessionService.takeMemberPage({ ...params, excludeIds: this.excludeIds })
          : await resignSuccessionService.memberPage(params)
        if (code === 200 && data) {
          this.selectData.push(...data.records)
          this.total = data.totalCount
        } else {
          this.$message.error(message)
        }

        // 数据缓存
        if (!this.cache.loading) {
          this.cache.data = [...this.selectData]
          this.cache.loading = true
          this.cache.total = this.total
        }
      } finally {
        isMore ? (this.isMoreLoading = false) : (this.isLoading = false)
      }
    },
    /**
     * @description 下拉加载更多
     */
    loadMore() {
      if (this.isMoreLoading) return
      const {
        params: { limit, start },
        total,
      } = this
      const isLoad = total > limit * start

      if (isLoad) {
        this.params.start++
        this.fetchData('', false, true)
      }
    },
    /**
     * @description 根据输入字符设置 key
     */
    setParamsKey(keyword) {
      const key = keyword.trim()
      if (!key) return this.params

      let ret = {}
      if (regPhone(key)) {
        ret.phone = key
      } else if (regNo(key)) {
        ret.no = key
      } else {
        ret.name = key
      }

      return { ...ret, ...this.params }
    },
    /**
     * @description 选中后保存选中的值
     */
    changeHandler(val) {
      this.selectItem = this.selectData.find(e => e[this.valueKey] === val)
      this.$emit('change', val)
    },
    /**
     * @description 隐藏后重置为初始化时的 options 值
     */
    visibleChange(val) {
      if (val) return

      setTimeout(() => {
        this.selectData = [...this.cache.data]
        this.params.start = 1
        this.total = this.cache.total
        // 让 scrollTop 回到顶部,避免触发下拉加载更多
        this.$nextTick(() => (this.selectPanel.scrollTop = 0))
      }, 300)
    },
  },
}
</script>

<style lang="less">
.resign-select-member {
  width: 100%;
  .el-input__inner {
    font-size: 12px;
  }
}
.resign-member-select-dropdown .more-loading {
  height: 32px;
  line-height: 32px;
  text-align: center;
  color: #999;
  letter-spacing: 1px;
  font-size: 12px;
  i {
    margin-right: 8px;
    font-size: 14px;
  }
}
</style>