• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

React - 实现 Antd 的 Select 下拉懒加载组件

武飞扬头像
白雾茫茫丶
帮助346

前言

最近的开发迭代业主需要用到一个 Select 下拉加载功能,但项目组上没有这个组件,Antd Design官网也没有提供现成的,查阅资料也没有发现符合业务的组件,特此自己造轮子封装。

使用场景

注:当下拉框数据较多时,后端采用分页搜索模式,这个时候就可以使用这个组件。

实现思路

想要封装一个好的组件,我们需要考虑它的共用性,既然是下拉加载组件,它应具备以下功能:

  1. 应该继承 Select 组件的全部属性
  2. 滚动到底部,数据加载下一页,如果接口返回的条数小于当前设置的条数,就取消滚动加载功能
  3. 支持自定义 options ...

搭建基本结构

我们先看下后端返回的数据结构: 学新通技术网 如果数据结构不一样,可根据后端要求修改:

import { useBoolean, useRequest, useSetState } from 'ahooks'
import { Select, Spin } from 'antd'
import React, { FC, useState } from 'react'

import { Res } from '@/interface/api';

import { ComponentProps, PagetionProps } from './interface'

const { Option } = Select;

const LazyLoadSelect: FC<ComponentProps> = ({
  apiUrl,
  pageRows = 15,
  resultField = 'list',
  fieldNames = {
    label: 'id',
    value: 'name',
  },
  emptyText = '没有更多了',
  manual = false,
  ...props
}) => {
  // 默认分页参数
  const [requestParams, setRequestParams] = useSetState<PagetionProps>({
    currentPage: 1,
    pageRows,
    searchKey: '',
  })
  // 是否还有更多数据
  const [isEmptyData, { setTrue: setEmptyDataTrue, setFalse: setEmptyDataFalse }] = useBoolean(false)
  // 请求的数据列表
  const [requestList, setRequestList] = useState([])

  /**
   * @description: 获取请求数据
   * @author: Cyan
   */
  const { loading: requestLoading, runAsync: runAsyncRequestList } = useRequest(
    async (params: PagetionProps): Promise<Res> => await apiUrl(params),
    {
      manual,
      defaultParams: [{ ...requestParams }],
      onSuccess: (res: Res) => {
        if (res.code === 200) {
          // 获取列表数据
          const result = res?.data?.[resultField] || []
          setRequestList([...requestList, ...result])
          // 当返回条数小于分页条数,或者没有返回数据就代表没有更多数据
          if (result?.length < requestParams.pageRows) {
            setEmptyDataTrue()
          }
        }
      },
    },
  );
  return (
    <Select
      showSearch
      loading={requestLoading}
      filterOption={false}
      placeholder="请选择"
      {...props}
    >
      {
        requestList?.map((item) => {
          return (
            <Option
              {...item}
              value={item[fieldNames.value]}
              key={item[fieldNames.value]}
            >
              {item[fieldNames.label]}
            </Option>
          )
        })
      }
      {/* 没有更多数据 */}
      {
        isEmptyData &&
        <Option disabled>
          <div style={{ color: '#bfbfbf', textAlign: 'center', fontSize: 12, pointerEvents: 'none' }}>{emptyText}</div>
        </Option>
      }
      {/* 下拉加载按钮 */}
      {
        requestLoading &&
        <Option disabled>
          <div style={{ textAlign: 'center', pointerEvents: 'none' }}>
            <Spin size='small' />
          </div>
        </Option>
      }
    </Select>
  )
}
export default LazyLoadSelect

这里我使用了 ahooks库

完善功能

架子我们已经搭好了,现在在其添砖加瓦。

  1. 判断滚动距离,滚动到底部加载下一页 这里我们用到了 Select组件onPopupScroll API
// 判断是否滚动到底部
const { run: onPopupScroll } = useDebounceFn(
  async (e) => {
    e.persist();
    const { scrollTop, offsetHeight, scrollHeight } = e.target;
    // 距离底部多少距离开始加载下一页
    if (((scrollTop   offsetHeight) === scrollHeight) && !isEmptyData) {
      // 当滚动到底部时,我们下载下一页,同时更新分页状态
      setRequestParams({ currentPage: requestParams.currentPage   1 })
      await runAsyncRequestList({ ...requestParams, currentPage: requestParams.currentPage   1 })
    }
  },
  { wait: 350 },
);

