1.1.7 • Published yesterday
dynamic-table-vue2-element
Licence
MIT
Version
1.1.7
Deps
0
Size
203 kB
Vulns
0
Weekly
0
dynamic-table-vue2-element
基于 Vue2 + Element UI 的动态配置表格组件,支持动态列配置、冻结列(左右可选)、筛选、排序、分页、筛选方案保存、金额格式化、可配置操作列等功能。
安装
npm install dynamic-table-vue2-element
前置依赖
确保项目中已安装以下依赖:
npm install vue@^2.6.0 element-ui@^2.15.0 vuedraggable@^2.24.0
引入
全局注册
import Vue from 'vue'
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
import DynamicTable from 'dynamic-table-vue2-element'
import 'dynamic-table-vue2-element/lib/dynamic-table.css'
Vue.use(ElementUI)
Vue.use(DynamicTable)
按需引入
import DynamicTable from 'dynamic-table-vue2-element'
import 'dynamic-table-vue2-element/lib/dynamic-table.css'
export default {
components: { DynamicTable }
}
基础用法
<template>
<dynamic-table
menu-id="M001"
:field-meta-list="fieldMetaList"
:fetch-data-fn="fetchDataFn"
:load-config-fn="loadConfigFn"
:save-config-fn="saveConfigFn"
row-key="id"
@selection-change="handleSelectionChange"
@row-action="handleRowAction"
>
<template #toolbar-left>
<el-button type="primary" icon="el-icon-plus" size="small">新增</el-button>
</template>
<template #column-status="{ row }">
<el-tag :type="row.status === 1 ? 'success' : 'danger'" size="mini">
{{ row.status === 1 ? '启用' : '禁用' }}
</el-tag>
</template>
</dynamic-table>
</template>
<script>
import { fetchUserList, getTableConfig, saveTableConfig } from '@/api/table'
export default {
data() {
return {
fieldMetaList: [
{ fieldKey: '__selection', fieldLabel: '选择框', fieldType: 'selection', width: 50 },
{ fieldKey: '__index', fieldLabel: '序号', fieldType: 'index', width: 50 },
{ fieldKey: 'username', fieldLabel: '用户名', fieldType: 'string', filterable: true, sortable: true, width: 120, align: 'left' },
{ fieldKey: 'age', fieldLabel: '年龄', fieldType: 'number', filterable: true, sortable: true, width: 80, align: 'center' },
{ fieldKey: 'status', fieldLabel: '状态', fieldType: 'enum', filterable: true, sortable: true, width: 100, align: 'center',
enumValues: [{ label: '启用', value: 1 }, { label: '禁用', value: 0 }]
},
{ fieldKey: 'birthday', fieldLabel: '生日', fieldType: 'date', filterable: true, sortable: true, width: 120, align: 'center' },
{ fieldKey: 'salary', fieldLabel: '薪资', fieldType: 'currency', filterable: true, sortable: true, width: 130, align: 'right' },
{ fieldKey: 'department', fieldLabel: '部门', fieldType: 'string', filterable: true, sortable: true, width: 120, align: 'center',
enumValues: { '001': '技术部', '002': '市场部', '003': '人事部' }
},
{ fieldKey: 'createTime', fieldLabel: '创建时间', fieldType: 'date', filterable: true, sortable: true, width: 170, align: 'center' },
{ fieldKey: '__actions', fieldLabel: '操作', fieldType: 'actions', width: 150, actions: [
{ label: '查看', action: 'view' },
{ label: '编辑', action: 'edit' },
{ label: '删除', action: 'delete', style: { color: '#f56c6c' } }
]}
]
}
},
methods: {
fetchDataFn(params) {
return fetchUserList(params)
},
loadConfigFn(menuId) {
return getTableConfig(menuId)
},
saveConfigFn(config) {
return saveTableConfig(config)
},
handleSelectionChange(selection) {
console.log('选中行:', selection)
},
handleRowAction({ action, row }) {
if (action === 'view') console.log('查看:', row)
if (action === 'edit') console.log('编辑:', row)
if (action === 'delete') console.log('删除:', row)
}
}
}
</script>
API
Props
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| menuId | String | 是 | - | 菜单ID,用于配置隔离 |
| fieldMetaList | Array | 是 | - | 字段元数据列表,定义表格列(含选择框、序号、操作列等特殊列) |
| fetchDataFn | Function | 是 | - | 数据查询函数,接收 params 参数,返回 Promise { list, total } |
| loadConfigFn | Function | 否 | null | 加载配置函数,接收 menuId,返回 Promise 配置对象 |
| saveConfigFn | Function | 否 | null | 保存配置函数,接收 config 对象,返回 Promise |
| rowKey | String | 否 | 'id' | 行数据唯一标识字段 |
| border | Boolean | 否 | true | 是否显示边框 |
| stripe | Boolean | 否 | true | 是否斑马纹 |
| tableHeight | String/Number | 否 | undefined | 固定表格高度,不设置则自适应 |
| showPagination | Boolean | 否 | true | 是否显示分页 |
| pageSizes | Array | 否 | [10,20,50,100] | 每页条数选项 |
| headerAlign | String | 否 | 'center' | 表头对齐方式,默认居中 |
| actionColumnWidth | String/Number | 否 | 150 | 操作列默认宽度(当 fieldMetaList 中 actions 列未指定 width 时生效) |
fieldMetaList 字段定义
数据列
| 属性 | 类型 | 必填 | 说明 |
|---|---|---|---|
| fieldKey | String | 是 | 字段唯一标识 |
| fieldLabel | String | 是 | 字段显示名称 |
| fieldType | String | 是 | 字段类型:string / number / enum / date / boolean / currency |
| filterable | Boolean | 否 | 是否可筛选 |
| sortable | Boolean | 否 | 是否可排序 |
| width | Number | 否 | 列宽 |
| align | String | 否 | 对齐方式:left / center / right |
| enumValues | Array/Object | 否 | 枚举值,支持数组 [{ label, value }] 或 Map { '001': '技术部' } |
| frozenPosition | String | 否 | 冻结位置:'left' / 'right',不设置则不冻结 |
特殊列
特殊列(选择框、序号、操作列)通过 fieldType 统一管理,在 fieldMetaList 中声明即可,支持显隐、排序、宽度调整和冻结配置。
| fieldType | fieldKey(推荐) | 说明 | 额外属性 |
|---|---|---|---|
selection |
__selection |
选择框列,触发 selection-change 事件 |
width:列宽,默认 50 |
index |
__index |
序号列,显示行号 | width:列宽,默认 50 |
actions |
__actions |
操作列,通过 actions 属性配置按钮 |
actions:按钮数组,width:列宽,默认使用 actionColumnWidth prop |
actions 按钮配置:
{ fieldKey: '__actions', fieldLabel: '操作', fieldType: 'actions', width: 150, actions: [
{ label: '查看', action: 'view' },
{ label: '编辑', action: 'edit' },
{ label: '删除', action: 'delete', style: { color: '#f56c6c' } }
]}
| 属性 | 类型 | 必填 | 说明 |
|---|---|---|---|
| label | String | 是 | 按钮文字 |
| action | String | 是 | 操作标识,用于 row-action 事件回调中区分 |
| style | Object | 否 | 按钮自定义样式 |
特殊列的
fieldKey推荐使用__selection、__index、__actions前缀,组件内部通过fieldType识别。不声明对应fieldType的特殊列则不会显示。
fieldType 说明
| 类型 | 说明 |
|---|---|
| selection | 选择框列,支持多选,触发 selection-change 事件 |
| index | 序号列,自动显示行号 |
| actions | 操作列,通过 actions 属性配置按钮,触发 row-action 事件 |
| string | 文本,筛选时为操作符下拉+输入框,默认"包含" |
| number | 数值,筛选时为操作符下拉+输入框,默认"等于" |
| currency | 金额,自动千分位格式化并保留两位小数,筛选同 number |
| enum | 枚举,筛选时为多选下拉,列展示自动显示 label |
| date | 日期,筛选时为日期范围选择器(含快捷按钮),自动拼接 0000/2359 |
| boolean | 布尔,筛选时为是/否下拉 |
enumValues 格式
支持两种格式:
// 数组格式
enumValues: [{ label: '启用', value: 1 }, { label: '禁用', value: 0 }]
// Map 格式(key 为实际值,value 为显示名称)
enumValues: { '001': '技术部', '002': '市场部', '003': '人事部' }
非 enum 类型字段配置了 enumValues 后,筛选面板和表头搜索也会自动使用下拉选择,列展示会自动做编码→名称映射。
操作列
- 通过
fieldMetaList中fieldType: 'actions'的项配置操作按钮 actions数组中每个按钮通过action标识区分,点击触发@row-action事件- 操作列默认不冻结,可在配置抽屉中设置冻结方向
- 操作列支持显隐、排序、宽度调整,与其他列统一管理
Events
| 事件名 | 参数 | 说明 |
|---|---|---|
| selection-change | selection | 选中行变化时触发 |
| row-action | { action, row } | 操作列按钮点击时触发,action 为 rowActions 中配置的标识 |
Slots
| 插槽名 | 作用域参数 | 说明 |
|---|---|---|
| toolbar-left | - | 工具栏左侧按钮区域 |
| column-{fieldKey} | { row, value } | 自定义列内容渲染 |
fetchDataFn 参数格式
{
page: 1,
pageSize: 10,
filters: { // 筛选面板 + 表头搜索的合并条件
username: { operator: 'contains', value: '张' }, // 字符串:操作符+值
status: [1], // 枚举:多选值数组
age: { operator: 'eq', value: 25 }, // 数值:操作符+值
salary: { operator: 'gte', value: 5000 }, // 金额:操作符+值
birthday: { start: '2024-01-01 00:00:00', end: '2024-12-31 23:59:59' }, // 日期:自动拼接时间
isActive: true // 布尔:直接传值
},
sortBy: 'createTime',
sortOrder: 'descending'
}
筛选操作符说明
| 操作符值 | 含义 | 适用类型 |
|---|---|---|
| eq | 等于 | string, number, currency |
| neq | 不等于 | string, number, currency |
| contains | 包含 | string, number, currency |
| notContains | 不包含 | string, number, currency |
| startsWith | 开头是 | string |
| endsWith | 结尾是 | string |
| gt | 大于 | number, currency |
| lt | 小于 | number, currency |
| gte | 大于等于 | number, currency |
| lte | 小于等于 | number, currency |
字符串类型默认操作符为
contains,数值/金额类型默认操作符为eq
fetchDataFn 返回格式
{
list: [], // 当前页数据
total: 100 // 总条数
}
loadConfigFn 返回格式
{
menuId: 'M001',
visibleFields: '["username","age","status"]', // JSON 字符串
frozenFields: '["username"]',
frozenPositions: '{"username":"left","balance":"right"}',
columnWidths: '{"username":120,"age":80}', // 列宽配置
filterFields: '["username","status"]',
columnOrder: '["username","age","status"]',
filterSchemes: '[{"name":"方案1","filterValues":{}}]'
}
saveConfigFn 接收格式
与 loadConfigFn 返回格式一致。
功能说明
表头配置
- 勾选展示列,拖拽调整顺序
- 每列可配置宽度,最小值 50
- 两个冻结按钮(左 / 右),点击激活,再次点击取消,左右互斥
- 选择框列(
selection)、序号列(index)、操作列(actions)也支持显隐、排序、冻结配置和宽度调整 - 冻结位置和列宽持久化保存
金额类型
fieldType: 'currency'自动千分位格式化,保留两位小数- 如
50000显示为50,000.00 - 筛选和表头搜索同 number 类型
操作列
- 通过
fieldMetaList中fieldType: 'actions'配置操作按钮,无需手写 slot - 点击触发
@row-action事件,通过action标识区分操作类型 - 操作列默认不冻结,可在配置抽屉中设置冻结方向
编码映射
enumValues支持数组格式和 Map 格式- 非 enum 字段配置
enumValues后,列展示自动做编码→名称映射 - 筛选面板和表头搜索自动使用下拉选择替代输入框
表头对齐
headerAlign属性控制表头默认对齐方式,默认'center'- 各列仍可通过
align单独设置数据对齐方式
筛选配置
- 勾选需要作为筛选项的字段,拖拽调整顺序
- 筛选面板根据字段类型自动生成对应控件
- 字符串/数值/金额类型:操作符下拉 + 值输入框
- 日期类型:日期范围选择器,含快捷按钮(本月、上月、近三个月、本年、重置)
- 日期筛选值自动拼接时间:开始日期拼接
00:00:00,结束日期拼接23:59:59 - 筛选区域超过3行自动出现滚动条
筛选方案
- 最多保存5个常用筛选方案
- 支持修改方案名称(点击名称或编辑图标)
- 使用方案后修改筛选值,保存时提示覆盖或新建
- 重置按钮同时清除筛选面板条件、表头搜索条件和排序
还原默认配置
- 配置抽屉底部提供"还原默认"按钮
- 点击后二次确认,确认后将所有配置还原为初始化状态
- 还原内容包括:表头显隐/顺序/冻结/列宽、筛选字段配置、筛选方案
- 同时清除浏览器中当前菜单的本地缓存
- 还原后立即生效,表格自动刷新
表头搜索
- 点击表头名称弹出下拉菜单,支持排序和搜索
- 排序三态切换:升序 → 降序 → 无
- 字符串/数值/金额类型搜索支持操作符选择
- 日期类型搜索使用日期范围选择器,含快捷按钮
- 表头搜索条件与筛选面板条件合并后一起传给 fetchDataFn
分页
- 固定在表格底部,不随数据高度变化
后端接口设计
1. 保存配置接口
请求方式:POST /api/table/config
请求参数:
{
"menuId": "M001",
"visibleFields": "[\"username\",\"age\",\"status\"]",
"frozenFields": "[\"username\"]",
"frozenPositions": "{\"username\":\"left\"}",
"columnWidths": "{\"username\":120,\"age\":80}",
"filterFields": "[\"username\",\"status\"]",
"columnOrder": "[\"username\",\"age\",\"status\"]",
"filterSchemes": "[{\"name\":\"方案1\",\"filterValues\":{\"username\":{\"operator\":\"contains\",\"value\":\"张\"}}}]"
}
响应格式:
{
"code": 200,
"message": "success",
"data": null
}
设计要点:
- 以
menuId作为配置标识,userId由后端从登录态(Token/Session)中获取,前端不传递 - 同一用户同一菜单只保存一份配置
- 所有数组/对象字段以 JSON 字符串存储,后端无需解析,直接存取即可
columnWidths存储用户自定义的列宽,key 为 fieldKey,value 为像素值filterSchemes中的filterValues结构与fetchDataFn的filters参数一致- 建议使用
INSERT ... ON DUPLICATE KEY UPDATE或UPSERT语义实现保存
数据库表设计参考:
CREATE TABLE t_table_config (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
menu_id VARCHAR(64) NOT NULL COMMENT '菜单ID',
user_id VARCHAR(64) NOT NULL COMMENT '用户ID,后端从登录态获取',
visible_fields TEXT COMMENT '可见字段JSON',
frozen_fields TEXT COMMENT '冻结字段JSON',
frozen_positions TEXT COMMENT '冻结位置JSON',
column_widths TEXT COMMENT '列宽配置JSON',
filter_fields TEXT COMMENT '筛选字段JSON',
column_order TEXT COMMENT '列顺序JSON',
filter_schemes TEXT COMMENT '筛选方案JSON',
created_time DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_menu_user (menu_id, user_id)
);
2. 查询配置接口
请求方式:GET /api/table/config/{menuId}
响应格式:
{
"code": 200,
"message": "success",
"data": {
"menuId": "M001",
"visibleFields": "[\"username\",\"age\",\"status\"]",
"frozenFields": "[\"username\"]",
"frozenPositions": "{\"username\":\"left\"}",
"columnWidths": "{\"username\":120,\"age\":80}",
"filterFields": "[\"username\",\"status\"]",
"columnOrder": "[\"username\",\"age\",\"status\"]",
"filterSchemes": "[{\"name\":\"方案1\",\"filterValues\":{}}]"
}
}
设计要点:
- 无配置时返回
null或空对象,组件会使用默认配置 - 返回的 JSON 字符串由前端解析,后端无需处理
3. 列表数据查询接口
请求方式:POST /api/table/data
请求参数:
{
"page": 1,
"pageSize": 10,
"filters": {
"username": { "operator": "contains", "value": "张" },
"status": [1, 2],
"age": { "operator": "gte", "value": 20 },
"salary": { "operator": "lte", "value": 50000 },
"birthday": { "start": "2024-01-01 00:00:00", "end": "2024-12-31 23:59:59" },
"isActive": true
},
"sortBy": "createTime",
"sortOrder": "descending"
}
响应格式:
{
"code": 200,
"message": "success",
"data": {
"list": [
{ "id": 1, "username": "张三", "age": 25, "status": 1, "birthday": "2024-01-15", "salary": 8000, "isActive": true, "createTime": "2024-06-01 10:00:00" }
],
"total": 100
}
}
后端筛选条件处理逻辑:
| 字段类型 | filters 结构 | 后端处理方式 |
|---|---|---|
| string | { operator, value } |
根据 operator 拼接 SQL:eq → =, neq → !=, contains → LIKE %val%, notContains → NOT LIKE %val%, startsWith → LIKE val%, endsWith → LIKE %val |
| number/currency | { operator, value } |
根据 operator 拼接 SQL:eq → =, neq → !=, gt → >, lt → <, gte → >=, lte → <=, contains → LIKE %val%, notContains → NOT LIKE %val% |
| enum | [value1, value2] |
IN (value1, value2) |
| date | { start, end } |
start 已拼接 00:00:00,end 已拼接 23:59:59,直接用 BETWEEN start AND end |
| boolean | true/false |
= true 或 = false |
设计要点:
filters中的 key 为fieldKey,后端需映射为实际数据库字段名- 日期筛选值前端已自动拼接时间后缀,后端可直接用于范围查询
- 枚举类型值为数组,后端使用
IN查询 - 排序字段
sortBy需映射为数据库字段名,sortOrder为ascending/descending - 建议对
filters中的值做 SQL 注入防护(参数化查询)
后端公共方法封装建议:
由于每个列表页面的筛选处理逻辑高度一致,建议封装公共方法,避免每个页面重复编写。核心思路:通过反射获取分页查询对象的字段,根据字段类型自动构建查询条件。
1. 操作符枚举定义
public enum FilterOperator {
EQ("eq", "="),
NEQ("neq", "!="),
CONTAINS("contains", "LIKE"),
NOT_CONTAINS("notContains", "NOT LIKE"),
STARTS_WITH("startsWith", "LIKE"),
ENDS_WITH("endsWith", "LIKE"),
GT("gt", ">"),
LT("lt", "<"),
GTE("gte", ">="),
LTE("lte", "<=");
private final String code;
private final String sql;
// getter, fromCode(String code) 静态方法
}
2. 公共筛选方法
/**
* 通用筛选条件构建器
* 通过反射获取分页对象的所有字段,根据 filters 参数自动构建 QueryWrapper 条件
*
* @param wrapper MyBatis-Plus QueryWrapper
* @param filters 前端传入的 filters 对象
* @param clazz 分页查询对象的 Class(字段名即 fieldKey,通过 @TableField 映射数据库列名)
*/
public static <T> void buildFilters(QueryWrapper<T> wrapper, Map<String, Object> filters, Class<T> clazz) {
if (filters == null || filters.isEmpty()) return;
for (Map.Entry<String, Object> entry : filters.entrySet()) {
String fieldKey = entry.getKey();
Object value = entry.getValue();
// 通过反射获取字段,确定字段类型
Field field = ReflectionUtils.findField(clazz, fieldKey);
if (field == null) continue;
Class<?> fieldType = field.getType();
String column = getColumnName(field); // 获取 @TableField 注解的列名,无注解则驼峰转下划线
if (value instanceof Map) {
// 操作符筛选:{ operator, value } 或 { start, end }
Map<String, Object> filterMap = (Map<String, Object>) value;
if (filterMap.containsKey("operator")) {
String operatorCode = (String) filterMap.get("operator");
Object filterValue = filterMap.get("value");
FilterOperator op = FilterOperator.fromCode(operatorCode);
if (op == null) continue;
applyOperator(wrapper, column, op, filterValue, fieldType);
} else if (filterMap.containsKey("start") && filterMap.containsKey("end")) {
// 日期范围
wrapper.between(column, filterMap.get("start"), filterMap.get("end"));
}
} else if (value instanceof List) {
// 枚举多选
List<?> values = (List<?>) value;
if (!values.isEmpty()) {
wrapper.in(column, values);
}
} else if (value instanceof Boolean) {
wrapper.eq(column, value);
}
}
}
/**
* 根据操作符类型应用查询条件
*/
private static void applyOperator(QueryWrapper<?> wrapper, String column, FilterOperator op, Object value, Class<?> fieldType) {
if (value == null || value.toString().isEmpty()) return;
String strVal = value.toString();
switch (op) {
case EQ: wrapper.eq(column, strVal); break;
case NEQ: wrapper.ne(column, strVal); break;
case CONTAINS: wrapper.like(column, strVal); break;
case NOT_CONTAINS: wrapper.notLike(column, strVal); break;
case STARTS_WITH: wrapper.likeRight(column, strVal); break;
case ENDS_WITH: wrapper.likeLeft(column, strVal); break;
case GT: wrapper.gt(column, strVal); break;
case LT: wrapper.lt(column, strVal); break;
case GTE: wrapper.ge(column, strVal); break;
case LTE: wrapper.le(column, strVal); break;
}
}
3. 使用示例
@PostMapping("/data")
public Result<PageResult<UserVO>> listUsers(@RequestBody PageRequest params) {
QueryWrapper<User> wrapper = new QueryWrapper<>();
// 一行代码完成所有筛选条件构建
FilterHelper.buildFilters(wrapper, params.getFilters(), User.class);
// 排序
if (params.getSortBy() != null) {
wrapper.orderBy(true, "descending".equals(params.getSortOrder()),
getColumnName(User.class, params.getSortBy()));
}
Page<User> page = userService.page(new Page<>(params.getPage(), params.getPageSize()), wrapper);
return Result.success(PageResult.of(page, UserVO.class));
}
设计要点:
- 操作符枚举统一管理,前端
operator值与后端枚举code一一对应 - 通过反射自动识别字段类型,无需每个页面手写 if-else
- 公共方法
buildFilters可在所有列表接口复用,新增页面零代码 @TableField注解自动映射 fieldKey → 数据库列名,支持驼峰转下划线