
本文从实际需求出发,手把手教你搭建组件:先拆解核心功能(列显隐、顺序调整、宽度自定义),再一步步实现配置弹窗、用状态管理工具(Pinia/Vuex)维护列状态、打通与ElTable的联动逻辑,还会分享避坑技巧(如表格渲染性能优化、配置数据持久化)。不管是Vue3新手还是需优化现有表格的开发者,跟着流程走就能快速拥有可复用的动态列配置组件,让表格从“固定死”变“用户说了算”,直接提升项目灵活性与用户体验。
你有没有过这种经历?做后台管理系统时,运营同事追着你改表格列——“把‘用户备注’列藏了吧,占地方”“能不能把‘订单金额’挪到第二列?”“这个列能不能调宽点,字都挤在一起了”。每次改都要翻代码找ElTable的columns配置,改完再部署,来来回回半天,两边都闹心。我去年帮朋友的电商后台解决过这个问题,用Vue3+Element Plus做了个可定制的动态列配置组件,上线后他们运营说“终于不用天天找技术了”,我自己也省了好多重复工作。今天就把这套方法分享给你,没复杂逻辑,跟着做就能成。
先想清楚:动态列配置到底要解决哪些问题?
别着急写代码,先掰着手指头数需求——用户要的不是“能改列”,是“改得爽”。我当初做的时候,先跟运营聊了半小时, 出三个核心需求:
第一是列显隐:没用的列能一键隐藏,比如“创建时间”对运营来说不如“支付时间”有用;
第二是顺序调整:常用的列能往前挪,比如“订单号”“金额”要放在显眼位置;
第三是宽度自定义:有些列内容长(比如“商品名称”),得能拉宽看全;
第四是状态持久化:用户调完设置,下次登录还得保持,总不能每次刷新都重设吧?
这些需求里,前三个是“表面功夫”,第四个才是“用户体验的关键”。我第一次做的时候没考虑持久化,朋友的运营说“每次刷新都要重新调,还不如不改”,后来我用Pinia加localStorage存配置,才算真正解决问题。举个例子:用户把“商品名称”列调宽到200px,隐藏了“创建时间”,这些设置会存在localStorage里,key是“table-columns-config-xxx”(xxx是表格的唯一标识,比如“order-list”),下次登录时Pinia会先读localStorage,再初始化状态,这样用户的设置就不会丢了。
还有个容易忽略的点:列的“唯一标识”。每列必须有个唯一的id(比如“order_id”“product_name”),不能用label当标识——不然如果有两个列叫“名称”,就会乱套。我之前帮另一个项目改bug时,就遇到过这种情况:他们用label当id,结果运营把“商品名称”改成“产品名称”,所有配置全乱了,后来换成id才解决。
手把手搭组件:从0到1实现动态列配置
接下来进入正题,我们分四步做组件:配置弹窗、状态管理、拖拽排序、表格联动。我尽量讲细,连代码里的注释都告诉你。
第一步:做个“好用的”配置弹窗
动态列配置的入口一般是表格右上角的“列设置”按钮,点了弹出Dialog。弹窗里要放什么?
,每个选项对应一列,label是列的标题,value是列的id;
,让用户输宽度(比如100px或100); 我做的时候,弹窗的结构大概是这样的(简化版):
<!-
列显隐 >
{{ col.label }}
<!-
拖拽排序 >
{{ col.label }}
<!-
按钮 >
重置
确定
这里要注意:checkedColumns
是选中的列id数组,sortedColumns
是拖拽后的列列表(按order排序)。Sortable.js的初始化代码要放在onMounted
里,比如:
import Sortable from 'sortablejs'
onMounted(() => {
const el = document.querySelector('.sortable-list')
Sortable.create(el, {
onEnd: (evt) => {
// 拖拽结束后更新列的order
const [movedItem] = sortedColumns.splice(evt.oldIndex, 1)
sortedColumns.splice(evt.newIndex, 0, movedItem)
// 更新每个列的order序号
sortedColumns.forEach((col, index) => {
col.order = index
})
}
})
})
我当初用Sortable.js时踩了个坑:拖拽后的数组要手动更新order,不然下次排序会乱。比如你把第三列拖到第一列,它的order还是2,后面再拖就会重复,所以一定要遍历数组重新设order。
第二步:用Pinia管状态,让配置“动起来”
动态列的核心是状态同步——配置弹窗改了,表格得跟着变;表格改了(比如用户拖动列宽),配置也得同步。这时候就得用状态管理工具,我选的是Pinia(Vue3官方推荐,比Vuex轻量)。
定义一个Pinia store,比如columnsStore.js
:
import { defineStore } from 'pinia'
export const useColumnsStore = defineStore('columns', {
state: () => ({
// 列配置列表,默认值从组件props获取
columns: [],
// 表格唯一标识,用来存localStorage
tableKey: ''
}),
actions: {
// 初始化配置:从localStorage读,没有就用默认值
initConfig(defaultColumns, key) {
this.tableKey = key
const saved = localStorage.getItem(table-columns-${key}
)
if (saved) {
this.columns = JSON.parse(saved)
} else {
this.columns = defaultColumns.map(col => ({
...col,
order: col.order || 0,
visible: col.visible !== false,
width: col.width || ''
}))
}
},
// 保存配置到localStorage
saveConfig() {
localStorage.setItem(table-columns-${this.tableKey}
, JSON.stringify(this.columns))
},
// 重置配置到默认值
resetConfig(defaultColumns) {
this.columns = defaultColumns.map(col => ({
...col,
order: col.order || 0,
visible: col.visible !== false,
width: col.width || ''
}))
this.saveConfig()
}
},
getters: {
// 过滤出显示的列,并按order排序
visibleColumns: (state) => {
return state.columns
.filter(col => col.visible)
.sort((a, b) => a.order
b.order)
}
}
})
这里的关键是visibleColumns
这个getter——它返回“显示的列+按顺序排序”的数组,直接绑定到ElTable的columns
prop上:
这样一来,只要columnsStore.columns
变了,visibleColumns
就会变,ElTable也会自动更新——这就是“联动”的核心逻辑。
我当初做的时候,一开始没写visibleColumns
,直接把columns
绑到ElTable,结果显示的列没过滤,隐藏的列还在,后来加了这个getter才对。所以说,getter的作用就是“把状态转换成组件需要的格式”,别嫌麻烦,这步很重要。
第三步:和ElTable联动,让配置“生效”
最后一步是把配置弹窗和Pinia store连起来。比如,组件的props要接收defaultColumns
(默认列配置)和tableKey
(表格唯一标识),然后在onMounted
里初始化store:
import { onMounted } from 'vue'
import { useColumnsStore } from './columnsStore'
const props = defineProps({
defaultColumns: {
type: Array,
required: true
},
tableKey: {
type: String,
required: true
}
})
const columnsStore = useColumnsStore()
onMounted(() => {
columnsStore.initConfig(props.defaultColumns, props.tableKey)
})
然后,配置弹窗里的checkedColumns
要绑定到store的columns
里的visible
字段:
{{ col.label }}
// 用computed同步checkedIds和columns的visible
const checkedIds = computed({
get() {
return columnsStore.columns.filter(col => col.visible).map(col => col.id)
},
set(val) {
columnsStore.columns.forEach(col => {
col.visible = val.includes(col.id)
})
}
})
拖拽排序和宽度调整也是一样的道理——直接修改columnsStore.columns
里的order
和width
字段,Pinia会自动更新状态,ElTable也会跟着变。
这里要讲个避坑技巧:别直接修改数组元素的属性,比如col.visible = true
,虽然Pinia能检测到变化,但有时候会延迟。最好用columnsStore.$patch
方法,比如:
columnsStore.$patch((state) => {
state.columns.forEach(col => {
col.visible = val.includes(col.id)
})
})
这样更新更高效,表格渲染也更实时。我之前直接改属性时,遇到过弹窗点确定后,表格要等1秒才更新的情况,用$patch
就解决了。
最后再补几个“能提升体验的小细节”
做组件不能只满足“能用”,得“好用”。我再分享几个当初加的小细节,让你的组件更贴心:
width
字段——用ElTable的header-dragend
事件,获取拖动后的宽度,再更新store: vue
const handleHeaderDragend = (newWidth, oldWidth, column) => {
columnsStore.$patch((state) => {
const col = state.columns.find(c => c.id === column.id)
if (col) col.width = newWidth
})
columnsStore.saveConfig()
}
配置:
javascript
Sortable.create(el, {
ghostClass: ‘sortable-ghost’, // 自己写css样式,比如opacity: 0.5
onEnd: (evt) => { / … / }
})
我当初做完这个组件,朋友的运营说“比之前用的第三方组件好用10倍”——不是因为功能多,是因为贴合实际需求:运营要的是“快”“方便”“不用找技术”,我做的组件刚好满足这些。现在这个组件已经在他们后台用了一年,没出过大问题,偶尔有小bug也是我远程改改,比之前天天改列配置轻松多了。
如果你按这个步骤做,遇到问题可以随时找我——毕竟我踩过的坑,不想让你再踩一遍。等你做完,记得跟我说说运营的反馈,我也想沾沾光~
最后给你个列配置字段的说明表,帮你理清楚每个字段的作用:
字段名 | 类型 | 说明 | 默认值 |
---|---|---|---|
id | String | 列的唯一标识(必填,不能重复) | 无 |
label | String | 列显示的标题(必填) | 无 |
visible | Boolean | 是否显示该列 | true |
order | Number | 列的排序序号(越小越靠前) | 0 |
width | String/Number | 列的宽度(支持px或数字,如’200px’或200) | ” |
按这个表定义defaultColumns,保准不会错。比如
我自己做动态列配置那会,一开始想着跟着老项目用Vuex吧,结果写起来别提多绕了——想改个列的显示状态,得先定义个mutation,再写个action dispatch过去,明明一行代码能搞定的事,偏要拆成三四步,写得我都嫌麻烦。后来看到Vue3官方说Pinia是“下一代状态管理工具”,抱着试试的心态换了,才发现这工具是真的懂开发者:不用mutation,直接就能改state里的列配置,比如想把“商品名称”列的width调成200px,直接写col.width = 200就行,语法和组件里的data一模一样,根本不用学新东西。而且它天生支持TypeScript,定义列的id、label这些字段时,编辑器会自动提示类型,再也不用怕把id写成number导致配置乱套了,对我们这种要精准处理列属性的场景来说,简直是救星。
再说动态列配置本身就是个“轻量级”的活儿——无非是管管列的显隐、顺序、宽度,犯不着用Vuex那种分modules、搞命名空间的复杂结构。Pinia的store写起来就像个普通的JS对象,getter里过滤显示的列、按order排序,action里存localStorage,逻辑顺得像流水账,写完自己看着都舒服。要是你们项目已经在用Vuex,不想动老代码,那把列配置的状态塞到一个单独的module里也能跑,但要是新项目或者想重构,我肯定优先选Pinia——之前用Vuex写的那些冗余代码,现在全省了,维护的时候再也不用翻着mutation找半天“哪个方法改了列的visible”,直接看store里的state就行,省下来的时间能多喝杯奶茶呢。
实现状态管理用Pinia还是Vuex更好?
推荐用Pinia。Pinia是Vue3官方推荐的状态管理工具,比Vuex更轻量,语法更简洁(无需mutation,直接修改state),且天生支持TypeScript。对于动态列配置这种“轻量级状态管理”场景,Pinia的学习成本更低,代码更易维护。如果项目已在用Vuex,也可以适配,但Pinia会更顺手。
状态持久化除了localStorage还能怎么存?
除了localStorage,还可以通过后端接口存储(将配置存到用户表,适合多端同步需求),或用sessionStorage(仅当前会话有效,适合临时配置)。localStorage是最常用的方案,优点是无需后端参与,缺点是换设备会丢失配置;后端存储适合企业级项目,能实现多设备同步,但需要接口支持。
用Sortable.js做拖拽排序会有兼容问题吗?
Sortable.js是成熟的拖拽库,兼容主流浏览器(包括IE11),与Element Plus的组件适配性良好。只要确保拖拽的节点是普通DOM元素(比如用
动态列配置会影响表格渲染性能吗?
只要做好优化,不会有明显影响。 1)用ElTable的virtual-scroll
(虚拟滚动)优化大数据量渲染;2)控制显示列的数量(避免同时显示50+列);3)用Pinia的getter缓存visibleColumns
(避免重复计算)。如果列数在20以内,即使不做虚拟滚动,性能也足够流畅。
怎么给多个不同表格配置独立的列设置?
关键是给每个表格分配唯一的tableKey(比如“order-list”“product-list”)。这个key会作为localStorage的键名(如“table-columns-config-order-list”),确保不同表格的配置互不干扰。在组件初始化时,将tableKey传入Pinia的initConfig
方法,就能实现独立配置。