el-select滚动获取下拉数据;el-select滚动加载

场景:

下拉数据超大几十万条,后端提供了一个分页查询接口;前端通过滚动加载分页的下拉数据,同时后端接口支持使用名称搜索查询;前端实现数据回显

在这里插入图片描述

1.解决问题

场景:下拉数据量过大,后端提供一个分页查询接口;需要每次滚动加载下一页的下拉数据
且单选的状态,需要支持回显,通过name名称查询回显;–本文已包含
如果是多选回显,可以让后端提供一个根据idList能反向找到对应id的下拉集合的接口;–可自己试试

2.封装MyScrollSelect组件

<template>
  <div>list长度:{{ list.length }}</div>
  <div>$attrs:{{ $attrs }}</div>
  <el-select @change="changeVal" v-bind="$attrs" v-on="proxyEvents" :remote-method="remoteMethod" style="width: 100%">
    <div v-infinite-scroll="loadMore" style="overflow: hidden">
      <el-option v-for="item in list" :key="item[valueKey]" :label="item[labelKey]" :value="item[valueKey]" />
      <template v-if="showDisabled">
        <!-- 解决找不到对应id但是需要正确回显 需要将下拉值设置为不可选取 -->
        <el-option v-if="!filters($attrs.modelValue, list) && $attrs.modelValue" :disabled="true" :key="$attrs.modelValue" :value="$attrs.modelValue" :label="props.searchName" />
      </template>
      <div v-if="loading" class="loading-text">加载中...</div>
    </div>
  </el-select>
</template>

<script setup lang="ts">
import { ref, watch, onMounted, useAttrs, computed, PropType } from "vue"
import { debounce } from "lodash"
import type { ElSelect } from "element-plus"

// 类型定义
interface QueryParams {
  pageNum: number
  totalPage: number
  pageSize: number
  [key: string]: any
}

interface ListItem {
  [key: string]: any
}

// 事件类型
type EmitTypes = {
  (e: "update:searchName", value: string): void
  (e: "change", value: any): void
}

const emit = defineEmits<EmitTypes>()

// Props 类型
const props = defineProps({
  // v-model绑定值不为空时传递初始数据列表
  initialOptions: {
    type: Array as PropType<ListItem[]>,
    default: () => []
  },
  // 传入对应的列表加载api
  methods: {
    type: Function as PropType<(params: QueryParams) => Promise<{ records: ListItem[]; total: number }>>,
    required: true
  },
  // 传入查询关键字
  searchKey: {
    type: String,
    default: ""
  },
  // 所选key对用name
  searchName: {
    type: String,
    default: undefined
  },
  labelKey: {
    type: String,
    default: "name"
  },
  valueKey: {
    type: String,
    default: "id"
  },
  // 查询的其他参数
  queryData: {
    type: Object as PropType<Record<string, any>>,
    default: () => ({})
  },
  // 是否将不在接口数据里的旧数据以disabled方式回显(例如删除的数据回显)
  showDisabled: {
    type: Boolean,
    default: false
  }
})

// 属性处理
const attrs = useAttrs()

// 事件代理
const proxyEvents = computed(() => {
  const { on } = attrs
  return Object.keys(on || {}).reduce((acc: Record<string, any>, key) => {
    acc[key] = (...args: any[]) => (on as any)[key](...args)
    return acc
  }, {})
})

// 响应式数据
const isMounted = ref(false)
const loading = ref(false)
const list = ref<ListItem[]>([])
const queryFrom = ref<QueryParams>({
  pageNum: 1,
  totalPage: 1,
  pageSize: 20
})

// 远程搜索方法
const remoteMethod = (query: string) => {
  queryFrom.value.pageNum = 1
  list.value = []
  queryFrom.value[props.searchKey] = query
  Object.assign(queryFrom.value, props.queryData)
  getList()
}

