头图

公司新开了一个管理项目,首先就是需要有一个灵活的平台,包含的最主要的功能:
能够任意放大和缩小核心区域,能够在行头和列头新增和删除行列,能够修改行列编号为任意值

能够拖动左侧和上部的物品摆放到图中的对应位置,并能够做出一定的限制,能够对已摆放的位置进行变更。

能够在左侧点击后高亮选中的数据,并且能够删除已经摆放的数据

能够对存在连续物品新增列进行判断,能够对有数据的物品行列进行判断,阻止其进行删除操作

以上就是这次的需求,下面讲讲编码过程中的想法。


首先创建一个组件

const IdcTableTest: React.FC = () => {

    return (
        <div>
            这是一个测试组件
        </div>
    )
}

export default IdcTableTest

image.png

然后就是最重要的问题 数据结构

先不管其他功能怎样,我们首先需要创建一个X*Y的格子出来

一想到x*y个格子,首先想到的是利用table进行构建,但是想了想还是放弃了使用,主要是担心对某行某列的操作不够能够完全自控,比如在鼠标移动上去某个特定的地方以后,改变某行某列的颜色。 然鹅,在实践完以后,我发觉用table应该也没有什么问题,因为实际上原生的一个个去搭建,在需要批量改变某行某列的背景色的时候,也还是要一个个的去找对应节点,然后再将其变色,不过这都是后话了,下次有机会,利用table做一个也不是不行。

那么按照一开始的想法, 我们首先需要画一个x*y个格子出来,比如说,画一个3*4的格子呢?
既然是x*y那么就需要两个数组,双重循环,才能达到效果
这里又有两个方案,是先画行还是先画列呢,这里我采用的是先画列再画行的方案。

//tsx文件
/** 列数据 */
    const columnData=[
        {
            key:'1',
            value:1
        },
        {
            key:'2',
            value:2
        },
        {
            key:'3',
            value:3
        },
        {
            key:'4',
            value:4
        },
        {
            key:'5',
            value:5
        },
    ]

    /** 行数据 */
    const rowData=[
        {
            key:'11',
            value:'A'
        },
        {
            key:'22',
            value:'B'
        },
        {
            key:'33',
            value:'C'
        },
    ]

    const [rowDataState,setRowDataState] = useState(rowData);

    const [columnDataState,setColumnDataState] = useState(columnData);

如上面代码所示,我创建了行数据rowData和列数据columnData两者的数据结构都是一样的,包含keyvaluekey用来给循环出来的每一个数据加上唯一键,value就是改行/列的表头的值,现在我们把订好的数据渲染到页面上去

    /** 列数据 */
    const columnData=[
        {
            key:'1',
            value:1
        },
        {
            key:'2',
            value:2
        },
        {
            key:'3',
            value:3
        },
        {
            key:'4',
            value:4
        },
        {
            key:'5',
            value:5
        },
    ]

    /** 行数据 */
    const rowData=[
        {
            key:'11',
            value:'A'
        },
        {
            key:'22',
            value:'B'
        },
        {
            key:'33',
            value:'C'
        },
    ]

    const [rowDataState,setRowDataState] = useState(rowData);

    const [columnDataState,setColumnDataState] = useState(columnData);

    return (
        <div>
            {
                columnDataState.map((column,y)=>{
                    return(
                        <div key={column.key}>
                            {
                                rowDataState.map((row,y)=>{
                                    return(
                                        <span key={row.key}>
                                            {column.value+row.value}
                                        </span>
                                    )
                                })
                            }
                        </div>
                    )
                })
            }
        </div>
    )

image.png

可以看到页面上渲染出来了对应的内容,我们加上点样式更方便查看

//tsx
import styles from './xxx.module.scss'
...
return (
        <div>
            {
                columnDataState.map((column,y)=>{
                    return(
                        <div key={column.key} className={styles.columncontainer}>
                            {
                                rowDataState.map((row,y)=>{
                                    return(
                                        <span key={row.key} className={styles.rowitem}>
                                            {column.value+row.value}
                                        </span>
                                    )
                                })
                            }
                        </div>
                    )
                })
            }
        </div>
    )

