【React+antd】做一个自定义列的组件

前言:因为我是半途接手,之前的前端已经做了一部分,所以有些东西是二次修改,代码冗余之类的请勿在意(功能实现就好),只是一个小总结,有空优化~

效果:

 

 基本功能:左侧定位栏(大类),中间checkbox.group用来选择,右侧展示已选择的数据&&排序&删除功能,上面搜索列

 

首先我们需要的数据先定义一下:

 

  state: ProState = {
    active: null, // 用来设置初始化首位
    indicatorsListRef: {}, // 用于定位
    checkIndicatorsList: [], //用来存储传入格式的数组
    defaultIndicatorsList: [], // 用来存储右边选中数组
    checkArr: this.props.checkArr || [], // 传入的全部数据,大类里包着对应数据的格式
    navList: this.props.navList || [], // 标题数组
    itemOrder: this.props.itemOrder || [] // 传入-右边选中数组
  };

  

传入的数据格式是这样的:

 

 所以我们为了方便右边已选列表展示,定义一个方法来获取勾选结果:

  /**
   * @description  获取勾选结果
   * @param {array} data  勾选的数据
   */
  getDefaultList = (data: any) => {
    const resultArr: any[] = [];
    data && data.forEach((check: any) => {
      if (check.defaultCheckedList) {
        resultArr.push(check.defaultCheckedList);
      }
    });
    const list: any[] = resultArr && resultArr.length > 0&& resultArr.reduce((pre: any, next: any) => {
      return pre.concat(next);
    });
    return list;
  };

  

格式是这样的:

 

jsx是这样滴:

  renderContent = () => {
    const {
      active,
      checkIndicatorsList,
      indicatorsListRef,
      defaultIndicatorsList,
      navList,
    } = this.state;

    return (
      <>
        <div style={{ display: 'flex' }}>
          <div className={styles['m-add-indicators']}>
            <TheListTitle title="可添加的指标">
              <Search
                placeholder="请输入列名称搜索"
                // onChange={this.searchColumnChange}
                onSearch={this.searchColumn}
                style={{ width: 300 }}
              />
            </TheListTitle>
            <div style={{ display: 'flex' }}>
              <div className={styles['m-add-indicators-nav']}>
                {navList.map((item: any) => {
                  return (
                    <div className={styles['m-add-indicators-nav-item']} key={item.id}>
                      <div className={styles['m-add-indicators-nav-title']}> {item.value}</div>
                      {item.subList &&
                        item.subList.map((el: any) => {
                          return (
                            <div
                              className={`${styles['m-add-indicators-nav-subtitle']} ${
                                active && active.id === el.id
                                  ? styles['m-add-indicators-nav-subtitle-active']
                                  : ''
                              }`}
                              key={el.id}
                              onClick={() => this.handleAnchor(el)}
                            >
                              {el.label}
                            </div>
                          );
                        })}
                    </div>
                  );
                })}
              </div>
              <div
                className={styles['m-select-content']}
                ref={(el: any) => {
                  this.checkboxRef = el;
                }}
                style={{position:'relative'}}
              >
                {checkIndicatorsList.map((item: any) => {
                  return (
                    <div
                      ref={(el: any) => {
                        indicatorsListRef[item.id] = el;
                      }}
                      key={item.id}
                    >
                      <div className={`${styles['m-select-item']}${item.list.filter((item:any)=>item.hidden === false).length > 0 ? ` m-select-item-show` : ` m-select-item-hidden`}`}>
                        <BasisCheckbox
                          allName={item.label}
                          plainOptions={item.list}
                          defaultCheckedList={item.defaultCheckedList || []}
                          getGroupCheckedResult={(values) => {
                            this.getGroupCheckedResult(values, item);
                          }}
                        />
                      </div>
                    </div>
                  );
                })}
              </div>
            </div>
          </div>
          <DragResultList list={defaultIndicatorsList} deleteItem={this.deleteItem} />
          
        </div>
      </>
    );
  };

  

