zoukankan      html  css  js  c++  java
  • 【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={{  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;
    

      

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

  • 相关阅读:
    PAT甲题题解-1030. Travel Plan (30)-最短路+输出路径
    PAT甲题题解-1029. Median (25)-求两序列的中位数,题目更新了之后不水了
    PAT甲题题解-1028. List Sorting (25)-水排序
    BZOJ 1492 货币兑换Cash
    Codeforces 276D Little Girl and Maximum XOR
    Codeforces 526E Transmitting Levels
    Codeforces 335B Palindrome
    BZOJ 2527 Meteors
    Codeforces 449D Jzzhu and Numbers
    FJ省队集训DAY4 T3
  • 原文地址:https://www.cnblogs.com/nangras/p/14780167.html
Copyright © 2011-2022 走看看