【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;

  

至此一個復雜的要死套來套去的自定義列組件就做好啦,嗚嗚嗚嗚


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM