select下拉框筛选器,分页加载(ts+elementPlus+vue3)

401 阅读2分钟

在开发过程中经常遇到分页加载select下拉框数据的情况,所以封装了一个通用组件。

该组件的优化点超级多,后续有空会持续优化。

组件相关的代码:

特别需要注意的是netFun异步请求中,返回的参数格式为

 const data = {
        pagingData: XXX,
        pageIndex: XXX,
        pageSize: XXX,
        totalCount: XXX
    }

组件代码:

<template>
  <el-select
    ref="selectScroll" v-model="chooseShowValue" clearable filterable :filter-method="filterMethod"
    v-scrollChange="loadMore" @visible-change="visibleChange" @change="changeData" :placeholder="props.placeholder"
    :disabled="props.disabled" :loading="loading" :multiple="props.multiple" :style="props.style"
  >
    <div :class="classSelectRef">
      <el-option
        v-for="(item, index) in selecOptions" :key="'labelSelect' + index" :value="item[showValue]"
        :label="getShowLabel(item)"
      >
        {{ getShowLabel(item) }}
      </el-option>
    </div>
  </el-select>
</template>

<script lang="ts" setup>
import { onMounted, reactive, ref, Directive, DirectiveBinding, toRaw, watch } from 'vue'
import Request from '../../utils/request'
const emit = defineEmits(['update:chooseValue', 'change'])

const dom = ref()
const selectScroll = ref(null)
const loading = ref(false)

const selecOptions = ref()
const options = ref([])
const rangeNumber = ref(10)
const chooseShowValue = ref()
const timeOut = ref()
const queryRef = ref('')
const curResOptions = ref()

const classSelectRef = ref('')

const requsetObj = reactive({
  current: 1,
  size: 10
})

let requestObjData: {
  [key: string]: {};
} = {}

let isLoadEnd = false
let requestLoading = false

let curNetFun: Function

const props = defineProps({
  isShow: {
    type: Boolean,
    default: false
  },
  chooseValue: {
    type: [Array, String],
    default: () => ''
  },
  showValue: {
    type: String,
    default: () => 'value'
  },
  showAllLabel: {
    type: String,
    default: () => 'label'
  },
  showLabel: {
    type: String,
    default: () => 'label'
  },
  otherShowLabel: {
    type: String,
    default: () => ''
  },
  changeFunction: {
    type: Function,
    default: () => null
  },
  placeholder: {
    type: String,
    default: '请选择'
  },
  disabled: {
    type: Boolean,
    default: false
  },
  multiple: {
    type: Boolean,
    default: false
  },
  style: {
    type: String,
    default: ''
  },
  netFun: {
    type: Function,
    default: () => null
  },
  selectNetFun: {
    type: Function,
    default: () => null
  },
  showLabelFun: {
    type: Function,
    default: () => null
  },
  isShowLabelFun: {
    type: Boolean,
    default: false
  },
  pageSize: {
    type: String,
    default: 'pageSize'
  },
  pageIndex: {
    type: String,
    default: 'pageIndex'
  },
  classSelect: {
    type: String,
    default: 'labelSelectCpmBox'
  }
})

onMounted(() => {
  classSelectRef.value = props.classSelect + (new Date().getTime()).toString()
  chooseShowValue.value = props.chooseValue
  initData()
})

const initData = () => {
  curNetFun = props.netFun
  curResOptions.value = []
  selecOptions.value = []
  requsetObj.current = 1
  requsetObj.size = 10
  isLoadEnd = false
  requestLoading = false
  requestObjData = {}
  if (props.isShow) {
    loadMore()
  }
}

const loadMore = async () => {
  if (curNetFun) {
    let param = {} as any
    param = { ...requsetObj }
    try {
      requestLoading = true
      const request = await curNetFun(param)
      requestLoading = false
      const pagingData = request.pagingData
      curResOptions.value.push(...pagingData)
      selecOptions.value.push(...pagingData)
      if (pagingData.length < request.pageSize) {
        isLoadEnd = true
      }

      if (toRaw(curResOptions.value).length >= request.totalCount) {
        isLoadEnd = true
      }

      pagingData.forEach((item: any) => {
        const tmpValue = item[props.showValue]
        requestObjData[tmpValue] = item
      })
    } catch (error) {
      requestLoading = false
    }
  }
}

// 防抖
const debounce = (fn: any, delay: number) => {
  if (timeOut.value) {
    clearTimeout(timeOut.value)
  }
  timeOut.value = setTimeout(() => {
    fn()
  }, delay)
}

const findValueInData = (data: any, value: any) => {
  return data && data.toString().indexOf(value) >= 0
}

const filterMethodByQuery = async () => {
  if (queryRef.value) {
    if (props.selectNetFun) {
      const request = await props.selectNetFun(queryRef.value)
      const pagingData = request.pagingData
      // curResOptions.value = []
      // curResOptions.value.push(...pagingData)
      selecOptions.value = []
      selecOptions.value.push(...pagingData)

      pagingData.forEach((item: any) => {
        const tmpValue = item[props.showValue]
        requestObjData[tmpValue] = item
      })

    } else {
      const filterArr = curResOptions.value.filter((item: any) => {
        const tmpData = toRaw(item)
        const tmpLabel = tmpData[props.showLabel]
        const tmpValue = tmpData[props.showValue]
        const query = queryRef.value
        if (props.otherShowLabel) {
          const otherTmpValue = tmpData[props.otherShowLabel]
          return findValueInData(tmpLabel, query) || findValueInData(tmpValue, query) || findValueInData(otherTmpValue, query)
        } else {
          return findValueInData(tmpLabel, query) || findValueInData(tmpValue, query)
        }
      })
      options.value = filterArr
      selecOptions.value = options.value.slice(0, rangeNumber.value)
    }

  } else {
    options.value = curResOptions.value
    selecOptions.value = options.value.slice(0, rangeNumber.value)
  }
  if (!props.multiple) {
    chooseShowValue.value = queryRef.value
    changeData()
  }
}

const filterMethod = (query = '') => {
  if (query) {
    queryRef.value = query
    debounce(filterMethodByQuery, 500)
  } else {
    if (queryRef.value !== query) {
      queryRef.value = query
      debounce(filterMethodByQuery, 500)
    }
  }
}

// 下拉框出现时,调用过滤方法
const visibleChange = (flag: any) => {
  if (flag) {
    filterMethod()
  }
}

const getShowLabel = (item: any) => {
  if (props.showLabelFun) {
    return props.showLabelFun(item)
  } else {
    return item[props.showLabel]
  }
}

const changeData = () => {
  const tmpValue = chooseShowValue.value
  queryRef.value = ''
  emit('update:chooseValue', tmpValue)
  emit('change', requestObjData[tmpValue])

  if (!chooseShowValue.value) {
    options.value = curResOptions.value
    selecOptions.value = options.value.slice(0, rangeNumber.value)
  }
}

/* 滚动监听函数 */
const scrollAddEventFn = (e: any) => {
  const self = e.target as any
  if (self.scrollHeight - self.scrollTop <= self.clientHeight) {
    console.log('分页查询', isLoadEnd, requestLoading)
    if (!isLoadEnd && !requestLoading) {
      requsetObj.current++
      loadMore()
    }
  }
}

const vScrollChange: Directive = (el, binding: DirectiveBinding) => {
  /* 在数据渲染完之后的回调 */
  /* 初始化滚动监听 (由于 dom 渲染未完成,所以需要开启一个 timeout 在 1s 后实现监听) */
  const parentDom = document.querySelectorAll('.el-select-dropdown__wrap.el-scrollbar__wrap.el-scrollbar__wrap--hidden-default') as any
  setTimeout(() => {
    parentDom.forEach((e: any, idx: number) => {
      const className = '.' + toRaw(classSelectRef.value)
      // if (e.querySelector('.labelSelectCpmBox') && e.querySelector('.labelSelectCpmBox').children && e.querySelector('.labelSelectCpmBox').children.length > 0) {
      if (e.querySelector(className) && e.querySelector(className).children && e.querySelector(className).children.length > 0) {
        dom.value = parentDom[idx]
        dom.value.addEventListener('scroll', scrollAddEventFn, false)
      }
    })
  }, 1000)
}

watch(
  () => props.isShow,
  () => {
    if (props.isShow) {
      initData()
    }
  },
  {
    deep: true,
    immediate: true
  }
)

watch(
  () => props.chooseValue,
  (newValue) => {
    chooseShowValue.value = JSON.parse(JSON.stringify(newValue))
  },
  {
    deep: true,
    immediate: true
  }
)

</script>