基于material-ui组件封装的下拉树形选择组件

344 阅读4分钟

最终效果

屏幕录制 2024-08-19 181217.gif

为什么要使用material-ui组件?

因为公司最近想要产品升级,并且发现material-ui组件的美观程度很合适,因此选择了material-ui的前端组件库.

material-ui组件的评价

material-ui组件库的美观程度还是很好的,让人眼前一亮,但是对于组件库的封装就有点一言难尽了,对国内这种只使用轮子的cv模式来说 不是很友好,且因为是英文文档 中文社区也很少 对国内不是很友好.

下拉树形选择实现的功能

因为以前一直使用antd组件,使用对下拉树形组件的实现功能基本上是参考antd的树形下拉去实现的 因此支持

  • 1.支持自定义label value children字段
  • 2.支持下拉多选.单选

首选进行组件介绍

  • 1.Autocomplete自动完成组件:因为## material-ui组件的下拉选择不支持搜索功能 所有选择了自动完成组件
    • import { RichTreeView } from "@mui/x-tree-view/RichTreeView";
  • 2.RichTreeView 树形下拉组件:实现下拉组件的树形效果
    • import { Autocomplete, FormHelperText, TextField } from "@mui/material";

代码详情

import * as React from "react";
import FormControl from "@mui/material/FormControl";
import { RichTreeView } from "@mui/x-tree-view/RichTreeView";
import { Autocomplete, FormHelperText, TextField } from "@mui/material";
import { generateTreeList } from "@/utils/index";
import { isEmpty } from "lodash";
import { MyObject } from "@/utils/index";
interface MyProps {
  multiple?: boolean;
  items?: MyObject[];
  value?: string | string[];
  valueExpr?: string;
  displayExpr?: string;
  label?: string;
  width?: number;
  placeholder?: string;
  disabled?: boolean;
  onChange?: Function;
  childenExpr?: string;
  eventList?: MyObject;
  size?: "small" | "medium" | string;
  errors?: any;
  name?: string;
}
//树形下拉组件
const TreeSelect = (props: MyProps, ref: any) => {
  const {
    multiple = false,
    items = [],
    valueExpr = "id",
    displayExpr = "label",
    childenExpr = "children",
    width = undefined,
    disabled = false,
    onChange,
    label,
    eventList = {},
    size,
    name,
    errors,
    placeholder = "请选择",
  } = props;

  const value = props.value ?? (multiple ? [] : undefined);
  const treevalue = multiple ? [] : null;
  const [open, setOpen] = React.useState(false);
  const [treeList, setTreeList] = React.useState<MyObject[]>([]);
  const [treeValue, setTreeValue] = React.useState<any>(treevalue); //存储回显的字段
  const [valueField, setValueField] = React.useState(value); //存储当初的value值 用于回显
  const [searchText, setSearchText] = React.useState(""); //搜索查询
  //   默认让组件全部展开(返回他们的全部id)
  const getExpandedItems = (items: any) => {
    let dataList: any[] = [];
    const expandeList = (option: any) => {
      dataList.push(option.id);
      if (option[childenExpr] && option[childenExpr].length) {
        option[childenExpr].forEach((item: any) => {
          expandeList(item);
        });
      }
    };
    expandeList(items);
    return dataList;
  };
  //   遍历查找点击的当前组件的对象
  const getTreeData = (list: any, id: string) => {
    let data: any = null;
    const expandeList = (Option: any) => {
      if (Option[valueExpr] == id) {
        data = Option;
        return;
      } else if (Option[childenExpr] && Option[childenExpr].length) {
        Option[childenExpr].forEach((item: any) => {
          expandeList(item);
        });
      }
    };
    list.forEach((listitem: any) => {
      expandeList(listitem);
    });
    return data;
  };
  //在选择时不会触发自动完成的下拉事件 因此这里是树形组件的选择事件
  const handleItemSelectionToggle = (event: any, itemId: any) => {
    // console.log("handleItemSelectionToggle", event, itemId);
    if (multiple) {
      let data: any = getTreeData(displayedOptions, itemId[0]);
      setValueField(itemId);
      treeValue.push(data);
      setTreeValue([...treeValue]);
      if (onChange) {
        onChange(itemId);
      }
    } else {
      let data = getTreeData(displayedOptions, itemId);
      setValueField(itemId);
      setTreeValue(data);
      if (onChange) {
        onChange(itemId);
      }
      setOpen(false);
    }
  };
  // 用于返回当前数据 主要是用于配合下拉搜索功能
  const containsText = (text: any, searchText: string) => {
    let flag: Boolean = false;
    const judgment = (option: any) => {
      let data = option[displayExpr]
        .toLowerCase()
        .indexOf(searchText.toLowerCase());
      if (data > -1) {
        flag = true;
        return;
      } else if (option[childenExpr] && option[childenExpr].length) {
        // 进行遍历孩子组件的每一个数据
        option[childenExpr].forEach((item: any) => {
          judgment(item);
        });
      } else {
        return;
      }
    };
    if (!searchText) {
      flag = true;
    } else {
      judgment(text);
    }
    return flag;
  };
  // 因为需要进行下拉搜索过滤 所有树形数据为动态获取
  const displayedOptions = React.useMemo(() => {
    if (!isEmpty(items)) {
      return (
        treeList.filter((option) => containsText(option, searchText)) || []
      );
    } else {
      return [];
    }
  }, [searchText, treeList]);
  // 自动完成的搜索事件
  const handleInputValue = (event: any) => {
    setSearchText(event.target.value);
  };
  // 自动完成的改变事件(注意 选择下拉时不会触发这个事件 只有在使用自动完成组件自带的 清除时才会触发 所有才需要写)
  const handleChange = (event: any, data: any) => {
    const {
      target: { value },
    } = event;
    // console.log("111333", value, data);
    let newData;
    if (multiple) {
      newData = data.map((item: any) => {
        return item[valueExpr];
      });
      setValueField(newData ?? []);
    } else {
      newData = value;
    }
    setValueField(newData);
    setTreeValue(data);
    setSearchText("");
    setOpen(false);
    if (onChange) {
      onChange(newData);
    }
  };
  React.useEffect(() => {

    if (!isEmpty(items)) {
      // 将树形的children进行绑定
      let TreeListData = generateTreeList(items, childenExpr);
      setTreeList(TreeListData);
      if (multiple) {
        //初始化 如果是多选 初始值必须是数组 如果是单选 初始值必须是null或对象
        let values: string[] = props.value as string[];
        if (typeof props.value == "string" && props.value) {
          values = props.value.split(",");
        } else if (!props.value) {
          values = [];
        }
        let data: any = [];
        values.map((item: string) => {
          let list = getTreeData(displayedOptions, item);
          list && data.push(list);
        });
        // console.log("data", data, values);
        setTreeValue(data ?? []);
        setValueField(values);
      } else {
        let data = getTreeData(displayedOptions, props.value as string);
        setValueField(props.value);
        setTreeValue(data ?? null);
      }
    }
  }, [items,multiple]);
  return (
    <FormControl fullWidth sx={{ width: width }} size="small">
      <Autocomplete
        id="grouped-demo"
        getOptionLabel={(option: any) => option[displayExpr]} //显示label字段
        options={displayedOptions} //接收数据 树形下拉类型数据
        value={multiple ? treeValue?? []:treeValue} //配合getOptionLabel 就得接收一个对象 并且空值时只能为null 多选必须是数组类型
        open={open} //控制下拉弹出打开关闭
        size={size} //控制组件的大小
        disabled={disabled}
        placeholder={placeholder}
        multiple={multiple} //多选
        {...eventList} //其他额外配置
        renderInput={(params) => (
          <TextField
            {...params}
            label={label}
            onChange={handleInputValue}
            error={name ? !!props?.errors[name] : false}
          />
        )}
        onChange={(e, data: any) => {
          handleChange(e, data);
        }}
        onFocus={() => {
          setOpen(true);
        }}
        onBlur={() => {
          setSearchText('')
          setOpen(false);
        }}
        filterOptions={(options) => options}
        renderOption={(
          props,
          option //自定义渲染内容
        ) => (
          <li key={option.id ?? option[valueExpr]}>
            <RichTreeView //树形下拉
              checkboxSelection={multiple} //下拉多选带前置复选框
              multiSelect={multiple} //多选带着个才能多选
              selectedItems={valueField ?? (multiple ? [] : null)} //用于点开下拉时回显已选中的选项
              onSelectedItemsChange={handleItemSelectionToggle} //当点击下拉选项时触发的事件 相对于onChange事件
              expandedItems={getExpandedItems(option)} //设置它可以让下拉数据默认全部展开
              items={[option]} //对每一项就行绑定
            />
          </li>
        )}
      />
      {name && props?.errors[name] && (
        <FormHelperText error>{props?.errors[name].message}</FormHelperText>
      )}
    </FormControl>
  );
};
export default React.forwardRef(TreeSelect);