【论术】基于t-table的表格拖拽
在老之将至的这几年,监听员千万遍问自己:这就是我的一生吗?他又千万次回答:是的,这就是你的一生。 - 刘慈欣《三体》
项目需要:表格中须支持拖拽功能,如图:
此功能参照了fastgpt
的知识库文件对象管理,以公司主流的t-design为基础进行开发,t-table的api仅提供了dragSort
API,即拖拽排序,无法分别切割拖拽功能与排序功能。前后思索了下成本后决定自己手写一个。
PS: 下方为t-table的拖拽排序:
准备工作
为了完善主功能,因而也给自己提了几点需求:
1.鼠标经过表格行时展示为移动图标;
2.移动时目录须展示不同底色
3.仅允许文件向目录移动,不允许目录向目录或者文件向文件或者目录向文件移动。
4.按住移动时手标为移动图标(另一种)
看了t-table的dom结构后发现表格的表格行的父单位是t-table__body
,每一个tr
即为一行表格:
我们的需求是拖动行元素,因而可以获取到所有的tr,为了防止未来此页面可能有多个表格元素,可以在t-table中给每个行元素添加自定义类元素:
由于项目的table是基于t-table的二开,写法与原生t-table略有不同,但是大同小异,只要f12 dom元素的tr元素有自定义class即可
t-table开放了行class设置函数rowClassName定义 :
rowClassName: ({ row: { knowledgeType } }) => {
if (knowledgeType === 1) return 'custom-class-name custom-class-move' // 如果knowledgeType为1,则此元素是文件元素,赋予其经过时cursor为move
return 'custom-class-name'
},
因而我们可以直接在style中去书写对应的class,其实给空也可以,这里做的目的仅仅是为了标识它就是我们要找的row元素。
注意:必须要在全局style中写,scope下不生效
tip:为了防止全局class下的样式被污染,我们可以使用当前页面+当前模块+当前元素作为class名,这是一个不成熟的做法
<style>
// 标识row元素
.custom-class-name {
}
// 标识row元素里的文件元素
.custom-class-move {
cursor: move;
}
<style>
实现
我们是基于vue3写的,为了确保有row元素,则须使dom加载完毕后开始写实现过程,不过不知道是不是记忆错乱了,依稀记得mounted并不保证所有dom都加载完毕,在实现过程中也有某些row找不到的情况,无奈就又加了nextTick+setTimeOut:
interface KnowledgeItem {
createName: string
createTime: string
fileId: string
id: string
knowledgeDesc: string
knowledgeName: string
knowledgeStatus: number
knowledgeType: number
parentId: string
permission: string
rootId: string
textNum: number
}
const tableData = ref<KnowledgeItem[]>([])
//加载dom
onMounted(() => {
//
nextTick(() => {
setTimeout(() => {
// 在这里其实可以做一步优化,如果tableData没有值或者没有目录类row则可以终止接下来的动作
//获取所有row元素
const rows: NodeListOf<HTMLElement> = document.querySelectorAll('.custom-class-name')
for (let index = 0; index < rows.length; index++) {
const row = rows[index] as HTMLDivElement
// 如果 knowledgeType 不等于 0则为文件格式,则设置为可拖拽
if (tableData.value[index].knowledgeType !== 0) {
row.draggable = true
row.ondragstart = (event: DragEvent) => handleDragStart(event, index)
row.ondragover = (event: DragEvent) => handleDragOver(event, index)
} else {
// 如果 knowledgeType 等于 0则为目录,则设置为不可拖拽
row.draggable = false
// 移除原有的拖拽开始和经过的事件监听器
row.ondragstart = null
row.ondragover = (event: DragEvent) => {
// 阻止默认行为以允许放置
event.preventDefault()
}
}
// 设置ondrop 事件监听器
row.ondrop = (event: DragEvent) => handleDrop(event, index)
}
})
})
//
})
// 设置移动时其在tableData中的元素
const metaDropIndex = ref(-1)
//鼠标长按移动时
const handleDragStart = (event: DragEvent, index:number) => {
event.stopPropagation() //阻止默认事件
metaDropIndex.value = index
event.dataTransfer.effectAllowed = 'move' // 设置拖拽效果为移动
}
//按住不丢经过row元素时
const handleDragOver = (event: DragEvent, index: number) => {
event.preventDefault() // 允许放置
const rows: NodeListOf<HTMLElement> = document.querySelectorAll('.custom-class-name')
// 将所有目录着重展示
for (let index = 0; index < rows.length; index++) {
const element = rows[index]
if (tableData.value[index].knowledgeType === 0) {
// 此方式在t-table中无效
// element.classList.add('custom-class-name-body-bd')
element.style.background = '#f2f3ff'
element.style.fontWeight = 'bold'
}
}
if (tableData.value[index].knowledgeType === 1) event.dataTransfer.effectAllowed = 'none'
}
//鼠标放下时
const handleDrop = async (event: DragEvent, targetIndex: number) => {
event.preventDefault()
// 如果目标索引与拖动索引一致则return
if (metaDropIndex.value === targetIndex) return
// 如果目标dom不是目录 上同
if (tableData.value[targetIndex].knowledgeType !== 0) return
const rows: NodeListOf<HTMLElement> = document.querySelectorAll('.custom-class-name')
for (let index = 0; index < rows.length; index++) {
const element = rows[index]
if (tableData.value[index].knowledgeType === 0) {
// 取消将所有目录着重展示
element.style.background = '#ffffff'
element.style.fontWeight = 'normal'
}
}
console.log('metaDropIndex.value[targetIndex] :>> ', tableData.value[metaDropIndex.value])
console.log('targetIndex.value[targetIndex] :>> ', tableData.value[targetIndex])
const params = {
id: tableData.value[metaDropIndex.value].id,
targetId: tableData.value[targetIndex].id
}
const res: boolean = await moveFileApi(params)
// 移动成功后将源下标赋为非法值
!!res && (metaDropIndex.value = -1)
// 重新刷新当前列表
refreshTable(true)
}
tip: 个人感觉metaDropIndex似乎没有必要,可以使用 event.dataTransfer.setData(type,value)
来设置值,使用event.dataTransfer.getData(type)
来获取值。
为了使设置row函数更具备适用性,我们也可以将其封装成通用函数,vue与jq的区别在此也体现的淋漓尽致:
请看vcr:
//公有拖拽函数
const setDomDraggable (){
//
nextTick(() => {
setTimeout(() => {
// 在这里其实可以做一步优化,如果tableData没有值或者没有目录类row则可以终止接下来的动作
//获取所有row元素
const rows: NodeListOf<HTMLElement> = document.querySelectorAll('.custom-class-name')
for (let index = 0; index < rows.length; index++) {
const row = rows[index] as HTMLDivElement
// 如果 knowledgeType 不等于 0则为文件格式,则设置为可拖拽
if (tableData.value[index].knowledgeType !== 0) {
row.draggable = true
row.ondragstart = (event: DragEvent) => handleDragStart(event, index)
row.ondragover = (event: DragEvent) => handleDragOver(event, index)
} else {
// 如果 knowledgeType 等于 0则为目录,则设置为不可拖拽
row.draggable = false
// 移除原有的拖拽开始和经过的事件监听器
row.ondragstart = null
row.ondragover = (event: DragEvent) => {
// 阻止默认行为以允许放置
event.preventDefault()
}
}
// 设置ondrop 事件监听器
row.ondrop = (event: DragEvent) => handleDrop(event, index)
}
})
})
}
onMounted(() => {
setDomDraggable()
})
watch(
() => tableData.value,
(n) => {
n.length > 0 && setDomDraggable()
},
{ deep: true }
)
tip:其实依然还有许多可增补的点,比如可以直接获取tbody的dom然后直接获取它的子项,比如在每次拖拽时遍历的优化,比如当在非目录下松手时在return前应重置目录样式...
发散思维下其实这种方式不仅限于t-table,因为道理都是共通的,放到antd或者element也可使用,只需获取到tr上一级的父元素或者表格给与行class的api即可,不过尽管如此,依然不建议在响应式框架下操作dom(具体原因我不说)。
这样就实现了本次需求,
以上。