//scss
.rowitem{
    border: 1px solid rgba(0, 0, 0, 0.1);
    display: inline-block;
    width: 100px;
    height: 100px;
    text-align: center;
}

image.png

可以看到中间的边框稍宽一点,优化一下样式代码,首先把所有的border只保留左上的边框

.rowitem{
    border: 1px solid rgba(0, 0, 0, 0.1);
    border-right: 0;
    border-bottom: 0;
    display: inline-block;
    width: 100px;
    height: 100px;
    text-align: center;
 
}

image.png

然后最后一排加上右边的边框

.rowitem{
    border: 1px solid rgba(0, 0, 0, 0.1);
    border-right: 0;
    border-bottom: 0;
    display: inline-block;
    width: 100px;
    height: 100px;
    text-align: center;
    &:last-child{
        border-right:1px solid rgba(0, 0, 0, 0.1) ;
    }
}

image.png

最后给最底下一排加上底部边框

.columncontainer{
    &:last-child{
        .rowitem{
            border-bottom: 1px solid rgba(0, 0, 0, 0.1) ;
        }
    }
}

.rowitem{
    border: 1px solid rgba(0, 0, 0, 0.1);
    border-right: 0;
    border-bottom: 0;
    display: inline-block;
    width: 100px;
    height: 100px;
    text-align: center;
    &:last-child{
        border-right:1px solid rgba(0, 0, 0, 0.1) ;
    }
}

image.png

对比一下图,发现实际上这种排列组合并不是我们想要的,我们要的是
1A2A3A4A5A
1B2B3B4B5B
这种排列顺序,而现在图上的是
1A1B1C
2A2B2C

image.png

图里可以看到,实际上外层的div不应该独占一行的,这导致整个排版不是我们想要的,应该改成行内元素,然后里面的每一项独占其一行才行。

对代码和样式进行修改

//tsx
 <div>
            {
                columnDataState.map((column,y)=>{
                    return(
                        <div key={column.key} className={styles.columncontainer}>
                            {
                                rowDataState.map((row,y)=>{
                                    return(
                                        <div key={row.key} className={styles.rowitem}>
                                            <span>
                                                {column.value+row.value}
                                            </span>
                                        </div>
                                    )
                                })
                            }
                        </div>
                    )
                })
            }
        </div>
//scss
.columncontainer{
    display: inline-block;
    &:last-child{
        .rowitem{
            border-right:1px solid rgba(0, 0, 0, 0.1)
        }
    }
}

.rowitem{
    border: 1px solid rgba(0, 0, 0, 0.1);
    border-right: 0;
    border-bottom: 0;
    width: 100px;
    height: 100px;
    display: flex;
    justify-content: center;
    align-items: center;
    &:last-child{
        border-bottom: 1px solid rgba(0, 0, 0, 0.1)
    }
}

image.png

这样就是我们想要的了

接下来需要加上行头和列头

//tsx
...
   useEffect(()=>{
        if(_.findIndex(columnData,{type:'head'})===-1){
            rowDataState.unshift({
                key:'125512351235',
                type:'head'
            })
            columnDataState.unshift({
                key:'1235123613241324',
                type:'head'
            })
        }
        setRowDataState([...rowDataState]);
        setColumnDataState([...columnDataState]);
    },[])
...

image.png

很明显不是我们想要的效果,我们想要的是第一列显示表头名称,然后第一行显示列头名称

修改代码

//tsx
 <div>
            {
                columnDataState.map((column,y)=>{
                    return(
                        <div key={column.key} className={styles.columncontainer}>
                            {
                                rowDataState.map((row,x)=>{
                                    return(
                                        <div key={row.key} className={styles.rowitem}>
                                            {
                                                y===0?
                                                (
                                                    <span>
                                                        {row.value}
                                                    </span>
                                                ):
                                                (
                                                    x===0?
                                                    (
                                                        <span>
                                                            {column.value}
                                                        </span>
                                                    ):
                                                    (
                                                        <span>
                                                            {column.value+row.value}
                                                        </span>
                                                    )
                                                    
                                                )
                                            }
                                        </div>
                                    )
                                })
                            }
                        </div>
                    )
                })
            }
        </div>

image.png