// 获取数据
const getList = () => {
  loading.value = true
  if (props.methods) {
    props
      .methods(queryFrom.value)
      .then(res => {
        list.value = [...list.value, ...res.records]
        queryFrom.value.totalPage = Math.ceil(res.total / 20)
      })
      .finally(() => {
        loading.value = false
      })
  } else {
    setTimeout(() => {
      let lengthIndex = list.value.length
      let records = Array.from({ length: 20 }).map((ele, index) => {
        return {
          name: "模拟数据" + (index + lengthIndex + 1),
          id: "" + (index + lengthIndex + 1)
        }
      })
      list.value = [...list.value, ...records]
      queryFrom.value.totalPage = Math.ceil(100000 / 20)
      loading.value = false
    }, 100)
  }
}

// 无限滚动加载
const loadMore = debounce(() => {
  if (queryFrom.value.pageNum >= queryFrom.value.totalPage || loading.value) return
  queryFrom.value.pageNum++
  getList()
}, 200)

// 值变化处理
const changeVal = (e: string | number) => {
  const selected = list.value.find(item => item[props.valueKey] === e)
  if (selected) {
    emit("update:searchName", selected[props.labelKey])
  }
  emit("change", e)
}

// 下拉过滤回显
const filters = (key, arr = [], valueKey = "value", labelKey = "name") => {
  const names = []
  const value = Array.isArray(key) ? key : [key]

  for (let i = 0; i < arr.length; i++) {
    if (value.includes(arr[i]?.[valueKey])) {
      names.push(arr[i]?.[labelKey])
    }
  }
  return names.length ? names.join(";") : undefined
}

// 监听初始数据
watch(
  () => props.initialOptions,
  newVal => {
    if (newVal?.length) {
      list.value.push(...newVal)
    } else if (props.searchName && attrs.modelValue) {
      // 塞入单选默认值
      list.value.push({ [props.valueKey]: attrs.modelValue, [props.labelKey]: props.searchName })
    }
  },
  { immediate: true }
)

// 生命周期
onMounted(() => {
  isMounted.value = true
  props.searchName ? remoteMethod(props.searchName) : getList()
})
</script>

<style scoped>
.loading-text {
  padding: 5px;
  text-align: center;
  color: #999;
  font-size: 12px;
}
</style>


3.使用MyScrollSelect组件

<template>
  <div class="page-view wbg pall">
    <pre>{{ form }}</pre>

    <div style="margin-top: 50px">多选:只能存id</div>
    <MyScrollSelect
      v-if="isMounted"
      ref="reviewStageRef"
      v-model="form.idList1"
      :placeholder="'滚动加载或搜索-单选'"
      clearable
      filterable
      remote
      collapse-tags
      collapse-tags-tooltip
      multiple
      :initialOptions="initialOptions"
      :methods="getDeviceNameListApi"
      searchKey="terminalDeviceName"
      valueKey="id"
      labelKey="terminalDeviceName"
    />

    <div style="margin-top: 50px">单选:可存id和name 根据name可回显</div>
    <MyScrollSelect
      v-if="isMounted"
      ref="reviewStageRef"
      v-model="form.terminalDeviceId"
      v-model:searchName="form.terminalDeviceName"
      :placeholder="'滚动加载或搜索-单选'"
      clearable
      filterable
      remote
      :initialOptions="initialOptions"
      :methods="getDeviceNameListApi"
      searchKey="terminalDeviceName"
      valueKey="id"
      labelKey="terminalDeviceName"
    />
  </div>
</template>

<script setup>
import { onMounted, ref } from 'vue'
import { getDeviceNameListApi } from "@/api/ipManagement.js" // 后端获取下拉分页接口

defineOptions({
  name: 'FactorySiteAddressLedger'
})

const isMounted = ref(false)
const form = ref({
  idList1: [], // 多选参数

  terminalDeviceId: '710241160000004443', // 单选参数
  terminalDeviceName: '益海电厂网监工作站',
})

const reviewStageRef = ref(null)