定义一个初始化列表的方法,以及处理数据更新时触发数据重新渲染:

 

  /**
   * @description 页面初始化加载
   */
  componentDidMount() {
    this.initList();
  }
    /**
   * @description 页面props更新
   */
  componentWillReceiveProps(nextProps:any) {
    // 如果不是第一次数据改变,不触发重初始化
    const { checkArr }  =this.state;
    if(JSON.stringify(nextProps.checkArr) === JSON.stringify(checkArr)) return;
    this.setState({
      checkArr: nextProps.checkArr,
      navList: nextProps.navList,
      itemOrder: nextProps.itemOrder
    },() => {
      this.initList();
    });
   }
  initList = () =>{
    const {navList , checkArr, itemOrder } = this.state;
    if (navList && navList.length > 0 && navList[0].subList) {
      // 设置初始化首位
      this.setState({
        active: navList[0].subList[0],
      });
    }
    // 默认选中项列表
    checkArr && checkArr.length > 0 && checkArr.forEach((item: any) => {
      item.defaultCheckedList = item.list && item.list.filter((el: any) => el.state === 1);
      item.list && item.list.forEach((el: any) => { el.hidden = false });
    });
    let initItemOrder:any = [];
    if(itemOrder && itemOrder.length && itemOrder.length > 0){
      initItemOrder = itemOrder && itemOrder.map((el: any) => { 
        return {...el,hidden: false}
      })
    }else {
      initItemOrder = this.getDefaultList(checkArr) && this.getDefaultList(checkArr).map((el: any) => { 
        return {...el,hidden: false}
      })
    }
    
    this.setState({
      checkIndicatorsList: checkArr,
      defaultIndicatorsList: initItemOrder || []
    });
  }

  

关于中间那块的数据改变,页面会返回对应的values(之前的大佬写的),这个values包括了两个数组,defaultList和checkedList,但是它defaultList就是当前这个group的对象数组,checkedList就是id的数组,如果这个group没有选中东西,它就是空的,对外层的使用略不友好,尽管如此因为实在没时间去重构了所以我直接用了QAQ,外层定义一个checkbox.group改变时的callback的方法:

 

  /**
   * @description  checkbox 结果
   * @param {any} values
   *  @param {any} item  项
   */
  getGroupCheckedResult = (values: any, item: any) => {
    const { checkIndicatorsList,  defaultIndicatorsList } = this.state;
    checkIndicatorsList.forEach((el: any) => {
      if (el.id === item.id) {
        el.defaultCheckedList = values.defaultList;
      }
    });
    // 新的数组>旧的数组 => add
    if(this.getDefaultList(checkIndicatorsList).length >  defaultIndicatorsList.length){
      const resIDs = defaultIndicatorsList.map(item => item.id)
      // 新增的
      const diff = this.getDefaultList(checkIndicatorsList).filter(item => !resIDs.includes(item.id))
      this.setState({
        defaultIndicatorsList: defaultIndicatorsList.concat([...diff]),
      });
    }else{
      const resIDs = this.getDefaultList(checkIndicatorsList).map(item => item.id)
      // 删掉的
      const diff =  defaultIndicatorsList.filter(item => !resIDs.includes(item.id))
      const diffIDs = diff.map(item => item.id)
      const newArr  =  defaultIndicatorsList.filter(item => !diffIDs.includes(item.id))
      this.setState({
        defaultIndicatorsList: newArr,
      });
    }
    
  };

  

也就是其实实际右侧的展示其实是直接通过返回的values.defaultList跟源数据进行了处理,然后通过getDefaultList获取到了目前整个中间选中的项。

因为每次操作都只是增加或者删除单独的一项,我们就得到了onChange之后的值并且进行了处理和展示,所以我的逻辑很简单:

如果是增加,那么新旧数组之差集就是新增的,通过filter筛选出来新数组返回来的这条新增的数据(因为是条对象),concat接到旧数组后面,这样就可以按照添加的顺序依次展示在右侧了~