<Select
  showSearch
  loading={requestLoading}
  filterOption={false}
  onPopupScroll={(e) => onPopupScroll(e)}
  onSearch={(value: string) => handleSearch(value)}
  placeholder="请选择"
  onClear={handlerClear}
  {...props}
>
</Select>
  1. 增加搜索功能 组件支持搜索功能,当用户搜索后重新触发接口加载,并重置分页状态,这里还要加一个防抖函数,避免输入过程中频繁触发接口请求:
// 重新请求
const initRequest = async (newValue: string) => {
  // 搜索重置分页
  setRequestParams({ currentPage: 1, searchKey: newValue })
  setEmptyDataFalse()
  // 重置数据列表
  setRequestList([])
  await runAsyncRequestList({ ...requestParams, ...extraParams, currentPage: 1, searchKey: newValue })
}

// 搜索回调
const { run: handleSearch } = useDebounceFn(
  async (newValue: string) => {
    // 重置参数,重新请求
    initRequest(newValue)
  },
  { wait: 500 },
);
  1. 增加清空数据回调 当用户搜索的数据只有一条时,选中后清空数据,这时候我们应该重新请求数据:
// 清除内容时的回调
const handlerClear = async () => {
  // 当数据不足一页时,重新加载
  if (requestList.length < requestParams.pageRows) {
    initRequest('')
  }
}
  1. 自定义 options 有时候需求需要自定义内容,如下图: 学新通技术网 这时我们还要提供一个 props 给组件自定义,并提供当前的列表数据:
// 父组件
// 自定义 options
const renderOptions = (ownerHouseList: OwnerHouseProps[]) => {
  return (
    ownerHouseList?.map((house) => {
      return (
        <Option
          value={house.id}
          label={`${house.address}(${house.houseCode})`}
          disabled={house.optional === 0}
          key={house.id}
        >
          <Row>
            <Col span={24}>{house.houseCode}</Col>
            <Col span={24}>盘源地址:{house.address}</Col>
          </Row>
        </Option>
      )
    })
  )
}

// 子组件
{
  customizeOptions ? customizeOptions(requestList) :
    requestList?.map((item) => {
      return (
        <Option
          {...item}
          value={item[fieldNames.value]}
          key={item[fieldNames.value]}
        >
          {item[fieldNames.label]}
        </Option>
      )
    })
}
  1. 细节完善 作为开发者,我们还需要考虑组件的共用性:
    1. 接口可能返回的字段不一样
    2. 下拉滚动到底部多少距离触发
    3. 一些自定义文案
    4. 父组件需要调用子组件的请求方法 ...

最终我们的代码如下:

/*
 * @Description: Select 下拉懒加载组件
 * @Version: 2.0
 * @Author: Cyan
 * @Date: 2023-03-10 10:04:17
 * @LastEditors: Cyan
 * @LastEditTime: 2023-03-10 16:17:13
 */
import { useBoolean, useDebounceFn, useRequest, useSetState } from 'ahooks'
import { Select, Spin } from 'antd'
import React, { FC, useImperativeHandle, useState } from 'react'

import { Res } from '@/interface/api';

import { ComponentProps, PagetionProps } from './interface'

const { Option } = Select;

