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>