如果是删除,那么新旧数组之差集就是删除的,把这一项的id获取到,filter出旧数组id不等于这个id的其他项保存,就正常删除了~如果是普通数组就更方便了,splice(id,1)舒服的要死 TAT

 

实现了中间对已选栏的增删后,就是已选栏自己的删除和排序了,在大佬封装的DragResultList这个组件里,删除返回的是当前item对象,清空返回的是-1,所以外层删除的方法就是这样:

 

  /**
   * @description  删除项
   * @param {object | number } item  项 -1全部
   */
  deleteItem = (item: any) => {
    const { checkIndicatorsList, defaultIndicatorsList } = this.state;
    // -1 === 清空
    if (item !== -1) {
      checkIndicatorsList.forEach((check: any) => {
        if (check.defaultCheckedList) {
          check.defaultCheckedList = check.defaultCheckedList.filter((el: any) => el.id !== item.id);
        }
      });
      this.setState({
        defaultIndicatorsList: defaultIndicatorsList && defaultIndicatorsList.filter((element:any)=> element.id !== item.id ), 
      });
    } else {
      checkIndicatorsList.forEach((check: any) => {
        check.defaultCheckedList = [];
      });
      this.setState({
        defaultIndicatorsList: [], 
      });
    }
  };

  

哦还有搜索列功能~:

  /**
   * 搜索列名称
   * @param {string} value  - 搜索的值
   */
  searchColumn = (value: string) => {
    this.setState({
      searchColumnValue: value
    })
    const { checkIndicatorsList } = this.state;
      const newCheckArr = JSON.parse(JSON.stringify(checkIndicatorsList))
      // 默认选中项列表
      newCheckArr && newCheckArr.length > 0 && newCheckArr.forEach((item: any) => {
        item.defaultCheckedList = item.list && item.list.filter((el: any) => el.state === 1);
        item.list && item.list.forEach((el: any) => {
          if(el.value.indexOf(value) !== -1){
            el.hidden = false
          }else{
            el.hidden = true
          }
        });
      });
      this.setState({
        checkIndicatorsList: newCheckArr,
      });
  };

  

这里就是我们为什么初始化的时候全部统一给数据加上hidden为false的原因了,因为这个hidden属性是用来控制是否展示的!(夸一下机智的我)

看看效果:

 

 然后就是提交数据了,这个没有什么好说的,给父组件一个是全部数据,一个是排序数据,父组件那边可以随便选用:

 

  /**
   * @description 确定
   */
  onSubmit = async () => {
    const { channelId } = this.props;
    if (!channelId) return;
    const { checkIndicatorsList, defaultIndicatorsList } = this.state;
    // 处理当前选中的指标项数据,提交
    const newList = checkIndicatorsList.map((item:any) => {
      let newObj = { id: item.id, label: item.label, list: [] as any };
      item.list && item.list.forEach((listItem:any) => {
        const newItem = {
          id: listItem.id,
          value: listItem.value,
          state: 0,
          isOrder: listItem.isOrder,
        };
        defaultIndicatorsList && defaultIndicatorsList.forEach((obj:any) => {
          if (newItem.id == obj.id) {
            newItem.state = 1;
          }
        })
        newObj.list.push(newItem)
      })
      return newObj
    });
    if(this.props.onSubmitCallback) this.props.onSubmitCallback(newList, defaultIndicatorsList)
  };

  

至于右侧的拖拽列表(DragResultList)组件不是我写的,也附上代码一起学习叭:

// 第三方库
import React, { useEffect, useState } from 'react';
import { Space } from 'antd';
import {
  UnorderedListOutlined,
  LockOutlined,
  DeleteOutlined,
  VerticalAlignTopOutlined,
} from '@ant-design/icons';

// 组件
import { BasisEmpty } from '@/components/index';
import { TheCardDragList, TheListTitle } from '@/modules';

// 类型声明
import { Props } from './index.type';

// 样式
import styles from './style.less';

