bw-pro-table

基于 ant-design-vue 的高级业务表格组件(ProTable),支持请求/数据源双模式、行内编辑、搜索筛选、列拖拽排序、导出导入、跨页多选、持久化等
3,314
Install
npm install bw-pro-table
Latest Version:0.1.9
Vue:3
License:MIT
Last Updated:Oct 21, 2026
Links: npm
Maintainer: weiloong

BwProTable

基于 ant-design-vue 4 的高级业务表格组件(ProTable),提供开箱即用的数据表格体验。采用 Vue 3 Composition API + TypeScript 编写,提供完整的类型推导。

特性

  • 🚀 双模式数据加载:request 异步请求模式 与 dataSource 静态数据模式互斥,支持自动竞态保护与页码越界校验;分页支持自定义每页条数选项(pagination.pageSizeOptions)
  • 🎨 13 种内置渲染器:text、number、date、dateTime、tag、image、link、progress、avatar、action、index、currency、boolean(含 enum 别名),支持 defineColumnType 注册自定义类型
  • 🔍 三管道筛选系统:搜索栏 + 表头筛选(ant-design-vue 原生)+ 列头搜索(Popover),独立运行并合并为 mergedSearchParams
  • 📊 客户端筛选与排序:DataSource 模式下自动应用 applyClientFilters(三管道合并)与 applyClientSort(支持自定义 sorter 函数)
  • ✏️ 行内编辑:单元格级 / 行级双模式编辑,草稿数据管理,字段校验(required/type/min/max/pattern/validator),撤销/重做(Command Pattern),批量编辑
  • 📋 列管理:HTML5 拖拽排序列、列宽调节(rAF 批处理节流)、列显隐设置面板,特殊列自动排除
  • 💾 状态持久化:列配置、分页、密度、筛选、排序持久化到 localStorage / sessionStorage / 自定义 StorageAdapter,支持版本管理与数据校验
  • ✅ 跨页多选:跨页保持选中状态,支持全选所有页(DataSource 内存 / Request 回调),最大选择数限制
  • 📎 Excel 导入导出:xlsx 按需懒加载,导出 Excel/CSV/选中行,导入预览/自动列映射/校验/模板下载
  • 🔐 权限控制:permissions 声明式裁剪列与操作按钮(渲染、搜索字段、列设置、导出列、导入映射、批量编辑、编辑校验全管线收口),列级编辑权限(可见但只读)、单元格脱敏(fail-closed,导出同样脱敏)、细粒度能力门控与权限表达式;不传即不启用
  • 🔄 工具栏:标题、刷新、密度切换、全屏、列设置、边框/斑马纹切换、清除筛选、选中计数显示
  • 🌐 国际化:vue-i18n 集成,locale prop 部分覆写,优先级:props.locale > vue-i18n > zh-CN 默认
  • 🎯 TypeScript 全类型支持:完整的 Props / Events / Slots / Methods / Composables 类型导出

安装

npm install bw-pro-table

依赖要求

必选 peerDependencies:

依赖 版本 说明
vue ^3.3.0 核心框架
ant-design-vue ^4.0.0 UI 组件库
@ant-design/icons-vue ^7.0.0 图标库
dayjs ^1.11.0 日期处理(渲染器与编辑态使用)
vue-i18n ^9.0.0 || ^10.0.0 || ^11.0.0 国际化

可选 peerDependencies(按需安装):

依赖 说明 何时需要
xlsx ^0.18.0 Excel 导入导出功能(按需动态 import() 懒加载)
screenfull ^6.0.0 工具栏全屏切换功能

运行环境:

  • Node.js >=20.19.0

快速开始

<template>
  <BwProTable
    :columns="columns"
    :request="fetchUsers"
    :row-selection="rowSelection"
    :toolbar="{ title: '用户列表', refresh: true, columnSetting: true }"
    :search="{}"
  />
  <!-- 注意:搜索栏与持久化均为「显式开启」型 prop。
       未传 :search 时不会渲染搜索栏;持久化同理需要 :persistence="{}"。
       详见下方 Props 表格与「版本变更与迁移说明」。 -->
</template>

<script setup lang="ts">
import { BwProTable } from 'bw-pro-table';
import type { ProColumnsType } from 'bw-pro-table';

interface User {
  id: number;
  name: string;
  age: number;
  email: string;
  status: 'active' | 'inactive';
  createdAt: string;
}

const columns: ProColumnsType<User> = [
  { title: 'ID', dataIndex: 'id', width: 80, valueType: 'index' },
  { title: '姓名', dataIndex: 'name', valueType: 'text', headerSearch: true },
  { title: '年龄', dataIndex: 'age', valueType: 'number', sorter: true },
  { title: '邮箱', dataIndex: 'email', valueType: 'link' },
  {
    title: '状态',
    dataIndex: 'status',
    valueType: 'tag',
    valueEnum: {
      active: { text: '启用', status: 'Success' },
      inactive: { text: '禁用', status: 'Error' },
    },
  },
  { title: '创建时间', dataIndex: 'createdAt', valueType: 'date' },
];

const fetchUsers = async (params: { current: number; pageSize: number; [key: string]: any }) => {
  const res = await fetch('/api/users?' + new URLSearchParams(params));
  const json = await res.json();
  return { data: json.list, total: json.total, success: true };
};

const rowSelection = {
  type: 'checkbox' as const,
  onChange: (keys: (string | number)[], rows: User[]) => {
    console.log('选中:', keys, rows);
  },
};
</script>

API 文档

Props

核心数据
参数 类型 默认值 说明
columns ProColumnsType<RecordType> 必填 列定义数组,详见 ProColumnType
request (params) => Promise<{data, total, success}> - 异步数据请求函数,与 dataSource 互斥
dataSource RecordType[] - 静态数据源,与 request 互斥
rowKey string | GetRowKey 'id' 行唯一标识字段名或函数(函数式 rowKey 时编辑/选择等功能可能受限,建议用字符串)
tableId string 随机生成 表格唯一标识,持久化命名空间必需
cacheKey string - 持久化缓存键(与 tableId 二选一,优先使用 cacheKey)
权限
参数 类型 默认值 说明
permissions PermissionItem[] undefined 当前用户权限列表({ name: string }[])。三态语义:不传 = 权限控制未启用,全部放行;[] = 已加载但无任何权限,隐藏所有带 authority 的项;列表 = 按白名单判定。详见 权限控制

注意:未声明 authority 的列与操作按钮视为公开项,任何权限状态下都可见。 传入 permissions 后,无权限列会从渲染、搜索字段、列设置面板、导出列、导入映射、模板下载、批量编辑与编辑校验中一并移除。 ⚠️ 权限异步下发时请保持 undefined(例如 computed(() => userStore.role?.permissions)),不要在加载中回退为 []。

参数 类型 默认值 说明
permissionConfig PermissionConfig undefined 细粒度能力门控(actions.export / import / columnSetting / batchEdit)。不传则不做任何追加门控
搜索
参数 类型 默认值 说明
search boolean | SearchConfig false 搜索栏配置。显式开启型:未传入时为 false,因此不渲染搜索栏;true 全默认值启用,对象启用并自定义
searchValues Record<string, any> undefined 受控搜索条件(配合 v-model:searchValues)。undefined 表示非受控;仅托管搜索栏条件,详见下文「代码控制搜索条件」
columnSearch false | HeaderSearchConfig - 列头搜索全局配置,false 禁用所有列的 headerSearch