const initialOptions = ref([]) // 初始下拉数据

onMounted(() => {
  isMounted.value = true
})
</script>
<style lang="scss" scoped></style>
### 实现 Vue3 `el-select` 下拉滚动分页加载 为了实现在 Vue3 中使用 Element Plus 的 `el-select` 组件并实现下拉列表中的滚动分页加载,可以采用自定义指令的方式。这种方法能够有效地监听滚动事件,并在接近底部时触发数据加载。 #### 自定义指令注册 通过创建一个名为 `elSelectLoadmore` 的自定义指令来增强 `el-select` 功能[^2]: ```typescript // src/directives/elSelectLoadMore.ts import { DirectiveBinding } from 'vue'; const elSelectLoadmore = { mounted(el: HTMLElement, binding: DirectiveBinding) { const selectWrapper = el.querySelector('.el-select-dropdown .el-scrollbar__wrap'); if (selectWrapper) { selectWrapper.addEventListener('scroll', () => { const scrollTop = selectWrapper.scrollTop; const clientHeight = selectWrapper.clientHeight; const scrollHeight = selectWrapper.scrollHeight; // 当滚动条距离底部小于等于0时触发加载更多逻辑 if (scrollTop + clientHeight >= scrollHeight - 5 && !binding.value.loading) { binding.value.loadMore(); } }); } }, }; export default elSelectLoadmore; ``` 此代码片段展示了如何监听 `.el-select-dropdown .el-scrollbar__wrap` 元素上的滚动事件,并判断是否到达了容器的底部位置。一旦满足条件,则调用绑定对象内的 `loadMore()` 方法执行进一步的数据获取操作。 #### 使用自定义指令 接着,在应用初始化阶段全局注册该指令以便于后续组件内直接使用: ```typescript // main.ts or setup file import { createApp } from 'vue'; import App from './App.vue'; import elementPlus from 'element-plus'; import 'element-plus/lib/theme-chalk/index.css'; import directive from '@/directives'; // 假设这是存放所有自定义指令的地方 createApp(App).use(elementPlus).use(directive).mount('#app'); ``` 最后,在具体的页面或组件中利用这个新引入的功能完成实际业务需求: ```html <template> <div class="example"> <el-select v-model="value" placeholder="请选择..." style="width: 100%;"> <!-- 这里放置选项 --> <el-option v-for="(item,index) in options" :key="index" :label="item.label" :value="item.value"></el-option> <!-- 如果正在加载则显示loading图标 --> <p v-if="loading">Loading...</p> <!-- 若无更多数据加载提示 --> <p v-else-if="noMoreData">No more data</p> </el-select> </div> </template> <script lang="ts"> import { defineComponent, ref } from 'vue'; export default defineComponent({ name: "Example", directives: { 'select-load-more': require('@/directives/elSelectLoadMore').default, }, setup() { let value = ref(''); let loading = ref(false); let noMoreData = ref(false); function loadMore() { console.log("尝试加载更多..."); // 设置标志位防止重复请求 loading.value = true; setTimeout(() => { // 模拟异步接口返回新的option项 for(let i=0; i<10; ++i){ options.value.push({ label:`Option ${options.value.length}`, value: `${Math.random().toString(36).substr(-8)}`}); } // 更新状态 loading.value = false; // 判断是否有更多的数据可供加载 if(options.value.length>=totalOptionsCount){ noMoreData.value=true; } }, 1000); }; return { value, loading, noMoreData, options:ref([]), totalOptionsCount:100,// 总共可能有的选项数量 loadMore }; } }) </script> <style scoped> .example{ width: 300px; } </style> ``` 上述模板部分包含了两个额外的状态变量用于指示当前是否处于加载过程中 (`loading`) 和是否存在未加载完毕的数据 (`noMoreData`) 。当用户滚动至最下方时会自动触发 `loadMore()` 函数模拟网络请求追加新项目到现有列表之中直到达到预设的最大数目为止。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值