前言:因為我是半途接手,之前的前端已經做了一部分,所以有些東西是二次修改,代碼冗余之類的請勿在意(功能實現就好),只是一個小總結,有空優化~
效果:
基本功能:左側定位欄(大類),中間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;
至此一個復雜的要死套來套去的自定義列組件就做好啦,嗚嗚嗚嗚