SearchConfig:{ labelWidth?: number; defaultCollapsed?: boolean; resetText?: string; searchText?: string; collapsedCount?: number; colon?: boolean; searchFields?: SearchFieldConfig[]; showSearchBtn?: boolean; showResetBtn?: boolean; showCollapseBtn?: boolean; searchBtnProps?: Record<string, any>; resetBtnProps?: Record<string, any>; collapseBtnProps?: Record<string, any>; renderSearchActions?: (context) => VNode; responsiveColumns?: Record<number, number> }

  • labelWidth:搜索栏标签宽度,默认 80
  • defaultCollapsed:搜索栏默认是否折叠,默认 true(字段数不超过 collapsedCount 时视觉无差异)
  • collapsedCount:折叠状态下最多显示的搜索字段数量,默认 4(persistOnCollapse 字段不占用该上限)
  • colon:是否显示 label 后的冒号,默认 true
  • searchFields:自定义搜索字段列表。未提供时降级为从 columns 自动生成(向后兼容)
  • showSearchBtn:是否显示查询按钮,默认 true
  • showResetBtn:是否显示重置按钮,默认 true
  • showCollapseBtn:是否显示展开/收起按钮,默认当字段数 > collapsedCount 时显示
  • searchBtnProps:透传给查询按钮的额外 props(如 disabled、size、ghost 等)
  • resetBtnProps:透传给重置按钮的额外 props
  • collapseBtnProps:透传给展开/收起按钮的额外 props
  • renderSearchActions:自定义渲染整个操作按钮区域(最高优先级),接收 { search, reset, collapsed, toggleCollapsed, canCollapse } 上下文
  • responsiveColumns:响应式列数配置,Key 为容器最小宽度(px),Value 为列数。默认 { 576: 2, 768: 3, 992: 4 }。基于 ResizeObserver 监听搜索表单容器实际宽度(非 viewport),适配弹窗/抽屉等非全宽布局
工具栏
参数 类型 默认值 说明
toolbar false | ToolbarConfig - 工具栏配置,false 隐藏整个工具栏

ToolbarConfig:{ title?: string; refresh?: boolean; density?: boolean; fullscreen?: boolean; columnSetting?: boolean; export?: boolean | ExportConfig; clearFilter?: boolean; import?: boolean | ImportConfig; bordered?: boolean; striped?: boolean }

  • title:工具栏左侧标题文本(注意:这是 BwProTable 自己的工具栏标题,非 antd Table 原生的 title,后者通过 $attrs 透传)
  • bordered:是否显示边框切换开关(默认 true)
  • striped:是否显示斑马纹切换开关(默认 true)
功能配置
参数 类型 默认值 说明
editConfig EditConfig - 行内编辑配置,详见 行内编辑
dragConfig DragConfig - 拖拽配置,columnDraggable 启用表头列拖拽排序(固定列自动排除)
rowSelection TableRowSelection - 行选择配置(透传 ant-design-vue)
crossPageSelection false | CrossPageSelectionConfig - 跨页多选配置,false 禁用
persistence false | PersistenceConfig false 持久化配置。显式开启型:未传入时为 false,因此禁用;传 {} 或 { enabled: true } 启用

注意:导出/导入配置通过 toolbar.export / toolbar.import 传入,不存在独立的 exportConfig / importConfig 顶层 prop。

样式与外观
参数 类型 默认值 说明
size 'small' | 'middle' | 'default' 'default' 表格尺寸
bordered boolean false 是否显示边框
striped boolean false 是否显示斑马纹
defaultEmptyText string '-' 空单元格占位符(null/undefined/'' 时显示)
loading boolean | { spinning?: boolean; tip?: string } - 外部加载态(支持 antd SpinProps 对象形式,与内部状态机合并,用户传入值优先)
pagination false | TablePaginationConfig {} 分页配置,false 禁用分页;支持 pageSize / pageSizeOptions 等,详见 分页配置
透传与扩展
参数 类型 默认值 说明
locale Partial<BwProTableLocale> - 国际化语言包,传入部分字段即可覆写
tableLocale Record<string, any> - 透传给底层 <a-table> 的 antd 原生 locale(如 Empty 组件文案)。若同时使用 antd 原生 :locale prop,tableLocale 优先级更高;若 tableLocale 未设置,则 :locale 正常工作
rowClassName (record, index) => string - 行 className 函数(与内置斑马纹/选中样式合并)

纯透传属性:以下 ant-design-vue Table 原生属性不单独声明,通过 Vue $attrs + v-bind="$attrs" 自然流通至底层 <a-table>: scroll、sticky、customRow、customHeaderRow、customCell、customHeaderCell、showHeader、indentSize、tableLayout、defaultExpandAllRows、class、style 等。

注意:customCell 的 class 会与内部溢出 class 合并(非替换);customHeaderCell 的事件处理器会与内部拖拽处理器链式调用(非替换)。

Events

事件 参数 说明
load { dataSource: any[], total: number } 数据加载成功完成
error Error 数据加载失败
change (pagination, filters, sorter, extra) 表格变化(分页/排序/筛选),extra 含 action: 'paginate' | 'sort' | 'filter' 和 currentDataSource: any[]
resizeColumn (width: number, column: ProColumnType) 列宽调整
update:searchValues Record<string, any> 搜索栏条件回写(配合 v-model:searchValues)。触发时机:用户点查询 / 用户点重置 / 调用 setSearchParams 系列 API;仅包含搜索栏条件,不含表头筛选、列头搜索与排序

Methods(通过 ref 调用)

数据与选择
方法 签名 说明
fetchData () => Promise<void> 手动刷新数据
getDataSource () => any[] 获取当前数据源
getSelectedRowKeys () => (string | number)[] 获取当前页选中行 keys
getSelectedRows () => any[] 获取当前页选中行数据
clearSelection () => void 清空所有选择(含跨页)
getCrossPageSelectedKeys () => (string | number)[] 获取跨页选中的所有 keys
getCrossPageSelectedRows () => (any | null)[] 获取跨页选中的行数据(Request 模式下部分可能为 null)
列信息
方法 签名 说明
getDisplayColumns () => ProColumnType[] 获取当前显示中的列快照(已按 permissions 裁剪权限、过滤 hidden 列、按列设置/拖拽后的顺序排列、含 width/filteredValue/sortOrder 注入后的元信息)。返回值为浅拷贝数组,外部 push/splice 不影响内部状态
getAllColumns () => ProColumnType[] 获取全部列定义(含 hidden: true 的隐藏列与当前无权限的列),按原始声明顺序返回。不受 permissions 影响

典型用法:拿到列快照后,配合 getDataSource() 自行过滤数据列、生成自定义下载文件(CSV/Excel/JSON 等),或作为自定义导出列选择器的数据源。

