在这里插入图片描述

组件封装:

<template>
  <el-select v-model="selectedValue" filterable remote clearable :placeholder="placeholder" remote-show-suffix
    :remote-method="remoteMethod" @visible-change="emits('visible-change')" popper-class="cit-scroll-select">
    <el-option v-for="item in props.options" :key="item[dataConfig.value]" :label="item[dataConfig.name]"
      :value="item[dataConfig.value]" />
    <template #footer v-if="showFooter">
      <div class="text-center">
        <span v-if="props.loading && props.options?.length < props.total">
          {{ props.loading ? '加载中...' : '' }}
        </span>
        <span v-else-if="props.options?.length === props.total">
          没有更多数据了
        </span>
      </div>
    </template>
  </el-select>
</template>

<script setup>
/**
* @author        全易
* @time          2025-12-08 17:38:17 星期一
* @description   触底加载 选择器
* @example       <scroll-select v-model="value" :options="options" :loading="loading" :total="total" :remoteMethod="remoteMethod" @reachBottom="reachBottom" />
**/



const selectedValue = defineModel()
const emits = defineEmits(['reachBottom'])
const props = defineProps({
  loading: {
    type: Boolean,
    default: false
  },
  options: {
    type: Array,
    default: () => []
  },
  total: {
    type: Number,
    required: true
  },
  placeholder: {
    type: String,
    default: ''
  },
  // 取值配置
  dataConfig: {
    type: Object,
    default: () => ({
      name: 'name',
      value: 'id'
    })
  },
  remoteMethod: {
    type: Function,
    required: true
  }
})

// 显示底部加载更多的文本
const showFooter = computed(() => {
  return (props.loading && props.options?.length < props.total) || (props.options?.length === props.total)
})

// 监听下拉框滚动事件
const setupScrollListener = () => {
  const popperEl = document.querySelector('.cit-scroll-select .el-select-dropdown__wrap')
  if (popperEl) {
    popperEl.addEventListener('scroll', handleScroll)
  }
}

// 移除滚动监听
const removeScrollListener = () => {
  const popperEl = document.querySelector('.cit-scroll-select .el-select-dropdown__wrap')
  if (popperEl) {
    popperEl.removeEventListener('scroll', handleScroll)
  }
}

// 滚动处理函数
const handleScroll = (e) => {
  const { scrollTop, scrollHeight, clientHeight } = e.target
  // 距离底部 60px 时开始加载
  if (scrollHeight - scrollTop - clientHeight < 60 && !props.loading && (props.options.length < props.total)) {
    emits('reachBottom')
  }
}

// 使用 MutationObserver 监听下拉框的出现
let observer = null
const initObserver = () => {
  observer = new MutationObserver((mutations) => {
    mutations.forEach((mutation) => {
      if (mutation.addedNodes.length) {
        const selectDropdown = document.querySelector('.cit-scroll-select .el-select-dropdown__wrap')
        if (selectDropdown) {
          setupScrollListener()
        }
      }
    })
  })

  // 监听 body 的变化
  observer.observe(document.body, {
    childList: true,
    subtree: true
  })
}

// 组件挂载时初始化
onMounted(() => {
  initObserver()
})

// 组件卸载前清理
onBeforeUnmount(() => {
  if (observer) {
    observer.disconnect()
  }
  removeScrollListener()
})
</script>

<style scoped lang="scss"></style>

使用案例:

<template>
  <scroll-select v-model="selectedValue" :options="options" :loading="loading" :total="total" :disabled="disabled"
    :placeholder="placeholder" @reachBottom="reachBottom" :remoteMethod="remoteMethod"
    :dataConfig="{ name: 'customerFullName', value: 'id' }" />
</template>

<script setup>
/**
* @author        全易
* @time          2025-12-09 09:27:35 星期二
* @description   客户选择器 (触底加载)
* @example       <customer-selector v-model="value" />
**/
import { customerListByDataScope } from '@/views/customer/api/customer.js';




const props = defineProps({
  placeholder: {
    type: String
  },
  disabled: {
    type: Boolean,
    default: false
  },
  // 接口参数
  params: {
    type: Object,
  }
})


const selectedValue = defineModel({
  type: [String, Number],
  default: null
})
const loading = ref(false)
const options = ref([])
const total = ref(0)
// 初始化参数
const { queryForm } = toRefs(reactive({
  queryForm: {
    pageNum: 1,
    pageSize: 20,
    isNormal: false
  }
}));


watch(() => selectedValue.value, (now) => {
  if (!now) { return }
  getData()
}, { immediate: true })

// 获取数据
function getData(loadMore = false) {
  if (loading.value) return
  loading.value = true
  customerListByDataScope({ ...queryForm.value, ...props.params }).then((res) => {
    if (res.code !== 200) { return }
    if (loadMore) {
      options.value = [...options.value, ...res.data.list]
    } else {
      options.value = res?.data?.list
    }
    total.value = res.data.total
  }).catch(() => {
    if (loadMore && queryForm.value.pageNum !== 1) {
      queryForm.value.pageNum--
    }
  }).finally(() => {
    loading.value = false
  })
};

// 远程获取数据
function remoteMethod(query) {
  // if (!query) { return }
  options.value = []
  queryForm.value.customerName = query
  queryForm.value.pageNum = 1
  getData(false)
}

// 触底追加
function reachBottom() {
  if (options.value.length != 0 && options.value.length < total.value) {
    queryForm.value.pageNum++
    getData(true)
  }
}
</script>

<style scoped lang="scss"></style>
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