bw-pro-table
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 集成,
localeprop 部分覆写,优先级: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:搜索栏标签宽度,默认80defaultCollapsed:搜索栏默认是否折叠,默认true(字段数不超过collapsedCount时视觉无差异)collapsedCount:折叠状态下最多显示的搜索字段数量,默认4(persistOnCollapse字段不占用该上限)colon:是否显示 label 后的冒号,默认truesearchFields:自定义搜索字段列表。未提供时降级为从 columns 自动生成(向后兼容)showSearchBtn:是否显示查询按钮,默认trueshowResetBtn:是否显示重置按钮,默认trueshowCollapseBtn:是否显示展开/收起按钮,默认当字段数 >collapsedCount时显示searchBtnProps:透传给查询按钮的额外 props(如disabled、size、ghost等)resetBtnProps:透传给重置按钮的额外 propscollapseBtnProps:透传给展开/收起按钮的额外 propsrenderSearchActions:自定义渲染整个操作按钮区域(最高优先级),接收{ 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:
按以下顺序对数据应用三管道筛选:
- searchFormParams(搜索栏):
tag/boolean/number/currency/date/dateTime类型 → 精确匹配(===)text/其他类型 → 子串匹配(大小写不敏感)
- headerSearchParams(列头搜索):子串匹配(大小写不敏感)
- 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 校验 |
「只隐藏渲染、导出仍取全量列」是典型的数据泄漏路径,本组件已将二者一并收口。
注意事项
-
columnState的持久化配置始终基于全量列 无权限列不会从 localStorage 的列配置中被删除。因此同一浏览器下切换账号/角色后, 各方的列顺序、显隐与列宽配置互不影响。 -
权限变化实时生效,
getAllColumns()始终返回全量列 修改permissions后列可正常增删;getAllColumns()仍返回全部列(含当前无权限的列), 便于外部做权限自检。getDisplayColumns()则会包含权限过滤结果。 -
行级(数据级)权限请用
hidden/disabled函数authority是列/操作级的静态声明,在渲染前完成裁剪。 「只有数据属主才能删除」这类按行判定的需求,请使用ActionItem.hidden(record)或ActionItem.disabled(record)
Related Packages
Vue 3 composables for ys.knife.crud (column config, excel export, row actions, r...
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 | |
| 0.1.4 | ^3.3.0 | MIT | |
| 0.1.3 | ^3.3.0 | MIT | |
| 0.1.2 | ^3.3.0 | MIT | |
| 0.1.1 | ^3.3.0 | MIT | |
| 0.1.0 | ^3.3.0 | MIT | |
| 0.0.9 | ^3.3.0 | MIT | |
| 0.0.8 | ^3.3.0 | MIT | |
| 0.0.7 | ^3.3.0 | MIT | |
| 0.0.6 | ^3.3.0 | MIT | |
| 0.0.5 | ^3.3.0 | MIT | |
| 0.0.4 | ^3.3.0 | MIT | |
| 0.0.3 | ^3.3.0 | MIT | |
| 0.0.2 | ^3.3.0 | MIT | |
| 0.0.1 | ^3.3.0 | MIT |