行内编辑
方法 签名 说明
startRowEdit (rowKey) => Promise<void> 进入行编辑模式
startCellEdit (rowKey, field) => Promise<void> 进入单元格编辑模式
saveEdit (rowKey) => Promise<boolean> 保存编辑(返回是否校验通过)
cancelEdit (rowKey, field?) => void 取消编辑(传入 field 时仅取消该单元格)
addRow (position?: 'top' | 'bottom') => void 新增空行(默认追加到底部)
cloneRow (rowKey) => void 克隆行(插入到顶部)
deleteRow (rowKey) => Promise<void> 删除单行
deleteRows (keys) => Promise<void> 批量删除行
isCellEditing (rowKey, field) => boolean 判断单元格是否编辑中
getEditingKeys () => Set<string | number> 获取当前编辑中的行 keys
canUndo () => boolean 是否可撤销
canRedo () => boolean 是否可重做
undo () => void 撤销
redo () => void 重做
openBatchEdit () => void 打开批量编辑弹窗
搜索
方法 签名 说明
resetSearch () => void 重置搜索表单到初始状态(恢复各字段 defaultValue)并重新加载数据,效果等同点击搜索栏「重置」按钮
setSearchParams (values, options?) => void 用代码设置搜索栏条件。默认合并、默认立即查询、默认写入持久化;详见下文「代码控制搜索条件」
setSearchFieldValue (field, value, options?) => void 单字段语法糖,等价于 setSearchParams({ [field]: value }, options)
getSearchParams () => Record<string, any> 读取当前搜索区实际条件(浅拷贝)。以搜索表单当前值为准(用户已输入但未点击「查询」也能读到);不含分页 / 表头筛选 / 列头搜索。需要「上一次请求实际使用」的完整参数请用 getQueryParams()
getQueryParams () => QueryParams 获取当前完整查询参数(含分页),结构与 request 回调收到的参数一致,即 { current, pageSize, ...搜索栏/表头筛选/列头搜索/排序参数 }
导入导出
方法 签名 说明
exportExcel () => Promise<void> 导出 Excel
exportCsv () => Promise<void> 导出 CSV
exportSelected () => Promise<void> 导出选中行(选中行数据由组件内部解析,无需传参)
openFileDialog () => Promise<void> | void 打开导入文件对话框
downloadTemplate () => Promise<void> 下载导入模板(表头取自当前显示列,已按权限裁剪)

以上五个方法均已内置权限门控(permissionConfig.actions.export / import),无权限时拒绝执行并输出一次性告警。

Slots

