npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

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

About

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

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

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

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

Open Software & Tools

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

© 2026 – Pkg Stats / Ryan Hefner

bw-pro-table

v0.0.9

Published

基于 ant-design-vue 的高级业务表格组件(ProTable),支持请求/数据源双模式、行内编辑、搜索筛选、列拖拽排序、导出导入、跨页多选、持久化等

Readme

BwProTable

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

特性

特性

  • 🚀 双模式数据加载request 异步请求模式 与 dataSource 静态数据模式互斥,支持自动竞态保护与页码越界校验
  • 🎨 13 种内置渲染器textnumberdatedateTimetagimagelinkprogressavataractionindexcurrencyboolean(含 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 集成,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 }"
  />
</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:搜索栏标签宽度,默认 80
  • defaultCollapsed:搜索栏默认是否折叠,默认 false
  • collapsedCount:折叠时显示的行数,默认 4
  • colon:是否显示 label 后的冒号,默认 true
  • searchFields:自定义搜索字段列表。未提供时降级为从 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>scrollstickycustomRowcustomHeaderRowcustomCellcustomHeaderCellshowHeaderindentSizetableLayoutdefaultExpandAllRowsclassstyle 等。

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

Events

| 事件 | 参数 | 说明 | |------|------|------| | load | { dataSource: any[], total: number } | 数据加载成功完成 | | error | Error | 数据加载失败 | | change | (pagination, filters, sorter, extra) | 表格变化(分页/排序/筛选),extraaction: '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 原生插槽(如 emptyTexttitlefooter 等)均通过 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 | 千分位格式化数字 | precisionemptyText | | date | dayjs 格式化日期 | dateFormat(默认 'YYYY-MM-DD')、emptyText | | dateTime | dayjs 格式化日期时间 | dateTimeFormat(默认 'YYYY-MM-DD HH:mm:ss')、dateFormat(回退)、emptyText | | currency | 货币符号 + 千分位 | precisioncurrencyPrefix(默认 '¥')、currencySuffixemptyText | | tag / enum | a-tag + 颜色 | valueEnumemptyText | | 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-switchbooleanSwitch: true) | booleanSwitchemptyText | | 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

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

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

客户端排序 applyClientSort

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

模式切换

监听 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' },
  ],
}

校验规则执行顺序:requiredtypeminmaxpatternvalidator(每个字段仅报第一个错误)。

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

注意:启用持久化时建议设置固定的 tableIdcacheKey,否则使用随机 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.0PERSISTED_DATA_VERSION,定义于 constants.ts
  • 加载时校验 version 字段,版本不匹配自动清除旧缓存并重新初始化
  • validatePersistedState() 校验数据结构完整性:
    • columns 数组每项必须含 key/checked/order
    • filters_searchForm/filters_tableFilter/filters_headerSearch 若存在必须是 Record 格式(非数组)

恢复守卫(isRestoringFilters)

onMounted 阶段从 localStorage 恢复筛选值时,会触发 deep watcher → persistFilters() 回写链路。为避免无意义的重复写入(恢复后又立即写回相同数据),引入 isRestoringFilters 模块级守卫:

  • 恢复开始onMounted 入口)→ 设为 truepersistFilters() 入口直接 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 表示不限制)
}

工作机制

  • crossPageKeysSet<string | number> 存储所有跨页选中的 key
  • selectedRowsMapMap<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 xlsx

ExportConfig 导出配置

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 | 传入的 rowsonExportSelected(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; // 校验目标字段名(必填)
}

导入流程

  1. 文件选择openFileDialog() 打开文件选择器(接受 .xlsx/.xls/.csv
  2. 解析预览:使用 FileReader + xlsx 解析,第一行为表头,预览最多 10 行(IMPORT_PREVIEW_MAX_ROWS
  3. 自动列映射autoMapColumns 匹配 Excel 表头与 column.title/dataIndex(忽略大小写),用户预设的 columnMapping 优先级最高
  4. 手动映射:预览弹窗中可手动调整列映射
  5. 校验runValidation 基于 validateRules + 列级 formRules 逐行逐字段校验(含 validator 自定义函数)
  6. 转换beforeImport 回调转换数据
  7. 提交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.borderedtoolbar.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 接口

语言包按功能分类:searchtoolbartablecolumnSettingeditexportimportcontextMenuviewerrorselectionfilterboolean。完整结构见 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