remote select component
7/22/2024
import React, {
  forwardRef,
  memo,
  useCallback,
  useEffect,
  useImperativeHandle,
  useMemo,
  useRef,
  useState,
} from 'react';
import { message, Select, SelectProps } from 'antd';
import { cloneDeep, debounce, get, isEqual, uniqBy } from 'lodash';
 
type OmitSelectProps = 'options' | 'loading' | 'onSelect' | 'labelInValue';
 
export interface SearchParams {
  pageNum?: number;
  pageSize?: number;
  [key: string]: any;
}
 
interface Res {
  records: any[];
  total: number;
}
 
interface Response {
  res: Res;
  error: string | null;
  code: number;
  trace: string | null;
}
 
export interface RemoteSelectProps extends Omit<SelectProps, OmitSelectProps> {
  /** 请求 */
  api: (params: SearchParams) => Promise<Response>;
  /** 下拉初始值 */
  defaultOptions?: SelectProps['options'];
  /** 搜索参数 */
  searchParams?: SearchParams;
  /** value 字段 支持 xxx.xxx 嵌套类型 */
  valueField?: string;
  /** label 字段 支持 xxx.xxx 嵌套类型 */
  labelField?: string;
}
 
export interface RemoteSelectRef {}
 
const DEFAULT_SEARCHPARAMS: SearchParams = {
  pageNum: 1,
  pageSize: 10,
};
 
/* 生成Options */
const genOptions = (item: any) => ({ label: item.label, value: item.value });
 
/**
 * 远程下拉组件
 * 由于下拉数据需要走请求,导致回显存在异常情况,默认采用 labelInValue 模式
 * 除 'options' | 'loading' | 'onSelect' | 'labelInValue' 字段,支持原生 antd Select 所有属性
 * @example
 * <RemoteSelect api={api}/>
 */
const RemoteSelect: React.ForwardRefRenderFunction<RemoteSelectRef, RemoteSelectProps> = (
  props,
  ref,
) => {
  const {
    api,
    defaultOptions = [],
    searchParams,
    labelField = 'name',
    valueField = 'id',
    ...selectProps
  } = props;
 
  const { value, showSearch, mode } = selectProps;
 
  const [loading, setLoading] = useState(false);
  const [options, setOptions] = useState<SelectProps['options']>(defaultOptions);
  const [extraOptions, setExtraOptions] = useState<SelectProps['options']>([]);
 
  const isManualSelect = useRef(false);
  const preSearchParams = useRef<SearchParams>();
 
  /* options 去重 */
  const optionsUniq = useMemo(() => {
    if (!extraOptions.length) return options;
    return uniqBy([...extraOptions, ...options], 'value');
  }, [options, extraOptions]);
 
  /* api-params */
  const params = useRef({ ...DEFAULT_SEARCHPARAMS });
 
  const handleSearch = useCallback(
    debounce((val: string) => {
      params.current.fuzzyKeyword = val;
      setLoading(true);
    }, 300),
    [],
  );
 
  useImperativeHandle(ref, () => ({}));
 
  useEffect(() => {
    /* 深比较是否相等 */
    if (isEqual(preSearchParams.current, searchParams)) return;
    preSearchParams.current = cloneDeep(searchParams);
 
    params.current = {
      ...params.current,
      ...searchParams,
    };
    setLoading(true);
  }, [searchParams]);
 
  useEffect(() => {
    // @ts-ignore
    if (!loading) return;
    let lock = false;
    try {
      api(params.current).then((ress) => {
        if (lock) return;
        if (ress.code !== 0) throw new Error(ress.error || '未知错误');
 
        const _options = ress.res?.records?.map((item, index: number) => ({
          label: get(item, labelField)?.toString() || `${labelField}字段不存在`,
          value: get(item, valueField)?.toString() || `${valueField}字段不存在`,
        }));
 
        setOptions(_options);
        setLoading(false);
      });
    } catch (err: any) {
      message.error(err.message || err.toString());
      setLoading(false);
    }
    return () => {
      lock = true;
    };
  }, [loading]);
 
  useEffect(() => {
    /* 手动选择 */
    if (isManualSelect.current) {
      isManualSelect.current = false;
      setExtraOptions([]);
      return;
    }
 
    if (mode === 'multiple') {
      /* 多选模式下,没有值时,不重置 */
      if (!value || !value.length) return;
      /* 额外 Option */
      setExtraOptions(value.map(genOptions));
    } else {
      /* 空数据 */
      if (!value || !value?.value) return;
      /* 额外 Option */
      setExtraOptions([{ value: value.value, label: value.label }]);
    }
  }, [value]);
 
  return (
    <Select
      {...selectProps}
      showSearch={showSearch}
      filterOption={false}
      loading={loading}
      options={optionsUniq}
      onSearch={showSearch ? handleSearch : undefined}
      onSelect={() => (isManualSelect.current = true)}
      labelInValue
    />
  );
};
 
export default memo(forwardRef(RemoteSelect));