插槽名 作用域参数 说明
search - 自定义整个搜索栏(替换默认 BwProTableSearch 组件)
search-actions { search, reset, collapsed, toggleCollapsed, canCollapse } 自定义搜索栏操作按钮区域(仅在默认搜索组件下生效,使用 #search 替换后无效)
toolbar - 自定义整个工具栏(替换默认 BwProTableToolbar 组件)
toolbar-extra - 工具栏左侧额外内容(在标题与内置按钮组之间)
toolbar-right - 工具栏右侧额外内容(在内置按钮组右侧)
bodyCell { column, record, index, text, value, defaultContent, maskedValue, editing, valueType, isEmpty } 自定义单元格渲染(覆盖 valueType 渲染器)。编辑态下会被绕过(由组件接管渲染编辑控件);text / value 是原始值,需要脱敏展示请改用 defaultContent 或 maskedValue
headerCell { column } 自定义表头单元格(列头搜索 Popover 与拖拽图标仍会追加)

插槽透传机制:除上述具名插槽外,所有 ant-design-vue Table 原生插槽(如 emptyText、title、footer 等)均通过 v-for="$slots" 透传支持。

⚠️ bodyCell 与 headerCell 不在盲目透传之列 —— 二者由组件显式接管: bodyCell 需要「编辑态优先 + 富 Slot Props + 单元格脱敏」,headerCell 需要在用户内容旁追加 列头搜索 Popover 与拖拽图标。若把它们原样注册到 a-table,会覆盖这些逻辑 (历史缺陷:编辑态优先失效、defaultContent 不可得、脱敏被绕过、列头搜索不显示)。

ProColumnType 列定义

ProColumnType 在 ant-design-vue TableColumnType 基础上扩展以下字段:

interface ProColumnType<RecordType = any> extends Omit<TableColumnType<RecordType>, 'children'> {
  // === 值渲染 ===
  valueType?: ValueType; // 内置渲染器类型
  valueEnum?: ValueEnumMap; // 枚举值映射(tag/enum 渲染 + 搜索栏 select)
  dateFormat?: string; // 日期格式(valueType='date',默认 'YYYY-MM-DD')
  dateTimeFormat?: string; // 日期时间格式(valueType='dateTime',默认 'YYYY-MM-DD HH:mm:ss')
  precision?: number; // 小数精度(valueType='number'/'currency')
  currencyPrefix?: string; // 货币前缀(默认 '¥')
  currencySuffix?: string; // 货币后缀
  customRender?: RenderFn; // 自定义渲染函数(优先级高于 valueType)

  // === 空值与省略 ===
  emptyText?: string; // 空单元格占位符(覆盖全局 defaultEmptyText)
  /** @deprecated 已废弃且不再生效(开发环境输出一次性迁移告警):请改用 antd 原生 `ellipsis`('ellipsis'→ellipsis:true,'clip'→ellipsis:false) */
  overflow?: 'ellipsis' | 'wrap' | 'clip';

  // === 搜索 ===
  hideInSearch?: boolean; // 是否在搜索栏中隐藏该列(默认 false,即显示在搜索栏)
  headerSearch?: boolean | HeaderSearchConfig; // 列头搜索(仅 valueType='text' 生效)
  searchPlaceholder?: string | string[]; // 搜索栏占位文本(范围组件支持 [string, string])
  searchOrder?: number; // 搜索栏排序权重(值越小越靠前,覆盖默认值)
  initialValue?: any; // 搜索栏初始值/重置默认值
  renderFormItem?: (context: { model; search; resetField }) => VNode; // 自定义搜索表单控件

  // === 编辑 ===
  editable?: boolean | ((record) => boolean); // 是否可编辑(编辑控件由 valueType 推导)
  formRules?: FormRule[]; // 编辑校验规则

  // === 权限与脱敏 ===
  authority?: Authority; // 列级权限标识,无权限则从所有列管线移除该列
  editableAuthority?: Authority; // 列级编辑权限,未命中则「可见但只读」(与 editable 为逻辑与)
  mask?: MaskInput; // 列级数据脱敏(仅文本类列生效,fail-closed,导出同样脱敏)

  // === 渲染器专属 ===
  actions?: ActionItem[]; // 操作列按钮(valueType='action')
  imageWidth?: number; // 图片宽度(默认 32)
  imageHeight?: number; // 图片高度(默认 32)
  linkProps?: { href?; target?; onClick? }; // 链接配置
  progressProps?: { strokeColor? }; // 进度条配置
  avatarProps?: { srcField?; nameField?; showName?; size?; shape? }; // 头像配置
  booleanSwitch?: boolean; // 布尔列使用 Switch 开关(默认图标)

  // === 其他 ===
  hidden?: boolean; // 是否隐藏列
  description?: string; // 表头说明文字(Tooltip 显示)
  children?: ProColumnsType; // 子列(分组表头)
}

ActionItem 操作列按钮

interface ActionItem<RecordType = any> {
  label: string; // 按钮文字
  type?: 'primary' | 'dashed' | 'link' | 'text' | 'default'; // 按钮类型
  danger?: boolean; // 危险操作样式
  icon?: string; // @ant-design/icons-vue 图标名
  confirm?: string; // 气泡确认文案(设置后点击先弹 Popconfirm)
  disabled?: boolean | ((record) => boolean); // 是否禁用
  hidden?: boolean | ((record) => boolean); // 是否隐藏
  onClick?: (record, index) => void | Promise<void>; // 点击回调
}

FormRule 校验规则

interface FormRule {
  required?: boolean; // 是否必填
  message?: string; // 校验失败消息
  type?: 'string' | 'number' | 'boolean' | 'email' | 'url'; // 值类型
  min?: number; // 最小值(number 类型)或最小长度(string 类型)
  max?: number; // 最大值(number 类型)或最大长度(string 类型)
  pattern?: RegExp; // 正则校验
  validator?: (value, record) => Promise<void> | void; // 自定义校验(throw 或 reject 表示失败)
}

ValueType 渲染器

内置渲染器

valueType 渲染输出 支持的额外列属性
text 文本(ellipsis Tooltip 由 Table 层统一处理) emptyText
number 千分位格式化数字 precision、emptyText
date dayjs 格式化日期 dateFormat(默认 'YYYY-MM-DD')、emptyText
dateTime dayjs 格式化日期时间 dateTimeFormat(默认 'YYYY-MM-DD HH:mm:ss')、dateFormat(回退)、emptyText
currency 货币符号 + 千分位 precision、currencyPrefix(默认 '¥')、currencySuffix、emptyText
tag / enum a-tag + 颜色 valueEnum、emptyText
image a-image 缩略图(含预览) imageWidth(默认 32)、imageHeight(默认 32)、emptyText
link <a> 链接 + LinkOutlined 图标 linkProps: { href?(无时默认 '#'并阻止跳转), target?(默认'_blank', 自动设 rel='noopener noreferrer'), onClick?: (record, event) => void }、emptyText
progress a-progress 进度条 progressProps: { strokeColor? }、emptyText
avatar a-avatar + 名称 avatarProps: { srcField?(默认 'avatar'), nameField?(默认 'name'), showName?(默认 true), size?(默认 32), shape?(默认 'circle') }、emptyText
boolean ✓/✗ 图标(默认)或 a-switch(booleanSwitch: true) booleanSwitch、emptyText
action 按钮组 + Popconfirm actions: ActionItem[]
index 行号(index + 1) -

空值约定:所有渲染器对 null/undefined/'' 显示 emptyText(列级)或 defaultEmptyText(全局,默认 '-')。boolean 类型中 false 是合法值(显示红叉),不视为空值。

自定义 valueType

通过 defineColumnType 注册自定义渲染器:

import { defineColumnType } from 'bw-pro-table';
import { h } from 'vue';

defineColumnType('customStatus', ({ value, record, index, column }) => {
  return h('span', { style: { color: value === 'ok' ? 'green' : 'red' } }, value);
});

// 在列定义中使用
const columns = [{ title: '状态', dataIndex: 'status', valueType: 'customStatus' as any }];

渲染器函数签名:

type RendererFn = (ctx: {
  value: any; // 单元格值
  record: any; // 行数据
  index: number; // 行索引
  column: ProColumnType; // 列配置
}) => VNode | string | number;

渲染器 API

API 说明
defineColumnType(type, renderer) 注册自定义渲染器(已存在会警告并覆盖)
registerRenderer(type, renderer) 注册渲染器(底层 API)
getRenderer(type) 获取渲染器
hasRenderer(type) 判断渲染器是否存在
getRendererSafe(type) 获取渲染器(自动初始化内置渲染器)
ensureRenderers() 惰性初始化所有内置渲染器(幂等)
initBuiltinRenderers() 同 ensureRenderers(保留旧 API 兼容)
resetRenderers() 重置注册表(仅用于测试隔离)

数据加载模式

Request 模式(服务端)

传入 request 异步函数,组件管理分页、筛选、排序:

const request = async (params: {
  current: number; // 当前页码
  pageSize: number; // 每页条数

}) => {
  const res = await api.fetch(params);
  return {
    data: res.list, // 当前页数据数组
    total: res.total, // 数据总数
    success: true, // 是否成功
  };
};

参数合并:mergedSearchParams 将以下参数合并后传给 request:

  • searchFormParams(搜索栏表单值)
  • headerSearchParams(列头搜索文本)
  • tableFilterParams(表头筛选值)
  • sortState(排序状态,展开为 sortField + sortOrder)

竞态保护:fetchData 使用版本号(fetchVersion)防止过期响应覆盖最新数据。

页码越界校验:数据返回后若当前页超过最大页数,自动回退到最后一页并重试一次(防止后端 total 不稳定导致无限递归)。

DataSource 模式(客户端)

传入 dataSource 静态数组,组件自动应用客户端筛选与排序:

const dataSource = ref<User[]>([...]);
// 模板
<BwProTable :columns="columns" :data-source="dataSource" />

客户端筛选 applyClientFilters:

按以下顺序对数据应用三管道筛选:

  1. searchFormParams(搜索栏):
    • tag/boolean/number/currency/date/dateTime 类型 → 精确匹配(===)
    • text/其他类型 → 子串匹配(大小写不敏感)
  2. headerSearchParams(列头搜索):子串匹配(大小写不敏感)
  3. tableFilterParams(表头筛选):数组 includes 或精确匹配

未声明字段的处理:搜索栏条件中的键若未在 searchFields / columns 中声明,客户端无法推断其筛选语义, applyClientFilters 会跳过该条件并输出一次 console.warn(而非把整表过滤空)。常见原因与对策:

  • 键名拼写错误 → 用 getSearchParams() 或 v-model:searchValues 回写对象核对合法键集
  • 需要参与客户端筛选的字段 → 声明为 searchFields 项(配置 columnDataIndex / onFilter,或选用精确匹配控件)
  • 服务端专用字段 → 声明 searchFields 项并设置 serverOnly: true(DataSource 模式下该条件不产生任何筛选效果,仅出现在 getQueryParams() 中)

⚠️ 已知限制:columns[].dataIndex 为数组(嵌套路径,如 ['user','name'])时,搜索键会被摊平为 'user.name', 但 effectiveKey 仍按该摊平键直接读取记录属性,嵌套记录下取不到值 → 该条件会把数据全部过滤掉且无告警 (因 columnDataIndex 有值,存在性防护被跳过)。在 DataSource 模式下请为该列使用字符串 dataIndex, 或通过 searchFields 配置自定义 onFilter 承接嵌套筛选。

客户端排序 applyClientSort:

  • 若列定义了 sorter 函数(CompareFn 或 { compare })→ 使用自定义比较
  • 否则 → 通用比较(数字用 -,字符串用 localeCompare,null 值排末尾)
  • 支持多列排序(取第一个排序项作为主排序)

模式切换

监听 request prop 变化自动切换模式:

  • 切换到 Request 模式 → 重置页码到 1 并请求数据
  • 切换到 DataSource 模式 → 校验页码边界并刷新数据

分页配置

分页配置完整透传 antd 的 TablePaginationConfig。组件只接管「当前页」与「每页条数」两个受控状态,其余配置项原样下发给 <a-table>:

<BwProTable
  :pagination="{
    pageSize: 20, // 初始每页条数,默认 10(仅初始化时读取)
    pageSizeOptions: [10, 20, 50, 100], // 每页条数可选项,默认 ['10','20','50','100']
    showSizeChanger: true, // 默认 true
    showQuickJumper: true, // 默认 true
  }"
/>
配置项 类型 默认值 说明
pageSize number 10 初始每页条数,仅初始化时读取,运行时修改不生效
pageSizeOptions (string | number)[] ['10','20','50','100'] 每页条数下拉选项,支持数字或字符串(antd 内部按 toString() 比较)
showSizeChanger boolean true 是否显示条数切换器;antd 原生默认是「total > 50 才显示」
showQuickJumper boolean true 是否显示快速跳页
showTotal (total, range) => string 内置文案 自定义「共 N 条」显示,未传时使用内置国际化文案
total number 由数据量计算 DataSource 模式下可自行指定;Request 模式由返回值驱动
onChange / onShowSizeChange function - 用户回调在组件内部逻辑(翻页 / 重新取数 / 持久化)之后执行