// 交换数组索引位置位置
function swapPositions(arr: any[], preIndex: number, nextIndex: number) {
  arr[preIndex] = arr.splice(nextIndex, 1, arr[preIndex])[0];
  return arr;
}

// 拖拽列表
const DragResultList: React.FC<Props> = (props) => {
  const { list, deleteItem } = props;
  // 结果列表
  const [resultList, setResultList] = useState<any[]>([]);
  // 置顶Id
  const [unTopId, setUnTopId] = useState<number>(-1);

  /**
   * 设置为置顶
   * @param {array} data -数据
   */
  const setTopId = (data: any[]) => {
    if(data.length !== 0){
      // 非固定数组
      const unfixArr: any[] = data.filter((el: any) => !el.disabled);
      if (unfixArr && unfixArr.length > 0) {
        setUnTopId(unfixArr[0].id);
      }
    }else{
      setUnTopId(-1);
    }
  };

  useEffect(() => {
    // if (list && list.length > 0) {
    setResultList(list);
    setTopId(list);
    // }
  }, [list]);

  /**
   * 删除
   * @param {object} data -删除的项
   */
  const handleDelete = (data: any) => {
    deleteItem && deleteItem(data);
  };

  /**
   * 向上置顶
   * @param {object} data -置顶的项
   */
  const handlePlaceTop = (data: any) => {
    // 需要置顶的项
    const preIndex: number = resultList.findIndex((el: any) => el.id === data.id);
    // 当前置顶
    const nowIndex: number = resultList.findIndex((el: any) => el.id === unTopId);

    if (nowIndex !== -1) {
      const arr: any = swapPositions(resultList, preIndex, nowIndex);
      setResultList(arr);
      const unfixArr: any[] = arr.filter((el: any) => !el.disabled);
      setUnTopId(unfixArr[0].id);
    }
  };

  /**
   *  获取拖拽结果
   * @param {array} data -拖拽数据
   */
  const getDrageList = (data: any[]) => {
    setResultList(data);
    setTopId(data);
  };

  /**
   * 构建卡片
   * @param {object} item -卡片项
   */
  const renderCardItem = (item: any) => {
    return (
      <div className={styles['m-card-item']}>
        {!item.disabled ? <UnorderedListOutlined style={{ cursor: 'move' }} /> : <LockOutlined />}
        <div className={`${styles['m-card-item-title']} ${!item.disabled ? 'cursor_move' : ''}`}>
          {item.value}
        </div>
        {!item.disabled && (
          <Space>
            {unTopId !== item.id && (
              <VerticalAlignTopOutlined
                style={{ cursor: 'pointer' }}
                onClick={() => handlePlaceTop(item)}
              />
            )}
            <DeleteOutlined style={{ cursor: 'pointer' }} onClick={() => handleDelete(item)} />
          </Space>
        )}
      </div>
    );
  };

  // 构建已选结果
  const renderList = () => {
    return resultList && resultList.length > 0 ? (
      <TheCardDragList
        className={styles['m-drag-list']}
        list={resultList}
        renderCardItem={renderCardItem}
        getDrageList={getDrageList}
      />
    ) : (
      <BasisEmpty />
    );
  };
  return (
    <div className={styles['m-drag-result']}>
      <TheListTitle list={resultList} clearItems={handleDelete} />
      {renderList()}
    </div>
  );
};

export default DragResultList;

  

而左边的定位功能其实体验不太好,因为没有滚动效果,只是直接定位了中间的位置,但也记录一下大佬的代码共同学习:

  /**
   * @description  定位
   * @param {object} item  项
   */
  handleAnchor = (item: any) => {
    this.setState({
      active: item,
    });
    const { indicatorsListRef } = this.state;
    const keysList: any[] = Object.keys(indicatorsListRef);
    if (indicatorsListRef[item.id] && keysList.length > 0) {
      this.checkboxRef.scrollTop =
        indicatorsListRef[item.id].offsetTop - indicatorsListRef[keysList[0]].offsetTop;
    }
  };

  
