npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

dynamic-table-vue2-element

v1.8.5

Published

基于 Vue2 + Element UI 的动态配置表格组件

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 }
}

组件包还具名导出了 useTableConfiguseFilter 两个内部 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

协议与兼容原则(必读)

以下规则以组件实际传给回调函数的参数为准,页面和后端不要再各自推断或重新组装协议:

  1. 分页参数默认使用 pagepageSize 新接口应直接接收这两个默认字段。已有接口无需为了接入组件而改造:如果后端已经使用 pageNum 或其他字段名,通过 pageParamNamepageSizeParamName 配置组件即可。配置后,该名称就是组件实际输出的协议,后端必须按该名称接收。
  2. fetchDataFn 应原样透传组件参数。 不要在页面内把 page 改成 pageNum,也不要把 filters 再包一层;参数名适配应通过组件 Props 完成。
  3. 筛选结构必须按字段类型处理。 文本/数值使用 { operator, value },区间使用 { range },单个日期或未转换的单月使用 { value },枚举和布尔使用标量或数组。后端不能把 { value } 当作无效格式。
  4. 操作符语义不可混用。 eq 是精确等于,contains 才是包含;文本字段只是默认选择 contains,不代表 eq 也按模糊查询处理。
  5. 全量导出默认使用组件内置 exportDataFn 组件负责提交当前可见导出字段、字段顺序、合并后的筛选条件和排序条件,后端负责查询全部匹配数据并生成文件。业务可以通过 toolbar-left 另行实现特殊导出,但不要把它描述成组件默认导出协议。
  6. 业务逻辑只读取公开方法。 业务按钮需要使用当前筛选条件时调用 this.$refs.dynamicTable.getFilters();不要直接访问 filterValuescolumnSearchValues 等内部状态,因为公开方法才会返回筛选面板、表头搜索和万能筛选合并后的最终请求条件。

“后端服从组件协议”指后端接收组件最终输出的字段名。默认输出是 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 风格的 fixedvisibleshowOverflowTooltip 作为逐列控制项:

  • 初始显示列使用组件 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-editel-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' } |

筛选面板、表头筛选、万能筛选和筛选缓存使用相同结构。字段筛选类型发生变化时,组件会按当前结构校验缓存;不兼容的字段缓存会自动忽略。

操作列

  • 通过 fieldMetaListfieldType: '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.filterValuescolumnSearchValueslastCustomFilterValues。这些是组件内部的分段状态,单独读取任何一项都可能与当前列表的实际筛选条件不一致,后续版本也不保证其结构稳定。

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,数值/金额类型默认操作符为 eqin 操作符会将输入值按英文逗号 ,、中文逗号 、中文顿号 、斜杠 / 拆分为数组传递到后端。

这里的“默认”只表示控件初次选择的操作符,不会改变操作符本身的含义:

  • { operator: 'eq', value: '张' }:只匹配值等于“张”的记录。
  • { operator: 'contains', value: '张' }:匹配值中包含“张”的记录。

后端必须按 operator 分支处理,不能把所有字符串条件统一解释为精确匹配或统一解释为模糊匹配。

fetchDataFn 返回格式

{
  list: [],    // 当前页数据
  total: 100   // 总条数
}

组件默认兼容直接返回、Axios { data } 包装以及常见的 { code, data } 包装。如接口字段不是 listtotal,可使用:

<dynamic-table :data-response-adapter="res => ({ list: res.data.records, total: res.data.count })" />

后端导出

传入 exportDataFn 后,“表格配置”左侧会显示纯下载图标。组件只提交点击时的当前可见数据字段、字段顺序、筛选条件和排序条件,不提交 pagepageSize

点击下载图标后会提示“将根据表头配置进行导出,是否确认?”。确认后才调用 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 识别导出字段。
  • 展示格式函数不能序列化给后端;服务端应根据 exportFieldKeyfieldTypeexportFormat 完成格式化。
  • 导出条件包含筛选面板、表头搜索和万能筛选的合并结果。
  • 服务端负责查询全部匹配数据并生成文件;组件不会循环查询分页接口。
  • 支持直接返回 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 类型

操作列

  • 通过 fieldMetaListfieldType: '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']"
/>

说明:

  • 该属性只作为筛选项的初始化值,仅在没有用户已保存配置时生效。
  • 传值需为 fieldMetaListfilterable: true 的字段,无效或重复的字段会被自动过滤,顺序按传入顺序展示。
  • 用户通过"表格配置 → 筛选配置"调整并保存后,一律以用户保存的配置为准(即使保存为空数组,也会按用户的选择不展示任何筛选项)。
  • 点击"还原默认"后,筛选项会恢复为该属性定义的默认值。
  • 不传该属性时保持原行为:默认不展示任何筛选项。

万能筛选

  • 筛选面板底部左侧提供带“万能筛选”标题的万能筛选区域,可动态选择任意字段进行筛选
  • “常用方案”位于“保存筛选”按钮左侧,与查询、重置操作集中显示在右侧
  • 已输入或已选择值的筛选项会显示蓝色边框、浅蓝背景和高亮标题,清空后自动恢复默认样式
  • 筛选项标题宽度为 80px,超出时显示省略号,鼠标移入后通过 Tooltip 立即显示完整名称;下载图标悬停显示“导出”
  • 选择字段后自动根据字段类型渲染对应筛选控件
  • 可通过 showUniversalFilter 属性或在配置抽屉中控制显隐

合计行

  • 设置 showSummary 属性开启合计行
  • 当前页数据由组件求和,名称显示为“当前页合计”
  • 合计标签显示在第一个非金额/数字类型的可见数据列
  • 可在配置抽屉的"参数配置" tab 中开关合计行

动态属性与缓存兼容

  • menuId 变化时自动重新加载对应配置、筛选缓存和分页数据。
  • fieldMetaList 变化时自动删除失效字段配置并追加新字段,随后重新布局和查询。
  • 未保存用户配置时,defaultVisibleFieldsdefaultFilterFieldspageSizes 的变化会即时生效。
  • 字段筛选类型与旧缓存结构不匹配时,该字段旧缓存会被忽略,无需用户手动清除。

筛选方案

  • 最多保存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 的自定义条数
  • 清空用户保存的分页选项时,会回退到 pageSizes Prop;不会固定回退为 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 结构与 fetchDataFnfilters 参数一致
  • 建议使用 INSERT ... ON DUPLICATE KEY UPDATEUPSERT 语义实现保存

数据库表设计参考

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!=, containsLIKE %val%, notContainsNOT LIKE %val%, startsWithLIKE val%, endsWithLIKE %val, inIN (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,后端需映射为实际数据库字段名
  • daterangemonthrange 以及 monthToDateRange: true 的单月筛选使用 { range }date 和默认单月筛选使用 { value },后端必须同时支持这两种日期结构
  • 枚举单选值为标量,使用等值查询;多选值为数组,使用 IN 查询
  • 排序字段 sortBy 需映射为数据库字段名,sortOrderascending / 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 前会自动运行测试和生产模式组件库构建。