行为说明:

  • 默认行为与 antd 原生一致:不传 pageSizeOptions 时下拉为 10 / 20 / 50 / 100(即 antd 的默认值)。
  • 当前 pageSize 不在选项内时不会丢值:antd 会自动把当前条数并入下拉并排序,因此初始 pageSize: 6 或持久化恢复的条数不在选项中时,用户依然可以切回。
  • 持久化优先:开启 persistence 后,恢复的 pageSize 会覆盖 pagination.pageSize(用户上次的选择优先)。
  • pageSizeOptions 不参与持久化:持久化只记录 pageSize 数值,因此恢复值可能不在当前选项内(antd 会自动并入下拉,不会丢值);运行时切换选项也不会触发持久化写入或重新取数。
  • pagination: false 完全关闭分页,pageSizeOptions 等配置不再生效。
  • 运行时修改 pageSizeOptions(响应式数组 / 预设切换)会立即反映到下拉选项;修改 pageSize 则不会生效。

搜索系统

三管道架构

管道 来源 触发方式 适用模式
搜索栏 BwProTableSearch.vue 表单 点击"查询"按钮 两种模式
表头筛选 ant-design-vue 原生 column.filters 列头筛选下拉 两种模式(Request 传后端,DataSource 客户端)
列头搜索 BwProTableColumnSearch.vue Popover 点击搜索图标 两种模式

三个管道独立维护状态,最终合并到 mergedSearchParams 传给 request 或在客户端筛选中应用。

搜索栏控件自动匹配

搜索栏根据列的 valueType 自动渲染对应控件:

valueType 搜索控件
text / link / 无 valueType a-input(文本输入)
tag a-select(选项来自 valueEnum)
boolean a-select(是/否选项)
number / currency a-input-number
date a-date-picker
dateTime a-date-picker
image / avatar / progress / action / index 不可搜索(自动排除)
其他 a-input(兜底)

hideInSearch:列上设置 hideInSearch: true 可跳过搜索栏控件生成,适用于 ID、操作列等无需搜索的字段。

自定义搜索控件:通过 renderFormItem 完全自定义(优先级高于 valueType 自动匹配):

{
  title: '状态',
  dataIndex: 'status',
  renderFormItem: ({ model, search, resetField }) => {
    return h(ASelect, {
      value: model.status,
      'onUpdate:value': (v) => { model.status = v; search(); },
      options: [...],
    });
  },
}

renderFormItem 接收 { model, search, resetField } 上下文:model 可直接读写表单值,search() 触发查询,resetField(dataIndex) 清除指定字段。

自定义搜索字段(searchFields)

通过 searchConfig.searchFields 可完全脱离 columns 自定义搜索表单项。未提供时降级为从 columns 自动生成(向后兼容):

import type { SearchConfig, SearchFieldConfig } from 'bw-pro-table';

const searchConfig: SearchConfig = {
  searchFields: [
    // 日期范围选择器 — columnDataIndex 阻止同名列自动生成
    {
      field: 'createdDateRange',
      label: '创建日期',
      component: 'dateRangePicker',
      placeholder: ['开始日期', '结束日期'],
      columnDataIndex: 'createdAt',
    },
    // 自定义筛选函数 — onFilter 在 DataSource 模式下生效
    {
      field: 'keyword',
      label: '关键字',
      component: 'input',
      placeholder: '搜索姓名/邮箱',
      persistOnCollapse: true,
      onFilter: (value, record) => {
        const v = String(value).toLowerCase();
        return (
          String(record.name).toLowerCase().includes(v) ||
          String(record.email).toLowerCase().includes(v)
        );
      },
    },
    // 多选下拉 — 覆盖列自动生成
    {
      field: 'status',
      label: '状态',
      component: 'select',
      componentProps: { mode: 'multiple', options: [{ label: '启用', value: 'active' }] },
      columnDataIndex: 'status',
    },
    // 级联选择器
    {
      field: 'region',
      label: '地区',
      component: 'cascader',
      componentProps: { options: regionTree, changeOnSelect: true },
    },
    // 联动字段 — renderFormItem 获取其他字段值
    {
      field: 'city',
      label: '城市',
      allowClear: true,
      renderFormItem: ({ model }) => {
        const opts = cityDataMap[model.province] || [];
        return h(Select, {
          value: model.city,
          options: opts,
          'onUpdate:value': (v) => {
            model.city = v;
          },
        });
      },
    },
  ],
};

SearchFieldConfig 完整字段:

字段 类型 默认值 说明
field string 必填 搜索参数 key(出现在 request params 中)
label string 必填 搜索表单标签文本
component SearchComponentType 自动匹配 控件类型:input / select / datePicker / dateRangePicker / dateTimePicker / inputNumber / switch / radioGroup / checkboxGroup / cascader / treeSelect
valueType string - valueType 语义,用于自动匹配搜索组件
componentProps object - 透传给 antd 组件的额外 props
placeholder string | string[] 自动生成 占位文本(范围组件支持 [string, string])
defaultValue any - 初始化/重置默认值
renderFormItem function - 最高优先级自定义渲染
order number 自定义 0 / 自动 1000+index 排序权重(越小越靠前)
persistOnCollapse boolean false 折叠时始终显示
columnDataIndex string - 关联列 dataIndex,阻止同名列自动生成搜索项
allowClear boolean true 是否显示 clear 按钮
disabled boolean false 是否禁用
hidden boolean false 是否隐藏
serverOnly boolean false DataSource 模式下跳过客户端筛选
onFilter (value, record) => boolean - DataSource 模式自定义筛选函数

代码控制搜索条件

搜索条件除了声明式配置外,还支持在运行时由外部代码读写与整体托管。

命令式 API
const tableRef = ref();

// 合并设置(保留未传入字段)+ 立即查询 + 写入持久化
tableRef.value.setSearchParams({ status: 'active' });

// 单字段语法糖
tableRef.value.setSearchFieldValue('keyword', 'li');

// 全量替换:未出现的搜索字段一律清除(不回落 defaultValue)
tableRef.value.setSearchParams({ status: 'active' }, { replace: true });

// 只同步条件、不触发请求,且本次不写持久化
tableRef.value.setSearchParams({ tenantId: 't-1' }, { submit: false, persist: false });

// 读取搜索栏管道参数(浅拷贝)
// 读取搜索区当前条件(含已输入但未点「查询」的值)
const params = tableRef.value.getSearchParams();

SetSearchParamsOptions

选项 类型 默认值 说明
replace boolean false true 时以传入对象为唯一真源:未出现的搜索字段一律清除,不回落 defaultValue(需恢复默认值请用 resetSearch()),并会丢弃此前注入的未定义额外参数
submit boolean true 是否立即提交查询;false 时仅同步条件与 UI,等待用户点查询
resetPage boolean true submit 为 true 时是否重置到第 1 页
persist boolean true 是否写入本地持久化;false 时本次跳过 filters_searchForm 写入
clearSelection boolean true submit 为 true 时是否清空行选择(含跨页选择)

值语义:null / undefined / '' 表示清除该字段;false 与 0 是合法值,不会被判为空。

受控模式(v-model:searchValues)
<script setup lang="ts">
import { ref } from 'vue';

