项目背景
只要有系统写,那几乎就逃脱不了表格的各种花哨操作。好几年前前出过一篇《 el-table表格自定义列控制与el-table表格拖拽》的文章,收到掘友的反馈还是挺多的,但是我觉得写的还是有点low。这不再重操旧业写系统的背景下,发现了更高明的框架和UI,特拿出来与需要的朋友分享。
技术以及框架
这是一个vue3
的项目,UI组件主要是以antdesign
,vxe-table
来构建的,至于为啥要引入vxe-table。无非就是提供了好的api,居然没有免费开放还想要我的钱。抱歉,那我只能宠幸其他(vxe-table)了。

实现思路分
想要实现表格自定义列控制以及拖拽排序,我们将功能拆分一下就是2个点,分为是列的自定义控制和列的拖拽。为了全局通用,我们封装成一个组件,我们将每个页面的表格列项作为参数传值。再配合与列操作相关的方法,进行组件通信传值。
实现列控制显示与否
在Ant-design
的表格组件中,我们的列是通过一个数组遍历来实现的,那么我们想要控制列的展示与后,我们只要在循环之前过滤一下我们需要的列数组就行,在这我们定义一个字段icChecked
用来辨别这个列项是否需要被展示,需要的话我们赋值为true,不需要则赋值为false。在store中
定义一个改变状态的方法,方便组件调用。最后来一个勾选框,勾选时赋值true,取消就赋值false。我们通过轻松拿捏。废话不多说,先上一部分核心代码。
在store中存放的列控制原始数据
javascript
materialColumns: [
{
dataIndex: 'material_id',
title: '物料编码',
isChecked: true,
sorter: (a, b) => a.material_id.localeCompare(b.material_id)
},
{
dataIndex: 'material_name',
title: '物料名称',
isChecked: true,
sorter: (a, b) => a.material_name.localeCompare(b.material_name)
},
{
dataIndex: 'is_active',
title: '使用状态',
isChecked: false,
sorter: (a, b) => {
if (a.is_active === b.is_active) {
return 0
}
return a.is_active ? -1 : 1
}
},
{
dataIndex: 'status',
title: '单据状态',
isChecked: true,
sorter: (a, b) => a.status - b.status
}
]
在表格展示前的数据过筛选
ini
const tableColumns = computed(() => {
return materialStore.materialColumns.filter(item => item.isChecked)
})
列控制组件传值与方法
ini
<ColumnsSet
ref="columnsSetRef"
:tableData="materialStore.materialColumns"
@changeColumn="changeColumn"
@changeRow="changeRow"
/>
勾选框方法传值,控制显示与否
dart
const handleCheckboxChange = selection => {
emit('changeColumn', selection.row.dataIndex, selection.checked)
}