const LazyLoadSelect: FC<ComponentProps> = ({
  apiUrl,
  pageRows = 15,
  resultField = 'list',
  fieldNames = {
    label: 'id',
    value: 'name',
  },
  emptyText = '没有更多了',
  onRef,
  extraParams = {},
  manual = false,
  distanceBottom = 0,
  customizeOptions,
  ...props
}) => {
  // 默认分页参数
  const [requestParams, setRequestParams] = useSetState<PagetionProps>({
    currentPage: 1,
    pageRows,
    searchKey: '',
  })
  // 是否还有更多数据
  const [isEmptyData, { setTrue: setEmptyDataTrue, setFalse: setEmptyDataFalse }] = useBoolean(false)
  // 请求的数据列表
  const [requestList, setRequestList] = useState([])

  /**
   * @description: 业主当事盘源列表
   * @author: Cyan
   */
  const { loading: requestLoading, runAsync: runAsyncRequestList } = useRequest(
    async (params: PagetionProps): Promise<Res> => await apiUrl(params),
    {
      manual,
      defaultParams: [{ ...requestParams, ...extraParams }],
      onSuccess: (res: Res) => {
        if (res.code === 200) {
          // 获取列表数据
          const result = res?.data?.[resultField] || []
          setRequestList([...requestList, ...result])
          // 当返回条数小于分页条数,或者没有返回数据就代表没有更多数据
          if (result?.length < requestParams.pageRows) {
            setEmptyDataTrue()
          }
        }
      },
    },
  );

  // 判断是否滚动到底部
  const { run: onPopupScroll } = useDebounceFn(
    async (e) => {
      e.persist();
      const { scrollTop, offsetHeight, scrollHeight } = e.target;
      // 距离底部多少距离开始加载下一页
      if (((scrollTop   offsetHeight   distanceBottom) >= scrollHeight) && !isEmptyData) {
        setRequestParams({ currentPage: requestParams.currentPage   1 })
        await runAsyncRequestList({ ...requestParams, ...extraParams, currentPage: requestParams.currentPage   1 })
      }
    },
    { wait: 350 },
  );

  // 重新请求
  const initRequest = async (newValue: string) => {
    // 搜索重置分页
    setRequestParams({ currentPage: 1, searchKey: newValue })
    setEmptyDataFalse()
    // 重置数据列表
    setRequestList([])
    await runAsyncRequestList({ ...requestParams, ...extraParams, currentPage: 1, searchKey: newValue })
  }

  // 搜索回调
  const { run: handleSearch } = useDebounceFn(
    async (newValue: string) => {
      // 重置参数,重新请求
      initRequest(newValue)
    },
    { wait: 500 },
  );

  // 清除内容时的回调
  const handlerClear = async () => {
    // 当数据不足一页时,重新加载
    if (requestList.length < requestParams.pageRows) {
      initRequest('')
    }
  }

  // 用 useImperativeHandle 暴露一些外部ref能访问的属性
  useImperativeHandle(onRef, () => ({ runAsyncRequestList, requestParams }))
  return (
    <Select
      showSearch
      loading={requestLoading}
      filterOption={false}
      onPopupScroll={(e) => onPopupScroll(e)}
      onSearch={(value: string) => handleSearch(value)}
      placeholder="请选择"
      onClear={handlerClear}
      {...props}
    >
      {
        customizeOptions ? customizeOptions(requestList) :
          requestList?.map((item) => {
            return (
              <Option
                {...item}
                value={item[fieldNames.value]}
                key={item[fieldNames.value]}
              >
                {item[fieldNames.label]}
              </Option>
            )
          })
      }
      {/* 没有更多数据 */}
      {
        isEmptyData &&
        <Option disabled>
          <div style={{ color: '#bfbfbf', textAlign: 'center', fontSize: 12, pointerEvents: 'none' }}>{emptyText}</div>
        </Option>
      }
      {/* 下拉加载按钮 */}
      {
        requestLoading &&
        <Option disabled>
          <div style={{ textAlign: 'center', pointerEvents: 'none' }}>
            <Spin size='small' />
          </div>
        </Option>
      }
    </Select>
  )
}
export default LazyLoadSelect

使用方式

import LazyLoadSelect from '@/components/LazyLoadSelect'

<LazyLoadSelect
  key="LazyLoadSelect"
  onRef={lazyLoadSelectRef}
  allowClear
  placeholder="请搜索或选择经纪人姓名"
  apiUrl={getBrokerInfoListForReport}
  fieldNames={{ value: 'userId', label: 'userName' }}
  onChange={onChangeUserName}
  getPopupContainer={() => document.getElementById('black-container')}
/>

参数说明

参数 说明 类型 默认值 是否必传
apiUrl 请求接口 Promise -
pageRows 每页显示条数 number 15 -
resultField 后端返回数据字段 string list -
emptyText 空数据的显示文案 string 没有更多了 -
onRef 绑定子组件实例 React.RefObject - -
extraParams 额外参数 object {} -
manual 是否手动执行 boolean false -
distanceBottom 滚动距离多少触发加载,默认滚动到底部,也就是0 number 0 -
customizeOptions 自定义 option - - -
SelectProps Antd Select选择器 Props,配置项 - - -

注意事项

  1. 如果接口请求后,下拉框没看到数据,请检查参数 fieldNames 是否配置正确。

  2. 如果下拉过程出现数据重复等BUG,请检查后端返回的数据 fieldNames.value 是否唯一,且必传,否则组件会出现BUG。

  3. 如果需要自定义 option,请使用 customizeOptions 属性

  4. 如果组件的功能不符合您的业务场景,请联系我拓展完善。

让我们看看最终效果: 学新通技术网

如果此文章对你有帮助,请帮我点个赞吧!

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanffhih
系列文章
更多 icon
同类精品
更多 icon
继续加载