如何使用 Antd 的 Select 组件搭建多级选择框

1,179 阅读4分钟

「这是我参与2022首次更文挑战的第21天,活动详情查看:2022首次更文挑战

有几种情况:

  • 只有一级标题
{
  label: "春梅红",
  value: 201,
  children: [],
},
  • 有二级标题,但二级标题只有一个
{
  label: "淡曙红",
  value: 401,
  children: [
    {
      label: "粉团花红",
      value: 401,
    },
  ],
},
  • 有二级标题,二级标题有多个
{
  label: "暮云灰",
  value: 202,
  children: [
    {
      label: "凤仙花红",
      value: 201,
    },
    {
      label: "龙睛鱼紫",
      value: 202,
    },
  ],
},
  • 有三级标题,有一个或多个
{
  label: "槿紫",
  value: 301,
  children: [
    {
      label: "满天星紫",
      value: 301,
      children: [
        {
          label: "花青",
          value: 301,
        },
        {
          label: "井天蓝",
          value: 302,
        },
      ],
    },
    {
      label: "青矾绿",
      value: 30901,
      children: [
        {
          label: "麦秆黄",
          value: 380901,
        },
      ],
    },
  ],
},

先使用三个 select 搭建

import { useEffect, useState, useMemo } from "react";
import { Row, Col, Select } from "antd";
import { intersectionBy } from "lodash-es";

interface ThreeLevelSelectProps {
  value: string[];
  onChange: (val: string[] | undefined) => void;
  options: any[];
  disabled: boolean;
}

const ThreeLevelSelect = ({ value, onChange, options, disabled }: ThreeLevelSelectProps) => {
  return (
    <Row justify="space-between" gutter={12}>
      <Col span={8}>
        <Select />
      </Col>
      <Col span={8}>
        <Select />
      </Col>
      <Col span={8}>
        <Select />
      </Col>
    </Row>
  );
};

export default ThreeLevelSelect;

image.png

设置 value 和 onChange

const ThreeLevelSelect = ({ value, onChange, options, disabled }: ThreeLevelSelectProps) => {
  const [firstValue, setFirstValue] = useState<number>();
  const [secondValue, setSecondValue] = useState<string[] | number>();
  const [thirdValue, setThirdValue] = useState<string[] | number>();

  const onFirstChange = (val) => {
    setFirstValue(val);
  };

  const onSecondChange = (val) => {
    setSecondValue(val);
  };

  const onThirdChange = (val) => {
    setThirdValue(val);
  };

  return (
    <Row justify="space-between" gutter={12}>
      <Col span={8}>
        <Select value={firstValue} onChange={onFirstChange} />
      </Col>
      <Col span={8}>
        <Select value={secondValue} onChange={onSecondChange} />
      </Col>
      <Col span={8}>
        <Select value={thirdValue} onChange={onThirdChange} />
      </Col>
    </Row>
  );
};

设置 options

为第一个 select 设置 options,第一个的 options === 传入的 options

const ThreeLevelSelect = ({ value, onChange, options, disabled }: ThreeLevelSelectProps) => {
  const [firstValue, setFirstValue] = useState<number>();
  const [secondValue, setSecondValue] = useState<string[] | number>();
  const [thirdValue, setThirdValue] = useState<string[] | number>();

  const onFirstChange = (val) => {
    setFirstValue(val);
  };

  const onSecondChange = (val) => {
    setSecondValue(val);
  };

  const onThirdChange = (val) => {
    setThirdValue(val);
  };

  return (
    <Row justify="space-between" gutter={12}>
      <Col span={8}>
        <Select value={firstValue} onChange={onFirstChange} options={options} />
      </Col>
      <Col span={8}>
        <Select value={secondValue} onChange={onSecondChange} />
      </Col>
      <Col span={8}>
        <Select value={thirdValue} onChange={onThirdChange} />
      </Col>
    </Row>
  );
};

设置 第二个 和 第三个的 options

const [secondOptions, setSecondOptions] = useState<any>([]);
const [thirdOptions, setThirdOptions] = useState<any>([]);
return (
    <Row justify="space-between" gutter={12}>
      <Col span={8}>
        <Select value={firstValue} onChange={onFirstChange} options={options} />
      </Col>
      <Col span={8}>
        <Select value={secondValue} onChange={onSecondChange} options={secondOptions} />
      </Col>
      <Col span={8}>
        <Select value={thirdValue} onChange={onThirdChange} options={thirdOptions} />
      </Col>
    </Row>
  );
const onFirstChange = (val) => {
    setFirstValue(val);
    const secOptions = options?.find((item) => item?.value === val)?.children;
    setSecondOptions(secOptions);
  };

const onSecondChange = (val) => {
    setSecondValue(val);
    const thrOptions = secondOptions?.find((item) => item?.value === val)?.children;
    setThirdOptions(thrOptions);
  };

