dynamic-table-vue2-element
v1.8.5
Published
基于 Vue2 + Element UI 的动态配置表格组件
Maintainers
Readme
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 }
}组件包还具名导出了
useTableConfig、useFilter两个内部 mixin(组件本身已集成,一般无需单独引入),供有定制需求的场景复用。
基础用法
<template>
<dynamic-table
menu-id="M001"
:field-meta-list="fieldMetaList"
:fetch-data-fn="fetchDataFn"
:export-data-fn="exportDataFn"
export-file-name="用户数据"
: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, exportUserList, 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, filterMultiple: false, 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', icon: 'el-icon-view' },
{ label: '编辑', action: 'edit', type: 'primary', icon: 'el-icon-edit' },
{ label: '删除', action: 'delete', type: 'danger', icon: 'el-icon-delete' }
]}
]
}
},
methods: {
fetchDataFn(params) {
return fetchUserList(params)
},
exportDataFn(params) {
return exportUserList(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
协议与兼容原则(必读)
以下规则以组件实际传给回调函数的参数为准,页面和后端不要再各自推断或重新组装协议:
- 分页参数默认使用
page、pageSize。 新接口应直接接收这两个默认字段。已有接口无需为了接入组件而改造:如果后端已经使用pageNum或其他字段名,通过pageParamName、pageSizeParamName配置组件即可。配置后,该名称就是组件实际输出的协议,后端必须按该名称接收。 fetchDataFn应原样透传组件参数。 不要在页面内把page改成pageNum,也不要把filters再包一层;参数名适配应通过组件 Props 完成。- 筛选结构必须按字段类型处理。 文本/数值使用
{ operator, value },区间使用{ range },单个日期或未转换的单月使用{ value },枚举和布尔使用标量或数组。后端不能把{ value }当作无效格式。 - 操作符语义不可混用。
eq是精确等于,contains才是包含;文本字段只是默认选择contains,不代表eq也按模糊查询处理。 - 全量导出默认使用组件内置
exportDataFn。 组件负责提交当前可见导出字段、字段顺序、合并后的筛选条件和排序条件,后端负责查询全部匹配数据并生成文件。业务可以通过toolbar-left另行实现特殊导出,但不要把它描述成组件默认导出协议。 - 业务逻辑只读取公开方法。 业务按钮需要使用当前筛选条件时调用
this.$refs.dynamicTable.getFilters();不要直接访问filterValues、columnSearchValues等内部状态,因为公开方法才会返回筛选面板、表头搜索和万能筛选合并后的最终请求条件。
“后端服从组件协议”指后端接收组件最终输出的字段名。默认输出是
page/pageSize;已有接口通过 Props 配置后可以继续使用原字段名,不要求存量接口改名。
Props
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|------|------|------|--------|------|
| menuId | String | 是 | - | 菜单ID,用于配置隔离 |
| fieldMetaList | Array | 是 | - | 字段元数据列表,定义表格列(含选择框、序号、操作列等特殊列) |
| fetchDataFn | Function | 是 | - | 数据查询函数,接收 params 参数,返回 Promise { list, total } |
| dataResponseAdapter | Function | 否 | null | 自定义列表响应转换函数,将接口响应转换为 { list, total } |
| exportDataFn | Function | 否 | null | 后端导出函数;接收当前字段、筛选和排序配置,返回 Blob、{ blob, fileName }、Axios Blob 响应或 { downloadUrl, fileName } |
| dataErrorHandler | Function | 否 | null | 分页查询失败后的统一错误处理函数 |
| exportFileName | String | 否 | '表格数据' | 导出接口未返回文件名时使用的默认文件名 |
| loadConfigFn | Function | 否 | null | 加载配置函数,接收 menuId,返回 Promise 配置对象 |
| saveConfigFn | Function | 否 | null | 保存配置函数,接收 config 对象,返回 Promise |
| configResponseAdapter | Function | 否 | null | 自定义配置响应转换函数,将接口响应转换为配置对象或 null |
| configErrorHandler | Function | 否 | null | 配置加载失败后的统一错误处理函数 |
| rowKey | String | 否 | 'id' | 行数据唯一标识字段 |
| reserveSelection | Boolean | 否 | false | 是否跨分页保留勾选;开启时必须保证 rowKey 全局唯一 |
| border | Boolean | 否 | true | 是否显示边框 |
| stripe | Boolean | 否 | true | 是否斑马纹 |
| tableHeight | String/Number | 否 | undefined | 固定表格高度,不设置则自适应(flex 布局) |
| maxHeight | String/Number | 否 | undefined | 表格最大高度,作为 tableHeight 或自适应高度的上限,超出后表格内部滚动 |
| showPagination | Boolean | 否 | true | 是否显示分页 |
| pageSizes | Array | 否 | [10,50,100,500] | 每页条数选项;当前页面初始展示条数取其中最小值。配置界面允许 1~2000,后端允许的最大值应与业务传入的选项一致 |
| pageSizeParamName | String | 否 | 'pageSize' | 组件传给 fetchDataFn 的分页大小字段名;已有后端字段不同时在此适配 |
| pageParamName | String | 否 | 'page' | 组件传给 fetchDataFn 的页码字段名;已有后端使用 pageNum 时配置为 pageNum |
| headerAlign | String | 否 | 'center' | 表头对齐方式,默认居中 |
| actionColumnWidth | String/Number | 否 | 150 | 操作列默认宽度 |
| defaultFilterValues | Object | 否 | {} | 筛选条件默认值 |
| filterCacheKey | String | 否 | '' | 筛选缓存 key,优先于 menuId |
| cacheFilters | Boolean | 否 | true | 是否将筛选条件缓存到浏览器 |
| filterPopperAppendToBody | Boolean | 否 | true | 筛选弹层是否挂载到 body |
| defaultVisibleFields | Array | 否 | [] | 默认展示的列字段,不传则全部展示 |
| defaultFilterFields | Array | 否 | [] | 默认展示的筛选项字段(筛选条件项的初始化值),需为 filterable 的字段;用户保存过筛选配置后以用户配置为准 |
| showSummary | Boolean | 否 | false | 是否显示合计行 |
| showUniversalFilter | Boolean | 否 | true | 是否显示万能筛选 |
fieldMetaList 字段定义
数据列
| 属性 | 类型 | 必填 | 说明 |
|------|------|------|------|
| fieldKey | String | 是 | 字段唯一标识 |
| fieldLabel | String | 是 | 字段显示名称 |
| fieldType | String | 是 | 字段类型:string / number / enum / date / boolean / currency |
| filterable | Boolean | 否 | 是否可筛选 |
| sortable | Boolean | 否 | 是否可排序 |
| width | Number | 否 | 列最小宽度(使用 min-width,自动扩展填满容器) |
| minWidth | Number | 否 | 列最小宽度,仅在 width 未设置时生效(兜底值) |
| formatter | Function | 否 | 单元格展示格式化函数 (value) => string,在类型格式化之后执行 |
| exportable | Boolean | 否 | 是否允许导出,默认为 true |
| exportFieldKey | String | 否 | 后端导出使用的字段标识;未配置时与 fieldKey 相同 |
| exportFieldLabel | String | 否 | 导出列名;未配置时使用 fieldLabel |
| exportFormat | String | 否 | 传给后端的导出格式标识,例如日期或金额格式 |
| align | String | 否 | 对齐方式:left / center / right |
| enumValues | Array/Object | 否 | 枚举值,支持数组 [{ label, value }] 或 Map { '001': '技术部' } |
| filterMultiple | Boolean | 否 | 枚举下拉是否允许多选,默认 true;设为 false 时筛选面板、表头筛选和万能筛选均为单选 |
| frozenPosition | String | 否 | 冻结方向提示:'left' / 'right';列是否冻结仍由表格配置及已保存的 frozenFields 决定 |
| dateFilterType | String | 否 | 日期筛选类型:daterange(默认)/ monthrange / month / date |
| monthToDateRange | Boolean | 否 | 默认 false;当 dateFilterType: 'month' 时,设为 true 会将所选月份转换为当月月初至月末的 { range } |
| dateFormat | String | 否 | 日期展示格式:yyyy-MM-dd(默认)/ yyyy-MM / yyyy-MM-dd HH:mm:ss |
字段元数据当前不支持 Element UI 风格的 fixed、visible、showOverflowTooltip 作为逐列控制项:
- 初始显示列使用组件 Prop
defaultVisibleFields;用户调整后的显示状态由表格配置保存。 - 冻结列通过“表格配置”设置并保存;
frozenPosition只用于表达冻结方向,不等同于fixed。 - 数据列统一启用内容溢出提示,当前不提供逐列关闭或开启的
showOverflowTooltip。
迁移旧页面时应删除这些无效字段,避免使用者误以为配置已经生效。
特殊列
特殊列(选择框、序号、操作列)通过 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', icon: 'el-icon-view' },
{ label: '编辑', action: 'edit', type: 'primary', icon: 'el-icon-edit' },
{ label: '删除', action: 'delete', type: 'danger', icon: 'el-icon-delete' }
]}| 属性 | 类型 | 必填 | 说明 |
|------|------|------|------|
| label | String | 是 | 按钮文字 |
| action | String | 是 | 操作标识,用于 row-action 事件回调中区分 |
| type | String | 否 | 按钮类型,默认 text。可选 primary/success/warning/danger/info/text 等 |
| icon | String | 否 | 按钮图标,如 el-icon-edit、el-icon-delete |
| style | Object | 否 | 按钮自定义样式 |
| visible | Boolean/Function | 否 | true | 是否显示该按钮,传函数 (row) => boolean 可按行动态控制 |
| disabled | Boolean/Function | 否 | false | 是否禁用该按钮,传函数 (row) => boolean 可按行动态控制 |
特殊列的
fieldKey推荐使用__selection、__index、__actions前缀,组件内部通过fieldType识别。不声明对应fieldType的特殊列则不会显示。
fieldType 说明
| 类型 | 说明 |
|------|------|
| selection | 选择框列,支持多选,触发 selection-change 事件 |
| index | 序号列,自动显示行号(从1开始) |
| actions | 操作列,通过 actions 属性配置按钮,触发 row-action 事件 |
| string | 文本,筛选时为操作符下拉+输入框,默认"包含" |
| number | 数值,筛选时为操作符下拉+输入框,默认"等于" |
| currency | 金额,自动千分位格式化并保留两位小数,筛选同 number |
| enum | 枚举,筛选时为过滤输入框+下拉选择;默认多选(含全选),配置 filterMultiple: false 后为单选。下拉框不支持输入,仅通过过滤框筛选选项,列展示自动显示 label;若未配置 enumValues 或选项为空,筛选/表头搜索自动退化为字符串输入框 |
| date | 日期,筛选类型由 dateFilterType 控制,展示格式由 dateFormat 控制 |
| boolean | 布尔,筛选时为是/否下拉 |
enumValues 格式
支持两种格式:
// 数组格式
enumValues: [{ label: '启用', value: 1 }, { label: '禁用', value: 0 }]
// Map 格式(key 为实际值,value 为显示名称)
enumValues: { '001': '技术部', '002': '市场部', '003': '人事部' }非 enum 类型字段配置了 enumValues 后,筛选面板和表头搜索也会自动使用下拉选择,列展示会自动做编码→名称映射。
日期筛选格式
| dateFilterType | 控件 | 请求值 |
|----------------|------|--------|
| daterange | 开始日期、结束日期 | { range: ['2024-01-01', '2024-01-31'] } |
| monthrange | 开始月份、结束月份 | { range: ['2024-01-01', '2024-03-31'] } |
| month | 单个月份 | 默认 { value: '2024-02' } |
| month + monthToDateRange: true | 单个月份 | { range: ['2024-02-01', '2024-02-29'] } |
| date | 单个日期 | { value: '2024-02-15' } |
筛选面板、表头筛选、万能筛选和筛选缓存使用相同结构。字段筛选类型发生变化时,组件会按当前结构校验缓存;不兼容的字段缓存会自动忽略。
操作列
- 通过
fieldMetaList中fieldType: 'actions'的项配置操作按钮 actions数组中每个按钮通过action标识区分,点击触发@row-action事件- 操作列默认不冻结,可在配置抽屉中设置冻结方向
- 操作列支持显隐、排序、宽度调整,与其他列统一管理
Events
| 事件名 | 参数 | 说明 |
|--------|------|------|
| selection-change | selection | 选中行变化时触发 |
| row-action | { action, row } | 操作列按钮点击时触发,action 为 rowActions 中配置的标识 |
| config-saved | config | 配置保存时触发,仅在未提供 saveConfigFn 时触发(无后端模式),config 为完整配置对象 |
| config-error | error | 配置加载失败时触发;组件会使用默认展示,但阻止保存以免覆盖服务端配置 |
| config-save-error | error | 配置保存失败时触发 |
| config-save-blocked | error | 配置加载失败后尝试保存时触发 |
| export-success | result | 导出文件成功后触发 |
| export-error | error | 导出失败后触发 |
| data-error | error | 分页查询失败后触发 |
Slots
| 插槽名 | 作用域参数 | 说明 |
|--------|-----------|------|
| toolbar-left | - | 工具栏左侧按钮区域 |
| column-{fieldKey} | { row, value } | 自定义列内容渲染 |
| actions | { row } | 自定义操作列内容,覆盖默认的操作按钮渲染 |
| loading | - | 自定义数据加载状态 |
| empty | - | 自定义空数据状态 |
| error | { error, retry } | 自定义加载失败状态,retry 可重新请求 |
实例方法
通过表格组件 ref 可调用:refresh()、retry()、resetFilters()、getFilters()、getExportFields()、getTableConfig()、getSelection()、clearSelection()、toggleRowSelection(row, selected)、doLayout() 和 getElTable()。
getFilters() 返回的就是下一次列表查询和组件内置导出使用的合并筛选条件。同步、批量处理、业务导出等按钮如果需要沿用表格当前条件,应这样读取:
handleSync() {
const filters = this.$refs.dynamicTable
? this.$refs.dynamicTable.getFilters()
: {}
return syncData({ filters })
}不要读取 this.$refs.dynamicTable.filterValues、columnSearchValues 或 lastCustomFilterValues。这些是组件内部的分段状态,单独读取任何一项都可能与当前列表的实际筛选条件不一致,后续版本也不保证其结构稳定。
fetchDataFn 参数格式
{
page: 1,
pageSize: 10,
filters: { // 筛选面板 + 表头搜索的合并条件
username: { operator: 'contains', value: '张' }, // 字符串:操作符+值
status: 1, // 单选枚举:标量(多选枚举仍为数组)
age: { operator: 'eq', value: 25 }, // 数值:操作符+值
salary: { operator: 'gte', value: 5000 }, // 金额:操作符+值
birthday: { range: ['2024-01-01', '2024-12-31'] }, // 日期范围
periodDate: { range: ['2024-02-01', '2024-02-29'] }, // 单月转日期范围
isActive: true // 布尔:直接传值
},
sortBy: 'createTime',
sortOrder: 'descending'
}上例使用组件默认分页字段。如果已有后端接收 pageNum,页面只需配置参数名:
<dynamic-table
page-param-name="pageNum"
page-size-param-name="pageSize"
:fetch-data-fn="fetchDataFn"
/>此时 fetchDataFn 收到的是 { pageNum, pageSize, filters, sortBy, sortOrder }。已有接口不需要改字段名;fetchDataFn 仍然直接把 params 交给接口,不做二次转换。新接口未配置上述 Props 时,应按默认的 page/pageSize 接收。
fetchDataFn 的第二个参数为请求上下文,可将 signal 交给 Axios 或 Fetch,以便组件在翻页、快速筛选、切换菜单和销毁时取消旧请求:
fetchDataFn(params, { signal }) {
return axios.post('/api/table/data', params, { signal })
}单个月份转换为日期范围:
{
fieldKey: 'periodDate',
fieldLabel: '所属月份',
fieldType: 'date',
dateFilterType: 'month',
monthToDateRange: true,
filterable: true
}筛选操作符说明
| 操作符值 | 含义 | 适用类型 | |----------|------|----------| | eq | 等于 | string, number, currency | | neq | 不等于 | string, number, currency | | contains | 包含 | string | | in | 多个(按逗号/顿号/斜杠拆分) | string | | notContains | 不包含 | string | | startsWith | 开头是 | string | | endsWith | 结尾是 | string | | gt | 大于 | number, currency | | lt | 小于 | number, currency | | gte | 大于等于 | number, currency | | lte | 小于等于 | number, currency |
字符串类型默认操作符为
contains,数值/金额类型默认操作符为eq。in操作符会将输入值按英文逗号,、中文逗号,、中文顿号、、斜杠/拆分为数组传递到后端。
这里的“默认”只表示控件初次选择的操作符,不会改变操作符本身的含义:
{ operator: 'eq', value: '张' }:只匹配值等于“张”的记录。{ operator: 'contains', value: '张' }:匹配值中包含“张”的记录。
后端必须按 operator 分支处理,不能把所有字符串条件统一解释为精确匹配或统一解释为模糊匹配。
fetchDataFn 返回格式
{
list: [], // 当前页数据
total: 100 // 总条数
}组件默认兼容直接返回、Axios { data } 包装以及常见的 { code, data } 包装。如接口字段不是 list、total,可使用:
<dynamic-table :data-response-adapter="res => ({ list: res.data.records, total: res.data.count })" />后端导出
传入 exportDataFn 后,“表格配置”左侧会显示纯下载图标。组件只提交点击时的当前可见数据字段、字段顺序、筛选条件和排序条件,不提交 page、pageSize:
点击下载图标后会提示“将根据表头配置进行导出,是否确认?”。确认后才调用 exportDataFn,取消或关闭弹窗不会发起导出请求。
exportDataFn({ menuId, fields, filters, sortBy, sortOrder }) {
return axios.post('/api/table/export', {
menuId,
fields,
filters,
sortBy,
sortOrder
}, { responseType: 'blob' })
}exportDataFn 收到的参数示例:
{
menuId: 'M001',
fields: [
{ fieldKey: 'username', exportFieldKey: 'user_name', fieldLabel: '用户名', fieldType: 'string', exportFormat: '' },
{ fieldKey: 'status', fieldLabel: '状态', fieldType: 'enum' }
],
filters: {
status: 1,
createTime: { range: ['2024-01-01', '2024-12-31'] }
},
sortBy: 'createTime',
sortOrder: 'descending'
}说明:
fields按当前表格展示顺序传递。- 隐藏字段以及选择框、序号、操作列不会导出。
exportable: false的字段不会导出;后端优先使用exportFieldKey识别导出字段。- 展示格式函数不能序列化给后端;服务端应根据
exportFieldKey、fieldType和exportFormat完成格式化。 - 导出条件包含筛选面板、表头搜索和万能筛选的合并结果。
- 服务端负责查询全部匹配数据并生成文件;组件不会循环查询分页接口。
- 支持直接返回
Blob、{ blob, fileName }、Axios Blob 响应或{ downloadUrl, fileName }。 - Axios 模式必须配置
responseType: 'blob'。
这是组件的默认全量导出方式,接入方通常只需要实现 exportDataFn。如果某个页面存在完全不同的业务导出(例如导出勾选记录、导出固定模板),可以在 toolbar-left 中自行增加按钮;该按钮的参数和交互由业务页面负责,不会替代或改变 exportDataFn 的协议。为避免出现两个含义相同的导出入口,同一页面不要同时配置内置全量导出和同功能的自定义按钮。
loadConfigFn 返回格式
{
version: 2,
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":{}}]',
settings: '{"pageSizes":[10,50,100,500],"showSummary":true,"showUniversalFilter":true}'
}version: 2 将分页选项和功能开关独立存入 settings。组件仍兼容旧配置中保存在 columnWidths 的 __pageSizes、__showSummary、__showUniversalFilter,读取后自动迁移,后续保存即写为 version 2。
配置接口同样兼容直接对象、Axios 和 { code, data } 包装。若接口结构不同,可通过 configResponseAdapter 转换。配置加载异常会触发 config-error,且在重新加载成功前阻止保存;连续保存会按顺序执行,避免旧请求晚返回覆盖新配置。
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单独设置数据对齐方式
筛选配置
- 勾选需要作为筛选项的字段,拖拽调整顺序
- 筛选面板根据字段类型自动生成对应控件
- 字符串/数值/金额类型:操作符下拉 + 值输入框
- 枚举类型:过滤输入框 + 下拉选择;默认多选(含全选,选项 > 1 时显示),
filterMultiple: false时单选 - 日期类型:由
dateFilterType控制筛选格式(daterange/monthrange/month/date) - 布尔类型:是/否下拉
- 日期、月份区间和开启
monthToDateRange的单月筛选统一使用{ range: [开始日期, 结束日期] } - 筛选区域超过3行自动出现滚动条
- 筛选条件默认缓存到浏览器 localStorage,可通过
cacheFilters属性控制 - 重置按钮同时清除筛选面板条件、表头搜索条件、排序和浏览器缓存
默认筛选项
通过 defaultFilterFields 属性可指定页面首次加载时默认展示的筛选项字段(对应配置抽屉"筛选配置"中勾选的结果,即内部 filterFields 的初始化值):
<dynamic-table
menu-id="M001"
:field-meta-list="fieldMetaList"
:fetch-data-fn="fetchDataFn"
:default-filter-fields="['username', 'status', 'createTime']"
/>说明:
- 该属性只作为筛选项的初始化值,仅在没有用户已保存配置时生效。
- 传值需为
fieldMetaList中filterable: true的字段,无效或重复的字段会被自动过滤,顺序按传入顺序展示。 - 用户通过"表格配置 → 筛选配置"调整并保存后,一律以用户保存的配置为准(即使保存为空数组,也会按用户的选择不展示任何筛选项)。
- 点击"还原默认"后,筛选项会恢复为该属性定义的默认值。
- 不传该属性时保持原行为:默认不展示任何筛选项。
万能筛选
- 筛选面板底部左侧提供带“万能筛选”标题的万能筛选区域,可动态选择任意字段进行筛选
- “常用方案”位于“保存筛选”按钮左侧,与查询、重置操作集中显示在右侧
- 已输入或已选择值的筛选项会显示蓝色边框、浅蓝背景和高亮标题,清空后自动恢复默认样式
- 筛选项标题宽度为 80px,超出时显示省略号,鼠标移入后通过 Tooltip 立即显示完整名称;下载图标悬停显示“导出”
- 选择字段后自动根据字段类型渲染对应筛选控件
- 可通过
showUniversalFilter属性或在配置抽屉中控制显隐
合计行
- 设置
showSummary属性开启合计行 - 当前页数据由组件求和,名称显示为“当前页合计”
- 合计标签显示在第一个非金额/数字类型的可见数据列
- 可在配置抽屉的"参数配置" tab 中开关合计行
动态属性与缓存兼容
menuId变化时自动重新加载对应配置、筛选缓存和分页数据。fieldMetaList变化时自动删除失效字段配置并追加新字段,随后重新布局和查询。- 未保存用户配置时,
defaultVisibleFields、defaultFilterFields和pageSizes的变化会即时生效。 - 字段筛选类型与旧缓存结构不匹配时,该字段旧缓存会被忽略,无需用户手动清除。
筛选方案
- 最多保存5个常用筛选方案
- 支持修改方案名称(点击名称或编辑图标)
- 使用方案后修改筛选值,保存时提示覆盖或新建
- 重置按钮同时清除筛选面板条件、表头搜索条件和排序
跨页选择
- 设置
reserveSelection后,组件除使用 Element UI 的保留选择外,还会按rowKey独立保存选中数据。 - 翻页或表格配置导致表格重建时会自动恢复当前页勾选,
getSelection()返回全部已保留行。 rowKey必须在整个数据集内唯一;调用clearSelection()可清除全部跨页选择。
还原默认配置
- 配置抽屉底部提供"还原默认"按钮
- 点击后二次确认,确认后将所有配置还原为初始化状态
- 还原内容包括:表头显隐/顺序/冻结/列宽、筛选方案;筛选字段配置还原为
defaultFilterFields属性定义的默认筛选项(未传该属性则为空) - 同时清除浏览器中当前菜单的本地缓存
- 还原后立即生效,表格自动刷新
表头搜索
- 点击表头名称弹出下拉菜单,支持排序和搜索
- 排序三态切换:升序 → 降序 → 无
- 字符串/数值/金额类型搜索支持操作符选择
- 枚举类型搜索:过滤输入框 + 下拉选择 + 搜索确认按钮;默认多选,
filterMultiple: false时单选 - 日期类型搜索由
dateFilterType控制选择器格式 - 表头搜索条件与筛选面板条件合并后一起传给 fetchDataFn
自适应高度
- 不设置
tableHeight时,表格使用 flex 布局自适应填满容器 - 筛选区域展开/收起后自动重新分配空间
- 支持 keep-alive 缓存页面,激活时自动重新布局
列宽自适应
- 数据列使用
min-width,列不会小于配置宽度 - 当所有列宽之和小于容器宽度时,列自动按比例扩展填满
- 当所有列宽之和超过容器宽度时,正常横向滚动
分页
- 固定在表格底部,不随数据高度变化
- 可在配置抽屉的“参数配置” tab 中自定义分页条数选项,当前页面初始展示条数始终取已选选项中的最小值
- 默认已选条数为 10/50/100/500;支持预设条数快选(10/20/50/100/200/500/1000/2000),也可手动输入 1~2000 的自定义条数
- 清空用户保存的分页选项时,会回退到
pageSizesProp;不会固定回退为 1。未传pageSizes时回退到默认的 10/50/100/500,并取 10 作为当前条数 - 后端分页上限必须与页面实际提供的
pageSizes一致。如果后端最多只允许 500,页面就不应提供 1000/2000;若页面允许选择 2000,后端也必须正确接收 2000,不能静默截断后仍返回不匹配的分页结果 - 分页字段默认是
page/pageSize。存量接口可用pageParamName/pageSizeParamName保留原字段名,配置后的字段名就是后端必须接收的协议
后端接口设计
1. 保存配置接口
请求方式:POST /api/table/config
请求参数:
{
"version": 2,
"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\":\"张\"}}}]",
"settings": "{\"pageSizes\":[10,50,100,500],\"showSummary\":true,\"showUniversalFilter\":true}"
}响应格式:
{
"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,后端从登录态获取',
config_version INT NOT NULL DEFAULT 2 COMMENT '配置结构版本',
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',
settings 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": {
"version": 2,
"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\":{}}]",
"settings": "{\"pageSizes\":[10,50,100,500],\"showSummary\":true,\"showUniversalFilter\":true}"
}
}设计要点:
- 无配置时返回
null或空对象,组件会使用默认配置 - 返回的 JSON 字符串由前端解析,后端无需处理
3. 列表数据查询接口
请求方式:POST /api/table/data
请求参数:
{
"page": 1,
"pageSize": 10,
"filters": {
"username": { "operator": "contains", "value": "张" },
"status": 1,
"age": { "operator": "gte", "value": 20 },
"salary": { "operator": "lte", "value": 50000 },
"birthday": { "range": ["2024-01-01", "2024-12-31"] },
"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, in → IN (val1, val2, ...) |
| number/currency | { operator, value } | 根据 operator 拼接 SQL:eq → =, neq → !=, gt → >, lt → <, gte → >=, lte → <= |
| enum(单选) | value | = value |
| enum(多选) | [value1, value2] | IN (value1, value2) |
| date(日期/月份区间或单月转区间) | { range: [start, end] } | 使用 range[0]、range[1] 进行区间查询 |
| date(单个日期或未转换的单月) | { value } | 使用 value 进行等值查询;单月值格式为 yyyy-MM |
| boolean | true/false | = true 或 = false |
设计要点:
filters中的 key 为fieldKey,后端需映射为实际数据库字段名daterange、monthrange以及monthToDateRange: true的单月筛选使用{ range };date和默认单月筛选使用{ value },后端必须同时支持这两种日期结构- 枚举单选值为标量,使用等值查询;多选值为数组,使用
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"),
IN("in", "IN"),
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 }、{ range: [start, end] } 或 { value }
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("range")) {
// 日期范围
List<?> range = (List<?>) filterMap.get("range");
if (range.size() == 2) {
wrapper.between(column, range.get(0), range.get(1));
}
} else if (filterMap.containsKey("value")) {
// 单个日期或未转换的单月
Object singleValue = filterMap.get("value");
if (singleValue != null && !singleValue.toString().isEmpty()) {
wrapper.eq(column, singleValue);
}
}
} 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);
} else if (value instanceof String || value instanceof Number) {
// 枚举单选等标量筛选
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 IN:
if (value instanceof List) {
wrapper.in(column, (List<?>) value);
}
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 → 数据库列名,支持驼峰转下划线
4. 全量导出接口
请求方式:POST /api/table/export
导出接口不接收分页参数。后端必须对 fields 和排序字段进行白名单校验,并使用当前筛选条件查询全部匹配数据。
请求参数:
{
"menuId": "M001",
"fields": [
{ "fieldKey": "username", "fieldLabel": "用户名", "fieldType": "string" },
{ "fieldKey": "status", "fieldLabel": "状态", "fieldType": "enum" },
{ "fieldKey": "createTime", "fieldLabel": "创建时间", "fieldType": "date" }
],
"filters": {
"status": 1,
"createTime": { "range": ["2024-01-01", "2024-12-31"] }
},
"sortBy": "createTime",
"sortOrder": "descending"
}同步文件响应:
Content-Type: text/csv; charset=utf-8
Content-Disposition: attachment; filename*=UTF-8''users.csv前端应使用 responseType: 'blob'。组件会优先读取 Content-Disposition 文件名;读取不到时使用 exportFileName。
后端也可以返回已生成文件的地址:
{
"downloadUrl": "/api/table/export/download/abc123",
"fileName": "用户数据.xlsx"
}大数据量场景建议使用流式 CSV 或异步导出任务,避免在前端循环请求分页数据,也避免在服务端一次性将全部数据保存在内存中。
开发与发布检查
组件包包含 types/index.d.ts,TypeScript 项目可直接获得字段元数据、查询参数、导出参数及实例方法类型。
npm test # 运行纯函数单元测试
npm run check # 单元测试 + 组件库构建
npm run build # 示例应用生产构建执行 npm publish 前会自动运行测试和生产模式组件库构建。