实现列拖拽
实现列拖拽,我们可能最先想到的是利用sortable.js
,在这我主要是用的通过vxe-table
,它对sorttable.js
进行了一个二次封装,还能在表格中使用。
vxe-table拖拽实现原理
DOM 拖拽事件(dragstart
, dragover
, drop
) 根据对目标元素的拖拽,获取初始位置以及结束位置,拖拽完成时,对数据进行位置交换,从而实现位置的改变。核心代码:
ini
const rowDragendEvent = async ({ newRow, oldRow, dragPos }) => {
const tableDataValue = props.tableData
let oldIndex = tableDataValue.findIndex(
item => item.dataIndex === oldRow.dataIndex
)
let newIndex = tableDataValue.findIndex(
item => item.dataIndex === newRow.dataIndex
)
// 向下移动
if (oldIndex < newIndex && dragPos === 'top') {
newIndex = newIndex - 1
}
// 向上移动
if (oldIndex > newIndex && dragPos === 'bottom') {
newIndex = newIndex + 1
}
// 增加逻辑判断,如果 oldIndex 和 newIndex 相等,则不进行数据交换
if (oldIndex !== newIndex) {
const oldItem = tableDataValue.splice(oldIndex, 1)[0]
tableDataValue.splice(newIndex, 0, oldItem)
emit('changeRow', tableDataValue)
}
}
特别注意vxe-table插件有个bug,需要自己弥补一下:
当你在拖拽移动过程中,对元素进行了移动,但是没有移动到位就停止,此时页面上的操作显示数据还没发生移动。但是此时移动拖拽的方法rowDragendEvent
已经触发,那么数据也会进行交换,那就呵呵哒了,原始数据位置已经发生改变,页面没变化,以后的移动数据都会不准确。
进行缺陷补救措施
ini
// 向下移动
if (oldIndex < newIndex && dragPos === 'top') {
newIndex = newIndex - 1
}
// 向上移动
if (oldIndex > newIndex && dragPos === 'bottom') {
newIndex = newIndex + 1
}
在上面的代码中我们增加了2个注释,从注释中我们可以看到其作用,那就是使用其第三参数,判断它是往上移动还是往下移动。在元素移动过程中,刚接触到上面时,我们不对数据进行交换处理,只有数据撤离移动到目标位置的底部,我们才进行数据交换。这样就完美避坑了。
如何保持每个人专属的配置
在这里我们根据项目情况选择pinia
的持久化存储。那么这里就得使用到其对应的插件pinia-plugin-persistedstate
。
安装
css
npm i pinia-plugin-persistedstate
引入到pina实例中
javascript
import { createPinia } from "pinia";
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia();
pinia.use(piniaPluginPersistedstate)
export default pinia;
使用
css
persist: [{ pick: ['materialColumns'], storage: localStorage }],
在使用过程中,添加一个persist
数组。里面可以包含多个对象,其中对象的pick
对应将要进行持久化的数据,storage
为要进行持久化处理的方式(sessionStorage,localStorage
)。
控制台数据

列控制以及拖拽组件源码
ini
<script setup>
import { ref, reactive } from 'vue'
const emit = defineEmits(['changeColumn', 'changeRow'])
const open = ref(false)
const title = '自定义显示列项'
const props = defineProps({
tableData: {
type: Array, // 数据类型为数组
default: () => [] // 默认值为空数组
}
})
const rowConfig = reactive({
drag: true
})
const handleCheckboxChange = selection => {
emit('changeColumn', selection.row.dataIndex, selection.checked)
}
const rowDragendEvent = async ({ newRow, oldRow, dragPos }) => {
const tableDataValue = props.tableData
let oldIndex = tableDataValue.findIndex(
item => item.dataIndex === oldRow.dataIndex
)
let newIndex = tableDataValue.findIndex(
item => item.dataIndex === newRow.dataIndex
)
// 向下移动
if (oldIndex < newIndex && dragPos === 'top') {
newIndex = newIndex - 1
}
// 向上移动
if (oldIndex > newIndex && dragPos === 'bottom') {
newIndex = newIndex + 1
}
// 增加逻辑判断,如果 oldIndex 和 newIndex 相等,则不进行数据交换
if (oldIndex !== newIndex) {
const oldItem = tableDataValue.splice(oldIndex, 1)[0]
tableDataValue.splice(newIndex, 0, oldItem)
emit('changeRow', tableDataValue)
}
}
const showModal = () => {
open.value = true
}
const handleOk = async () => {}
// 重置表单数据和校验状态的方法
defineExpose({
showModal
})
</script>
<template>
<a-modal
v-model:open="open"
:title="title"
@ok="handleOk"
:maskClosable="false"
:footer="null"
>
<vxe-table
border
ref="tableRef"
:row-config="rowConfig"
max-height="460px"
size="small"
:checkbox-config="{ checkField: 'isChecked', showHeader: false }"
@checkbox-change="handleCheckboxChange"
@row-dragend="rowDragendEvent"
:data="props.tableData"
>
<vxe-column
type="checkbox"
title="显示"
align="center"
width="60"
></vxe-column>
<vxe-column field="title" title="列明"></vxe-column>
<vxe-column
field=""
title="拖动排序"
drag-sort
align="center"
></vxe-column>
</vxe-table>
</a-modal>
</template>
总结
温故而知新,通过不同的技术栈,以及框架,重新实现了一遍列控制和拖拽排序。