差不多是那个意思了,接下来更改样式,第一行和第一列不需要那么宽,而且需要背景色

//tsx
<div>
            {
                columnDataState.map((column,y)=>{
                    return(
                        <div key={column.key} className={styles.columncontainer}>
                            {
                                rowDataState.map((row,x)=>{
                                    return(
                                        <div key={row.key} 
                                            className={[
                                                styles.rowitem,
                                                y===0?styles.rowHead:'',
                                                x===0?styles.colHead:'',
                                                (x===0&&y===0)?styles.firstItem:'',
                                            ].join(' ')}
                                        
                                        >
                                            {
                                                y===0?
                                                (
                                                    <span>
                                                        {row.value}
                                                    </span>
                                                ):
                                                (
                                                    x===0?
                                                    (
                                                        <span>
                                                            {column.value}
                                                        </span>
                                                    ):
                                                    (
                                                        <span>
                                                            {column.value+row.value}
                                                        </span>
                                                    )
                                                    
                                                )
                                            }
                                        </div>
                                    )
                                })
                            }
                        </div>
                    )
                })
            }
        </div>

//scss
$headColor:rgb(244, 250, 254);
$borderColor:rgba(0, 0, 0, 0.1);
.columncontainer{
    display: inline-block;
    vertical-align: bottom;
    position: relative;
    .firstItem{
        &::after{
            content:'';
            position: absolute;
            right: 2px;
            bottom: 2px;
            border: 20px solid transparent;
            border-right: 20px solid rgba(0, 0, 0, 0.1);
            border-bottom: 20px solid rgba(0, 0, 0, 0.1);
        }
    }
    .rowHead,.colHead{
        background-color: $headColor;
    }
    .rowHead{
        width: 50px;
    }
    .colHead{
        height: 50px;
    }
    &:last-child{
        .rowitem{
            border-right:1px solid $borderColor;
        }
    }
}

.rowitem{
    position: relative;
    border: 1px solid $borderColor;
    border-right: 0;
    border-bottom: 0;
    width: 100px;
    height: 100px;
    display: flex;
    justify-content: center;
    align-items: center;
    &:last-child{
        border-bottom: 1px solid $borderColor;
    }
}

image.png

这样,就是一个理想状态下的壳子了,测试一下,在代码中增加一行和一列,看看表现如何

//tsx

    /** 列数据 */
    const columnData:Array<headModel>=[
        {
            key:'1',
            value:1
        },
        {
            key:'2',
            value:2
        },
        {
            key:'3',
            value:3
        },
        {
            key:'4',
            value:4
        },
        {
            key:'5',
            value:5
        },
        {
            key:'6',
            value:6
        }
    ]

    /** 行数据 */
    const rowData:Array<headModel>=[
        {
            key:'11',
            value:'A'
        },
        {
            key:'22',
            value:'B'
        },
        {
            key:'33',
            value:'C'
        },
        {
            key:'44',
            value:'D'
        }
    ]
...

image.png

页面正常变化,说明没有问题。

接下来就是把图片和文字显示到特定位置上,首先把除了首行首列以外的文字内容屏蔽

image.png

然后就是数据设计,一个框框内,首先要个图片展示占位的是什么东西,然后下面是一个描述,描述产品的名称之类的,即起码包含两个字段,一个img,一个desc

image.png

那么如何把数据塞到对应的位置上去展示呢?

首先想到的是通过行和列value来进行判断,即例如上面未清除表格中文字的图里的3A,5B这类的。
但是思考一番后不行,因为value的值不是唯一的,很有可能后期需要修改值,比如所有的行和列的value值全都改为A,如下图,这样肯定是不能够进行判断的

image.png

最终还是决定根据行和列的index值进行判断,即第几行第几列的数据,因为每个格子的位置是唯一的,不随value值变化。

还有个问题,图片是保存在本地还是直接数据传递过来呢?
分两种情况,如果图片内容不固定,那么就接口传递过来
如果图片内容固定,那就直接保存在本地,直接调用就好。

既然决定数据使用编号来定位,那么首先先创建一个模拟的数据出来,显示到页面上

//tsx
interface headModel{
    key:string,
    value?:any,
    type?:any
}