const searchValues = ref<Record<string, any>>({ status: 'active' });
</script>

<template>
  <BwProTable
    v-model:searchValues="searchValues"
    :search="{}"
    :columns="columns"
    :request="fetchData"
  />
</template>
  • 未传该 prop 时为非受控,组件行为与不传时完全一致(非受控回退)
  • 外部改值 → 同步搜索区 UI 与查询参数,不自动触发请求(需自行调用 fetchData(),或直接改用 setSearchParams)
  • 用户点查询 / 重置 → 通过 update:searchValues 回写,形成闭环
  • 托管边界:仅托管搜索栏条件。表头筛选、列头搜索与排序仍由组件内部自治,不出现在回写中;需要完整条件请用 getQueryParams()
  • 传入受控 prop 后,filters_searchForm 的持久化恢复会被跳过(写入仍默认生效)。建议同时配置 persistence: { persistSearchForm: false },避免产生永不读取的无用缓存
  • 请传入稳定引用(reactive / computed / ref),避免模板内联对象字面量造成重复同步
动作与结果对照
动作 搜索栏条件 分页 行选择 持久化 触发请求
用户点「查询」 用表单当前值整体替换 重置到第 1 页 清空 写入 是
用户点「重置」 恢复各字段 defaultValue / 清空 重置到第 1 页 不变 写入 是
resetSearch() 同上 重置到第 1 页 不变 写入 是
setSearchParams(v, { submit: false }) 合并或替换(受 replace 控制) 不变 不变 默认写入,persist: false 关闭 否
setSearchParams(v) 同上 重置到第 1 页 清空(clearSelection: false 可保留) 默认写入 是
searchValues prop 变化 按整体托管替换 不变 不变 写入 否

代码通过命令式 API 设置的值不会成为「重置基线」:resetSearch() 与用户点重置仍会回到各字段的 defaultValue。

边界与注意事项
  • 搜索区隐藏时仍可用:search={false} 或使用 #search 插槽替换搜索区时,命令式 API 仍会写入查询参数并参与请求,只是不做 UI 同步(首次调用会输出一条 console.warn 提示)
  • 未定义字段:未在 searchFields / columns 中声明的字段可随 setSearchParams / searchValues 进入请求参数,但不渲染搜索项。DataSource 模式下请避免——客户端筛选无法为未知字段建立匹配规则,组件会跳过该字段的筛选并告警(若不跳过会因空串匹配失败而清空整表)。服务端专用字段请显式声明为 searchFields 项并设置 serverOnly: true
  • 额外参数的存活范围:用户点查询 / 重置会用表单值整体替换搜索栏参数,因此「未定义为搜索字段的额外参数」在用户查询后会被丢弃。需要长期存续的额外条件请声明为可渲染的 searchFields 项并设置 serverOnly: true。注意:不要给该字段加 hidden: true —— hidden 字段会被 useSearchFields 与 collectFormValues() 一并过滤掉,反而无法参与参数收集(hidden: true + serverOnly: true 的组合是无效的)
  • defaultValue / defaultCollapsed 支持运行时变更:修改 searchFields[i].defaultValue(就地修改或替换数组均可)或 search.defaultCollapsed 会即时生效。但用户已手动改过的字段与用户已手动切换过的折叠状态不再被配置覆盖(避免打断用户操作)
  • getSearchParams() 与 getQueryParams() 的取值差异(有意设计):
    • getSearchParams() = 搜索区当前所见:读取搜索表单实时值,用户在输入框里改了但**未点「查询」**的条件也能读到;已定义为搜索字段但当前为空的字段会从结果中移除;代码注入的未定义额外参数会被保留
    • getQueryParams() = 上一次请求实际使用的完整快照(含分页、表头筛选、列头搜索、排序)
    • 存在「未提交的搜索区改动」时二者不同:需要复现请求用后者,需要读取界面条件用前者。日期类字段二者均返回 dayjs 对象

响应式列数布局(responsiveColumns)

搜索表单使用 ResizeObserver + CSS Grid 实现基于容器宽度(而非 viewport)的响应式列数布局,适配弹窗/抽屉等非全宽场景:

const searchConfig: SearchConfig = {
  responsiveColumns: { 400: 1, 600: 2, 900: 3, 1200: 4, 1600: 6 },
  // 默认值:{ 576: 2, 768: 3, 992: 4 }
};

断点按数值升序匹配,取 ≤ 当前宽度的最大断点。宽度小于最小断点时默认 1 列。该配置不参与持久化。

列头搜索(headerSearch)

仅 valueType='text' 的列支持列头搜索(HEADER_SEARCHABLE_TYPES 白名单)。在列上配置:

{ title: '姓名', dataIndex: 'name', valueType: 'text', headerSearch: true }
// 或带配置
{ title: '姓名', dataIndex: 'name', valueType: 'text', headerSearch: { placeholder: '搜索姓名' } }

HeaderSearchConfig:{ enabled?: boolean; placeholder?: string; searchText?: string; resetText?: string }

全局配置通过 columnSearch prop:

  • columnSearch: false → 禁用所有列的列头搜索
  • columnSearch: { placeholder: '...' } → 覆写默认配置

清除筛选

调用 clearFilter 或工具栏"清除筛选"按钮会清空所有三个管道的筛选状态及排序,并重置到第一页。

行内编辑

EditConfig 配置

interface EditConfig<RecordType = any> {
  mode?: 'cell' | 'row'; // 编辑模式(默认 'cell')
  newRecord?: Partial<RecordType>; // 新增行默认值
  onEditStart?: (record, field?) => Promise<boolean> | boolean; // 编辑前回调(返回 false 阻止)
  onSave?: (record, isNew) => Promise<void> | void; // 保存回调
  onDelete?: (keys) => Promise<void> | void; // 删除回调
  addable?: boolean; // 是否允许新增(默认 true)
  deletable?: boolean; // 是否允许删除(默认 true)
}

编辑模式

模式 触发 行为
cell startCellEdit(rowKey, field) 仅单个单元格进入编辑态,其他单元格正常显示
row startRowEdit(rowKey) 整行进入编辑态

草稿数据管理:编辑期间的修改存储在 draftData: Map<rowKey, Record<field, value>>,保存时合并到数据源,取消时回滚到原始数据备份。

编辑组件自动匹配

编辑态根据列的 valueType 自动渲染编辑组件:

valueType 编辑组件
number / currency a-input-number
date a-date-picker(格式使用 dateFormat 或默认 'YYYY-MM-DD')
dateTime a-date-picker(格式使用 dateTimeFormat,回退 dateFormat,默认 'YYYY-MM-DD HH:mm:ss')
tag / enum a-select(选项来自 valueEnum)
boolean a-switch
其他(text 等) a-input

校验失败时编辑组件边框变红,下方显示错误消息。

字段校验

在列上配置 formRules,保存时自动校验:

{
  title: '年龄',
  dataIndex: 'age',
  editable: true,
  formRules: [
    { required: true, message: '年龄不能为空' },
    { type: 'number', min: 0, max: 150, message: '年龄范围 0-150' },
  ],
}

校验规则执行顺序:required → type → min → max → pattern → validator(每个字段仅报第一个错误)。

editable 支持函数形式动态判断可编辑性:

{ dataIndex: 'status', editable: (record) => record.role === 'admin' }

撤销/重做(Command Pattern)

编辑操作以命令(EditCommand)形式压入撤销栈:

interface EditCommand {
  type: 'update' | 'add' | 'delete';
  rowKey: string | number;
  field?: string; // update 命令的字段名
  oldValue?: any; // update 的旧值
  newValue?: any; // update 的新值
  record?: any; // add/delete 的完整记录
  index?: number; // delete 的原始索引(用于撤销时恢复位置)
}
  • 撤销栈上限:2 * DEFAULT_PAGE_SIZE - 1(默认 19 条),超出时移除最旧命令
  • 新操作入栈时清空重做栈
  • undo()/redo() 通过 ref 方法调用

批量编辑

通过 openBatchEdit() 打开批量编辑弹窗,选择字段和值后应用到所有选中行:

const tableRef = ref();
// 选中多行后
tableRef.value.openBatchEdit();
// 弹窗确认后自动调用 onSave 回调逐行保存

跨页多选模式下,批量编辑应用于所有跨页选中的行。

新增与克隆行

tableRef.value.addRow('top'); // 在顶部新增空行
tableRef.value.addRow('bottom'); // 在底部新增空行(默认)
tableRef.value.cloneRow(rowKey); // 克隆行(插入到顶部)

新增行使用 editConfig.newRecord 作为默认值,自动进入编辑态。新增行的 key 格式为 new_{timestamp}_{random}。

列管理

DragConfig 拖拽配置

interface DragConfig {
  columnDraggable?: boolean; // 是否启用表头列拖拽排序(固定列自动排除)
}

列拖拽排序

启用列拖拽后,表头显示拖拽图标,可拖动调整列顺序:

<BwProTable :columns="columns" :drag-config="{ columnDraggable: true }" />

实现细节:

  • 使用 HTML5 Drag and Drop API
  • 拖拽时自定义幽灵图(蓝色背景白色文字的列标题)
  • dragVersion 计数器变化触发表头重渲染(更新拖拽视觉反馈)
  • reorderColumns(fromDataIndex, toDataIndex) 分离可见/隐藏列各自保持相对顺序
  • 列排除规则(isDragTargetColumn,定义于 constants.ts):
    • 无 dataIndex 的列不参与拖拽
    • 特殊列(valueType='index'/'action'、dataIndex='action')不参与拖拽
    • 有 children 的分组父列不参与拖拽
    • 固定列(fixed='left'/'right'/true)不参与拖拽,因为固定列的视觉定位语义与拖拽重排冲突,会导致 ant-design-vue 渲染异常

列宽调节

列宽调节通过 rAF(requestAnimationFrame)批处理节流,避免每像素触发响应式更新:

  • 宽度变更先收集到 pendingWidths 对象
  • 每帧通过 flushColumnWidths() 统一写入 shallowRef<Record<string, number>>
  • 持久化写入也合并到同一 rAF 周期
  • 组件卸载时确保刷新残留的待持久化数据

列设置面板

工具栏的列设置按钮打开面板,支持:

  • 列显隐切换(checked)
  • 列顺序调整(order)
  • 列宽显示
  • 重置到初始状态(onColumnReset)

列配置变化通过 onColumnChange 自动持久化。

权限控制

通过 permissions prop 声明当前用户权限,组件据此裁剪列与操作按钮。 默认不传即不启用,行为与未接入权限时逐位一致。

三态语义

permissions 取值 含义 行为
undefined(不传) 权限控制未启用 全部放行
[] 权限已加载但无任何权限 拒绝所有带 authority 的项;未声明的公开项保留
[{ name }] 按白名单判定 声明的多个标识任一命中即通过(OR 语义)
// 推荐写法:角色/权限未加载完成时自然为 undefined → 落回「未启用」语义
const permissions = computed(() => userStore.role?.permissions);

⚠️ 异步下发时务必保持 undefined。 若在加载过程中就把 permissions 回退为 [], 首帧会出现「全部显示 → 闪没」的瞬时信息泄漏窗口。

声明权限

const columns: ProColumnsType = [
  { title: '姓名', dataIndex: 'name' }, // 未声明 authority → 公开列,始终可见
  { title: '手机号', dataIndex: 'phone', authority: 'user:phone:read' }, // 无权限 → 整列消失
  {
    title: '操作',
    dataIndex: 'action',
    valueType: 'action',
    actions: [
      { label: '查看', onClick: open }, // 公开按钮
      { label: '删除', authority: 'user:delete', danger: true, onClick: del },
    ],
  },
];
<BwProTable :columns="columns" :permissions="permissions" :request="fetchData" />
  • 列级:ProColumnType.authority?: Authority
  • 操作按钮级:ActionItem.authority?: Authority
  • 分组列:children 递归应用同一规则;子列全部无权限时,父分组一并隐藏(不渲染空表头)
  • 空数组声明(authority: [])视为「无约束」,等同未声明

权限表达式与通配符

Authority 支持三种形态:

形态 语义 示例
string 需要该权限 authority: 'user:edit'
string[] 任一命中即通过(OR) authority: ['user:edit', 'user:admin']
AuthorityRule 完整表达式 authority: { any: [...], all: [...], none: [...] }
// 任一满足
{ authority: { any: ['user:edit', 'user:admin'] } }
// 必须同时具备
{ authority: { all: ['user:edit', 'dept:a'] } }
// 显式排除(优先级最高)
{ authority: { any: ['user:edit'], none: ['user:banned'] } }

判定优先级:none > all > any(拒绝优先于允许)。

尾部通配符(需求侧与授权侧均可用,实现为前缀比较、不使用正则):

写法 含义
authority: 'user:*' 需要任意 user: 前缀的权限
authority: '*' 需要任意权限
授权侧授予 * 超级管理员:覆盖一切需求
授权侧授予 user:* 覆盖其前缀下的全部需求

空数组统一表达「无约束」:[]、{ any: [] }、{ all: [] } 均放行(而非拒绝)。

细粒度能力门控(permissionConfig)

permissions 只控制「列与操作按钮」;工具栏等内置操作通过 permissionConfig.actions 追加门控:

<BwProTable
  :permissions="permissions"
  :permission-config="{
    actions: {
      export: 'user:export',
      import: 'user:import',
      columnSetting: 'user:column',
      batchEdit: 'user:batch',
    },
  }"
  :toolbar="{ export: true, import: true, columnSetting: true }"
/>
actions 项 作用 未配置时
export 工具栏导出按钮 不受权限控制,保持原有显隐
import 工具栏导入按钮 同上
columnSetting 列设置面板入口 同上
batchEdit 批量编辑(含命令式 openBatchEdit() 防绕过) 同上

actions 是「追加的门控」而非「显隐开关」:toolbar.export 未显式设为 false 时按钮本就显示, 配置 actions.export 后仅在其权限未命中时额外隐藏。toolbar.* 显式 false 始终优先。

编辑权限(可见但只读)

editable 与 editableAuthority 共同决定某列能否进入编辑态,关系为逻辑与(任一为假即只读):

const columns: ProColumnsType = [
  { title: '姓名', dataIndex: 'name', editable: true }, // 可编辑
  { title: '邮箱', dataIndex: 'email', editable: false }, // 只读
  { title: '薪资', dataIndex: 'salary', editableAuthority: 'hr:salary' }, // 无权限 → 可见但只读
  { title: '备注', dataIndex: 'remark' }, // 未声明 → 可编辑(默认)
];
配置 行为
均未声明 可编辑(默认行为,零回归)
editable: false / 函数返回 false 只读
editableAuthority 未命中权限 只读(列仍然可见,与 authority 的区别在于后者会整列隐藏)
任一为假 只读