关于这个我觉得也许可以考虑用antd的锚点组件,但是实在没空了QAQ回头有时间试一下

补充:

经过师傅的提醒,在中间的包裹层加scroll-behavior: smooth;这个样式,即可有滚动效果,滚动中间高亮左边可用 IntersectionObserver。

因为我是点击左边定位,没有滚动包裹层高亮的需求,因此不做赘述。

e.g.

学习文档:https://developer.mozilla.org/zh-CN/docs/Web/API/IntersectionObserver

阮一峰:http://www.ruanyifeng.com/blog/2016/11/intersectionobserver_api.html

 

顺便大佬的BaseCheckbox的代码也贴一下,基本没改什么:

// 第三方库
import _ from 'lodash';
import { Checkbox, Tooltip } from 'antd';
import React, { useState, useEffect } from 'react';

// 类型声明
import { Props, CheckProps } from './index.type';

// 样式
import styles from './style.less';

const { Group } = Checkbox;

const BaseCheckbox: React.FC<Props> = (props) => {
  // 父级数据
  const {
    plainOptions,
    defaultCheckedList,
    showAll,
    showTip,
    limit,
    disabled,
    allName,
    showDefault,
    defaultSystem,
    getGroupCheckedResult,
  } = props;
  
  // 默认的id
  const formattedCheckedList = defaultCheckedList && defaultCheckedList.map((item) => item.id);

  // disabled 的 id
  const disabledCheckedList =
    plainOptions && plainOptions.filter((item: any) => item.disabled).map((item) => item.id);

  // 初始化数据
  const [state, setState] = useState<CheckProps>({
    plainOptions: [],
    checkedList: [],
    indeterminate: false,
    checkAll: false,
  });

  // 全选
  const [allValuesChecked, setAllValuesChecked] = useState([]);

  //默认
  const [checkDefault, setDefaultCheck] = useState<any>(true);

  /**
   * @description 依赖默认项 与初始化数据
   */
  useEffect(() => {
    if (plainOptions) {
      const formattedValues = plainOptions.map((item) => {
        return { label: item.value, value: item.id, ...item };
      });
      // @ts-ignore
      setAllValuesChecked(() => {
        return plainOptions.map((item) => {
          return item.id;
        });
      });

      setState({
        plainOptions: formattedValues,
        checkedList: formattedCheckedList,
        indeterminate:
          !!formattedCheckedList.length && formattedCheckedList.length < plainOptions.length,
        checkAll: formattedCheckedList.length === plainOptions.length,
      });
    }
  }, [defaultCheckedList, plainOptions]);

  // 监听点击事件 超过个数禁止点击
  useEffect(() => {
    if (!limit) return;

    // 如果超过个数 未选中的 disabled true
    if (state.checkedList && state.checkedList.length === limit) {
      _.forEach(plainOptions, (o: any) => {
        if (_.includes(state.checkedList, o.id)) {
          o.disabled = false;
        } else {
          o.disabled = true;
        }
      });
    } else {
      _.forEach(plainOptions, (o: any) => {
        o.disabled = false;
      });
    }
  }, [state.checkedList, plainOptions]);

  /**
   * 改变选择项
   * @param {array} checkedList - 已选的项
   * @return 已选项为选择的项
   */
  const onChange = (checkedList: any[]) => {
    // console.log(checkedList,'--checkedList-', plainOptions)
    // 传递给父级
    const defaultList = plainOptions
      .map((item: any) => {
        if (checkedList.includes(item.id)) {
          return item;
        }
        return null;
      })
      .filter((item) => item != null);

    const checkInfo = {
      ...state,
      checkedList,
      indeterminate: !!checkedList.length && checkedList.length < plainOptions.length,
      checkAll: checkedList.length === plainOptions.length,
    };

    setState(checkInfo);
    if (getGroupCheckedResult) {
      getGroupCheckedResult({
        defaultList,
        checkedList,
      });
    }
  };

  /**
   * @description 依赖全选 与所选项
   */
  useEffect(() => {
    if (showDefault) {
      setDefaultCheck(_.isEqual(state.checkedList, defaultSystem));
    }
  }, [state]);

  /**
   * @description 默认数据
   * @return 已选项为默认数据
   */
  const onCheckDefaultChange = () => {
    if (!defaultSystem) return;

    // props.changeDefautUse();
    return setState({
      ...state,
      indeterminate: !!defaultSystem.length && defaultSystem.length < plainOptions.length,
      checkAll: defaultSystem.length === plainOptions.length,
      checkedList: defaultSystem,
    });
  };

  /**
   * 全选
   * @param {object} event - 原生项
   * @return 已选项为全部选项
   */
  const onCheckAllChange = (e: any) => {
    // 控制全选的选项项
    const checkedArr: any[] = e.target.checked
      ? allValuesChecked
      : _.intersection(allValuesChecked, disabledCheckedList);
    // 传递给父级
    const defaultList = plainOptions
      .map((item: any) => {
        if (checkedArr.includes(item.id)) {
          return item;
        }
        return null;
      })
      .filter((item) => item != null);
    // 传递给父级
    if (getGroupCheckedResult) {
      getGroupCheckedResult({
        defaultList,
        checkedList: checkedArr,
        checkAll: true,
      });
    }
    setState({
      ...state,
      checkedList: checkedArr,
      indeterminate: false,
      checkAll: e.target.checked,
    });
  };

  // 全选与默认
  const handleCheckbox = () => {
    const unHideList:any[] = plainOptions && plainOptions.filter((item:any)=>item.hidden === false)
    const hiddenGroup = unHideList && unHideList.length > 0 ? false : true;
    return (
      !hiddenGroup &&
      <div>
        {showAll && (
          <Checkbox
            indeterminate={state.indeterminate}
            onChange={onCheckAllChange}
            checked={state.checkAll}
          >
            {allName || (state.checkAll ? '取消全选' : '全选')}
          </Checkbox>
        )}
        {showDefault && (
          <Checkbox onChange={onCheckDefaultChange} checked={checkDefault}>
            系统默认
          </Checkbox>
        )}
      </div>
    );
  };

  // checkbox组
  const renderGroup = () => {
    const unHideList:any[] = plainOptions && plainOptions.filter((item:any)=>item.hidden === false)
    const hiddenGroup = unHideList && unHideList.length > 0 ? false : true;
    return (
      <>
        {/* 带有提示Tooltip */}
        {showTip ? (
          !hiddenGroup &&
          <Group disabled={disabled} value={state.checkedList} onChange={onChange}>
            {plainOptions.map((o) => (
              <Tooltip title={o.nouns} key={o.id} >
                <Checkbox disabled={o.disabled} key={o.id} value={o.id} style={{display: o.hidden ? 'none' : 'inline-block'}}>
                  {o.value}
                </Checkbox>
              </Tooltip>
            ))}
          </Group>
        ) : (
          !hiddenGroup &&
          <Group disabled={disabled} value={state.checkedList} onChange={onChange}>
            {plainOptions.map((o) => (
              <Checkbox disabled={o.disabled} key={o.id} value={o.id} style={{display: o.hidden ? 'none' : 'inline-block'}}>
                {o.value}
                {o.lock}
              </Checkbox>
            ))}
          </Group>
        )}
      </>
    );
  };
  return (
    <div className={styles['c-base-checkbox']}>
      {state.plainOptions.length > 0 && (
        <>
          {showAll && handleCheckbox()}
          {renderGroup()}
        </>
      )}
    </div>
  );
};

// 默认
BaseCheckbox.defaultProps = {
  showAll: true,
  showTip: false,
  defaultCheckedList: [],
};

export default BaseCheckbox;

  

至此一个复杂的要死套来套去的自定义列组件就做好啦,呜呜呜呜

posted @ 2021-05-18 11:15  芝麻小仙女  阅读(1485)  评论(0编辑  收藏  举报