Tabulator是一款轻量但功能极强的JavaScript表格库,它把显示逻辑(formatter)和编辑逻辑(editor)拆分成两个独立的环节,这一点正好契合「前台看标签、后台存ID」这类需求。这篇文章会用一个完整的部门管理示例,讲清楚每个环节怎么配置,以及踩坑之后总结出来的实践经验。

为什么需要「显示标签、存储ID」这种双层结构
假设一张员工表里有「所属部门」列,如果直接把部门名称存进数据,一旦部门改名,所有历史记录的关联就断了;更严重的是,名称可能重复,无法作为唯一标识。正确做法是数据里存dept_id,界面上渲染部门名称,用户编辑时通过下拉框选择,选中后回写的依然是dept_id。
Tabulator原生支持的list编辑器其实已经内置了这个能力,它允许通过formatterParams和editorParams分别定义「怎么显示」和「怎么编辑」。我们先从最基础的配置看起:
// 模拟从后台拉取的部门字典
var deptList = [
{ id: 1, label: "研发部" },
{ id: 2, label: "市场部" },
{ id: 3, label: "人力资源部" }
];
var table = new Tabulator("#example-table", {
data: [
{ id: 101, name: "张三", deptId: 1 },
{ id: 102, name: "李四", deptId: 3 }
],
columns: [
{ title: "姓名", field: "name", editor: "input" },
{
title: "所属部门",
field: "deptId",
// 显示时把ID翻译成标签
formatter: "lookup",
formatterParams: { 1: "研发部", 2: "市场部", 3: "人力资源部" },
// 编辑时使用list编辑器
editor: "list",
editorParams: {
values: deptList,
// 指定字典对象的取值字段
value: "id",
label: "label"
},
// 可选:校验ID必须在字典中
validator: ["required", "integer"]
}
]
});
注意formatter: "lookup"这个配置,它接收一个「ID到标签」的映射对象。当字典是从接口动态返回的数组结构时,需要先把数组转成映射,否则lookup查不到值,单元格会显示空白。这是新手最常踩的第一个坑。
动态字典与异步加载的处理方案
实际项目中,部门字典往往来自接口,页面渲染时字典可能还没返回。如果直接初始化表格,formatter拿不到映射数据,列就会是空的。推荐的做法是先加载字典、再初始化表格,或者用redraw在字典就绪后强制重绘。
var deptMap = {};
fetch("/api/departments")
.then(function (res) { return res.json(); })
.then(function (list) {
// 构建两个结构:数组给编辑器用,映射给formatter用
window.deptList = list;
list.forEach(function (item) {
deptMap[item.id] = item.label;
});
return initTable();
});
function initTable() {
return fetch("/api/employees")
.then(function (res) { return res.json(); })
.then(function (data) {
var table = new Tabulator("#example-table", {
data: data,
columns: [
{ title: "姓名", field: "name", editor: "input" },
{
title: "所属部门",
field: "deptId",
formatter: "lookup",
formatterParams: deptMap,
editor: "list",
editorParams: {
values: window.deptList,
value: "id",
label: "label",
// 清空按钮,允许用户取消选择
clearable: true
}
}
]
});
});
}
如果字典数据量很大(几千条),list编辑器还支持autocomplete模式,开启后下拉框会变成搜索框加过滤列表,用户输入关键字即可过滤,体验会好很多。只需要在editorParams里加上autocomplete: true和freetext: false,前者启用自动补全,后者禁止用户输入字典之外的自由文本,从而保证回写的一定是合法ID。
多选场景与自定义编辑器
有些业务要求一个单元格选多个标签,比如员工可以归属多个项目组。这时候用multiselect: true开启多选,单元格字段存的是ID数组[1,3],formatter则要换成自定义函数,把数组逐个翻译成标签拼接显示。
// 自定义formatter:把ID数组渲染成标签串
function multiTagFormatter(cell, formatterParams) {
var map = formatterParams.map;
var value = cell.getValue() || [];
return value.map(function (id) {
return '<span class="tag">' + (map[id] || id) + '</span>';
}).join(" ");
}
// 列定义
{
title: "项目组",
field: "groupIds",
formatter: multiTagFormatter,
formatterParams: { map: deptMap },
editor: "list",
editorParams: {
values: deptList,
value: "id",
label: "label",
multiselect: true
}
}
需要提醒的是,多选模式下Tabulator回写的是数组类型,提交后台前要确认接口能接受JSON数组,必要时用getData()拿到数据后做一次JSON.stringify转换。另外自定义formatter里拼HTML时,如果标签文案来自用户输入,务必做转义,否则存在XSS风险。
最后提交数据时,直接调用table.getData()或table.getChangedData(),拿到的deptId字段就是原始ID值,可以原样提交给后台做外键关联。整套方案显示层与数据层完全分离,字典改动只需要更新一份映射,维护成本非常低。