image.png

设置多选

返回的数据中,可能存在多选现象,因为我们需要设置多选。

const [isSecondMultiple, setIsSecondMultiple] = useState<boolean>(false);
const [isThirdMultiple, setIsThridMultiple] = useState<boolean>(false);
return (
    <Row justify="space-between" gutter={12}>
      <Col span={8}>
        <Select value={firstValue} onChange={onFirstChange} options={options} />
      </Col>
      <Col span={8}>
        <Select
          value={secondValue}
          onChange={onSecondChange}
          options={secondOptions}
          mode={isSecondMultiple ? "multiple" : undefined}
        />
      </Col>
      <Col span={8}>
        <Select
          value={thirdValue}
          onChange={onThirdChange}
          options={thirdOptions}
          mode={isThirdMultiple ? "multiple" : undefined}
        />
      </Col>
    </Row>
  );

一句话:没有 children 属性时,就是多选,有 chlidren 时,就是单选。

没有 children 属性时,就是证明说已经是最好一级了,那么就可以是多选。如果有 chlidren 属性时,说明还没有到最后一级。

const onFirstChange = (val) => {
    setFirstValue(val);
    const secOptions = options?.find((item) => item?.value === val)?.children;
    setSecondOptions(secOptions);
    // 无 children => 多选
    const secondIsMutiple = secOptions
      ?.map((item) => {
        return item.children === undefined;
      })
      .every((i) => i === true);
    setIsSecondMultiple(secondIsMutiple);
  };

三级标题处就是,长度大于 1,就是多选,只有一个就是单选。

const onSecondChange = (val) => {
    setSecondValue(val);
    const thrOptions = secondOptions?.find((item) => item?.value === val)?.children;
    setThirdOptions(thrOptions);
    // 只有一个就是单选
    const thirdIsMutiple = thrOptions?.length > 1;
    setIsThridMultiple(thirdIsMutiple);
  };

设置禁用熟悉 disabled:

第二个:第一个 disabled || secondOptions 不存在

第三个:第一个 disabled || secondOptions 不存在 || thirdOptions 不存在

const secondSelectDisabled = useMemo(
    () => disabled || !secondOptions?.length,
    [disabled, secondOptions],
  );

  const thirdSelectDisabled = useMemo(
    () => disabled || !thirdOptions?.length || !secondOptions?.length,
    [disabled, thirdOptions],
  );

return (
    <Row justify="space-between" gutter={12}>
      <Col span={8}>
        <Select value={firstValue} onChange={onFirstChange} options={options} disabled={disabled} />
      </Col>
      <Col span={8}>
        <Select
          value={secondValue}
          onChange={onSecondChange}
          options={secondOptions}
          mode={isSecondMultiple ? "multiple" : undefined}
          disabled={secondSelectDisabled}
        />
      </Col>
      <Col span={8}>
        <Select
          value={thirdValue}
          onChange={onThirdChange}
          options={thirdOptions}
          mode={isThirdMultiple ? "multiple" : undefined}
          disabled={thirdSelectDisabled}
        />
      </Col>
    </Row>
  );

兜底提示

return (
    <Row justify="space-between" gutter={12}>
      <Col span={8}>
        <Select
          value={firstValue}
          onChange={onFirstChange}
          options={options}
          disabled={disabled}
          placeholder={"请选择"}
        />
      </Col>
      <Col span={8}>
        <Select
          value={secondValue}
          onChange={onSecondChange}
          options={secondOptions}
          mode={isSecondMultiple ? "multiple" : undefined}
          disabled={secondSelectDisabled}
          placeholder={secondSelectDisabled ? "" : "请选择"}
        />
      </Col>
      <Col span={8}>
        <Select
          value={thirdValue}
          onChange={onThirdChange}
          options={thirdOptions}
          mode={isThirdMultiple ? "multiple" : undefined}
          disabled={thirdSelectDisabled}
          placeholder={thirdSelectDisabled ? "" : "请选择"}
        />
      </Col>
    </Row>
  );

但是我们发现,联动效果不太好,应该是前一个选择之后,后一个可以自动选择第一个。也就是,选中第一个后面的应该跟着改变。

当然,这个时候需要区分单选或多选,如果是多选,那应该设置数组,如果是单选应该设置数字。

const onFirstChange = (val) => {
    setFirstValue(val);
    const secOptions = options?.find((item) => item?.value === val)?.children;
    // 无二级标题
    if (secOptions?.length === 0) {
      return;
    }
    setSecondOptions(secOptions);
    // 无 children => 多选
    const secondIsMutiple = secOptions
      ?.map((item) => {
        return item.children === undefined;
      })
      .every((i) => i === true);
    setIsSecondMultiple(secondIsMutiple);
    // 多选
    if (secondIsMutiple) {
      setSecondValue([secOptions?.[0]?.value]);
    } else {
      setSecondValue(secOptions?.[0]?.value);
    }
  };