interface dataModel{
    location:string,
    image:any,
    desc:any
}

    /** 列数据 */
    const columnData:Array<headModel>=[
        {
            key:'1',
            value:1
        },
        {
            key:'2',
            value:2
        },
        {
            key:'3',
            value:3
        },
        {
            key:'4',
            value:4
        },
        {
            key:'5',
            value:5
        },
        {
            key:'6',
            value:6
        }
    ]

    /** 行数据 */
    const rowData:Array<headModel>=[
        {
            key:'11',
            value:'A'
        },
        {
            key:'22',
            value:'B'
        },
        {
            key:'33',
            value:'C'
        },
        {
            key:'44',
            value:'D'
        }
    ]

    /** 列表数据 */
    const listData:Array<dataModel> = [
        {
            location:'1-1',
            image:test,
            desc:'这是一段描述'
        }
    ]

    const ListDataOpt=()=>{
        const obj:any = {};
        columnDataState.forEach((column,y)=>{
            rowDataState.forEach((row,x)=>{
                obj[y+'-'+x]={
                    location:y+'-'+x
                }
            })
        })

        listData.forEach((item)=>{
            for(let key in obj){
                if(item.location===key){
                    obj[key]={
                        ...obj[key],
                        ...item
                    }
                }
            }
        })
        setListDataState({...obj})
        console.log('obj',obj)
    }

    const [rowDataState,setRowDataState] = useState(rowData);

    const [columnDataState,setColumnDataState] = useState(columnData);

    const [listDataState,setListDataState] = useState<{[propname: string]:dataModel}>({});

    useEffect(()=>{
        if(_.findIndex(columnData,{type:'head'})===-1){
            rowDataState.unshift({
                key:'125512351235',
                type:'head'
            })
            columnDataState.unshift({
                key:'1235123613241324',
                type:'head'
            })
        }
        setRowDataState([...rowDataState]);
        setColumnDataState([...columnDataState]);
    },[])

    useEffect(()=>{
        ListDataOpt()
    },[rowDataState,columnDataState])

    return (
        <div>
            {
                columnDataState.map((column,y)=>{
                    return(
                        <div key={column.key} className={styles.columncontainer}>
                            {
                                rowDataState.map((row,x)=>{
                                    return(
                                        <div key={row.key} 
                                            className={[
                                                styles.rowitem,
                                                y===0?styles.rowHead:'',
                                                x===0?styles.colHead:'',
                                                (x===0&&y===0)?styles.firstItem:'',
                                            ].join(' ')}
                                        >
                                            {
                                                y===0?
                                                (
                                                    <span>
                                                        {row.value}
                                                    </span>
                                                ):
                                                (
                                                    x===0?
                                                    (
                                                        <span>
                                                            {column.value}
                                                        </span>
                                                    ):
                                                    (
                                                        <div className={styles.itemcontainer}>
                                                            {/* {column.value+row.value} */}
                                                            {
                                                                //@ts-ignore
                                                                listDataState[y+'-'+x]?(
                                                                    <>
                                                                        <img src={listDataState[y+'-'+x].image}/>
                                                                        <span>{listDataState[y+'-'+x].desc}</span>
                                                                    </>
                                                                     
                                                                ):('')
                                                            }
                                                        </div>
                                                    )
                                                    
                                                )
                                            }
                                        </div>
                                    )
                                })
                            }
                        </div>
                    )
                })
            }
        </div>
    )
}

//scss
$headColor:rgb(244, 250, 254);
$borderColor:rgba(0, 0, 0, 0.1);
.columncontainer{
    display: inline-block;
    vertical-align: bottom;
    position: relative;
    .firstItem{
        &::after{
            content:'';
            position: absolute;
            right: 2px;
            bottom: 2px;
            border: 20px solid transparent;
            border-right: 20px solid rgba(0, 0, 0, 0.1);
            border-bottom: 20px solid rgba(0, 0, 0, 0.1);
        }
    }
    .rowHead,.colHead{
        background-color: $headColor;
    }
    .rowHead{
        width: 50px;
    }
    .colHead{
        height: 50px;
    }
    &:last-child{
        .rowitem{
            border-right:1px solid $borderColor;
        }
    }
}