只读列在行编辑模式下逐列降级为普通文本,同行其余列仍可正常编辑。 命令式入口(startCellEdit / startRowEdit)同样受门控,无法绕过。

⚠️ 行为变更:editable 在历史版本中从未被消费(声明了也不生效)。 本版本起显式声明 editable: false 的列会首次真正变为只读。

单元格脱敏

列级 mask 可按类型遮蔽敏感数据(脱敏类型:phone / email / idcard / bankcard / name):

const columns: ProColumnsType = [
  { title: '手机号', dataIndex: 'phone', valueType: 'text', mask: 'phone' },
];
// 13812345678 → 138****5678
规则 说明
生效范围 仅文本类列(valueType 为 text 或未声明)。其余类型配置 mask 时告警并忽略 —— 掩码串无法参与数字 / 日期格式化
三个出口 表格单元格、ellipsis Tooltip、导出内容 —— 保证「界面看到掩码,导出也是掩码」,避免声明脱敏却从导出口泄漏
失败模式 fail-closed:格式无法识别时保守遮蔽(保留首尾各 1 字符)并输出一次性告警,绝不返回原值
优先级 customRender 优先于脱敏(用户显式覆盖不得被静默改写)。⚠️ 同列并存时可见内容不会被脱敏,开发环境会告警,请改用 customRender 内部自行脱敏
渲染方式 命中脱敏时跳过 valueType 渲染器,直接以纯文本渲染掩码结果
空值 不脱敏,交回既有空值占位逻辑
入参 数字等非字符串值会先归一化为字符串,不会抛错
分级脱敏(mask 对象形式)

mask 支持携带 authority,用于表达「有全量查看权的人看明文」:

const columns: ProColumnsType = [
  // 所有人看到掩码
  { title: '手机号', dataIndex: 'phone', valueType: 'text', mask: 'phone' },
  // 命中 hr:full 者看明文(表格与导出一致)
  {
    title: '手机号',
    dataIndex: 'phone',
    valueType: 'text',
    mask: { type: 'phone', authority: 'hr:full' },
  },
];

判定语义与 editableAuthority 同向(命中 = 获得更多能力),但默认方向相反:

permissions 状态 行为
未传(权限控制未启用) 仍然脱敏 —— 无法确认查看权时必须保守
[](无任何权限) 脱敏
命中 authority 不脱敏(表格与导出均显示明文)

⚠️ 这一点与 permissionConfig.actions 等能力门控刻意不同:后者未配置即放行(保持历史行为), 而脱敏是安全策略,缺省必须保守。否则最常见的配置形态 「声明了 mask + authority,但页面尚未接入 permissions」会直接泄漏明文。

掩码规则与格式兼容

实现先剥离常见分隔符(空格 / - / +)再匹配,因此带格式的真实数据同样可正确遮蔽:

类型 规则 示例
phone 保留前 3 位 + 后 4 位 13812345678 → 138****5678;138 1234 5678 → 138****5678
email 保留 local 前 1~2 字符 + 域名 zhangsan@a.com → zh***@a.com;a@b.com → a***@b.com
idcard 保留前 6 位 + 后 4 位 兼容 18 位与 15 位旧证
bankcard 按位数分档,绝不暴露全部数字 6222021234567890 → 6222 **** **** 7890
name 保留首尾字 张三丰 → 张*丰;张三 → 张*;单字 → *

数字发生变化的项说明:带分隔符 / 带国码的手机号、短前缀邮箱、15 位旧身份证,此前会被原样输出 (String.replace 无匹配即返回原串),现在一律保守遮蔽;单字姓名此前返回「首字 + *」 (既泄漏全名又补出多余字符),现整体遮蔽。

#bodyCell 插槽与脱敏

bodyCell 的插槽参数中,text / value 是原始值,maskedValue 是脱敏后的展示值:

<template #bodyCell="{ column, text, maskedValue, defaultContent }">
  <component :is="defaultContent" v-if="column.dataIndex === 'phone'" />
  <span v-else>{{ text }}</span>
</template>

⚠️ 用户接管渲染即让渡控制权:直接渲染 text / value 会绕过脱敏。 需要安全展示时请使用 defaultContent 或 maskedValue。

⚠️ 能力边界(上线前请确认使用方已知悉)

mask 保护的是本组件自身的渲染与导出路径。以下路径不受其约束:

路径 说明
getDataSource() / getSelectedRows() 返回原始数据(本就是数据源语义),调用方据此自行导出会得到明文
ExportConfig.onExportAll / onExportSelected 由使用方回调提供数据,组件只处理「写入文件」这一步的值
request 返回的数据 组件不修改数据源本身;服务端返回什么就是什么
浏览器开发者工具 / 网络面板 前端脱敏不可能拦截

因此:脱敏是展示层的泄漏收敛,不是数据访问控制。 敏感字段的合规要求必须由服务端在接口与数据权限层面满足 —— 前端脱敏只能减少「误看」,无法阻止有意获取。

统一过滤契约

useBwPermission 返回 filterList(items),用于对任意带 authority 的列表做递归过滤 (例如页面上的自定义按钮组、外部菜单项):

const { filterList } = useBwPermission({ columns, permissions });
const visibleActions = filterList(customActionItems); // children 递归;子项全裁则父项移除

未启用权限控制或未发生裁剪时原样返回入参引用(零拷贝、零回归)。

裁剪范围(重要)

传入 permissions 后,无权限列会从以下全部管线中移除,而非仅仅不渲染:

管线 说明
表格渲染 列不出现,剩余列宽与排列自动适应
搜索栏 无权限字段不出现在搜索区
列设置面板 面板中不可见、不可恢复
导出列 导出内容不含无权限字段(安全关键)
导入映射 无法把上传文件的列映射到无权限字段
模板下载 下载的导入模板不含无权限列
批量编辑 不可批量修改无权限字段
行内编辑校验 无权限字段不参与 formRules 校验

「只隐藏渲染、导出仍取全量列」是典型的数据泄漏路径,本组件已将二者一并收口。

注意事项

  1. columnState 的持久化配置始终基于全量列 无权限列不会从 localStorage 的列配置中被删除。因此同一浏览器下切换账号/角色后, 各方的列顺序、显隐与列宽配置互不影响。

  2. 权限变化实时生效,getAllColumns() 始终返回全量列 修改 permissions 后列可正常增删;getAllColumns() 仍返回全部列(含当前无权限的列), 便于外部做权限自检。getDisplayColumns() 则会包含权限过滤结果。

  3. 行级(数据级)权限请用 hidden / disabled 函数 authority 是列/操作级的静态声明,在渲染前完成裁剪。 「只有数据属主才能删除」这类按行判定的需求,请使用 ActionItem.hidden(record) 或 ActionItem.disabled(record)

Related Packages

oceanx-component-ui-antd

基于 ant-design-vue 的 Schema 驱动表格与表单组件(antd-table / antd-form)

801
innoboxrr-vue-datatable

Datatable para vue3

2,321 0
@ys-knife-crud/vue

Vue 3 composables for ys.knife.crud (column config, excel export, row actions, r...

386 0

Version History

Version Released Vue License
0.1.9 ^3.3.0 MIT
0.1.8 ^3.3.0 MIT
0.1.7 ^3.3.0 MIT
0.1.6 ^3.3.0 MIT
0.1.5 ^3.3.0 MIT