bw-pro-table
v0.0.9
Published
基于 ant-design-vue 的高级业务表格组件(ProTable),支持请求/数据源双模式、行内编辑、搜索筛选、列拖拽排序、导出导入、跨页多选、持久化等
Maintainers
Readme
BwProTable
基于 ant-design-vue 4 的高级业务表格组件(ProTable),提供开箱即用的数据表格体验。采用 Vue 3 Composition API + TypeScript 编写,提供完整的类型推导。
特性
特性
- 🚀 双模式数据加载:
request异步请求模式 与dataSource静态数据模式互斥,支持自动竞态保护与页码越界校验 - 🎨 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/选中行,导入预览/自动列映射/校验/模板下载
- 🔄 工具栏:标题、刷新、密度切换、全屏、列设置、边框/斑马纹切换、清除筛选、选中计数显示
- 🌐 国际化: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 }"
/>
</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) |
搜索
| 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| search | boolean \| SearchConfig | - | 搜索栏配置,false 隐藏搜索栏,true 全部使用默认值 |
| columnSearch | false \| HeaderSearchConfig | - | 列头搜索全局配置,false 禁用所有列的 headerSearch |
SearchConfig:
{ labelWidth?: number; defaultCollapsed?: boolean; resetText?: string; searchText?: string; collapsedCount?: number; colon?: boolean; searchFields?: SearchFieldConfig[] }
labelWidth:搜索栏标签宽度,默认80defaultCollapsed:搜索栏默认是否折叠,默认falsecollapsedCount:折叠时显示的行数,默认4colon:是否显示 label 后的冒号,默认truesearchFields:自定义搜索字段列表。未提供时降级为从 columns 自动生成(向后兼容)
工具栏
| 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| 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 禁用 |
注意:导出/导入配置通过
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 禁用分页 |
透传与扩展
| 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| locale | Partial<BwProTableLocale> | - | 国际化语言包,传入部分字段即可覆写 |
| tableLocale | Record<string, any> | - | 透传给 ant-design-vue Table 组件的原生 locale(如 Empty 组件文案),优先级高于 locale |
| expandable | ExpandableConfig | - | 行展开配置(透传 ant-design-vue) |
| expandedRowKeys | (string \| number)[] | - | 受控展开行 keys(等价于 expandable.expandedRowKeys,二者合并后 expandable 内同名属性优先) |
| expandedRowRender | (props) => VNode | - | 展开行渲染函数(等价于 expandable.expandedRowRender,二者合并后 expandable 内同名属性优先) |
| 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) | 列宽调整 |
Methods(通过 ref 调用)
数据与选择
| 方法 | 签名 | 说明 |
|------|------|------|
| fetchData | () => Promise<void> | 手动刷新数据 |
| getDataSource | () => any[] | 获取当前数据源 |
| getSelectedRowKeys | () => (string \| number)[] | 获取当前页选中行 keys |
| getSelectedRows | () => any[] | 获取当前页选中行数据 |
| clearSelection | () => void | 清空所有选择(含跨页) |
| getCrossPageSelectedKeys | () => (string \| number)[] | 获取跨页选中的所有 keys |
| getCrossPageSelectedRows | () => (any \| null)[] | 获取跨页选中的行数据(Request 模式下部分可能为 null) |
行内编辑
| 方法 | 签名 | 说明 |
|------|------|------|
| 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 | 打开批量编辑弹窗 |
导入导出
| 方法 | 签名 | 说明 |
|------|------|------|
| exportExcel | () => Promise<void> | 导出 Excel |
| exportCsv | () => Promise<void> | 导出 CSV |
| exportSelected | (rows, keys?) => Promise<void> | 导出选中行 |
| openFileDialog | () => void | 打开导入文件对话框 |
| downloadTemplate | (columns: { title: string; dataIndex: string }[]) => Promise<void> | 下载导入模板 |
Slots
| 插槽名 | 作用域参数 | 说明 |
|--------|-----------|------|
| search | - | 自定义整个搜索栏(替换默认 BwProTableSearch 组件) |
| toolbar | - | 自定义整个工具栏(替换默认 BwProTableToolbar 组件) |
| toolbar-extra | - | 工具栏左侧额外内容(在标题与内置按钮组之间) |
| toolbar-right | - | 工具栏右侧额外内容(在内置按钮组右侧) |
| bodyCell | { column, record, index, text, value } | 自定义单元格渲染(覆盖 valueType 渲染器) |
| headerCell | { column } | 自定义表头单元格(列头搜索 Popover 仍会追加在右侧) |
插槽透传机制:除上述具名插槽外,所有 ant-design-vue Table 原生插槽(如
emptyText、title、footer等)均通过v-for="$slots"透传支持。
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` 替代 */
// === 搜索 ===
hideInSearch?: boolean; // 是否在搜索栏中隐藏该列(默认 false,即显示在搜索栏)
headerSearch?: boolean | HeaderSearchConfig; // 列头搜索(仅 valueType='text' 生效)
renderFormItem?: (context: { model, search, resetField }) => VNode; // 自定义搜索表单控件
// === 编辑 ===
editable?: boolean | ((record) => boolean); // 是否可编辑
formRules?: FormRule[]; // 编辑校验规则
editComponent?: string; // 编辑态组件类型(当前按 valueType 自动匹配)
editComponentProps?: Record<string, any>; // 编辑态组件额外属性
// === 渲染器专属 ===
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 显示)
columnGroup?: string; // 列所属分组
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; // 每页条数
[key: string]: any; // 合并的搜索/筛选/排序参数
}) => {
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或精确匹配
客户端排序 applyClientSort:
- 若列定义了
sorter函数(CompareFn或{ compare })→ 使用自定义比较 - 否则 → 通用比较(数字用
-,字符串用localeCompare,null值排末尾) - 支持多列排序(取第一个排序项作为主排序)
模式切换
监听 request prop 变化自动切换模式:
- 切换到 Request 模式 → 重置页码到 1 并请求数据
- 切换到 DataSource 模式 → 校验页码边界并刷新数据
搜索系统
三管道架构
| 管道 | 来源 | 触发方式 | 适用模式 |
|------|------|----------|----------|
| 搜索栏 | 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)清除指定字段。
列头搜索(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}。
列管理
列拖拽排序
启用列拖拽后,表头显示拖拽图标,可拖动调整列顺序:
<BwProTable
:columns="columns"
:drag-config="{ columnDraggable: true }"
/>实现细节:
- 使用 HTML5 Drag and Drop API
- 拖拽时自定义幽灵图(蓝色背景白色文字的列标题)
dragVersion计数器变化触发表头重渲染(更新拖拽视觉反馈)reorderColumns(fromDataIndex, toDataIndex)分离可见/隐藏列各自保持相对顺序- 特殊列排除:
valueType='index'、valueType='action'、dataIndex='action'的列不参与拖拽(isSpecialColumn)
列宽调节
列宽调节通过 rAF(requestAnimationFrame)批处理节流,避免每像素触发响应式更新:
- 宽度变更先收集到
pendingWidths对象 - 每帧通过
flushColumnWidths()统一写入shallowRef<Record<string, number>> - 持久化写入也合并到同一 rAF 周期
- 组件卸载时确保刷新残留的待持久化数据
列设置面板
工具栏的列设置按钮打开面板,支持:
- 列显隐切换(
checked) - 列顺序调整(
order) - 列宽显示
- 重置到初始状态(
onColumnReset)
列配置变化通过 onColumnChange 自动持久化。
持久化系统
PersistenceConfig 配置
interface PersistenceConfig {
enabled?: boolean; // 是否启用持久化(默认 true)
storage?: 'localStorage' | 'sessionStorage' | StorageAdapter; // 存储方式(默认 'localStorage')
keyPrefix?: string; // 键名前缀(默认 'bw-pro-table')
onError?: (error: Error, operation: 'get' | 'set' | 'remove') => void; // 存储操作错误回调
debug?: boolean; // 调试模式(启用后输出 console.warn 日志,默认 false)
persistSearchForm?: boolean; // 是否持久化搜索栏筛选状态(默认 true)
persistTableFilter?: boolean; // 是否持久化表格列头筛选状态(默认 true)
persistHeaderSearch?: boolean; // 是否持久化列头搜索状态(默认 true)
}持久化范围
| 状态 | 存储字段 | 说明 |
|------|----------|------|
| 列配置 | columns | 显隐(checked)、顺序(order)、宽度(width) |
| 分页大小 | pageSize | 最近一次分页切换的每页条数 |
| 当前页码 | current | 最近一次翻页的页码(DataSource 模式下会校验边界) |
| 密度 | density | 'small' \| 'middle' \| 'default' |
| 边框状态 | bordered | 工具栏边框 Switch 开关状态 |
| 斑马纹状态 | striped | 工具栏斑马纹 Switch 开关状态 |
| 搜索栏筛选 | filters_searchForm | 搜索栏表单值(V1.1.0 分离存储,可通过 persistSearchForm 关闭) |
| 表头筛选 | filters_tableFilter | ant-design-vue 原生列头筛选值(V1.1.0 分离存储,可通过 persistTableFilter 关闭) |
| 列头搜索 | filters_headerSearch | 列头搜索 Popover 文本值(V1.1.0 分离存储,可通过 persistHeaderSearch 关闭) |
| 排序状态 | sorter | { field, order }(仅在有效排序方向时写入) |
存储键格式
{keyPrefix}:{tableId 或 cacheKey}:cache默认示例:bw-pro-table:user-table:cache
注意:启用持久化时建议设置固定的
tableId或cacheKey,否则使用随机tableId会导致刷新后配置无法恢复(调试模式下会输出警告)。
筛选状态分离存储(V1.1.0)
自 V1.1.0 起,筛选状态从单一的 filters 字段拆分为三个独立字段:
| 字段 | 对应管道 | 读/写方法 |
|------|----------|-----------|
| filters_searchForm | 搜索栏表单值 | loadFiltersSearchForm() / saveFiltersSearchForm() |
| filters_tableFilter | antd 原生列头筛选 | loadFiltersTableFilter() / saveFiltersTableFilter() |
| filters_headerSearch | 列头搜索文本 | loadFiltersHeaderSearch() / saveFiltersHeaderSearch() |
迁移说明:旧的 filters 字段和 saveFilters()/loadFilters() 方法已标记为 @deprecated,保留用于向后兼容读取旧版本持久化数据。新写入统一使用分离字段。
版本管理
- 当前版本:
V1.1.0(PERSISTED_DATA_VERSION,定义于constants.ts) - 加载时校验
version字段,版本不匹配自动清除旧缓存并重新初始化 validatePersistedState()校验数据结构完整性:columns数组每项必须含key/checked/orderfilters_searchForm/filters_tableFilter/filters_headerSearch若存在必须是 Record 格式(非数组)
恢复守卫(isRestoringFilters)
onMounted 阶段从 localStorage 恢复筛选值时,会触发 deep watcher → persistFilters() 回写链路。为避免无意义的重复写入(恢复后又立即写回相同数据),引入 isRestoringFilters 模块级守卫:
- 恢复开始(
onMounted入口)→ 设为true,persistFilters()入口直接return - 恢复完成(
nextTick)→ 设为false,恢复正常的筛选持久化
自定义 StorageAdapter
import type { StorageAdapter, PersistedTableState } from 'bw-pro-table';
// StorageAdapter 接口为同步方法(与 localStorage API 对齐)
interface StorageAdapter {
get: (key: string) => PersistedTableState | null;
set: (key: string, value: PersistedTableState) => void;
remove: (key: string) => void;
}
// 示例:自定义存储适配器(包裹错误处理)
const customAdapter: StorageAdapter = {
get: (key) => {
try {
const raw = localStorage.getItem(key);
return raw ? JSON.parse(raw) : null;
} catch {
return null;
}
},
set: (key, value) => {
localStorage.setItem(key, JSON.stringify(value));
},
remove: (key) => {
localStorage.removeItem(key);
},
};
<BwProTable :persistence="{ storage: customAdapter }" />跨页多选
CrossPageSelectionConfig 配置
interface CrossPageSelectionConfig {
enabled?: boolean; // 是否启用(默认 false)
onSelectAllPages?: (searchParams) => Promise<(string | number)[]> | (string | number)[]; // Request 模式下全选回调
maxSelectCount?: number; // 最大选择数(0 表示不限制)
}工作机制
- crossPageKeys:
Set<string | number>存储所有跨页选中的 key - selectedRowsMap:
Map<key, record>存储对应的行数据 - syncSelectedRowKeys():每次数据更新后,用
crossPageKeys ∩ dataSource计算当前页应显示的选中状态 - diffing 逻辑:翻页选择时,通过对比前后
selectedRowKeys差集更新crossPageKeys
全选所有页
| 模式 | 行为 |
|------|------|
| DataSource | 直接从内存 dataSource 提取所有 key |
| Request | 调用 onSelectAllPages(mergedSearchParams) 回调获取全量 key |
Request 模式下"选择所有页"按钮仅当提供
onSelectAllPages回调时才显示(该操作需后端返回全量 key,是重操作)。DataSource 模式下始终显示。
跨页选择菜单
跨页模式下自动注入以下选择菜单项到 ant-design-vue selections:
- 选择当前页(
SELECTION_ALL) - 反选当前页(
SELECTION_INVERT) - 选择所有页(条件显示)
- 清空所有(
clear-all)
示例
<BwProTable
:row-selection="{ type: 'checkbox' }"
:cross-page-selection="{
enabled: true,
maxSelectCount: 100,
onSelectAllPages: async (params) => {
const res = await api.getAllKeys(params);
return res.keys;
},
}"
/>Excel 导入导出
需要安装 xlsx 依赖(按需动态 import() 懒加载,不影响首屏体积):
npm install xlsxExportConfig 导出配置
interface ExportConfig {
filename?: string; // 导出文件名(默认 'export')
chunkSize?: number; // 分片大小(默认 10000,超出时分片构建+进度条)
maxRows?: number; // 最大导出行数警告阈值(默认 50000,不阻止仅警告)
type?: 'xlsx' | 'csv'; // 导出类型
columnMapping?: Record<string, string>; // 自定义列映射(dataIndex → 表头文字)
beforeExport?: () => Promise<boolean> | boolean; // 导出前拦截(返回 false 阻止)
onExportAll?: (params) => Promise<any[]> | any[]; // Request 模式获取全量数据
onExportSelected?: (keys) => Promise<any[]> | any[]; // 获取选中行完整数据
}导出方式
| 方法 | 说明 | 数据来源 |
|------|------|----------|
| exportExcel() | 导出 .xlsx | onExportAll 回调或当前 dataSource |
| exportCsv() | 导出 .csv(UTF-8 BOM) | onExportAll 回调或当前 dataSource |
| exportSelected(rows, keys?) | 导出选中行为 .xlsx | 传入的 rows 或 onExportSelected(keys) 回调 |
导出列处理:自动排除 hidden: true 的列和特殊列(index/action),表头使用 columnMapping[dataIndex] 或 column.title。
分片导出:数据量超过 chunkSize 时分片构建 sheet,每 5 片 yield 到主线程避免阻塞。
ImportConfig 导入配置
interface ImportConfig {
columnMapping?: Record<string, string>; // 预设列映射(col_X → dataIndex)
validateRules?: ImportValidateRule[]; // 校验规则(含必填 field 字段)
beforeImport?: (data) => Promise<any[]> | any[]; // 导入前数据转换
onImport?: (data) => Promise<void> | void; // 导入提交回调
}
interface ImportValidateRule extends FormRule {
field: string; // 校验目标字段名(必填)
}导入流程
- 文件选择:
openFileDialog()打开文件选择器(接受.xlsx/.xls/.csv) - 解析预览:使用 FileReader + xlsx 解析,第一行为表头,预览最多 10 行(
IMPORT_PREVIEW_MAX_ROWS) - 自动列映射:
autoMapColumns匹配 Excel 表头与column.title/dataIndex(忽略大小写),用户预设的columnMapping优先级最高 - 手动映射:预览弹窗中可手动调整列映射
- 校验:
runValidation基于validateRules+ 列级formRules逐行逐字段校验(含validator自定义函数) - 转换:
beforeImport回调转换数据 - 提交:
onImport回调提交数据,成功后 Request 模式自动刷新数据
模板下载
tableRef.value.downloadTemplate(
columns
.filter(c => c.dataIndex && !isSpecialColumn(c))
.map(c => ({ title: c.title, dataIndex: c.dataIndex }))
);生成仅含表头行的空 .xlsx 模板文件。
工具栏与全屏
工具栏配置
<BwProTable
:toolbar="{
title: '用户列表', // 工具栏标题
refresh: true, // 刷新按钮
density: true, // 密度切换(small/middle/default)
fullscreen: true, // 全屏切换
columnSetting: true, // 列设置面板
export: true, // 导出按钮
import: true, // 导入按钮
clearFilter: true, // 清除筛选按钮
bordered: true, // 显示边框切换 Switch
striped: true, // 显示斑马纹切换 Switch
}"
/>注意:
toolbar.title是 BwProTable 工具栏自带的标题,与 ant-design-vue Table 原生的title插槽/属性不同,后者通过$attrs透传。
全屏
使用 screenfull 库实现全屏切换。全屏时动态切换 getPopupContainer,确保 Popover/Dropdown 挂载到全屏容器内(通过 a-config-provider 实现)。
密度切换
切换表格 size('small' | 'middle' | 'default'),变更后自动持久化。
边框与斑马纹
工具栏提供边框(bordered)和斑马纹(striped)Switch 切换按钮,分别通过 toolbar.bordered、toolbar.striped 控制显隐(默认 true,传 false 隐藏对应开关)。斑马纹通过 getRowClassName 注入 bw-pro-table-row-striped 类(偶数行)。
国际化
优先级链
props.locale > vue-i18n 全局消息 > zh-CN 默认值使用方式
方式一:locale prop 部分覆写
<BwProTable
:locale="{ toolbar: { refresh: 'Reload', export: 'Export Excel' } }"
/>方式二:vue-i18n 集成
组件通过 useI18n({ useScope: 'global' }) 读取当前 locale 对应的消息,支持 ^9.0.0 || ^10.0.0 || ^11.0.0 版本的 vue-i18n。在 vue-i18n 配置中添加对应的消息键即可。
方式三:导入默认中文包
import { zhCN } from 'bw-pro-table';BwProTableLocale 接口
语言包按功能分类:search、toolbar、table、columnSetting、edit、export、import、contextMenu、view、error、selection、filter、boolean。完整结构见 types.ts 中的 BwProTableLocale 接口。
独立 Composables
以下 composables 可独立于 BwProTable 组件使用(均已从 index.ts 导出):
| Composable | 说明 | 状态 |
|------------|------|------|
| useBwRowEdit | 行内编辑(草稿/校验/撤销重做/批量编辑) | ✅ 已集成 |
| useExportExcel | Excel 导出(xlsx 懒加载/分片/进度) | ✅ 已集成 |
| useImportExcel | Excel 导入(解析/映射/校验/预览) | ✅ 已集成 |
| usePersistence | 状态持久化(localStorage/sessionStorage/自定义) | ✅ 已集成 |
| useBwRowSelection | 行选择管理 | ✅ 可独立使用 |
| useQueryParams | URL 查询参数管理 | ✅ 可独立使用 |
| useColumnHeaderSearch | 列头搜索状态管理 | ✅ 已集成 |
| useDragSort | 拖拽排序(列拖拽已集成) | ✅ 已集成 |
| useLocale / mergeLocale | 国际化工具 | ✅ 已集成 |
✅ 已集成 = 在
BwProTable.vue中直接使用;可独立使用 = composable 已实现且类型导出,但主组件未引用。
使用示例:
import { useBwRowEdit, useExportExcel } from 'bw-pro-table';
const { editingKeys, startRowEdit, saveEdit, undo, redo } = useBwRowEdit({
dataSource,
columns,
rowKey: 'id',
editConfig,
onSave: async (record, isNew) => { /* ... */ },
});完整导出清单
// 组件
export { BwProTable } from 'bw-pro-table';
// 渲染器
export {
defineColumnType, ensureRenderers, getRendererSafe,
initBuiltinRenderers, resetRenderers,
} from 'bw-pro-table';
// 依赖注入
export { BW_PRO_TABLE_KEY } from 'bw-pro-table';
export type { BwProTableContext } from 'bw-pro-table';
// Composables
export {
useQueryParams, useBwRowEdit, useBwRowSelection,
useExportExcel, useImportExcel, usePersistence,
useContextMenu, useDragSort, useBwKeyboard,
useTableView, useBwPermission, useColumnHeaderSearch,
useLocale, mergeLocale, BW_PRO_TABLE_LOCALE_KEY,
} from 'bw-pro-table';
// 类型
export type {
ProColumnType, ProColumnsType, ValueType,
ValueEnumItem, ValueEnumMap, ActionItem, FormRule,
GetRowKey, TableRowSelection, ExpandableConfig,
BwProTableProps, EditConfig, ContextMenuConfig, ContextMenuItem,
DragConfig, ExportConfig, ImportConfig, ViewConfig,
TableState, TableStatus, PersistedColumnState, PersistedTableState,
StorageAdapter, PersistenceConfig, EditCommand,
FilterOperator, AdvancedFilterCondition, AdvancedFilterGroup,
RendererFn, RendererContext, BwProTableLocale,
} from 'bw-pro-table';
// 常量
export { DEFAULT_EMPTY_TEXT, DEFAULT_PAGE_SIZE, DEFAULT_DATE_FORMAT } from 'bw-pro-table';
// 国际化
export { zhCN } from 'bw-pro-table';高级用法
自定义搜索控件
import { h } from 'vue';
import { Select as ASelect } from 'ant-design-vue';
const columns = [
{
title: '状态',
dataIndex: 'status',
renderFormItem: ({ model, search }) => {
return h(ASelect, {
value: model.status,
'onUpdate:value': (v) => { model.status = v; search(); },
options: [
{ label: '启用', value: 'active' },
{ label: '禁用', value: 'inactive' },
],
});
},
},
];自定义渲染器
import { defineColumnType } from 'bw-pro-table';
import { h } from 'vue';
import { Tag } from 'ant-design-vue';
defineColumnType('priority', ({ value }) => {
const colorMap: Record<string, string> = { high: 'red', medium: 'orange', low: 'green' };
return h(Tag, { color: colorMap[value] }, value);
});
// 使用
const columns = [{ title: '优先级', dataIndex: 'priority', valueType: 'priority' as any }];自定义 bodyCell 插槽
<BwProTable :columns="columns" :request="request">
<template #bodyCell="{ column, record, index, text }">
<template v-if="column.dataIndex === 'name'">
<a-tag color="blue">{{ text }}</a-tag>
</template>
<template v-else>
{{ text }}
</template>
</template>
</BwProTable>使用
bodyCell插槽会覆盖 valueType 渲染器的自动匹配。
跨页多选 + 批量编辑
<template>
<BwProTable
ref="tableRef"
:columns="columns"
:request="fetchUsers"
:row-selection="{ type: 'checkbox' }"
:cross-page-selection="{
enabled: true,
maxSelectCount: 100,
onSelectAllPages: fetchAllKeys,
}"
:edit-config="editConfig"
>
<template #toolbar-right>
<a-button @click="tableRef?.openBatchEdit()" :disabled="selectionCount === 0">
批量编辑选中项
</a-button>
</template>
</BwProTable>
</template>自定义持久化存储适配器
import type { StorageAdapter, PersistedTableState } from 'bw-pro-table';
// StorageAdapter 需实现同步的 get/set/remove 方法(与 localStorage API 对齐)
const customAdapter: StorageAdapter = {
get: (key: string): PersistedTableState | null => {
try {
const raw = localStorage.getItem(key);
return raw ? JSON.parse(raw) : null;
} catch {
return null;
}
},
set: (key: string, value: PersistedTableState): void => {
localStorage.setItem(key, JSON.stringify(value));
},
remove: (key: string): void => {
localStorage.removeItem(key);
},
};
<BwProTable tableId="user-table" :persistence="{ storage: customAdapter }" />列变换管道(高级)
理解列的变换顺序有助于自定义 customCell / customHeaderCell 等高级行为:
用户定义的 columns
→ Stage 1: BwProTable.vue injectFilteredValue()
注入 filterValue/sortOrder/width 覆盖
→ Stage 2: BwProTable.vue displayColumns (computed)
过滤 hidden 列,按持久化顺序重排
→ Stage 3: BwProTableCore.vue wrappedColumns
customCell class 合并(非替换),customHeaderCell 事件链式调用
→ Stage 4: <a-table> 渲染| 列字段 | 行为说明 |
|--------|----------|
| customCell | 直接透传用户定义,不再注入内部类名 |
| customHeaderCell | 拖拽事件链式调用:用户处理器 → 内部处理器 |
| filteredValue | 被覆盖为内部控制值 |
| sortOrder | 被覆盖:当前排序列注入排序值,其余列强制 null |
| width | 用户值优先,列宽拖拽覆盖值通过 rAF 批处理注入 |
License
MIT