.rowitem{
    position: relative;
    border: 1px solid $borderColor;
    border-right: 0;
    border-bottom: 0;
    width: 100px;
    height: 100px;
    display: flex;
    justify-content: center;
    align-items: center;
    &:last-child{
        border-bottom: 1px solid $borderColor;
    }
    .itemcontainer{
        display: flex;
        flex-direction: column;
        justify-content: center;
        align-items: center;
        img{
            width: 50px;
        }
    }
}

image.png

可以看到经过修改后的代码,现在能够正常显示图片和文字到页面上了

修改数据的location5-3

...
 /** 列表数据 */
    const listData:Array<dataModel> = [
        {
            location:'5-3',
            image:test,
            desc:'这是一段描述'
        }
    ]
...

image.png

页面上显示的地方更改了,说明代码运行正常。

然后就是最重要的拖拽功能,

首先需要明白几个事件

dragstart事件
dragstart类似于鼠标按下事件:当元素开始被拖拽的时候触发的事件
作用于:被拖拽的元素

dragenter事件
dragenter类似于鼠标进入元素的事件:当拖拽的元素进入目标元素的上方时,触发的事件
作用于:目标元素

dragover事件
dragover类似于鼠标在某元素上的事件:拖拽元素在目标元素上移动的时候触发的事件
作用于:目标元素

dragleave事件
dragleave类似于鼠标离开元素的事件:拖拽元素离开目标元素的时候触发事件
作用于:目标元素

drop事件
drop类似于鼠标松开的事件:被拖拽的元素在目标元素上,鼠标放开触发的事件
作用于:目标元素

dragend事件
类似于鼠标松开的事件:当拖拽完成后触发的事件
作用于:被拖拽的元素

同时,需要注意
首先需要把拖拽元素和目标元素的dragable属性设为true,不然无法拖拽
同时dragover事件需要加上e.preventDefault()不然鼠标样式会变成禁止的样式
代码如下

...
 <div>
            {
                columnDataState.map((column,y)=>{
                    return(
                        <div key={column.key} className={styles.columncontainer}>
                            {
                                rowDataState.map((row,x)=>{
                                    return(
                                        <div key={row.key} 
                                            className={[
                                                styles.rowitem,
                                                y===0?styles.rowHead:'',
                                                x===0?styles.colHead:'',
                                                (x===0&&y===0)?styles.firstItem:'',
                                            ].join(' ')}


                                            draggable
                                            onDragStart={(e)=>{
                                                console.log('ondragstart触发')
                                            }}
                                            onDragEnter={(e)=>{
                                                console.log('ondragenter触发')
                                            }}
                                            onDragOver={(e)=>{
                                                e.preventDefault();
                                                console.log('ondragover触发')
                                            }}
                                            onDragLeave={(e)=>{
                                                console.log('ondragleave触发')
                                            }}
                                            onDrop={(e)=>{
                                                console.log('ondrop触发')
                                            }}
                                            onDragEnd={(e)=>{
                                                console.log('ondragend触发')
                                            }}

                                        >
                                            {
                                                y===0?
                                                (
                                                    <span>
                                                        {row.value}
                                                    </span>
                                                ):
                                                (
                                                    x===0?
                                                    (
                                                        <span>
                                                            {column.value}
                                                        </span>
                                                    ):
                                                    (
                                                        <div 
                                                        className={styles.itemcontainer}
                                                     
                                                        >
                                                            {/* {column.value+row.value} */}
                                                            {
                                                                //@ts-ignore
                                                                listDataState[y+'-'+x]?(
                                                                    <>
                                                                        <img src={listDataState[y+'-'+x].image}   draggable="false"/>
                                                                        <span>{listDataState[y+'-'+x].desc}</span>
                                                                    </>
                                                                     
                                                                ):('')
                                                            }
                                                        </div>
                                                    )
                                                    
                                                )
                                            }
                                        </div>
                                    )
                                })
                            }
                        </div>
                    )
                })
            }
        </div>

可以看到事件正常触发了。

同时要注意到一点就是dragenter事件会先于dragleave事件触发

既然拖拽事件正常触发,那么接下来就是真正的要把东西拖过去了

