公司新开了一个管理项目,首先就是需要有一个灵活的平台,包含的最主要的功能:
能够任意放大和缩小核心区域,能够在行头和列头新增和删除行列,能够修改行列编号为任意值
能够拖动左侧和上部的物品摆放到图中的对应位置,并能够做出一定的限制,能够对已摆放的位置进行变更。
能够在左侧点击后高亮选中的数据,并且能够删除已经摆放的数据
能够对存在连续物品新增列进行判断,能够对有数据的物品行列进行判断,阻止其进行删除操作
以上就是这次的需求,下面讲讲编码过程中的想法。
首先创建一个组件
const IdcTableTest: React.FC = () => {
return (
<div>
这是一个测试组件
</div>
)
}
export default IdcTableTest
然后就是最重要的问题 数据结构
先不管其他功能怎样,我们首先需要创建一个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
两者的数据结构都是一样的,包含key
和value
,key
用来给循环出来的每一个数据加上唯一键,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>
)
可以看到页面上渲染出来了对应的内容,我们加上点样式更方便查看
//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;
}
可以看到中间的边框稍宽一点,优化一下样式代码,首先把所有的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;
}
然后最后一排加上右边的边框
.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) ;
}
}
最后给最底下一排加上底部边框
.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) ;
}
}
对比一下图,发现实际上这种排列组合并不是我们想要的,我们要的是
1A2A3A4A5A
1B2B3B4B5B
这种排列顺序,而现在图上的是
1A1B1C
2A2B2C
图里可以看到,实际上外层的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)
}
}
这样就是我们想要的了
接下来需要加上行头和列头
//tsx
...
useEffect(()=>{
if(_.findIndex(columnData,{type:'head'})===-1){
rowDataState.unshift({
key:'125512351235',
type:'head'
})
columnDataState.unshift({
key:'1235123613241324',
type:'head'
})
}
setRowDataState([...rowDataState]);
setColumnDataState([...columnDataState]);
},[])
...
很明显不是我们想要的效果,我们想要的是第一列显示表头名称,然后第一行显示列头名称
修改代码
//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>
差不多是那个意思了,接下来更改样式,第一行和第一列不需要那么宽,而且需要背景色
//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;
}
}
这样,就是一个理想状态下的壳子了,测试一下,在代码中增加一行和一列,看看表现如何
//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'
}
]
...
页面正常变化,说明没有问题。
接下来就是把图片和文字显示到特定位置上,首先把除了首行首列以外的文字内容屏蔽
然后就是数据设计,一个框框内,首先要个图片展示占位的是什么东西,然后下面是一个描述,描述产品的名称之类的,即起码包含两个字段,一个img
,一个desc
那么如何把数据塞到对应的位置上去展示呢?
首先想到的是通过行和列value
来进行判断,即例如上面未清除表格中文字的图里的3A,5B这类的。
但是思考一番后不行,因为value
的值不是唯一的,很有可能后期需要修改值,比如所有的行和列的value
值全都改为A,如下图,这样肯定是不能够进行判断的
最终还是决定根据行和列的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;
}
}
}
可以看到经过修改后的代码,现在能够正常显示图片和文字到页面上了
修改数据的location
为5-3
...
/** 列表数据 */
const listData:Array<dataModel> = [
{
location:'5-3',
image:test,
desc:'这是一段描述'
}
]
...
页面上显示的地方更改了,说明代码运行正常。
然后就是最重要的拖拽功能,
首先需要明白几个事件
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>
编码过程中需要注意一点就是,在给dragData
赋值的时候,一定不要直接赋值setDragData(data)
这样赋值的话,会造成listDataState
里面的值被污染,导致拖到其他地方以后,再拖回之前拖过的地方,数据会消失
解决办法两个,一个是用lodash
的深拷贝,也就是setDragData(_.cloneDepp(data))
另一个方法就是解构赋值setDragData({...data})
至此,需求的整体的底层功能就搭建完成了,后续的删除,图例的拖拽也不过是在这个基础上进行拓展罢了,如果有朋友感兴趣,我也可以再往下写写。
感谢阅读。
本文参与了SegmentFault 思否写作挑战赛活动,欢迎正在阅读的你也加入。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。