二次封装elementPlus select,实现滚动加载分页和远程搜索,用vue3实现

该文章已生成可运行项目,

工作中经常需要用到下拉框滚动分页加载,但是elementPlus没有,只能自己封装一个

功能:支持分页、远程搜索、配置分页、配置搜索参数、配置其他参数、配置下拉框的值等

注意:如果没有对el-select加class区分,一个页面引入多个下拉框分页组件,会出现滚动加载没有效果,是因为多个组件同时监听了同一个滚动事件,导致事件处理混乱,加上 :popper-class="popperClass"做区分可解决这个问题

1.下拉框分页组件

<template>
  
  <el-select
    v-model="selectedValue"
    filterable
    remote
    reserve-keyword
    :remote-method="remoteMethod"
    :loading="loading"
    @clear="handleClear"
    @change="handleValueChange"
    v-bind="$attrs"
    :popper-class="popperClass"
  >
    <el-option 
      v-for="item in options"
      :key="item.value"
      :label="item.label"
      :value="valueKey ? item :item.value"
    ></el-option>
    <div v-if="hasMore" v-loading="loadingMore" class="infinite-loading">
      <span>加载中...</span>
    </div>
    <div v-else-if="options.length > 0" class="no-more-data">
      <span>没有更多数据了</span>
    </div>
  </el-select>
</template>

<script lang="ts" setup>
import { ref, onMounted, onUnmounted,watch } from 'vue';
import { ElSelect, ElOption } from 'element-plus';

interface OptionItem {
  value: string | number;
  label: string;
}

const props = defineProps({
  modelValue: {
    type: [String, Number, Array,Object],
    default: '',
  },
  // 统一的数据获取函数
  fetchData: {
    type: Function,
    required: true,
  },
  // 每页大小
  pageSize: {
    type: Number,
    default: 50,
  },
  // 搜索
  keywordKey: {
    type: String,
    default: 'keyword',
  },
  valueKey:{
    type: Boolean,
    default: true,  //下拉框值,是item 或者 item.value
  },
  //分页对应的字段
  pageOrSizeKey:{
    type:Object,
    default:{
      page:'page',
      size:'size'
    }
  },
  //其他搜索参数
  otherSearchParams:{
    type:Object,
    default:{}
  }
});

const popperClass = `popperClass${getRandom4DigitInt()}`
const selectedValue = ref(props.modelValue);
const options = ref<OptionItem[]>([]);
const loading = ref(false);
const loadingMore = ref(false);
const hasMore = ref(true);
const currentPage = ref(1);
const keyword = ref('');
const scrollContainer = ref<HTMLElement | null>(null);

function getRandom4DigitInt() {
  return Math.floor(Math.random() * 9000) + 1000;
}

const emit = defineEmits(['update:modelValue', 'change']);
watch(() => props.modelValue, (newVal) => {
  // 只有当新值与当前值不同时才更新
  if (newVal !== selectedValue.value) {
    selectedValue.value = newVal;
    emit('update:modelValue', newVal);
  }
}, { immediate: true ,deep: true});
// 监听值变化
const handleValueChange = (value: string | number | Array<string | number>) => {
  selectedValue.value = value;
  emit('update:modelValue', value);
  emit('change', value);
};

// 统一加载数据方法
const loadData = async (reset = false) => {
  if (reset) {
    currentPage.value = 1;
    hasMore.value = true;
  }

  if (!hasMore.value) return;

  const loadingRef = reset ? loading : loadingMore;
  loadingRef.value = true;

  try {
    const params = {
      [props.pageOrSizeKey.page]: currentPage.value,
      [props.pageOrSizeKey.size]: props.pageSize,
      ...props.otherSearchParams
    };

    // 如果有搜索关键词,加入参数
    if (keyword.value) {
      params[props.keywordKey] = keyword.value;
    }

    const result = await props.fetchData(params);

    if (reset) {
      options.value = result.list;
    } else {
      options.value = [...options.value, ...result.list];
    }

    hasMore.value = result.totalPages > currentPage.value;
    currentPage.value += 1;

  } catch (error) {
    console.error('加载数据失败:', error);
  } finally {
    loadingRef.value = false;
  }
};

// 远程搜索方法
const remoteMethod = (query: string) => {
  keyword.value = query;
  loadData(true);
};

// 处理清空
const handleClear = () => {
  keyword.value = '';
  loadData(true);
};

// 滚动加载
const handleScroll = () => {
  if (!scrollContainer.value) return;

  const { scrollTop, scrollHeight, clientHeight } = scrollContainer.value;
  const isBottom = scrollHeight - (scrollTop + clientHeight) < 50;

  if (isBottom && !loadingMore.value && hasMore.value) {
    loadData();
  }
};

// 初始化滚动监听
const initScrollListener = () => {
  const el = document.querySelector(
    `.${popperClass} .el-select-dropdown .el-select-dropdown__wrap`,
  );
  if (el) {
    scrollContainer.value = el as HTMLElement;
    scrollContainer.value.addEventListener('scroll', handleScroll);
  }
};

// 移除滚动监听
const removeScrollListener = () => {
  if (scrollContainer.value) {
    scrollContainer.value.removeEventListener('scroll', handleScroll);
  }
};

onMounted(() => {
  setTimeout(initScrollListener, 300);
});

onUnmounted(() => {
  removeScrollListener();
});
</script>

<style scoped>
.infinite-loading,
.no-more-data {
  padding: 8px 0;
  font-size: 12px;
  color: var(--el-text-color-secondary);
  text-align: center;
}

.no-more-data {
  padding: 16px 0;
}

</style>

2.实现

<template>
  <div>
    <infinite-scroll-select
      v-model="selectedValue"
      :fetchData="searchData"
      placeholder="请选择"
      :keywordKey="'name'"
      @change="handleChange"
      clearable
    />
  </div>
</template>

<script lang="ts" setup>
import { ref } from 'vue';
import InfiniteScrollSelect from './selectPage.vue';

import { getPageApi } from '#/api/index';
const selectedValue = ref('');

// 模拟搜索数据
const searchData = async (params: {
  page: number;
  size: number;
  keyword: string;
}) => {
  const res = await getPageApi(params); //后端接口
  const list = res.data.content.map((item) => ({
    label: item.name,
    value: item.code,
  }));
  return {
    list,
    totalPages: res.data.totalPages, //总页数
  };
};

const handleChange = (value: string | number) => {
  console.log('选中值变化:', value);
  console.log(selectedValue.value)
};
</script>

本文章已经生成可运行项目
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值