代码就是透过现象看本质,我们的目的就是把一个数据从某个位置,换到另一个位置

代码层面的解释就是原本A位置有数据,现在通过拖拽操作,A位置没有数据了,变成B位置有数据。

即在dragstart的时候记录一下被拖拽的数据,然后在ondrop的时候把被拖拽的数据移动到触发ondrop事件的位置就Ok了。

进行编码,注意,我把dataModel的数据结构改了一层

interface dataModel{
    location?:string,
    data?:{
        image?:any,
        desc?:any
    }
    
}
//tsx
...
const [dragData,setDragData] = useState<dataModel>({})
...
/** 拖拽开始 */
const onDragStart=(data:dataModel)=>{
    setDragData({...data})
}

/** 拖拽结束 */
const onDrop=(data:dataModel)=>{
    let targetlocation:any = data.location;
    let orglocation:any = dragData.location;
    dragData.location = targetlocation;
    listDataState[targetlocation].data = _.cloneDeep(dragData.data);
    delete listDataState[orglocation].data
    setListDataState({...listDataState});
    setDragData(null!)
}
...
  <div>
            {
                columnDataState.map((column,y)=>{
                    return(
                        <div key={column.key} className={styles.columncontainer}>
                            {
                                rowDataState.map((row,x)=>{
                                    return(
                                        <div key={row.key} 
                                            className={[
                                                styles.rowitem,
                                                y===0?styles.rowHead:'',
                                                x===0?styles.colHead:'',
                                                (x===0&&y===0)?styles.firstItem:'',
                                            ].join(' ')}
                                            draggable
                                            onDragStart={(e)=>{
                                                onDragStart(listDataState[y+'-'+x]);
                                                console.log('ondragstart触发',listDataState[y+'-'+x])
                                            }}
                                            onDragEnter={(e)=>{
                                                console.log('ondragenter触发',listDataState[y+'-'+x])
                                            }}
                                            onDragOver={(e)=>{
                                                e.preventDefault();
                                                // console.log('ondragover触发')
                                            }}
                                            onDragLeave={(e)=>{
                                                console.log('ondragleave触发',listDataState[y+'-'+x])
                                            }}
                                            onDrop={(e)=>{
                                                onDrop(listDataState[y+'-'+x])
                                                console.log('ondrop触发',listDataState[y+'-'+x])
                                            }}
                                            onDragEnd={(e)=>{
                                                console.log('ondragend触发',listDataState[y+'-'+x])
                                            }}

                                        >
                                            {
                                                y===0?
                                                (
                                                    <span>
                                                        {row.value}
                                                    </span>
                                                ):
                                                (
                                                    x===0?
                                                    (
                                                        <span>
                                                            {column.value}
                                                        </span>
                                                    ):
                                                    (
                                                        <div 
                                                        className={styles.itemcontainer}
                                                     
                                                        >
                                                            {/* {column.value+row.value} */}
                                                            {
                                                                //@ts-ignore
                                                                listDataState[y+'-'+x]?(
                                                                    <>
                                                                        <img src={listDataState[y+'-'+x].data?.image}   draggable="false"/>
                                                                        <span>{listDataState[y+'-'+x].data?.desc}</span>
                                                                    </>
                                                                     
                                                                ):('')
                                                            }
                                                        </div>
                                                    )
                                                    
                                                )
                                            }
                                        </div>
                                    )
                                })
                            }
                        </div>
                    )
                })
            }
        </div>

7.gif

编码过程中需要注意一点就是,在给dragData赋值的时候,一定不要直接赋值
setDragData(data)这样赋值的话,会造成listDataState里面的值被污染,导致拖到其他地方以后,再拖回之前拖过的地方,数据会消失
解决办法两个,一个是用lodash的深拷贝,也就是setDragData(_.cloneDepp(data))另一个方法就是解构赋值setDragData({...data})

至此,需求的整体的底层功能就搭建完成了,后续的删除,图例的拖拽也不过是在这个基础上进行拓展罢了,如果有朋友感兴趣,我也可以再往下写写。

感谢阅读。

本文参与了SegmentFault 思否写作挑战赛活动,欢迎正在阅读的你也加入。

munergs
25 声望2 粉丝

现在即是最好。