const onSecondChange = (val) => {
    setSecondValue(val);
    const trdOptions = secondOptions?.find((item) => item?.value === val)?.children;
    setThirdOptions(trdOptions);
    // 只有一个就是单选
    const thirdIsMutiple = trdOptions?.length > 1;
    setIsThridMultiple(thirdIsMutiple);
    if (thirdIsMutiple) {
      setThirdValue([trdOptions?.[0]?.value]);
    } else {
      setThirdValue(trdOptions?.[0]?.value);
    }
  };

因为我们打算封装成一个组件,那么我们就需要向父组件传递选中的值,以供父组件使用。

const onFirstChange = (val) => {
    if (!val) {
      onChange([]);
      setSecondValue(undefined);
      setSecondOptions([]);
      return;
    }
    setFirstValue(val);
    const secOptions = options?.find((item) => item?.value === val)?.children;
    // 无二级标题
    if (secOptions?.length === 0) {
      console.log([`${val}_0`]);
      onChange([`${val}_0`]);
      return;
    }
    setSecondOptions(secOptions);
    // 无 children => 多选
    const secondIsMutiple = secOptions
      ?.map((item) => {
        return item.children === undefined;
      })
      .every((i) => i === true);
    setIsSecondMultiple(secondIsMutiple);
    // 多选
    if (secondIsMutiple) {
      setSecondValue([secOptions?.[0]?.value]);
    } else {
      setSecondValue(secOptions?.[0]?.value);
    }
    console.log([`${val}_${secOptions?.[0]?.value || 0}`]);
    onChange([`${val}_${secOptions?.[0]?.value || 0}`]);
};
const onSecondChange = (val) => {
    if (!val) {
      onChange([]);
      setThirdValue(undefined);
      setThirdOptions([]);
      return;
    }
    setSecondValue(val);
    const trdOptions = secondOptions?.find((item) => item?.value === val)?.children;
    setThirdOptions(trdOptions);
    // 只有一个就是单选
    const thirdIsMutiple = trdOptions?.length > 1;
    setIsThridMultiple(thirdIsMutiple);
    if (thirdIsMutiple) {
      setThirdValue([trdOptions?.[0]?.value]);
    } else {
      setThirdValue(trdOptions?.[0]?.value);
    }
    // 多选
    if (isSecondMultiple) {
      const res = val.map((e) => `${firstValue}_${e}`);
      console.log(res);
      onChange(res);
    } else {
      console.log([`${firstValue}_${val}`]);
      onChange([`${firstValue}_${val}`]);
    }
};

到这里,我们的功能,差不多就实现了。

但是,就是说有没有那么一种可能,数据是需要渲染初始值的,对,有可能的。["301_301_3022222"], [201_20444]

获取一级标题的值

useEffect(() => {
    if (value?.length) {
      const firstLevelValue = parseInt(value?.[0]?.split("_")?.[0]);
      setFirstValue(firstLevelValue);
    }
}, [options, value]);

接下来需要判断二级标题还是三级标题

const [isTwoLevel, setIsTwoLevel] = useState<boolean>(false);

const isTheSecondaryTitle =
        value.map((e) => {
          const sec = e.split("_");
          return sec;
        }).length === 2;

      setIsTwoLevel(isTheSecondaryTitle);
 
//....
      
if (isTwoLevel) {
  //....
} else {
  //....
}

设置二级标题的 value

const secOptions = options?.find((item) => item?.value === firstLevelValue)?.children || [];
setSecondOptions(secOptions);
const optionsValues = secOptions.map((e) => e.value);
const secondLevelValues = value.map((e) => {
  const sec = e.split("_")?.[1];
  return sec;
});
const secondLevelValue = intersectionBy(optionsValues, secondLevelValues, parseInt);

同时还需要判断是否需要多选

if (isTwoLevel) {
  if (secondLevelValue.length > 1) {
    setIsSecondMultiple(true);
  }
  setSecondValue(secondLevelValue || []);
}

设置三级标题的 value

if (isTwoLevel) {
  if (secondLevelValue.length > 1) {
    setIsSecondMultiple(true);
  }
  setSecondValue(secondLevelValue || []);
} else {
  setSecondValue(secondLevelValue || []);
  const trdOptions =
    secOptions?.find((item) => item?.value === secondLevelValue[0])?.children || [];
  setThirdOptions(trdOptions);
  const _optionsValues = trdOptions.map((e) => e.value);
  const thirdLevelValues = value.map((e) => {
    const sec = e.split("_")?.[2];
    return sec;
  });
  const thirdLevelValue = intersectionBy(_optionsValues, thirdLevelValues, parseInt);
  setIsThridMultiple(true);
  setThirdValue(thirdLevelValue || undefined);
}

到此,我们的需要就实现完成了。